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,13 +51,13 @@ test/
batchApply.test.ts Batch template and operation count parsing (16 tests)
binary.test.ts Binary discovery, managed install, compatibility, workspace env (66 tests)
binaryDiscovery.test.ts Real executable discovery on PATH (13 tests)
initializeProject.test.ts Status display, agents file classification, formatError (39 tests)
initializeProject.test.ts Status display, agents file classification, formatError (41 tests)
managedLifecycle.test.ts Managed install with real file I/O (22 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)
quickActions.test.ts Quick action command building, path containment, patch merge (60 tests)
quickActions.test.ts Quick action command building, path containment, patch merge (62 tests)
verifyMcp.test.ts MCP server verify and JSON-RPC response parsing (15 tests)
downloadIntegration.test.ts HTTP download, redirect, streaming SHA-256 (9 tests)
suite/
Expand Down
24 changes: 20 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,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`. 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.
`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+). On CLI 0.29+, `--surface core` honors `--mode` the same way the full surface does. If `AGENTS.md` already exists, the extension opens a diff so you can merge updates manually.

### MCP server configuration

Expand All @@ -65,7 +65,7 @@ Run `Patchloom: Setup Workspace` to walk through everything your project needs:

When configuring, pick **Full tool inventory** (default) or **Core pack**. Core sets `PATCHLOOM_MCP_SURFACE=core` on the server entry.

