Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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

Expand Down Expand Up @@ -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 |
Expand Down
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
20 changes: 20 additions & 0 deletions src/commands/configureMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,32 @@ export async function configureMcp(): Promise<void> {
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");
Expand Down
30 changes: 28 additions & 2 deletions src/commands/initializeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,31 @@ export async function initializeProject(): Promise<void> {
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)}`);
Expand Down Expand Up @@ -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") {
Expand 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;
}

Expand Down
31 changes: 26 additions & 5 deletions src/mcp/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
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<McpTargetStatus[]> {
Expand Down Expand Up @@ -56,11 +64,12 @@ export async function configureMcpTargets(inputs: McpApplyInputs): Promise<McpTa
const targets = resolveMcpTargets(inputs.workspaceFolderPath, inputs.homeDir, inputs.includeUserTarget)
.filter((target) => !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;
Expand Down Expand Up @@ -113,15 +122,27 @@ export function resolveMcpTargets(
return targets;
}

export function buildPatchloomMcpEntry(commandPath: string): Record<string, unknown> {
return {
export function buildPatchloomMcpEntry(
commandPath: string,
mcpSurface: McpSurface = "full"
): Record<string, unknown> {
const entry: Record<string, unknown> = {
command: commandPath,
args: ["mcp-server"]
};
if (mcpSurface === "core") {
entry.env = { PATCHLOOM_MCP_SURFACE: "core" };
}
return entry;
}

function withPatchloomEntry(kind: McpTargetKind, config: Record<string, unknown>, commandPath: string): Record<string, unknown> {
const entry = buildPatchloomMcpEntry(commandPath);
function withPatchloomEntry(
kind: McpTargetKind,
config: Record<string, unknown>,
commandPath: string,
mcpSurface: McpSurface = "full"
): Record<string, unknown> {
const entry = buildPatchloomMcpEntry(commandPath, mcpSurface);
if (kind === "windsurf-user") {
const servers = objectValue(config.mcpServers);
return {
Expand Down
26 changes: 26 additions & 0 deletions test/unit/initializeProject.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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", () => {
Expand All @@ -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 }[] = [];
Expand Down
36 changes: 36 additions & 0 deletions test/unit/mcpConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -23,6 +24,19 @@ async function readJson(filePath: string): Promise<Record<string, unknown>> {
return JSON.parse(content) as Record<string, unknown>;
}

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({
Expand All @@ -46,6 +60,28 @@ test("configureMcpTargets writes VS Code mcp.json to a real temp workspace", asy
const entry = servers.patchloom as Record<string, unknown>;
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<string, unknown>;
const entry = servers.patchloom as Record<string, unknown>;
assert.deepEqual(entry.env, { PATCHLOOM_MCP_SURFACE: "core" });
});
});

Expand Down
3 changes: 2 additions & 1 deletion walkthrough/configure-mcp.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
3 changes: 2 additions & 1 deletion walkthrough/initialize.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading