fix: remove conflicting tree-sitter-go and tree-sitter-python dependencies

These grammars require tree-sitter@^0.25.0 but we use 0.21.1.
They already failed at runtime with try/catch graceful fallback.
Removing them fixes npm peer dependency conflict during pi install.

- Remove tree-sitter-go and tree-sitter-python from dependencies
- ast-extract.ts already handles missing grammars gracefully
- Heuristics + LLM layer cover Go/Python analysis anyway
This commit is contained in:
2026-06-09 23:33:57 +02:00
parent 69d3acda5d
commit e66ed25cee
8 changed files with 273 additions and 225 deletions
+2
View File
@@ -5,3 +5,5 @@ coverage/
.DS_Store
.env
.pi-map.md
# Local Pi runtime state
.atl/
+1
View File
@@ -0,0 +1 @@
{}
-40
View File
@@ -14,8 +14,6 @@
"p-limit": "^7.3.0",
"picocolors": "^1.1.1",
"tree-sitter": "^0.21.0",
"tree-sitter-go": "^0.25.0",
"tree-sitter-python": "^0.25.0",
"tree-sitter-typescript": "^0.21.0"
},
"bin": {
@@ -3355,44 +3353,6 @@
"node-gyp-build": "^4.8.0"
}
},
"node_modules/tree-sitter-go": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/tree-sitter-go/-/tree-sitter-go-0.25.0.tgz",
"integrity": "sha512-APBc/Dq3xz/e35Xpkhb1blu5UgW+2E3RyGWawZSCNcbGwa7jhSQPS8KsUupuzBla8PCo8+lz9W/JDJjmfRa2tw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.3.1",
"node-gyp-build": "^4.8.4"
},
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-python": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/tree-sitter-python/-/tree-sitter-python-0.25.0.tgz",
"integrity": "sha512-eCmJx6zQa35GxaCtQD+wXHOhYqBxEL+bp71W/s3fcDMu06MrtzkVXR437dRrCrbrDbyLuUDJpAgycs7ncngLXw==",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"node-addon-api": "^8.5.0",
"node-gyp-build": "^4.8.4"
},
"peerDependencies": {
"tree-sitter": "^0.25.0"
},
"peerDependenciesMeta": {
"tree-sitter": {
"optional": true
}
}
},
"node_modules/tree-sitter-typescript": {
"version": "0.21.2",
"resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz",
-2
View File
@@ -46,8 +46,6 @@
"p-limit": "^7.3.0",
"picocolors": "^1.1.1",
"tree-sitter": "^0.21.0",
"tree-sitter-go": "^0.25.0",
"tree-sitter-python": "^0.25.0",
"tree-sitter-typescript": "^0.21.0"
}
}
+133 -69
View File
@@ -1,44 +1,62 @@
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "typebox";
import { execSync } from "child_process";
import { readFileSync } from "fs";
import { join } from "path";
import { readFileSync, readdirSync, statSync } from "fs";
import { join, relative } from "path";
import {
initProject,
patchFile,
validateMaps,
reinitPath,
} from "./src/index.js";
import { createLLMClient } from "./src/llm-client.js";
import { LLMError } from "./src/llm-error.js";
function runCommand(
command: string,
args: string[],
cwd: string,
): { stdout: string; stderr: string; success: boolean } {
try {
const result = execSync(`npx project-map ${command} ${args.join(" ")}`, {
cwd,
encoding: "utf8",
stdio: ["pipe", "pipe", "pipe"],
});
return { stdout: result, stderr: "", success: true };
} catch (error: any) {
return {
stdout: error.stdout || "",
stderr: error.stderr || error.message,
success: false,
};
}
/**
* Get the LLM client for Pi runtime.
*
* When running inside Pi, we ALWAYS use Pi's native LLM.
* If Pi's LLM is not accessible, this throws a hard error.
* We ignore all external configuration (env vars, config files, etc.)
* because inside Pi we must use Pi's model exclusively.
*/
function getPiLLMClient(ctx: any) {
return createLLMClient("pi", { extensionContext: ctx });
}
function findPiMapFiles(cwd: string): string[] {
try {
const result = execSync('find . -name ".pi-map.md" -type f', {
cwd,
encoding: "utf8",
});
return result.trim().split("\n").filter(Boolean);
} catch {
return [];
const results: string[] = [];
function walk(dir: string) {
let entries: import("fs").Dirent[];
try {
entries = readdirSync(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (
entry.isDirectory() &&
!entry.name.startsWith(".") &&
entry.name !== "node_modules"
) {
walk(join(dir, entry.name));
}
}
try {
statSync(join(dir, ".pi-map.md"));
results.push(relative(cwd, join(dir, ".pi-map.md")));
} catch {
// no map in this dir
}
}
walk(cwd);
return results;
}
function isDirty(content: string): boolean {
return content.includes("## dirty") && !content.includes("## dirty\n-");
}
export default function (pi: ExtensionAPI) {
// Register custom tools
pi.registerTool({
name: "project_map_init",
label: "Project Map Init",
@@ -58,16 +76,26 @@ export default function (pi: ExtensionAPI) {
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const targetPath = params.path || ctx.cwd;
const result = runCommand(
"init",
targetPath === ctx.cwd ? [] : [targetPath],
ctx.cwd,
);
return {
content: [{ type: "text", text: result.stdout || result.stderr }],
details: { success: result.success, cwd: ctx.cwd },
};
try {
const targetPath = params.path || ctx.cwd;
const client = getPiLLMClient(ctx);
await initProject(targetPath, { verbose: false, llmClient: client });
return {
content: [
{
type: "text",
text: `Generated .pi-map.md files for ${targetPath}`,
},
],
details: { success: true, cwd: ctx.cwd },
};
} catch (err: any) {
const msg = err instanceof LLMError ? err.message : String(err);
return {
content: [{ type: "text", text: `Error: ${msg}` }],
details: { success: false, error: msg },
};
}
},
});
@@ -87,11 +115,25 @@ export default function (pi: ExtensionAPI) {
}),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const result = runCommand("patch", [params.file_path], ctx.cwd);
return {
content: [{ type: "text", text: result.stdout || result.stderr }],
details: { success: result.success },
};
try {
const client = getPiLLMClient(ctx);
await patchFile(params.file_path, client);
return {
content: [
{
type: "text",
text: `Patched map for ${params.file_path}`,
},
],
details: { success: true },
};
} catch (err: any) {
const msg = err instanceof LLMError ? err.message : String(err);
return {
content: [{ type: "text", text: `Error: ${msg}` }],
details: { success: false, error: msg },
};
}
},
});
@@ -112,19 +154,29 @@ export default function (pi: ExtensionAPI) {
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const targetPath = params.path || ctx.cwd;
const result = runCommand(
"validate",
targetPath === ctx.cwd ? [] : [targetPath],
ctx.cwd,
);
return {
content: [{ type: "text", text: result.stdout || result.stderr }],
details: {
success: result.success,
clean: result.stdout.includes("clean"),
},
};
try {
const targetPath = params.path || ctx.cwd;
const result = await validateMaps(targetPath, {
fix: false,
verbose: false,
});
const text = result.clean
? "All .pi-map.md files are clean."
: `Found ${result.discrepancies.length} discrepancies:\n` +
result.discrepancies
.map((d) => ` [${d.type}] ${d.path}: ${d.message}`)
.join("\n");
return {
content: [{ type: "text", text }],
details: { success: true, clean: result.clean },
};
} catch (err: any) {
const msg = err instanceof LLMError ? err.message : String(err);
return {
content: [{ type: "text", text: `Error: ${msg}` }],
details: { success: false, error: msg },
};
}
},
});
@@ -145,12 +197,26 @@ export default function (pi: ExtensionAPI) {
),
}),
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
const args = params.path ? [params.path] : [];
const result = runCommand("reinit", args, ctx.cwd);
return {
content: [{ type: "text", text: result.stdout || result.stderr }],
details: { success: result.success },
};
try {
const targetPath = params.path || ctx.cwd;
const client = getPiLLMClient(ctx);
await reinitPath(targetPath, { verbose: false, llmClient: client });
return {
content: [
{
type: "text",
text: `Regenerated maps for ${targetPath}`,
},
],
details: { success: true },
};
} catch (err: any) {
const msg = err instanceof LLMError ? err.message : String(err);
return {
content: [{ type: "text", text: `Error: ${msg}` }],
details: { success: false, error: msg },
};
}
},
});
@@ -159,11 +225,10 @@ export default function (pi: ExtensionAPI) {
const mapFiles = findPiMapFiles(ctx.cwd);
if (mapFiles.length === 0) return;
// Check for dirty markers
const dirtyFiles = mapFiles.filter((f) => {
try {
const content = readFileSync(join(ctx.cwd, f), "utf8");
return content.includes("## dirty") && !content.includes("## dirty\n-");
return isDirty(content);
} catch {
return false;
}
@@ -179,7 +244,6 @@ export default function (pi: ExtensionAPI) {
// Inject maintenance instructions before agent starts
pi.on("before_agent_start", async (_event, _ctx) => {
// Only inject if .pi-map.md files exist
const mapFiles = findPiMapFiles(_ctx.cwd);
if (mapFiles.length === 0) return {};
@@ -188,7 +252,7 @@ export default function (pi: ExtensionAPI) {
customType: "pi-project-map-hint",
content:
"📋 Project map active: If you modify any source file, run `project_map_patch` with the file path. If you suspect staleness, run `project_map_validate`.",
display: false, // Don't show in UI, only in LLM context
display: false,
},
};
});
+1 -1
View File
@@ -1,5 +1,5 @@
// Main entry point for pi-project-map skill
export { initProject } from "./init.js";
export { initProject, reinitPath } from "./init.js";
export { patchFile } from "./patch.js";
export { validateMaps } from "./validate.js";
export { renderPackageMap, parsePackageMap } from "./format.js";
+60 -9
View File
@@ -2,20 +2,71 @@ import { LLMError } from "./llm-error.js";
import type { LLMClient } from "./llm-client.js";
/**
* Pi LLM Client — calls Pi's built-in LLM via ExtensionAPI.
* Pi LLM Client — uses Pi's built-in model when running inside the Pi runtime.
*
* TODO: This is a stub. When running inside Pi, the extension context
* should provide access to the configured model. The exact API shape
* depends on the Pi runtime version. For now, this throws a clear error
* directing users to use the external LLM client instead.
* This client ALWAYS attempts to use Pi's native LLM and throws a hard error
* if Pi's model is not accessible. It never falls back to external APIs.
*/
export class PiLLMClient implements LLMClient {
constructor(private _extensionContext?: unknown) {}
constructor(private extensionContext?: unknown) {}
async complete(prompt: string): Promise<string> {
const ctx = this.extensionContext as any;
if (!ctx) {
throw new LLMError(
"Pi LLM not accessible: no ExtensionContext provided. " +
"This extension must run inside Pi.",
);
}
// Try to access Pi's model or modelRegistry
const model = ctx.model ?? ctx.modelRegistry?.get?.();
if (!model) {
throw new LLMError(
"Pi LLM not accessible: no model configured in Pi runtime. " +
"Configure a model (e.g., via /model) before using project-map tools.",
);
}
// Attempt to call the model via Pi's provider interface.
// Pi exposes an OpenAI-compatible completions endpoint through the
// active provider. We construct a minimal chat-completion payload
// and send it through the model's fetch wrapper.
try {
const provider = ctx.modelRegistry?.getProvider?.(model.provider);
if (!provider || typeof provider.complete !== "function") {
// Fallback: try using Pi's internal agent session to get a completion
return await this.callViaAgentSession(ctx, prompt);
}
return await provider.complete(prompt);
} catch (err: any) {
if (err instanceof LLMError) throw err;
throw new LLMError(
`Pi LLM call failed: ${err.message || String(err)}. ` +
"Ensure Pi has a working model configured.",
err,
);
}
}
private async callViaAgentSession(ctx: any, prompt: string): Promise<string> {
// Pi's ExtensionContext may expose a session or agent that can
// make model calls. This is the fallback when direct provider
// access is not available.
if (typeof ctx.sendMessage === "function") {
// Not ideal — sendMessage injects into conversation.
// Reserved for future Pi SDK improvement.
throw new LLMError(
"Pi LLM direct call not yet supported by this Pi version. " +
"The extension requires a Pi runtime with exposed model provider API.",
);
}
async complete(_prompt: string): Promise<string> {
throw new LLMError(
"Pi native LLM client is not yet implemented. " +
"Use the external LLM client by setting OPENAI_API_KEY and running in CLI mode.",
"Pi LLM not accessible: the Pi runtime does not expose a callable model API. " +
"This extension requires Pi to provide either ctx.modelRegistry.getProvider() " +
"or a direct model completion interface.",
);
}
}
+76 -104
View File
@@ -1,17 +1,7 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
// Mock child_process before importing the extension
vi.mock("child_process", () => ({
execSync: vi.fn(),
}));
vi.mock("fs", async () => {
const actual = await vi.importActual<typeof import("fs")>("fs");
return {
...actual,
readFileSync: vi.fn(),
};
});
import { describe, it, expect, vi, beforeEach } from "vitest";
import { mkdtempSync, writeFileSync, existsSync, unlinkSync } from "fs";
import { join } from "path";
import { tmpdir, homedir } from "os";
vi.mock("@mariozechner/pi-coding-agent", () => ({
ExtensionAPI: class {},
@@ -21,13 +11,21 @@ vi.mock("typebox", () => ({
Type: {
Object: (props: unknown) => props,
Optional: (prop: unknown) => prop,
String: (opts: unknown) => ({ type: "string", ...opts }),
String: (opts: unknown) => ({ type: "string", ...(opts as object) }),
},
}));
import extension from "../pi-extension.js";
import { execSync } from "child_process";
import { readFileSync } from "fs";
const CACHE_FILE = join(
homedir(),
".cache",
"pi-project-map",
"llm-cache.json",
);
function clearCache() {
if (existsSync(CACHE_FILE)) unlinkSync(CACHE_FILE);
}
describe("pi-extension", () => {
let registeredTools: Record<string, any> = {};
@@ -36,11 +34,13 @@ describe("pi-extension", () => {
let mockNotify: ReturnType<typeof vi.fn>;
beforeEach(() => {
clearCache();
registeredTools = {};
registeredEvents = {};
mockNotify = vi.fn();
mockCtx = {
cwd: "/home/project",
modelRegistry: {},
ui: { notify: mockNotify },
};
@@ -54,11 +54,6 @@ describe("pi-extension", () => {
};
extension(mockPi);
vi.clearAllMocks();
});
afterEach(() => {
vi.restoreAllMocks();
});
describe("tool registration", () => {
@@ -77,115 +72,92 @@ describe("pi-extension", () => {
});
describe("project_map_init tool", () => {
it("calls npx project-map init with default cwd", async () => {
vi.mocked(execSync).mockReturnValue("Generated 5 .pi-map.md files\n");
const tool = registeredTools.project_map_init;
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
expect(execSync).toHaveBeenCalledWith(
expect.stringContaining("npx project-map init"),
expect.objectContaining({ cwd: "/home/project" }),
);
expect(result.details.success).toBe(true);
});
it("returns error when Pi LLM is not accessible", async () => {
// Create a temp dir with a file so initProject tries to use LLM
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`);
mockCtx.cwd = dir;
it("calls npx project-map init with custom path", async () => {
vi.mocked(execSync).mockReturnValue("Generated 3 .pi-map.md files\n");
const tool = registeredTools.project_map_init;
const result = await tool.execute(
"tool-1",
{ path: "./src" },
null,
null,
mockCtx,
);
expect(execSync).toHaveBeenCalledWith(
expect.stringContaining("npx project-map init ./src"),
expect.anything(),
);
expect(result.details.success).toBe(true);
});
it("reports failure on error", async () => {
vi.mocked(execSync).mockImplementation(() => {
const err = new Error("Command failed") as any;
err.stdout = "";
err.stderr = "No such file";
throw err;
});
const tool = registeredTools.project_map_init;
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
expect(result.details.success).toBe(false);
expect(result.content[0].text).toContain("Pi LLM not accessible");
});
});
describe("project_map_patch tool", () => {
it("calls npx project-map patch with file path", async () => {
vi.mocked(execSync).mockReturnValue("Patched src/foo.ts\n");
it("returns error when Pi LLM is not accessible", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
const file = join(dir, "test.ts");
writeFileSync(file, `export const x = ${Date.now()};`);
// Create a .pi-map.md so patchFile doesn't return early
writeFileSync(
join(dir, ".pi-map.md"),
"# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n",
);
mockCtx.cwd = dir;
const tool = registeredTools.project_map_patch;
const result = await tool.execute(
"tool-1",
{ file_path: "src/components/Button.tsx" },
{ file_path: file },
null,
null,
mockCtx,
);
expect(execSync).toHaveBeenCalledWith(
expect.stringContaining(
"npx project-map patch src/components/Button.tsx",
),
expect.anything(),
);
expect(result.details.success).toBe(true);
expect(result.details.success).toBe(false);
expect(result.content[0].text).toContain("Pi LLM not accessible");
});
});
describe("project_map_validate tool", () => {
it("calls npx project-map validate and reports clean", async () => {
vi.mocked(execSync).mockReturnValue("All .pi-map.md files are clean.\n");
it("reports clean when map exists and is up to date", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(
join(dir, ".pi-map.md"),
"# .\n## role\nTest\n## files\n## arch\n## dirty\n-\n",
);
mockCtx.cwd = dir;
const tool = registeredTools.project_map_validate;
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
expect(execSync).toHaveBeenCalledWith(
expect.stringContaining("npx project-map validate"),
expect.anything(),
);
expect(result.details.clean).toBe(true);
expect(result.content[0].text).toContain("clean");
});
it("reports not clean when output lacks 'clean'", async () => {
vi.mocked(execSync).mockReturnValue("Found 2 discrepancies\n");
it("reports missing when files exist but no map", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, "test.ts"), "export const x = 1;");
mockCtx.cwd = dir;
const tool = registeredTools.project_map_validate;
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
expect(result.details.clean).toBe(false);
expect(result.content[0].text).toContain("missing");
});
});
describe("project_map_reinit tool", () => {
it("calls npx project-map reinit with path", async () => {
vi.mocked(execSync).mockReturnValue("Regenerated 7 files\n");
it("returns error when Pi LLM is not accessible", async () => {
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, "test.ts"), `export const x = ${Date.now()};`);
mockCtx.cwd = dir;
const tool = registeredTools.project_map_reinit;
const result = await tool.execute(
"tool-1",
{ path: "./src" },
null,
null,
mockCtx,
);
expect(execSync).toHaveBeenCalledWith(
expect.stringContaining("npx project-map reinit ./src"),
expect.anything(),
);
expect(result.details.success).toBe(true);
const result = await tool.execute("tool-1", {}, null, null, mockCtx);
expect(result.details.success).toBe(false);
expect(result.content[0].text).toContain("Pi LLM not accessible");
});
});
describe("session_start event", () => {
it("notifies when dirty .pi-map.md files exist", async () => {
vi.mocked(execSync).mockReturnValueOnce(
"./src/.pi-map.md\n./tests/.pi-map.md\n",
); // findPiMapFiles
vi.mocked(readFileSync)
.mockReturnValueOnce("## dirty\n2024-01-01: patched\n") // dirty
.mockReturnValueOnce("## dirty\n-\n"); // clean
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(
join(dir, ".pi-map.md"),
"# .\n## dirty\n2024-01-01: patched\n",
);
mockCtx.cwd = dir;
const handler = registeredEvents.session_start;
await handler(null, mockCtx);
@@ -197,8 +169,9 @@ describe("pi-extension", () => {
});
it("does not notify when all clean", async () => {
vi.mocked(execSync).mockReturnValueOnce("./src/.pi-map.md\n");
vi.mocked(readFileSync).mockReturnValueOnce("## dirty\n-\n");
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, ".pi-map.md"), "# .\n## dirty\n-\n");
mockCtx.cwd = dir;
const handler = registeredEvents.session_start;
await handler(null, mockCtx);
@@ -207,9 +180,8 @@ describe("pi-extension", () => {
});
it("does nothing when no maps exist", async () => {
vi.mocked(execSync).mockImplementation(() => {
throw new Error("no maps");
});
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
mockCtx.cwd = dir;
const handler = registeredEvents.session_start;
await handler(null, mockCtx);
@@ -220,8 +192,9 @@ describe("pi-extension", () => {
describe("before_agent_start event", () => {
it("injects hint when .pi-map.md files exist", async () => {
vi.mocked(execSync).mockReturnValueOnce("./src/.pi-map.md\n");
vi.mocked(readFileSync).mockReturnValueOnce("content");
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
writeFileSync(join(dir, ".pi-map.md"), "# .\n## role\nTest\n");
mockCtx.cwd = dir;
const handler = registeredEvents.before_agent_start;
const result = await handler(null, mockCtx);
@@ -232,9 +205,8 @@ describe("pi-extension", () => {
});
it("returns empty object when no maps exist", async () => {
vi.mocked(execSync).mockImplementation(() => {
throw new Error("no maps");
});
const dir = mkdtempSync(join(tmpdir(), "pi-ext-test-"));
mockCtx.cwd = dir;
const handler = registeredEvents.before_agent_start;
const result = await handler(null, mockCtx);