diff --git a/AGENTS.md b/AGENTS.md index 726389f..2839f73 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,9 +51,9 @@ test/ batchApply.test.ts Batch template and operation count parsing (15 tests) binary.test.ts Binary discovery, managed install, compatibility, workspace env (59 tests) binaryDiscovery.test.ts Real executable discovery on PATH (13 tests) - initializeProject.test.ts Status display, agents file classification, formatError (32 tests) + initializeProject.test.ts Status display, agents file classification, formatError (34 tests) managedLifecycle.test.ts Managed install with real file I/O (22 tests) - mcpConfig.test.ts MCP config with real temp directories (9 tests) + mcpConfig.test.ts MCP config with real temp directories (12 tests) outputChannel.test.ts Output channel logging wrapper (10 tests) patchloomCli.test.ts Patchloom CLI integration with real binary + managed install e2e MCP (40 tests incl. e2e) propertyBased.test.ts Property-based tests with fast-check (13 tests) diff --git a/README.md b/README.md index 2aae815..f511343 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ Run `Patchloom: Setup Workspace` to walk through everything your project needs: ### Agent rules generation -`Patchloom: Initialize Project` generates an `AGENTS.md` file from `patchloom agent-rules`. If one already exists, the extension opens a diff so you can merge updates manually. +`Patchloom: Initialize Project` generates an `AGENTS.md` file from `patchloom agent-rules`. You pick integration mode (CLI/MCP/all), shell platform, and surface (`full` document or `core` pack for system-prompt injection, CLI 0.24+). If `AGENTS.md` already exists, the extension opens a diff so you can merge updates manually. ### MCP server configuration @@ -64,7 +64,9 @@ Run `Patchloom: Setup Workspace` to walk through everything your project needs: - **Cursor** (`.cursor/mcp.json`) - **Windsurf** (`~/.codeium/windsurf/mcp_config.json`) -CLI 0.24.0 exposes **58** MCP tools by default (including `list_files` and `apply_fragment`). For coding agents that need a smaller handshake inventory, set `PATCHLOOM_MCP_SURFACE=core` in the server environment (11 tools: `read_file`, `search_files`, `list_files`, `replace_text`, `batch_replace`, `doc_get`, `doc_set`, `doc_query`, `md_replace_section`, `execute_plan`, `server_info`). Absolute paths that resolve inside the MCP workspace root are allowed; `../` and outside paths still reject. +When configuring, pick **Full tool inventory** (default) or **Core pack**. Core sets `PATCHLOOM_MCP_SURFACE=core` on the server entry. + +CLI 0.24.0 exposes **58** MCP tools by default (including `list_files` and `apply_fragment`). The core pack is 11 tools: `read_file`, `search_files`, `list_files`, `replace_text`, `batch_replace`, `doc_get`, `doc_set`, `doc_query`, `md_replace_section`, `execute_plan`, `server_info`. Absolute paths that resolve inside the MCP workspace root are allowed; `../` and outside paths still reject. ### Status bar @@ -133,8 +135,8 @@ The extension detects outdated CLI builds and warns with upgrade guidance. It re | Command | Description | |---------|-------------| | `Patchloom: Setup Workspace` | Guided walkthrough for binary, AGENTS.md, and MCP readiness | -| `Patchloom: Initialize Project` | Generate or diff `AGENTS.md` from `patchloom agent-rules` (mode: all/cli/mcp, platform: all/linux/windows) | -| `Patchloom: Configure MCP` | Inject Patchloom MCP server config into editor config files | +| `Patchloom: Initialize Project` | Generate or diff `AGENTS.md` from `patchloom agent-rules` (mode, platform, surface full/core) | +| `Patchloom: Configure MCP` | Inject Patchloom MCP server config (full or core tool surface) into editor config files | | `Patchloom: Quick Action` | Build a Patchloom CLI command from an interactive picker | | `Patchloom: Batch Apply` | Open a batch plan and execute all operations atomically | | `Patchloom: Show Output` | Open the Patchloom output channel for CLI logs and diagnostics | diff --git a/package-lock.json b/package-lock.json index f5e50fe..821fc1f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "license": "MIT", "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "@types/vscode": "^1.90.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", @@ -1014,9 +1014,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", + "version": "26.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz", + "integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index e686394..a2f4c90 100644 --- a/package.json +++ b/package.json @@ -261,7 +261,7 @@ }, "devDependencies": { "@types/mocha": "^10.0.10", - "@types/node": "^26.1.1", + "@types/node": "^26.1.2", "@types/vscode": "^1.90.0", "@vscode/test-electron": "^3.1.0", "@vscode/vsce": "^3.0.0", diff --git a/src/commands/configureMcp.ts b/src/commands/configureMcp.ts index 6c8bfb3..0156eee 100644 --- a/src/commands/configureMcp.ts +++ b/src/commands/configureMcp.ts @@ -43,12 +43,32 @@ export async function configureMcp(): Promise { return; } + const surfacePick = await vscode.window.showQuickPick( + [ + { + label: "Full tool inventory", + description: "Default (58 tools on CLI 0.24+)", + surface: "full" as const + }, + { + label: "Core pack", + description: "Sets PATCHLOOM_MCP_SURFACE=core (11 tools; CLI 0.22+)", + surface: "core" as const + } + ], + { placeHolder: "Which MCP tool surface should the server expose?" } + ); + if (!surfacePick) { + return; + } + const selectedKinds = selections.map((selection) => selection.target.kind); const results = await configureMcpTargets({ workspaceFolderPath, includeKinds: selectedKinds, includeUserTarget: environment.supportsUserMcpConfig, patchloomPathSetting: binaryPath, + mcpSurface: surfacePick.surface, readFile: async (filePath) => { try { return await fs.readFile(filePath, "utf8"); diff --git a/src/commands/initializeProject.ts b/src/commands/initializeProject.ts index 70a91a5..c40129a 100644 --- a/src/commands/initializeProject.ts +++ b/src/commands/initializeProject.ts @@ -53,11 +53,31 @@ export async function initializeProject(): Promise { return; } + const surfacePick = await vscode.window.showQuickPick( + [ + { + label: "Full document", + description: "Default agent-rules (full tool inventory)", + surface: "full" as const + }, + { + label: "Core pack", + description: "Short rules for system-prompt injection (CLI 0.24+ --surface core)", + surface: "core" as const + } + ], + { placeHolder: "Which agent-rules surface?" } + ); + if (!surfacePick) { + return; + } + let rules: string; try { rules = await generateAgentRules(binaryPath, folder.uri.fsPath, { mode: modePick.mode, - platform: platformPick.platform + platform: platformPick.platform, + surface: surfacePick.surface }); } catch (error) { await vscode.window.showErrorMessage(`Failed to run patchloom agent-rules in ${folder.name}: ${formatError(error)}`); @@ -116,13 +136,16 @@ export function classifyAgentsFile(existingContent: string | undefined, generate export type AgentRulesMode = "all" | "cli" | "mcp"; export type AgentRulesPlatform = "all" | "linux" | "windows"; +/** CLI 0.24+ agent-rules surface (full document vs short core pack). */ +export type AgentRulesSurface = "full" | "core"; export interface AgentRulesOptions { readonly mode?: AgentRulesMode; readonly platform?: AgentRulesPlatform; + readonly surface?: AgentRulesSurface; } -/** Build `patchloom agent-rules` argv, omitting default `all` flags. */ +/** Build `patchloom agent-rules` argv, omitting default `all` / `full` flags. */ export function buildAgentRulesArgs(options: AgentRulesOptions = {}): string[] { const args = ["agent-rules"]; if (options.mode && options.mode !== "all") { @@ -131,6 +154,9 @@ export function buildAgentRulesArgs(options: AgentRulesOptions = {}): string[] { if (options.platform && options.platform !== "all") { args.push("--platform", options.platform); } + if (options.surface && options.surface !== "full") { + args.push("--surface", options.surface); + } return args; } diff --git a/src/mcp/config.ts b/src/mcp/config.ts index 5ab42db..906ab4b 100644 --- a/src/mcp/config.ts +++ b/src/mcp/config.ts @@ -25,10 +25,18 @@ export interface McpInspectionInputs { readonly includeUserTarget?: boolean; } +export type McpSurface = "full" | "core"; + export interface McpApplyInputs extends McpInspectionInputs { readonly writeFile: (filePath: string, content: string) => Promise; readonly patchloomPathSetting?: string; readonly includeKinds?: readonly McpTargetKind[]; + /** + * MCP tool inventory for coding agents (CLI 0.22+ / 0.24+). + * `core` sets `PATCHLOOM_MCP_SURFACE=core` on the server entry (11 tools). + * Default `full` omits the env var so the CLI uses its full inventory. + */ + readonly mcpSurface?: McpSurface; } export async function inspectMcpTargets(inputs: McpInspectionInputs): Promise { @@ -56,11 +64,12 @@ export async function configureMcpTargets(inputs: McpApplyInputs): Promise !includeKinds || includeKinds.has(target.kind)); const results: McpTargetResult[] = []; + const mcpSurface = inputs.mcpSurface ?? "full"; for (const target of targets) { const content = await readFile(target.filePath); const original = parseJsonObject(content); - const updated = withPatchloomEntry(target.kind, original, patchloomCommand); + const updated = withPatchloomEntry(target.kind, original, patchloomCommand, mcpSurface); const serialized = `${JSON.stringify(updated, null, 2)}\n`; const previousSerialized = content === undefined ? undefined : `${JSON.stringify(original, null, 2)}\n`; const changed = previousSerialized !== serialized; @@ -113,15 +122,27 @@ export function resolveMcpTargets( return targets; } -export function buildPatchloomMcpEntry(commandPath: string): Record { - return { +export function buildPatchloomMcpEntry( + commandPath: string, + mcpSurface: McpSurface = "full" +): Record { + const entry: Record = { command: commandPath, args: ["mcp-server"] }; + if (mcpSurface === "core") { + entry.env = { PATCHLOOM_MCP_SURFACE: "core" }; + } + return entry; } -function withPatchloomEntry(kind: McpTargetKind, config: Record, commandPath: string): Record { - const entry = buildPatchloomMcpEntry(commandPath); +function withPatchloomEntry( + kind: McpTargetKind, + config: Record, + commandPath: string, + mcpSurface: McpSurface = "full" +): Record { + const entry = buildPatchloomMcpEntry(commandPath, mcpSurface); if (kind === "windsurf-user") { const servers = objectValue(config.mcpServers); return { diff --git a/test/unit/initializeProject.test.ts b/test/unit/initializeProject.test.ts index 4c4f633..f8ab0ae 100644 --- a/test/unit/initializeProject.test.ts +++ b/test/unit/initializeProject.test.ts @@ -111,6 +111,19 @@ test("formatCliOutput surfaces binary kind (CLI 0.20+)", () => { ); }); +test("formatCliOutput surfaces invalid_encoding kind (CLI 0.20+)", () => { + const stdout = JSON.stringify({ + ok: false, + error: "target is not valid UTF-8 text: notes.txt", + error_kind: "invalid_encoding", + applied: false + }); + assert.equal( + formatCliOutput({ exitCode: 1, stdout, stderr: "" }), + "invalid_encoding: target is not valid UTF-8 text: notes.txt" + ); +}); + test("formatCliOutput surfaces fuzzy_span_suspicious kind (CLI 0.22+)", () => { const stdout = JSON.stringify({ ok: false, @@ -459,6 +472,7 @@ test("configureMcpTargets creates or updates only the selected target kinds", as test("buildAgentRulesArgs omits default all modes", () => { assert.deepEqual(buildAgentRulesArgs(), ["agent-rules"]); assert.deepEqual(buildAgentRulesArgs({ mode: "all", platform: "all" }), ["agent-rules"]); + assert.deepEqual(buildAgentRulesArgs({ surface: "full" }), ["agent-rules"]); }); test("buildAgentRulesArgs includes non-default mode and platform", () => { @@ -477,6 +491,18 @@ test("buildAgentRulesArgs includes non-default mode and platform", () => { ]); }); +test("buildAgentRulesArgs includes --surface core (CLI 0.24+)", () => { + assert.deepEqual(buildAgentRulesArgs({ surface: "core" }), [ + "agent-rules", + "--surface", + "core" + ]); + assert.deepEqual( + buildAgentRulesArgs({ mode: "mcp", platform: "linux", surface: "core" }), + ["agent-rules", "--mode", "mcp", "--platform", "linux", "--surface", "core"] + ); +}); + test("generateAgentRules logs error to output channel on CLI failure", async () => { const logged: { exitCode: number; stdout: string; stderr: string }[] = []; const commands: { binary: string; args: readonly string[]; cwd: string }[] = []; diff --git a/test/unit/mcpConfig.test.ts b/test/unit/mcpConfig.test.ts index 76498a5..3b4adfb 100644 --- a/test/unit/mcpConfig.test.ts +++ b/test/unit/mcpConfig.test.ts @@ -4,6 +4,7 @@ import * as os from "node:os"; import * as path from "node:path"; import test from "node:test"; import { + buildPatchloomMcpEntry, configureMcpTargets, inspectMcpTargets, resolveMcpTargets @@ -23,6 +24,19 @@ async function readJson(filePath: string): Promise> { return JSON.parse(content) as Record; } +test("buildPatchloomMcpEntry omits env for full surface", () => { + const entry = buildPatchloomMcpEntry("/usr/bin/patchloom"); + assert.equal(entry.command, "/usr/bin/patchloom"); + assert.deepEqual(entry.args, ["mcp-server"]); + assert.equal(entry.env, undefined); +}); + +test("buildPatchloomMcpEntry sets PATCHLOOM_MCP_SURFACE for core pack", () => { + const entry = buildPatchloomMcpEntry("patchloom", "core"); + assert.deepEqual(entry.args, ["mcp-server"]); + assert.deepEqual(entry.env, { PATCHLOOM_MCP_SURFACE: "core" }); +}); + test("configureMcpTargets writes VS Code mcp.json to a real temp workspace", async () => { await withTempDir(async (workspace) => { const results = await configureMcpTargets({ @@ -46,6 +60,28 @@ test("configureMcpTargets writes VS Code mcp.json to a real temp workspace", asy const entry = servers.patchloom as Record; assert.equal(entry.command, "/usr/local/bin/patchloom"); assert.deepEqual(entry.args, ["mcp-server"]); + assert.equal(entry.env, undefined, "full surface should not inject env"); + }); +}); + +test("configureMcpTargets writes core surface env when requested", async () => { + await withTempDir(async (workspace) => { + await configureMcpTargets({ + workspaceFolderPath: workspace, + homeDir: workspace, + includeKinds: ["vscode-workspace"], + patchloomPathSetting: "patchloom", + mcpSurface: "core", + writeFile: async (filePath, content) => { + await fs.mkdir(path.dirname(filePath), { recursive: true }); + await fs.writeFile(filePath, content, "utf8"); + } + }); + + const written = await readJson(path.join(workspace, ".vscode", "mcp.json")); + const servers = written.servers as Record; + const entry = servers.patchloom as Record; + assert.deepEqual(entry.env, { PATCHLOOM_MCP_SURFACE: "core" }); }); }); diff --git a/walkthrough/configure-mcp.md b/walkthrough/configure-mcp.md index 1a4ad7c..90550af 100644 --- a/walkthrough/configure-mcp.md +++ b/walkthrough/configure-mcp.md @@ -4,7 +4,8 @@ The Model Context Protocol (MCP) lets AI agents call Patchloom operations directly: search, replace, tidy, and more. Click **Configure MCP** above to set up the MCP server configuration -for your editor. +for your editor. Choose the **full** tool inventory or the **core** pack +(sets `PATCHLOOM_MCP_SURFACE=core` for a smaller 11-tool handshake). ## Supported Editors diff --git a/walkthrough/initialize.md b/walkthrough/initialize.md index fd38449..c1d952c 100644 --- a/walkthrough/initialize.md +++ b/walkthrough/initialize.md @@ -5,7 +5,8 @@ your codebase. Click **Initialize Project** above to run `patchloom agent-rules` in your workspace. You can choose integration mode (CLI + MCP, CLI only, or -MCP only) and shell platform examples (all, Linux/macOS, or Windows). +MCP only), shell platform examples (all, Linux/macOS, or Windows), and +surface (full document or core pack for system-prompt injection). ## What AGENTS.md Contains