CLI **0.28.0** (and 0.24+) 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; empty paths, `../`, and outside paths still reject with stable `error_kind` peels.
CLI **0.31.0** (and 0.24+) 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`. `search_files` accepts `files_without_match` (CLI 0.29+). `apply_patch` accepts unified diffs, Codex `*** Begin Patch`, and Aider SEARCH/REPLACE (CLI 0.30+). Absolute paths that resolve inside the MCP workspace root are allowed; empty paths, `../`, and outside paths still reject with stable `error_kind` peels.

### Status bar

Expand Down Expand Up @@ -94,6 +94,7 @@ Click it to see full diagnostics, including per-editor MCP configuration status
| **Tidy file** | Whitespace and newline cleanup with diff preview |
| **Set structured value** | Update a JSON, YAML, or TOML key with diff preview |
| **Search text** | Find pattern matches across workspace files (results in output channel) |
| **Search files without match** | List files that do not contain the pattern (`search -L`, CLI 0.29+) |
| **Create file** | Scaffold a new file with optional content and open it in the editor |
| **Append to file** | Append content to an existing file |
| **Prepend to file** | Prepend content to the start of an existing file (CLI 0.9+) |
Expand Down Expand Up @@ -175,7 +176,7 @@ The extension detects outdated CLI builds and warns with upgrade guidance. It re
Set `patchloom.path` in settings, or add the CLI to your `PATH`.

**CLI compatibility warning / upgrade path**
The extension requires Patchloom **0.3.0** or newer; **0.28.0** is recommended. Which fix to use depends on how the CLI was resolved (status shows Source):
The extension requires Patchloom **0.3.0** or newer; **0.31.0** is recommended. Which fix to use depends on how the CLI was resolved (status shows Source):

1. **Source: managed install** → **Patchloom: Update Patchloom** (checksum-verified GitHub release into extension storage)
2. **Source: PATH** → upgrade that install in place (**Scoop** `scoop update patchloom` on Windows; Homebrew / npm / cargo / the official installer elsewhere). Managed Install will not override a PATH binary.
Expand All @@ -189,6 +190,21 @@ Quick Actions and Batch Apply pass `--contain` so paths stay inside the open wor
**Empty or blank path**
On CLI 0.28+, empty, whitespace-only, or format-character-only paths fail early with `error_kind: invalid_input` and message `path must not be empty` (they no longer look like workspace-root failures). Prefer a real relative or workspace-absolute path.

**Create or rename parent is not a directory**
On CLI 0.31+, create and rename check each parent of the destination before staging. A parent that is a regular file (or a dangling / file symlink) reports `error_kind: invalid_input` and `parent path is not a directory`. Create the missing directory first, or pick a destination whose parents are directories.

**Numeric or negated doc selector**
On CLI 0.30+, selectors accept `!=`, `>`, `>=`, `<`, `<=`, and `[!key]` (for example `servers[port>8000]`). A non-numeric compare reports `error_kind: invalid_input`. Use a number on the right-hand side, or a concrete index path.

**Search files without match**
On CLI 0.29+, `search -L` / `--files-without-match` lists files that do not contain the pattern. Combining it with `--files-with-matches` or `--count` is `invalid_input`. When every scanned file contains the pattern, the CLI reports `error_kind: no_matches` and the text `no files without matches for 'PATTERN' in SCOPE` (that is not a content miss).

**YAML mapping alias stayed an alias**
On CLI 0.31+, `doc set` on a mapping that is only `service_a: *shared` writes a merge key plus local fields (`<<: *shared` and your new keys) instead of inlining the whole object. Sequence items (`- *shared`) still expand or stay not-applied.

**Patch apply formats**
On CLI 0.30+, `patch apply` (and MCP `apply_patch`) accepts unified diffs, Codex `*** Begin Patch`, and Aider SEARCH/REPLACE. Update and SEARCH matches must be unique unless you pass `--replace-all` (SEARCH/REPLACE only). The Quick Action **Merge patch (three-way)** is still `patch merge` for stale unified diffs.

**Batch replace shape**
Batch lines use `replace PATH OLD NEW` (and optional flags such as `--fuzzy`). Do not paste CLI form `replace OLD --new NEW path` into a batch plan; CLI 0.18+ returns a clear parse error with the PATH OLD NEW hint.

Expand Down Expand Up @@ -240,7 +256,7 @@ File bugs and feature requests at [patchloom/patchloom-vscode/issues](https://gi
## Requirements

- VS Code 1.90 or newer (or compatible editors: Cursor, Windsurf, VSCodium)
- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (**0.28.0+ recommended** for empty-path fail-closed (`path must not be empty`), `suggested_op` on fail-closed doc navigation, `not_regular_file` soft peels, ambiguous markdown headings, `list_files` MCP inventory, `apply-fragment`, full `error_kind` peels (`binary` / `invalid_encoding` / `fuzzy_span_suspicious` / `already_exists` / `guard_rejected` / `ambiguous`), optional `PATCHLOOM_MCP_SURFACE=core` 11-tool pack, multi-doc `doc merge --selector`, line-oriented inserts, batch `replace PATH OLD NEW` hints, 58 MCP tools, and agent-facing JSON envelopes)
- [Patchloom CLI](https://github.com/patchloom/patchloom) 0.3.0 or newer (**0.31.0+ recommended** for YAML alias-to-merge on `doc set`, create/rename `parent path is not a directory`, numeric selector compares (`servers[port>8000]`), `search -L` / `files_without_match`, Codex Begin Patch and Aider SEARCH/REPLACE on `patch apply`, `agent-rules --surface core` honoring `--mode`, empty-path fail-closed (`path must not be empty`), `suggested_op` on fail-closed doc navigation, `not_regular_file` soft peels, ambiguous markdown headings, `list_files` MCP inventory, `apply-fragment`, full `error_kind` peels (`binary` / `invalid_encoding` / `fuzzy_span_suspicious` / `already_exists` / `guard_rejected` / `ambiguous`), optional `PATCHLOOM_MCP_SURFACE=core` 11-tool pack, multi-doc `doc merge --selector`, line-oriented inserts, batch `replace PATH OLD NEW` hints, 58 MCP tools, and agent-facing JSON envelopes)

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion src/commands/configureMcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export async function configureMcp(): Promise<void> {
[
{
label: "Full tool inventory",
description: "Default (58 tools on CLI 0.24+; verified on 0.28)",
description: "Default (58 tools on CLI 0.24+; verified on 0.31)",
surface: "full" as const
},
{
Expand Down
2 changes: 1 addition & 1 deletion src/commands/initializeProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export async function initializeProject(): Promise<void> {
},
{
label: "Core pack",
description: "Short rules for system-prompt injection (CLI 0.24+ --surface core)",
description: "Short rules for system-prompt injection (CLI 0.24+ --surface core; 0.29+ honors --mode)",
surface: "core" as const
}
],
Expand Down
71 changes: 65 additions & 6 deletions src/commands/quickActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,8 +259,8 @@ export async function runQuickAction(): Promise<void> {
}

const selector = await vscode.window.showInputBox({
prompt: "Selector path",
placeHolder: "scripts.test",
prompt: "Selector path (CLI 0.30+ accepts numeric compares such as servers[port>8000])",
placeHolder: "scripts.test or servers[port>8000]",
validateInput: (value) => value.length > 0 ? undefined : "Selector is required."
});
if (selector === undefined) {
Expand Down Expand Up @@ -325,6 +325,55 @@ export async function runQuickAction(): Promise<void> {
}
}
},
{
label: "Search files without match",
description: "List files that do not contain the pattern (CLI 0.29+ -L)",
detail: "Builds `patchloom search <pattern> --files-without-match [--glob <glob>] <workspace>`",
run: async () => {
const folder = await activeWorkspaceFolder({
promptIfMany: true,
placeHolder: "Select workspace folder for Patchloom search -L"
});
if (!folder) {
await vscode.window.showWarningMessage("Open a workspace folder before running Patchloom search.");
return;
}

const pattern = await vscode.window.showInputBox({
prompt: "Pattern that listed files must not contain",
placeHolder: "TODO|FIXME",
validateInput: (value) => value.length > 0 ? undefined : "Pattern is required."
});
if (pattern === undefined) {
return;
}

const glob = await vscode.window.showInputBox({
prompt: "File glob (optional, leave empty for all files)",
placeHolder: "*.ts"
});
if (glob === undefined) {
return;
}

const action = buildSearchQuickAction(folder.uri.fsPath, pattern, glob || undefined, {
filesWithoutMatch: true
});
const result = await executePatchloom(binaryPath, action.args, folder.uri.fsPath);
const log = getPatchloomLog();

if (result.exitCode === 3) {
await vscode.window.showInformationMessage(`Every scanned file contains "${pattern}".`);
} else if (result.exitCode !== 0) {
await vscode.window.showErrorMessage(`Patchloom search failed: ${formatCliOutput(result)}`);
} else {
log?.show();
await vscode.window.showInformationMessage(
"Files without matches are listed in the Patchloom output channel."
);
}
}
},
{
label: "Create a new file",
description: "Scaffold a new file in the workspace",
Expand Down Expand Up @@ -440,8 +489,8 @@ export async function runQuickAction(): Promise<void> {
}

const selector = await vscode.window.showInputBox({
prompt: "Selector path",
placeHolder: "scripts.test",
prompt: "Selector path (CLI 0.30+ accepts numeric compares such as servers[port>8000])",
placeHolder: "scripts.test or servers[port>8000]",
validateInput: (value) => value.length > 0 ? undefined : "Selector is required."
});
if (selector === undefined) {
Expand Down Expand Up @@ -1097,16 +1146,26 @@ export function buildDocSetQuickAction(targetPath: string, selector: string, val
};
}

export function buildSearchQuickAction(workspacePath: string, pattern: string, glob?: string): PlannedQuickAction {
export function buildSearchQuickAction(
workspacePath: string,
pattern: string,
glob?: string,
options?: { readonly filesWithoutMatch?: boolean }
): PlannedQuickAction {
const args: string[] = ["search", pattern];
if (options?.filesWithoutMatch) {
args.push("--files-without-match");
}
if (glob) {
args.push("--glob", glob);
}
args.push(workspacePath);
const targetIndex = args.length - 1;

return {
title: `Search for "${pattern}"`,
title: options?.filesWithoutMatch
? `Search files without "${pattern}"`
: `Search for "${pattern}"`,
targetPath: workspacePath,
targetArgIndices: [targetIndex],
args
Expand Down
4 changes: 2 additions & 2 deletions test/unit/binary.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,8 @@ test("preferredBinaryRemediationAction is undefined when ready and supported", (
source: "path",
message: "Using Patchloom from PATH.",
binaryPath: "/usr/local/bin/patchloom",
version: "patchloom 0.28.0",
detectedVersion: "0.28.0",
version: "patchloom 0.31.0",
detectedVersion: "0.31.0",
compatibility: "supported",
minimumSupportedVersion: MINIMUM_SUPPORTED_PATCHLOOM_VERSION
});
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 @@ -150,6 +150,32 @@ test("formatCliOutput surfaces empty path invalid_input (CLI 0.28+)", () => {
);
});

test("formatCliOutput surfaces parent path invalid_input (CLI 0.31+)", () => {
const stdout = JSON.stringify({
ok: false,
error: "parent path is not a directory: /tmp/notdir",
error_kind: "invalid_input",
applied: false
});
assert.equal(
formatCliOutput({ exitCode: 1, stdout, stderr: "" }),
"invalid_input: parent path is not a directory: /tmp/notdir"
);
});

test("formatCliOutput surfaces numeric selector invalid_input (CLI 0.30+)", () => {
const stdout = JSON.stringify({
ok: false,
error: "selector error: comparison operand must be numeric (got 'abc' after >)",
error_kind: "invalid_input",
applied: false
});
assert.equal(
formatCliOutput({ exitCode: 1, stdout, stderr: "" }),
"invalid_input: selector error: comparison operand must be numeric (got 'abc' after >)"
);
});

test("formatCliOutput appends suggested_op when present (CLI 0.27+)", () => {
const stdout = JSON.stringify({
ok: false,
Expand Down
51 changes: 51 additions & 0 deletions test/unit/patchloomCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
buildApplyFragmentQuickAction,
buildInsertAfterMatchQuickAction,
buildReplaceQuickAction,
buildSearchQuickAction,
retargetQuickAction,
withApplyFlag
} from "../../src/commands/quickActions.js";
Expand Down Expand Up @@ -581,6 +582,56 @@ describe("patchloom CLI integration", async () => {
});
});

test("search --files-without-match via Quick Action args (CLI 0.29+)", async (t) => {
const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 5000 });
const version = parsePatchloomVersion(`${stdout}${stderr}`);
if (!version || comparePatchloomVersions(version, "0.29.0") < 0) {
t.skip(`requires patchloom >= 0.29.0 (found ${version ?? "unknown"})`);
return;
}

await withTempDir(async (dir) => {
await fs.writeFile(path.join(dir, "hit.txt"), "TODO\n", "utf8");
await fs.writeFile(path.join(dir, "miss.txt"), "ok\n", "utf8");

const action = buildSearchQuickAction(dir, "TODO", undefined, { filesWithoutMatch: true });
const result = await execFileAsync(binaryPath, ["--json", ...action.args], { timeout: 5000 });
const parsed = JSON.parse(result.stdout) as {
files?: Array<{ path?: string }>;
};
const files = (parsed.files ?? []).map((entry) => path.basename(entry.path ?? ""));
assert.ok(files.includes("miss.txt"), "files without the pattern should be listed");
assert.ok(!files.includes("hit.txt"), "files that contain the pattern should be omitted");
});
});

test("create through a file parent is invalid_input (CLI 0.31+)", async (t) => {
const { stdout, stderr } = await execFileAsync(binaryPath, ["--version"], { timeout: 5000 });
const version = parsePatchloomVersion(`${stdout}${stderr}`);
if (!version || comparePatchloomVersions(version, "0.31.0") < 0) {
t.skip(`requires patchloom >= 0.31.0 (found ${version ?? "unknown"})`);
return;
}

await withTempDir(async (dir) => {
const parent = path.join(dir, "notdir");
await fs.writeFile(parent, "file\n", "utf8");
const dest = path.join(parent, "child.txt");

try {
await execFileAsync(binaryPath, ["--json", "create", dest, "--content", "x", "--apply"], {
timeout: 5000
});
assert.fail("create through a file parent should fail");
} catch (error) {
const failed = error as { stdout?: string; stderr?: string };
const payload = `${failed.stdout ?? ""}${failed.stderr ?? ""}`;
assert.match(payload, /parent path is not a directory/);
assert.match(payload, /invalid_input/);
}
});
});

test("doc append adds an item to a JSON array", async () => {
await withTempDir(async (dir) => {
const file = path.join(dir, "config.json");
Expand Down
22 changes: 22 additions & 0 deletions test/unit/quickActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,28 @@ test("buildSearchQuickAction includes glob when provided", () => {
assert.deepEqual(action.targetArgIndices, [4]);
});

test("buildSearchQuickAction includes --files-without-match (CLI 0.29+)", () => {
const action = buildSearchQuickAction("/workspace/demo", "TODO", undefined, {
filesWithoutMatch: true
});

assert.equal(action.title, 'Search files without "TODO"');
assert.deepEqual(action.args, ["search", "TODO", "--files-without-match", "/workspace/demo"]);
assert.deepEqual(action.targetArgIndices, [3]);
});

test("buildSearchQuickAction combines --files-without-match with glob", () => {
const action = buildSearchQuickAction("/workspace/demo", "TODO", "*.ts", {
filesWithoutMatch: true
});

assert.equal(action.title, 'Search files without "TODO"');
assert.deepEqual(action.args, [
"search", "TODO", "--files-without-match", "--glob", "*.ts", "/workspace/demo"
]);
assert.deepEqual(action.targetArgIndices, [5]);
});

test("buildCreateQuickAction builds a create command with content and apply", () => {
const action = buildCreateQuickAction("/workspace/demo/src/newfile.ts", "hello");

Expand Down
3 changes: 2 additions & 1 deletion walkthrough/initialize.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,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), shell platform examples (all, Linux/macOS, or Windows), and
surface (full document or core pack for system-prompt injection).
surface (full document or core pack for system-prompt injection). On
CLI 0.29+, the core pack text follows the mode you pick.

## What AGENTS.md Contains

Expand Down
Loading