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 @@ -48,10 +48,10 @@ src/
workspace/readiness.ts Workspace readiness: environment detection, folder selection
test/
unit/ Unit tests (node:test, dependency-injected, no VS Code API)
batchApply.test.ts Batch template and operation count parsing (15 tests)
batchApply.test.ts Batch template and operation count parsing (16 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 (34 tests)
initializeProject.test.ts Status display, agents file classification, formatError (39 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)
Expand Down
36 changes: 25 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,16 @@ Or search for **Patchloom** in the Extensions view (`Ctrl+Shift+X` / `Cmd+Shift+

## Get started in 30 seconds

1. Install the [Patchloom CLI](https://github.com/patchloom/patchloom) (or run **Patchloom: Install Patchloom** from the command palette)
1. Install the [Patchloom CLI](https://github.com/patchloom/patchloom) (or run **Patchloom: Install Patchloom** from the command palette; recommended: tracks GitHub Releases with checksum verification)
```sh
brew install patchloom/tap/patchloom # macOS / Linux (Homebrew)
npm install -g patchloom # npm (Node.js)
curl -LsSf https://github.com/patchloom/patchloom/releases/latest/download/patchloom-installer.sh | sh # shell script
cargo install patchloom # from source
scoop bucket add patchloom https://github.com/patchloom/scoop-bucket
scoop install patchloom # Windows (Scoop)
choco install patchloom # Windows (Chocolatey; first listing may wait on moderation)
winget install Patchloom.Patchloom # Windows (WinGet; when the community PR is approved)
scoop install patchloom # Windows (Scoop; preferred PATH channel)
```
On Windows, prefer the extension managed installer or Scoop. Avoid winget and Chocolatey for install or upgrade: both lag GitHub Releases and often leave you on an old CLI.
2. Open a project and run **Patchloom: Setup Workspace**

<p align="center">
Expand Down Expand Up @@ -66,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.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.
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.

### Status bar

Expand Down Expand Up @@ -118,7 +117,7 @@ Workspace Quick Actions and Batch Apply pass `--contain` so CLI paths stay insid

### Batch operations

`Patchloom: Batch Apply` opens a line-oriented plan template where you can compose multiple operations (replace, fuzzy replace, doc set, multi-doc `doc.merge`, file append, markdown section inserts, tidy). The extension pipes the plan to `patchloom batch --apply` so all changes land atomically.
`Patchloom: Batch Apply` opens a line-oriented plan template where you can compose multiple operations (replace, fuzzy replace, `doc.set`, multi-match `doc.update`, multi-doc `doc.merge`, file append, markdown section inserts, tidy). The extension pipes the plan to `patchloom batch --apply` so all changes land atomically.

### Output channel

Expand Down Expand Up @@ -175,24 +174,39 @@ The extension detects outdated CLI builds and warns with upgrade guidance. It re
**Patchloom not found**
Set `patchloom.path` in settings, or add the CLI to your `PATH`.

**CLI compatibility warning**
Run `Patchloom: Open Releases` to download the latest release. The extension requires 0.3.0 or newer; 0.24.0 is recommended.
**CLI compatibility warning / upgrade path**
The extension requires Patchloom **0.3.0** or newer; **0.28.0** is recommended. Prefer channels that track GitHub Releases the same day:

1. **Patchloom: Update Patchloom** (or **Install Patchloom**) for the extension managed install (checksum-verified download from GitHub Releases)
2. **Scoop** on Windows: `scoop update patchloom` after `scoop install patchloom`
3. Homebrew / npm / cargo / the official installer script on macOS and Linux

Do **not** rely on winget or Chocolatey to stay current. Those community packages lag moderation and Microsoft publish, so upgrades often stay stuck on older CLI versions.

**Path rejected by workspace guard**
Quick Actions and Batch Apply pass `--contain` so paths stay inside the open workspace folder. On CLI 0.18+, sandbox escapes report `error_kind: guard_rejected` (not a generic `invalid_input`). Keep targets under the workspace root, or open the folder that owns the files.

**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.

**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.

**Create or rename destination already exists**
On CLI 0.19+, create/rename conflicts report `error_kind: already_exists` (not a generic `invalid_input`). Use the force flag when overwriting is intentional, or pick a free destination path.

**Binary or invalid UTF-8 target**
On CLI 0.20+, sole-path loads of binary or invalid UTF-8 files report `error_kind: binary` or `invalid_encoding` (not a soft `no_matches`). Use a text file, or force-create when overwriting non-text is intentional.
**Binary, invalid UTF-8, or non-regular file**
On CLI 0.20+, sole-path loads of binary or invalid UTF-8 files report `error_kind: binary` or `invalid_encoding` (not a soft `no_matches`). On CLI 0.26+, FIFOs and other special nodes refuse with multi-path `refused[].reason: not_regular_file` (not a permission error). Use a regular text file, or force-create when overwriting non-text is intentional.

**Fuzzy match span refused**
On CLI 0.22+, over-wide fuzzy matches can report `error_kind: fuzzy_span_suspicious`. Prefer an exact `old` string, structured `doc`/`md`/`ast` edits, or `apply-fragment` with a unique anchor.

**Doc selector needs multi-match op**
On CLI 0.27+, `doc set` / `doc ensure` / `doc delete` with a predicate or wildcard selector stay `error_kind: invalid_input` and may include `suggested_op` (`doc.update` or `doc.delete_where`). The extension surfaces that hint in the Output channel and notifications. Use the multi-match op (or a concrete index path such as `items.0.val`).

**Ambiguous markdown heading**
On CLI 0.25+, section ops that match the same heading more than once report `error_kind: ambiguous`. Make the heading unique or use a level-qualified query (for example `## Rules`).

**MCP config not injected**
Run `Patchloom: Configure MCP` and select the target editor config.

Expand Down Expand Up @@ -226,7 +240,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.24.0+ recommended for `list_files` MCP inventory, `apply-fragment`, `error_kind` peels including `binary` / `invalid_encoding` / `fuzzy_span_suspicious` / `already_exists` / `guard_rejected`, 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, JSON `applied` honesty, and agent-facing envelopes)
- [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)

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@
{
"id": "installCli",
"title": "Install the Patchloom CLI",
"description": "Install the Patchloom CLI via the managed installer, Homebrew, or cargo.\n\n[Install Patchloom](command:patchloom.installBinary)",
"description": "Install the Patchloom CLI via the managed installer (recommended), Scoop on Windows, Homebrew, or cargo. Avoid winget and Chocolatey; they lag releases.\n\n[Install Patchloom](command:patchloom.installBinary)",
"media": {
"markdown": "walkthrough/install.md"
},
Expand Down
16 changes: 14 additions & 2 deletions src/binary/patchloom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,23 @@ export async function ensurePatchloomReadyOrNotify(

if (patchloomNeedsUpgrade(status)) {
const vscode = await import("vscode");
// Prefer managed install/update (GitHub Releases) over lagging community packages.
const canUpdateManaged = status.source === "managed" || status.managedInstall?.exists === true;
const canInstallManaged = status.managedInstall !== undefined;
const primaryAction = canUpdateManaged
? "Update Patchloom"
: canInstallManaged
? "Install Patchloom"
: "Open Releases";
const choice = await vscode.window.showWarningMessage(
`${status.compatibilityMessage}${contextSuffix ? `\n\n${contextSuffix}` : ""}`,
"Open Releases"
primaryAction
);
if (choice === "Open Releases") {
if (choice === "Update Patchloom") {
await vscode.commands.executeCommand("patchloom.updateBinary");
} else if (choice === "Install Patchloom") {
await vscode.commands.executeCommand("patchloom.installBinary");
} else if (choice === "Open Releases") {
await vscode.commands.executeCommand("patchloom.openPatchloomReleases");
}
return null;
Expand Down
2 changes: 2 additions & 0 deletions src/commands/batchApply.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ import { getPatchloomLog } from "../logging/outputChannel.js";
import { activeWorkspaceFolder } from "../workspace/readiness.js";

// Batch replace is PATH OLD NEW (not CLI `replace OLD --new NEW path`). See CLI 0.18+ batch --help.
// doc.update is the multi-match sibling of doc.set (CLI 0.27+ suggested_op hints this).
export const BATCH_TEMPLATE = [
"replace src/example.ts \"old text\" \"new text\"",
"replace src/example.ts \"typo_here\" \"fixed\" --fuzzy --min-fuzzy-score 0.80",
"replace src/example.ts \"anchor_line\" --insert-after=\"new sibling line\"",
"doc.set package.json version \"2.0.0\"",
"doc.update data.json \"items[*].enabled\" true",
"doc.merge multi-doc.yaml 0 \"{\\\"debug\\\": true}\"",
"file.append src/example.ts \"new appended line\"",
"md.insert_after_section README.md \"## Config\" \"## FAQ\"",
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+)",
description: "Default (58 tools on CLI 0.24+; verified on 0.28)",
surface: "full" as const
},
{
Expand Down
14 changes: 14 additions & 0 deletions src/status/details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,20 @@ export function preferredStatusAction(status: PatchloomStatus, workspaceReadines
}

if (patchloomNeedsUpgrade(status)) {
// Prefer managed install/update so users stay on GitHub Releases (not lagging
// community packages such as winget/Chocolatey).
if (status.source === "managed" || status.managedInstall?.exists) {
return {
title: "Update Patchloom",
command: "patchloom.updateBinary"
};
}
if (status.managedInstall) {
return {
title: "Install Patchloom",
command: "patchloom.installBinary"
};
}
return {
title: "Open Releases",
command: "patchloom.openPatchloomReleases"
Expand Down
20 changes: 16 additions & 4 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ export function formatError(error: unknown): string {
/**
* Prefer machine-readable CLI JSON error envelopes (error_kind + error) when
* present so agents and the UI surface kinds like guard_rejected (CLI 0.18+)
* instead of a flattened multi-line dump.
* instead of a flattened multi-line dump. On CLI 0.27+, append suggested_op
* when present so users can retry with doc.update / doc.delete_where.
*/
export function formatCliOutput(result: { exitCode: number; stdout: string; stderr: string }): string {
const jsonError = extractCliJsonError(result.stdout) ?? extractCliJsonError(result.stderr);
Expand All @@ -34,19 +35,30 @@ function extractCliJsonError(stream: string): string | undefined {
return undefined;
}
try {
const parsed = JSON.parse(trimmed) as { error?: unknown; error_kind?: unknown };
const parsed = JSON.parse(trimmed) as {
error?: unknown;
error_kind?: unknown;
suggested_op?: unknown;
};
if (typeof parsed.error !== "string" || parsed.error.length === 0) {
return undefined;
}
// CLI often prefixes "guard_rejected: …" already; avoid "kind: kind: …".
let message: string;
if (
typeof parsed.error_kind === "string" &&
parsed.error_kind.length > 0 &&
!parsed.error.startsWith(`${parsed.error_kind}:`)
) {
return `${parsed.error_kind}: ${parsed.error}`;
message = `${parsed.error_kind}: ${parsed.error}`;
} else {
message = parsed.error;
}
return parsed.error;
// CLI 0.27+: fail-closed doc navigation may hint the multi-match sibling.
if (typeof parsed.suggested_op === "string" && parsed.suggested_op.length > 0) {
message = `${message} (suggested_op: ${parsed.suggested_op})`;
}
return message;
} catch {
return undefined;
}
Expand Down
25 changes: 19 additions & 6 deletions test/unit/batchApply.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,20 @@ import {
parseBatchOperationCount
} from "../../src/commands/batchApply.js";

test("buildBatchTemplate returns line-oriented format with eight operations", () => {
test("buildBatchTemplate returns line-oriented format with nine operations", () => {
const template = buildBatchTemplate();
const lines = template.split("\n").filter((line) => line.trim().length > 0);

assert.equal(lines.length, 8);
assert.equal(lines.length, 9);
assert.ok(lines[0].startsWith("replace "), "first line should be a replace operation");
assert.ok(lines[1].startsWith("replace ") && lines[1].includes("--fuzzy"), "second line should be fuzzy replace");
assert.ok(lines[2].startsWith("replace ") && lines[2].includes("--insert-after"), "third line should be insert-after");
assert.ok(lines[3].startsWith("doc.set "), "fourth line should be a doc.set operation");
assert.ok(lines[4].startsWith("doc.merge "), "fifth line should be multi-doc doc.merge");
assert.ok(lines[5].startsWith("file.append "), "sixth line should be a file.append operation");
assert.ok(lines[6].startsWith("md.insert_after_section "), "seventh line should be md.insert_after_section");
assert.ok(lines[7].startsWith("tidy.fix "), "eighth line should be a tidy.fix operation");
assert.ok(lines[4].startsWith("doc.update "), "fifth line should be multi-match doc.update");
assert.ok(lines[5].startsWith("doc.merge "), "sixth line should be multi-doc doc.merge");
assert.ok(lines[6].startsWith("file.append "), "seventh line should be a file.append operation");
assert.ok(lines[7].startsWith("md.insert_after_section "), "eighth line should be md.insert_after_section");
assert.ok(lines[8].startsWith("tidy.fix "), "ninth line should be a tidy.fix operation");
});

test("buildBatchTemplate ends with a newline", () => {
Expand Down Expand Up @@ -119,6 +120,18 @@ test("buildBatchTemplate includes replace --insert-after example (CLI 0.16)", ()
);
});

test("buildBatchTemplate includes doc.update multi-match example (CLI 0.27+ suggested_op sibling)", () => {
const lines = buildBatchTemplate().split("\n");
const updateLine = lines.find((l) => l.startsWith("doc.update "));
assert.ok(updateLine, "template should contain a doc.update line");
assert.match(
updateLine,
/doc\.update \S+ ".+" \S+/,
"doc.update should have path, selector, and value"
);
assert.match(updateLine, /\[\*\]|\[.+=.+\]/, "selector should use wildcard or predicate form");
});

test("buildBatchApplyArgs prefixes global --contain before batch --apply", () => {
assert.deepEqual(buildBatchApplyArgs(), ["--contain", "batch", "--apply"]);
});
Loading
Loading