@ai-agent-forge/plugin-coding-tools 0.88.14 → 0.88.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/agent-forge.json +1 -1
  2. package/dist/atomic-ops.d.ts +35 -0
  3. package/dist/atomic-ops.d.ts.map +1 -0
  4. package/dist/atomic-ops.js +62 -0
  5. package/dist/atomic-ops.js.map +1 -0
  6. package/dist/capability.d.ts +2 -0
  7. package/dist/capability.d.ts.map +1 -1
  8. package/dist/capability.js +47 -4
  9. package/dist/capability.js.map +1 -1
  10. package/dist/edit.d.ts +25 -0
  11. package/dist/edit.d.ts.map +1 -1
  12. package/dist/edit.js +162 -35
  13. package/dist/edit.js.map +1 -1
  14. package/dist/find.d.ts.map +1 -1
  15. package/dist/find.js +25 -1
  16. package/dist/find.js.map +1 -1
  17. package/dist/grep.d.ts.map +1 -1
  18. package/dist/grep.js +3 -1
  19. package/dist/grep.js.map +1 -1
  20. package/dist/index.d.ts +1 -1
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/lease-check.d.ts +48 -0
  24. package/dist/lease-check.d.ts.map +1 -0
  25. package/dist/lease-check.js +77 -0
  26. package/dist/lease-check.js.map +1 -0
  27. package/dist/read.d.ts.map +1 -1
  28. package/dist/read.js +48 -8
  29. package/dist/read.js.map +1 -1
  30. package/dist/snapshot-ops.d.ts +31 -0
  31. package/dist/snapshot-ops.d.ts.map +1 -0
  32. package/dist/snapshot-ops.js +67 -0
  33. package/dist/snapshot-ops.js.map +1 -0
  34. package/dist/version-registry.d.ts +23 -0
  35. package/dist/version-registry.d.ts.map +1 -0
  36. package/dist/version-registry.js +54 -0
  37. package/dist/version-registry.js.map +1 -0
  38. package/dist/write-scope.d.ts +12 -0
  39. package/dist/write-scope.d.ts.map +1 -0
  40. package/dist/write-scope.js +46 -0
  41. package/dist/write-scope.js.map +1 -0
  42. package/dist/write.d.ts +27 -2
  43. package/dist/write.d.ts.map +1 -1
  44. package/dist/write.js +193 -13
  45. package/dist/write.js.map +1 -1
  46. package/package.json +3 -4
  47. package/plugin.json +8 -1
package/agent-forge.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "packageFormatVersion": 1,
3
3
  "id": "agent-forge.plugin.coding-tools",
4
- "pluginVersion": "0.88.14",
4
+ "pluginVersion": "0.88.16",
5
5
  "apiVersion": "1",
6
6
  "minHostVersion": "0.88.0",
7
7
  "platform": {
@@ -0,0 +1,35 @@
1
+ import { type FileMutationLockHandleV1, type StatVersionV1 } from "@agent-forge/host/file-lock";
2
+ export interface AtomicCommitOperations {
3
+ statVersion: (absolutePath: string) => Promise<StatVersionV1 | undefined>;
4
+ writeTemp: (absolutePath: string, content: string) => Promise<string>;
5
+ commitSync: (tempPath: string, targetPath: string) => void;
6
+ }
7
+ export declare function hasAtomicCommitOperations(ops: Partial<AtomicCommitOperations>): ops is AtomicCommitOperations;
8
+ /** Full version comparison, including ino/ctime — used for the last-moment re-check. */
9
+ export declare function sameVersion(a: StatVersionV1 | undefined, b: StatVersionV1 | undefined): boolean;
10
+ /**
11
+ * Commit prepared content.
12
+ *
13
+ * Tier A: write the temp file first (slow IO outside the synchronous section),
14
+ * then re-check the target and replace it inside `lock.commit`.
15
+ * Tier B: no synchronous primitive exists; the async write happens after the
16
+ * token check. Safety still rests on "a live holder's record is never removed
17
+ * automatically", not on the absence of an await.
18
+ */
19
+ export declare function commitPreparedContent(input: {
20
+ readonly ops: Partial<AtomicCommitOperations> & {
21
+ writeFile: (absolutePath: string, content: string) => Promise<void>;
22
+ readFile?: (absolutePath: string) => Promise<Buffer>;
23
+ };
24
+ readonly lock: FileMutationLockHandleV1;
25
+ readonly targetPath: string;
26
+ readonly content: string;
27
+ /** Version the caller validated against; re-checked at the last possible moment in tier A. */
28
+ readonly expectedStat: StatVersionV1 | undefined;
29
+ /** Content hash the caller validated against, for tier B where no stat exists. */
30
+ readonly expectedHash?: string;
31
+ readonly makeMismatchError: () => Error;
32
+ }): Promise<{
33
+ atomic: boolean;
34
+ }>;
35
+ //# sourceMappingURL=atomic-ops.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-ops.d.ts","sourceRoot":"","sources":["../src/atomic-ops.ts"],"names":[],"mappings":"AAWA,OAAO,EAGN,KAAK,wBAAwB,EAC7B,KAAK,aAAa,EAIlB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,sBAAsB;IACtC,WAAW,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IAC1E,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtE,UAAU,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC3D;AAED,wBAAgB,yBAAyB,CAAC,GAAG,EAAE,OAAO,CAAC,sBAAsB,CAAC,GAAG,GAAG,IAAI,sBAAsB,CAE7G;AAED,0FAAwF;AACxF,wBAAgB,WAAW,CAAC,CAAC,EAAE,aAAa,GAAG,SAAS,EAAE,CAAC,EAAE,aAAa,GAAG,SAAS,GAAG,OAAO,CAG/F;AAED;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,KAAK,EAAE;IAClD,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC,sBAAsB,CAAC,GAAG;QAC/C,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;QACpE,QAAQ,CAAC,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;KACrD,CAAC;IACF,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,8FAA8F;IAC9F,QAAQ,CAAC,YAAY,EAAE,aAAa,GAAG,SAAS,CAAC;IACjD,kFAAkF;IAClF,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,KAAK,CAAC;CACxC,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC,CA6B/B","sourcesContent":["// Atomic-commit tier shared by the default file tools (design §13.2).\n//\n// Tier A (default local filesystem): operations expose version facts, a temp-file\n// writer and a synchronous replace, so the commit can run inside `lock.commit`\n// with no `await` between the token check and the rename.\n//\n// Tier B (remote/SSH operations that only have read/write/access): the lock still\n// serializes local writers and the version check still runs, but the final write\n// is a plain `writeFile`. That is reported as `atomic: false` rather than being\n// passed off as an atomic replace.\n\nimport {\n\tapplyModeSync,\n\tcommitWithRenameRetry,\n\ttype FileMutationLockHandleV1,\n\ttype StatVersionV1,\n\tsha256HexSync,\n\tstatVersionSync,\n\tunlinkQuietSync,\n} from \"@agent-forge/host/file-lock\";\n\nexport interface AtomicCommitOperations {\n\tstatVersion: (absolutePath: string) => Promise<StatVersionV1 | undefined>;\n\twriteTemp: (absolutePath: string, content: string) => Promise<string>;\n\tcommitSync: (tempPath: string, targetPath: string) => void;\n}\n\nexport function hasAtomicCommitOperations(ops: Partial<AtomicCommitOperations>): ops is AtomicCommitOperations {\n\treturn ops.statVersion !== undefined && ops.writeTemp !== undefined && ops.commitSync !== undefined;\n}\n\n/** Full version comparison, including ino/ctime — used for the last-moment re-check. */\nexport function sameVersion(a: StatVersionV1 | undefined, b: StatVersionV1 | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;\n}\n\n/**\n * Commit prepared content.\n *\n * Tier A: write the temp file first (slow IO outside the synchronous section),\n * then re-check the target and replace it inside `lock.commit`.\n * Tier B: no synchronous primitive exists; the async write happens after the\n * token check. Safety still rests on \"a live holder's record is never removed\n * automatically\", not on the absence of an await.\n */\nexport async function commitPreparedContent(input: {\n\treadonly ops: Partial<AtomicCommitOperations> & {\n\t\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t\treadFile?: (absolutePath: string) => Promise<Buffer>;\n\t};\n\treadonly lock: FileMutationLockHandleV1;\n\treadonly targetPath: string;\n\treadonly content: string;\n\t/** Version the caller validated against; re-checked at the last possible moment in tier A. */\n\treadonly expectedStat: StatVersionV1 | undefined;\n\t/** Content hash the caller validated against, for tier B where no stat exists. */\n\treadonly expectedHash?: string;\n\treadonly makeMismatchError: () => Error;\n}): Promise<{ atomic: boolean }> {\n\tconst { ops, lock, targetPath, content, expectedStat, expectedHash, makeMismatchError } = input;\n\n\tif (!hasAtomicCommitOperations(ops)) {\n\t\t// Tier B: no synchronous primitive. Re-verify by content when the caller gave\n\t\t// a hash, then write. Outside writers can still interleave here; that is the\n\t\t// documented tier-B boundary, surfaced as `atomic: false`.\n\t\tawait lock.commit(() => undefined);\n\t\tif (expectedHash !== undefined && ops.readFile !== undefined) {\n\t\t\tconst current = await ops.readFile(targetPath);\n\t\t\tif (sha256HexSync(current) !== expectedHash) throw makeMismatchError();\n\t\t}\n\t\tawait ops.writeFile(targetPath, content);\n\t\treturn { atomic: false };\n\t}\n\n\tconst tempPath = await ops.writeTemp(targetPath, content);\n\ttry {\n\t\tawait commitWithRenameRetry(lock, () => {\n\t\t\tif (!sameVersion(statVersionSync(targetPath), expectedStat)) throw makeMismatchError();\n\t\t\tif (expectedStat !== undefined) applyModeSync(tempPath, expectedStat.mode);\n\t\t\tops.commitSync(tempPath, targetPath);\n\t\t});\n\t} catch (error) {\n\t\t// A successful commit renamed the temp file away; cleanup here only runs on failure.\n\t\tunlinkQuietSync(tempPath);\n\t\tthrow error;\n\t}\n\treturn { atomic: true };\n}\n"]}
@@ -0,0 +1,62 @@
1
+ // Atomic-commit tier shared by the default file tools (design §13.2).
2
+ //
3
+ // Tier A (default local filesystem): operations expose version facts, a temp-file
4
+ // writer and a synchronous replace, so the commit can run inside `lock.commit`
5
+ // with no `await` between the token check and the rename.
6
+ //
7
+ // Tier B (remote/SSH operations that only have read/write/access): the lock still
8
+ // serializes local writers and the version check still runs, but the final write
9
+ // is a plain `writeFile`. That is reported as `atomic: false` rather than being
10
+ // passed off as an atomic replace.
11
+ import { applyModeSync, commitWithRenameRetry, sha256HexSync, statVersionSync, unlinkQuietSync, } from "@agent-forge/host/file-lock";
12
+ export function hasAtomicCommitOperations(ops) {
13
+ return ops.statVersion !== undefined && ops.writeTemp !== undefined && ops.commitSync !== undefined;
14
+ }
15
+ /** Full version comparison, including ino/ctime — used for the last-moment re-check. */
16
+ export function sameVersion(a, b) {
17
+ if (a === undefined || b === undefined)
18
+ return a === b;
19
+ return a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;
20
+ }
21
+ /**
22
+ * Commit prepared content.
23
+ *
24
+ * Tier A: write the temp file first (slow IO outside the synchronous section),
25
+ * then re-check the target and replace it inside `lock.commit`.
26
+ * Tier B: no synchronous primitive exists; the async write happens after the
27
+ * token check. Safety still rests on "a live holder's record is never removed
28
+ * automatically", not on the absence of an await.
29
+ */
30
+ export async function commitPreparedContent(input) {
31
+ const { ops, lock, targetPath, content, expectedStat, expectedHash, makeMismatchError } = input;
32
+ if (!hasAtomicCommitOperations(ops)) {
33
+ // Tier B: no synchronous primitive. Re-verify by content when the caller gave
34
+ // a hash, then write. Outside writers can still interleave here; that is the
35
+ // documented tier-B boundary, surfaced as `atomic: false`.
36
+ await lock.commit(() => undefined);
37
+ if (expectedHash !== undefined && ops.readFile !== undefined) {
38
+ const current = await ops.readFile(targetPath);
39
+ if (sha256HexSync(current) !== expectedHash)
40
+ throw makeMismatchError();
41
+ }
42
+ await ops.writeFile(targetPath, content);
43
+ return { atomic: false };
44
+ }
45
+ const tempPath = await ops.writeTemp(targetPath, content);
46
+ try {
47
+ await commitWithRenameRetry(lock, () => {
48
+ if (!sameVersion(statVersionSync(targetPath), expectedStat))
49
+ throw makeMismatchError();
50
+ if (expectedStat !== undefined)
51
+ applyModeSync(tempPath, expectedStat.mode);
52
+ ops.commitSync(tempPath, targetPath);
53
+ });
54
+ }
55
+ catch (error) {
56
+ // A successful commit renamed the temp file away; cleanup here only runs on failure.
57
+ unlinkQuietSync(tempPath);
58
+ throw error;
59
+ }
60
+ return { atomic: true };
61
+ }
62
+ //# sourceMappingURL=atomic-ops.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"atomic-ops.js","sourceRoot":"","sources":["../src/atomic-ops.ts"],"names":[],"mappings":"AAAA,uEAAsE;AACtE,EAAE;AACF,kFAAkF;AAClF,+EAA+E;AAC/E,0DAA0D;AAC1D,EAAE;AACF,kFAAkF;AAClF,iFAAiF;AACjF,gFAAgF;AAChF,mCAAmC;AAEnC,OAAO,EACN,aAAa,EACb,qBAAqB,EAGrB,aAAa,EACb,eAAe,EACf,eAAe,GACf,MAAM,6BAA6B,CAAC;AAQrC,MAAM,UAAU,yBAAyB,CAAC,GAAoC,EAAiC;IAC9G,OAAO,GAAG,CAAC,WAAW,KAAK,SAAS,IAAI,GAAG,CAAC,SAAS,KAAK,SAAS,IAAI,GAAG,CAAC,UAAU,KAAK,SAAS,CAAC;AAAA,CACpG;AAED,0FAAwF;AACxF,MAAM,UAAU,WAAW,CAAC,CAA4B,EAAE,CAA4B,EAAW;IAChG,IAAI,CAAC,KAAK,SAAS,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,KAAK,CAAC,CAAC;IACvD,OAAO,CAAC,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,CAAC,IAAI,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,IAAI,CAAC,CAAC,OAAO,KAAK,CAAC,CAAC,OAAO,CAAC;AAAA,CAClG;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,KAa3C,EAAgC;IAChC,MAAM,EAAE,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,YAAY,EAAE,iBAAiB,EAAE,GAAG,KAAK,CAAC;IAEhG,IAAI,CAAC,yBAAyB,CAAC,GAAG,CAAC,EAAE,CAAC;QACrC,8EAA8E;QAC9E,6EAA6E;QAC7E,2DAA2D;QAC3D,MAAM,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QACnC,IAAI,YAAY,KAAK,SAAS,IAAI,GAAG,CAAC,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC9D,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC;YAC/C,IAAI,aAAa,CAAC,OAAO,CAAC,KAAK,YAAY;gBAAE,MAAM,iBAAiB,EAAE,CAAC;QACxE,CAAC;QACD,MAAM,GAAG,CAAC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;QACzC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC1B,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,GAAG,CAAC,SAAS,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;IAC1D,IAAI,CAAC;QACJ,MAAM,qBAAqB,CAAC,IAAI,EAAE,GAAG,EAAE,CAAC;YACvC,IAAI,CAAC,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,EAAE,YAAY,CAAC;gBAAE,MAAM,iBAAiB,EAAE,CAAC;YACvF,IAAI,YAAY,KAAK,SAAS;gBAAE,aAAa,CAAC,QAAQ,EAAE,YAAY,CAAC,IAAI,CAAC,CAAC;YAC3E,GAAG,CAAC,UAAU,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAAA,CACrC,CAAC,CAAC;IACJ,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,qFAAqF;QACrF,eAAe,CAAC,QAAQ,CAAC,CAAC;QAC1B,MAAM,KAAK,CAAC;IACb,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAAA,CACxB","sourcesContent":["// Atomic-commit tier shared by the default file tools (design §13.2).\n//\n// Tier A (default local filesystem): operations expose version facts, a temp-file\n// writer and a synchronous replace, so the commit can run inside `lock.commit`\n// with no `await` between the token check and the rename.\n//\n// Tier B (remote/SSH operations that only have read/write/access): the lock still\n// serializes local writers and the version check still runs, but the final write\n// is a plain `writeFile`. That is reported as `atomic: false` rather than being\n// passed off as an atomic replace.\n\nimport {\n\tapplyModeSync,\n\tcommitWithRenameRetry,\n\ttype FileMutationLockHandleV1,\n\ttype StatVersionV1,\n\tsha256HexSync,\n\tstatVersionSync,\n\tunlinkQuietSync,\n} from \"@agent-forge/host/file-lock\";\n\nexport interface AtomicCommitOperations {\n\tstatVersion: (absolutePath: string) => Promise<StatVersionV1 | undefined>;\n\twriteTemp: (absolutePath: string, content: string) => Promise<string>;\n\tcommitSync: (tempPath: string, targetPath: string) => void;\n}\n\nexport function hasAtomicCommitOperations(ops: Partial<AtomicCommitOperations>): ops is AtomicCommitOperations {\n\treturn ops.statVersion !== undefined && ops.writeTemp !== undefined && ops.commitSync !== undefined;\n}\n\n/** Full version comparison, including ino/ctime — used for the last-moment re-check. */\nexport function sameVersion(a: StatVersionV1 | undefined, b: StatVersionV1 | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.ino === b.ino && a.size === b.size && a.mtimeNs === b.mtimeNs && a.ctimeNs === b.ctimeNs;\n}\n\n/**\n * Commit prepared content.\n *\n * Tier A: write the temp file first (slow IO outside the synchronous section),\n * then re-check the target and replace it inside `lock.commit`.\n * Tier B: no synchronous primitive exists; the async write happens after the\n * token check. Safety still rests on \"a live holder's record is never removed\n * automatically\", not on the absence of an await.\n */\nexport async function commitPreparedContent(input: {\n\treadonly ops: Partial<AtomicCommitOperations> & {\n\t\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t\treadFile?: (absolutePath: string) => Promise<Buffer>;\n\t};\n\treadonly lock: FileMutationLockHandleV1;\n\treadonly targetPath: string;\n\treadonly content: string;\n\t/** Version the caller validated against; re-checked at the last possible moment in tier A. */\n\treadonly expectedStat: StatVersionV1 | undefined;\n\t/** Content hash the caller validated against, for tier B where no stat exists. */\n\treadonly expectedHash?: string;\n\treadonly makeMismatchError: () => Error;\n}): Promise<{ atomic: boolean }> {\n\tconst { ops, lock, targetPath, content, expectedStat, expectedHash, makeMismatchError } = input;\n\n\tif (!hasAtomicCommitOperations(ops)) {\n\t\t// Tier B: no synchronous primitive. Re-verify by content when the caller gave\n\t\t// a hash, then write. Outside writers can still interleave here; that is the\n\t\t// documented tier-B boundary, surfaced as `atomic: false`.\n\t\tawait lock.commit(() => undefined);\n\t\tif (expectedHash !== undefined && ops.readFile !== undefined) {\n\t\t\tconst current = await ops.readFile(targetPath);\n\t\t\tif (sha256HexSync(current) !== expectedHash) throw makeMismatchError();\n\t\t}\n\t\tawait ops.writeFile(targetPath, content);\n\t\treturn { atomic: false };\n\t}\n\n\tconst tempPath = await ops.writeTemp(targetPath, content);\n\ttry {\n\t\tawait commitWithRenameRetry(lock, () => {\n\t\t\tif (!sameVersion(statVersionSync(targetPath), expectedStat)) throw makeMismatchError();\n\t\t\tif (expectedStat !== undefined) applyModeSync(tempPath, expectedStat.mode);\n\t\t\tops.commitSync(tempPath, targetPath);\n\t\t});\n\t} catch (error) {\n\t\t// A successful commit renamed the temp file away; cleanup here only runs on failure.\n\t\tunlinkQuietSync(tempPath);\n\t\tthrow error;\n\t}\n\treturn { atomic: true };\n}\n"]}
@@ -13,6 +13,8 @@ export interface CodingToolsPluginConfigV1 {
13
13
  version: 1;
14
14
  /** Working directory handed to every tool definition. */
15
15
  cwd?: string;
16
+ /** Relative write-path globs for edit/write (child task profile); absent = unrestricted. */
17
+ writeScope?: readonly string[];
16
18
  read?: {
17
19
  autoResizeImages?: boolean;
18
20
  };
@@ -1 +1 @@
1
- {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAmC,MAAM,yBAAyB,CAAC;AAUtH;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,CAAC,CAAC;IACX,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACtC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAmFD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,sBAAsB,EAAE,CAUjG","sourcesContent":["/**\n * Capability factory for the coding tools plugin: registers the default coding\n * tool definitions through the public `tools` capability, mirroring the host's\n * embedded `installSessionBootstrapTools` semantics (readonly mode keeps only\n * the readonly-safe read/grep/find/ls face).\n */\n\nimport type { ToolDefinition } from \"@agent-forge/host/tool-definition\";\nimport type { CapabilityAPI, DisposableRegistration, PluginConfig, PluginConfigValue } from \"@agent-forge/plugin-sdk\";\nimport { createBashToolDefinition } from \"./bash.ts\";\nimport { createEditToolDefinition } from \"./edit.ts\";\nimport { createFindToolDefinition } from \"./find.ts\";\nimport { createGrepToolDefinition } from \"./grep.ts\";\nimport { createLsToolDefinition } from \"./ls.ts\";\nimport { createPowerShellToolDefinition } from \"./powershell.ts\";\nimport { createReadToolDefinition } from \"./read.ts\";\nimport { createWriteToolDefinition } from \"./write.ts\";\n\n/**\n * Plugin manifest config shape (`agent-forge.json` config face). Every field\n * is optional; absent fields keep each tool's existing defaults.\n */\nexport interface CodingToolsPluginConfigV1 {\n\tversion: 1;\n\t/** Working directory handed to every tool definition. */\n\tcwd?: string;\n\tread?: { autoResizeImages?: boolean };\n\tbash?: { commandPrefix?: string; shellPath?: string };\n}\n\nfunction optionalString(value: PluginConfigValue | undefined, field: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a string`);\n\t}\n\treturn value;\n}\n\nfunction optionalBoolean(value: PluginConfigValue | undefined, field: string): boolean | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"boolean\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a boolean`);\n\t}\n\treturn value;\n}\n\nfunction configObject(\n\tvalue: PluginConfigValue | undefined,\n\tfield: string,\n): Readonly<Record<string, PluginConfigValue>> {\n\tif (value === undefined) return {};\n\tif (value === null || Array.isArray(value) || typeof value !== \"object\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be an object`);\n\t}\n\t// The array member of the PluginConfigValue union cannot be excluded by\n\t// narrowing, so project the validated object into a typed record instead\n\t// of casting.\n\tconst record: Record<string, PluginConfigValue> = {};\n\tfor (const [key, entry] of Object.entries(value)) {\n\t\trecord[key] = entry;\n\t}\n\treturn record;\n}\n\nfunction readPluginConfig(config: PluginConfig): CodingToolsPluginConfigV1 {\n\tconst version = config.version ?? 1;\n\tif (version !== 1) {\n\t\tthrow new Error(`coding-tools: unsupported config version ${JSON.stringify(version)}, expected 1`);\n\t}\n\tconst read = configObject(config.read, \"read\");\n\tconst bash = configObject(config.bash, \"bash\");\n\treturn {\n\t\tversion: 1,\n\t\tcwd: optionalString(config.cwd, \"cwd\"),\n\t\tread: { autoResizeImages: optionalBoolean(read.autoResizeImages, \"read.autoResizeImages\") },\n\t\tbash: {\n\t\t\tcommandPrefix: optionalString(bash.commandPrefix, \"bash.commandPrefix\"),\n\t\t\tshellPath: optionalString(bash.shellPath, \"bash.shellPath\"),\n\t\t},\n\t};\n}\n\nfunction createToolDefinitions(\n\tcwd: string,\n\toptions: CodingToolsPluginConfigV1,\n\treadonlyMode: boolean,\n): readonly ToolDefinition[] {\n\tif (readonlyMode) {\n\t\t// Equivalent of the host's createReadOnlyToolDefinitions: only read\n\t\t// carries per-tool options there.\n\t\treturn [\n\t\t\tcreateReadToolDefinition(cwd, options.read),\n\t\t\tcreateGrepToolDefinition(cwd),\n\t\t\tcreateFindToolDefinition(cwd),\n\t\t\tcreateLsToolDefinition(cwd),\n\t\t];\n\t}\n\t// Equivalent of the host's createAllToolDefinitions: read and bash carry\n\t// per-tool options, the rest keep defaults.\n\treturn [\n\t\tcreateReadToolDefinition(cwd, options.read),\n\t\tcreateBashToolDefinition(cwd, options.bash),\n\t\tcreatePowerShellToolDefinition(cwd),\n\t\tcreateEditToolDefinition(cwd),\n\t\tcreateWriteToolDefinition(cwd),\n\t\tcreateGrepToolDefinition(cwd),\n\t\tcreateFindToolDefinition(cwd),\n\t\tcreateLsToolDefinition(cwd),\n\t];\n}\n\nexport function createCodingToolsCapability(api: CapabilityAPI): readonly DisposableRegistration[] {\n\tconst config = readPluginConfig(api.config);\n\tconst cwd = config.cwd ?? process.cwd();\n\tconst readonlyMode = api.host.mode === \"readonly\";\n\tconst definitions = createToolDefinitions(cwd, config, readonlyMode);\n\tconst registrations: DisposableRegistration[] = [];\n\tfor (const definition of definitions) {\n\t\tregistrations.push(api.registerTool(definition));\n\t}\n\treturn registrations;\n}\n"]}
1
+ {"version":3,"file":"capability.d.ts","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAE,sBAAsB,EAAmC,MAAM,yBAAyB,CAAC;AA4BtH;;;GAGG;AACH,MAAM,WAAW,yBAAyB;IACzC,OAAO,EAAE,CAAC,CAAC;IACX,yDAAyD;IACzD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,4FAA4F;IAC5F,UAAU,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B,IAAI,CAAC,EAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IACtC,IAAI,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACtD;AAwGD,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,aAAa,GAAG,SAAS,sBAAsB,EAAE,CAgBjG","sourcesContent":["/**\n * Capability factory for the coding tools plugin: registers the default coding\n * tool definitions through the public `tools` capability, mirroring the host's\n * embedded `installSessionBootstrapTools` semantics (readonly mode keeps only\n * the readonly-safe read/grep/find/ls face).\n */\n\nimport type { ToolDefinition } from \"@agent-forge/host/tool-definition\";\nimport type { CapabilityAPI, DisposableRegistration, PluginConfig, PluginConfigValue } from \"@agent-forge/plugin-sdk\";\nimport { createBashToolDefinition } from \"./bash.ts\";\nimport { createEditToolDefinition } from \"./edit.ts\";\nimport { createFindToolDefinition } from \"./find.ts\";\nimport { createGrepToolDefinition } from \"./grep.ts\";\nimport type { CoordinationLeaseCheckV1 } from \"./lease-check.ts\";\nimport { createLsToolDefinition } from \"./ls.ts\";\nimport { createPowerShellToolDefinition } from \"./powershell.ts\";\nimport { createReadToolDefinition } from \"./read.ts\";\nimport { createWriteToolDefinition } from \"./write.ts\";\n\n/** Capability id of the advisory intent-lease provider (design §8.6). */\nconst FILE_COORDINATION_CAPABILITY_ID = \"file.coordination\";\n\n/**\n * Optional `file.coordination@1` consumer face. Absent provider (plugin\n * disabled/replaced by something that is not loaded) = fast path, no advisory\n * checks; the L0 lock and L1 version check are unaffected.\n */\nfunction requireCoordination(api: CapabilityAPI): CoordinationLeaseCheckV1 | undefined {\n\ttry {\n\t\tconst handle = api.capabilities.require(FILE_COORDINATION_CAPABILITY_ID, { version: 1 });\n\t\treturn { call: (request) => handle.call(request) };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Plugin manifest config shape (`agent-forge.json` config face). Every field\n * is optional; absent fields keep each tool's existing defaults.\n */\nexport interface CodingToolsPluginConfigV1 {\n\tversion: 1;\n\t/** Working directory handed to every tool definition. */\n\tcwd?: string;\n\t/** Relative write-path globs for edit/write (child task profile); absent = unrestricted. */\n\twriteScope?: readonly string[];\n\tread?: { autoResizeImages?: boolean };\n\tbash?: { commandPrefix?: string; shellPath?: string };\n}\n\nfunction optionalString(value: PluginConfigValue | undefined, field: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a string`);\n\t}\n\treturn value;\n}\n\nfunction optionalBoolean(value: PluginConfigValue | undefined, field: string): boolean | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"boolean\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a boolean`);\n\t}\n\treturn value;\n}\n\nfunction optionalWriteScope(value: PluginConfigValue | undefined, field: string): readonly string[] | undefined {\n\tif (value === undefined) return undefined;\n\tif (!Array.isArray(value)) throw new Error(`coding-tools: config.${field} must be an array of strings`);\n\tconst patterns: string[] = [];\n\tfor (const entry of value) {\n\t\tif (typeof entry !== \"string\" || entry.trim() === \"\") {\n\t\t\tthrow new Error(`coding-tools: config.${field} entries must be non-empty strings`);\n\t\t}\n\t\tpatterns.push(entry.trim().replace(/\\\\/g, \"/\"));\n\t}\n\treturn patterns;\n}\n\nfunction configObject(\n\tvalue: PluginConfigValue | undefined,\n\tfield: string,\n): Readonly<Record<string, PluginConfigValue>> {\n\tif (value === undefined) return {};\n\tif (value === null || Array.isArray(value) || typeof value !== \"object\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be an object`);\n\t}\n\t// The array member of the PluginConfigValue union cannot be excluded by\n\t// narrowing, so project the validated object into a typed record instead\n\t// of casting.\n\tconst record: Record<string, PluginConfigValue> = {};\n\tfor (const [key, entry] of Object.entries(value)) {\n\t\trecord[key] = entry;\n\t}\n\treturn record;\n}\n\nfunction readPluginConfig(config: PluginConfig): CodingToolsPluginConfigV1 {\n\tconst version = config.version ?? 1;\n\tif (version !== 1) {\n\t\tthrow new Error(`coding-tools: unsupported config version ${JSON.stringify(version)}, expected 1`);\n\t}\n\tconst read = configObject(config.read, \"read\");\n\tconst bash = configObject(config.bash, \"bash\");\n\treturn {\n\t\tversion: 1,\n\t\tcwd: optionalString(config.cwd, \"cwd\"),\n\t\twriteScope: optionalWriteScope(config.writeScope, \"writeScope\"),\n\t\tread: { autoResizeImages: optionalBoolean(read.autoResizeImages, \"read.autoResizeImages\") },\n\t\tbash: {\n\t\t\tcommandPrefix: optionalString(bash.commandPrefix, \"bash.commandPrefix\"),\n\t\t\tshellPath: optionalString(bash.shellPath, \"bash.shellPath\"),\n\t\t},\n\t};\n}\n\nfunction createToolDefinitions(\n\tcwd: string,\n\toptions: CodingToolsPluginConfigV1,\n\treadonlyMode: boolean,\n\tcoordination: CoordinationLeaseCheckV1 | undefined,\n): readonly ToolDefinition[] {\n\tif (readonlyMode) {\n\t\t// Equivalent of the host's createReadOnlyToolDefinitions: only read\n\t\t// carries per-tool options there.\n\t\treturn [\n\t\t\tcreateReadToolDefinition(cwd, options.read),\n\t\t\tcreateGrepToolDefinition(cwd),\n\t\t\tcreateFindToolDefinition(cwd),\n\t\t\tcreateLsToolDefinition(cwd),\n\t\t];\n\t}\n\t// Equivalent of the host's createAllToolDefinitions: read and bash carry\n\t// per-tool options, the rest keep defaults.\n\treturn [\n\t\tcreateReadToolDefinition(cwd, options.read),\n\t\tcreateBashToolDefinition(cwd, options.bash),\n\t\tcreatePowerShellToolDefinition(cwd),\n\t\tcreateEditToolDefinition(cwd, {\n\t\t\t...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),\n\t\t\t...(coordination === undefined ? {} : { coordination }),\n\t\t}),\n\t\tcreateWriteToolDefinition(cwd, {\n\t\t\t...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),\n\t\t\t...(coordination === undefined ? {} : { coordination }),\n\t\t}),\n\t\tcreateGrepToolDefinition(cwd),\n\t\tcreateFindToolDefinition(cwd),\n\t\tcreateLsToolDefinition(cwd),\n\t];\n}\n\nexport function createCodingToolsCapability(api: CapabilityAPI): readonly DisposableRegistration[] {\n\tconst config = readPluginConfig(api.config);\n\tif (config.writeScope !== undefined) {\n\t\t// Verification face for a profile field the session cannot read back: the\n\t\t// runner log shows the exact scope the plugin received.\n\t\tapi.logger?.info(\"coding-tools write scope active\", { patterns: [...config.writeScope] });\n\t}\n\tconst cwd = config.cwd ?? process.cwd();\n\tconst readonlyMode = api.host.mode === \"readonly\";\n\tconst coordination = readonlyMode ? undefined : requireCoordination(api);\n\tconst definitions = createToolDefinitions(cwd, config, readonlyMode, coordination);\n\tconst registrations: DisposableRegistration[] = [];\n\tfor (const definition of definitions) {\n\t\tregistrations.push(api.registerTool(definition));\n\t}\n\treturn registrations;\n}\n"]}
@@ -12,6 +12,22 @@ import { createLsToolDefinition } from "./ls.js";
12
12
  import { createPowerShellToolDefinition } from "./powershell.js";
13
13
  import { createReadToolDefinition } from "./read.js";
14
14
  import { createWriteToolDefinition } from "./write.js";
15
+ /** Capability id of the advisory intent-lease provider (design §8.6). */
16
+ const FILE_COORDINATION_CAPABILITY_ID = "file.coordination";
17
+ /**
18
+ * Optional `file.coordination@1` consumer face. Absent provider (plugin
19
+ * disabled/replaced by something that is not loaded) = fast path, no advisory
20
+ * checks; the L0 lock and L1 version check are unaffected.
21
+ */
22
+ function requireCoordination(api) {
23
+ try {
24
+ const handle = api.capabilities.require(FILE_COORDINATION_CAPABILITY_ID, { version: 1 });
25
+ return { call: (request) => handle.call(request) };
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ }
15
31
  function optionalString(value, field) {
16
32
  if (value === undefined)
17
33
  return undefined;
@@ -28,6 +44,20 @@ function optionalBoolean(value, field) {
28
44
  }
29
45
  return value;
30
46
  }
47
+ function optionalWriteScope(value, field) {
48
+ if (value === undefined)
49
+ return undefined;
50
+ if (!Array.isArray(value))
51
+ throw new Error(`coding-tools: config.${field} must be an array of strings`);
52
+ const patterns = [];
53
+ for (const entry of value) {
54
+ if (typeof entry !== "string" || entry.trim() === "") {
55
+ throw new Error(`coding-tools: config.${field} entries must be non-empty strings`);
56
+ }
57
+ patterns.push(entry.trim().replace(/\\/g, "/"));
58
+ }
59
+ return patterns;
60
+ }
31
61
  function configObject(value, field) {
32
62
  if (value === undefined)
33
63
  return {};
@@ -53,6 +83,7 @@ function readPluginConfig(config) {
53
83
  return {
54
84
  version: 1,
55
85
  cwd: optionalString(config.cwd, "cwd"),
86
+ writeScope: optionalWriteScope(config.writeScope, "writeScope"),
56
87
  read: { autoResizeImages: optionalBoolean(read.autoResizeImages, "read.autoResizeImages") },
57
88
  bash: {
58
89
  commandPrefix: optionalString(bash.commandPrefix, "bash.commandPrefix"),
@@ -60,7 +91,7 @@ function readPluginConfig(config) {
60
91
  },
61
92
  };
62
93
  }
63
- function createToolDefinitions(cwd, options, readonlyMode) {
94
+ function createToolDefinitions(cwd, options, readonlyMode, coordination) {
64
95
  if (readonlyMode) {
65
96
  // Equivalent of the host's createReadOnlyToolDefinitions: only read
66
97
  // carries per-tool options there.
@@ -77,8 +108,14 @@ function createToolDefinitions(cwd, options, readonlyMode) {
77
108
  createReadToolDefinition(cwd, options.read),
78
109
  createBashToolDefinition(cwd, options.bash),
79
110
  createPowerShellToolDefinition(cwd),
80
- createEditToolDefinition(cwd),
81
- createWriteToolDefinition(cwd),
111
+ createEditToolDefinition(cwd, {
112
+ ...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),
113
+ ...(coordination === undefined ? {} : { coordination }),
114
+ }),
115
+ createWriteToolDefinition(cwd, {
116
+ ...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),
117
+ ...(coordination === undefined ? {} : { coordination }),
118
+ }),
82
119
  createGrepToolDefinition(cwd),
83
120
  createFindToolDefinition(cwd),
84
121
  createLsToolDefinition(cwd),
@@ -86,9 +123,15 @@ function createToolDefinitions(cwd, options, readonlyMode) {
86
123
  }
87
124
  export function createCodingToolsCapability(api) {
88
125
  const config = readPluginConfig(api.config);
126
+ if (config.writeScope !== undefined) {
127
+ // Verification face for a profile field the session cannot read back: the
128
+ // runner log shows the exact scope the plugin received.
129
+ api.logger?.info("coding-tools write scope active", { patterns: [...config.writeScope] });
130
+ }
89
131
  const cwd = config.cwd ?? process.cwd();
90
132
  const readonlyMode = api.host.mode === "readonly";
91
- const definitions = createToolDefinitions(cwd, config, readonlyMode);
133
+ const coordination = readonlyMode ? undefined : requireCoordination(api);
134
+ const definitions = createToolDefinitions(cwd, config, readonlyMode, coordination);
92
135
  const registrations = [];
93
136
  for (const definition of definitions) {
94
137
  registrations.push(api.registerTool(definition));
@@ -1 +1 @@
1
- {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Capability factory for the coding tools plugin: registers the default coding\n * tool definitions through the public `tools` capability, mirroring the host's\n * embedded `installSessionBootstrapTools` semantics (readonly mode keeps only\n * the readonly-safe read/grep/find/ls face).\n */\n\nimport type { ToolDefinition } from \"@agent-forge/host/tool-definition\";\nimport type { CapabilityAPI, DisposableRegistration, PluginConfig, PluginConfigValue } from \"@agent-forge/plugin-sdk\";\nimport { createBashToolDefinition } from \"./bash.ts\";\nimport { createEditToolDefinition } from \"./edit.ts\";\nimport { createFindToolDefinition } from \"./find.ts\";\nimport { createGrepToolDefinition } from \"./grep.ts\";\nimport { createLsToolDefinition } from \"./ls.ts\";\nimport { createPowerShellToolDefinition } from \"./powershell.ts\";\nimport { createReadToolDefinition } from \"./read.ts\";\nimport { createWriteToolDefinition } from \"./write.ts\";\n\n/**\n * Plugin manifest config shape (`agent-forge.json` config face). Every field\n * is optional; absent fields keep each tool's existing defaults.\n */\nexport interface CodingToolsPluginConfigV1 {\n\tversion: 1;\n\t/** Working directory handed to every tool definition. */\n\tcwd?: string;\n\tread?: { autoResizeImages?: boolean };\n\tbash?: { commandPrefix?: string; shellPath?: string };\n}\n\nfunction optionalString(value: PluginConfigValue | undefined, field: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a string`);\n\t}\n\treturn value;\n}\n\nfunction optionalBoolean(value: PluginConfigValue | undefined, field: string): boolean | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"boolean\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a boolean`);\n\t}\n\treturn value;\n}\n\nfunction configObject(\n\tvalue: PluginConfigValue | undefined,\n\tfield: string,\n): Readonly<Record<string, PluginConfigValue>> {\n\tif (value === undefined) return {};\n\tif (value === null || Array.isArray(value) || typeof value !== \"object\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be an object`);\n\t}\n\t// The array member of the PluginConfigValue union cannot be excluded by\n\t// narrowing, so project the validated object into a typed record instead\n\t// of casting.\n\tconst record: Record<string, PluginConfigValue> = {};\n\tfor (const [key, entry] of Object.entries(value)) {\n\t\trecord[key] = entry;\n\t}\n\treturn record;\n}\n\nfunction readPluginConfig(config: PluginConfig): CodingToolsPluginConfigV1 {\n\tconst version = config.version ?? 1;\n\tif (version !== 1) {\n\t\tthrow new Error(`coding-tools: unsupported config version ${JSON.stringify(version)}, expected 1`);\n\t}\n\tconst read = configObject(config.read, \"read\");\n\tconst bash = configObject(config.bash, \"bash\");\n\treturn {\n\t\tversion: 1,\n\t\tcwd: optionalString(config.cwd, \"cwd\"),\n\t\tread: { autoResizeImages: optionalBoolean(read.autoResizeImages, \"read.autoResizeImages\") },\n\t\tbash: {\n\t\t\tcommandPrefix: optionalString(bash.commandPrefix, \"bash.commandPrefix\"),\n\t\t\tshellPath: optionalString(bash.shellPath, \"bash.shellPath\"),\n\t\t},\n\t};\n}\n\nfunction createToolDefinitions(\n\tcwd: string,\n\toptions: CodingToolsPluginConfigV1,\n\treadonlyMode: boolean,\n): readonly ToolDefinition[] {\n\tif (readonlyMode) {\n\t\t// Equivalent of the host's createReadOnlyToolDefinitions: only read\n\t\t// carries per-tool options there.\n\t\treturn [\n\t\t\tcreateReadToolDefinition(cwd, options.read),\n\t\t\tcreateGrepToolDefinition(cwd),\n\t\t\tcreateFindToolDefinition(cwd),\n\t\t\tcreateLsToolDefinition(cwd),\n\t\t];\n\t}\n\t// Equivalent of the host's createAllToolDefinitions: read and bash carry\n\t// per-tool options, the rest keep defaults.\n\treturn [\n\t\tcreateReadToolDefinition(cwd, options.read),\n\t\tcreateBashToolDefinition(cwd, options.bash),\n\t\tcreatePowerShellToolDefinition(cwd),\n\t\tcreateEditToolDefinition(cwd),\n\t\tcreateWriteToolDefinition(cwd),\n\t\tcreateGrepToolDefinition(cwd),\n\t\tcreateFindToolDefinition(cwd),\n\t\tcreateLsToolDefinition(cwd),\n\t];\n}\n\nexport function createCodingToolsCapability(api: CapabilityAPI): readonly DisposableRegistration[] {\n\tconst config = readPluginConfig(api.config);\n\tconst cwd = config.cwd ?? process.cwd();\n\tconst readonlyMode = api.host.mode === \"readonly\";\n\tconst definitions = createToolDefinitions(cwd, config, readonlyMode);\n\tconst registrations: DisposableRegistration[] = [];\n\tfor (const definition of definitions) {\n\t\tregistrations.push(api.registerTool(definition));\n\t}\n\treturn registrations;\n}\n"]}
1
+ {"version":3,"file":"capability.js","sourceRoot":"","sources":["../src/capability.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Capability factory for the coding tools plugin: registers the default coding\n * tool definitions through the public `tools` capability, mirroring the host's\n * embedded `installSessionBootstrapTools` semantics (readonly mode keeps only\n * the readonly-safe read/grep/find/ls face).\n */\n\nimport type { ToolDefinition } from \"@agent-forge/host/tool-definition\";\nimport type { CapabilityAPI, DisposableRegistration, PluginConfig, PluginConfigValue } from \"@agent-forge/plugin-sdk\";\nimport { createBashToolDefinition } from \"./bash.ts\";\nimport { createEditToolDefinition } from \"./edit.ts\";\nimport { createFindToolDefinition } from \"./find.ts\";\nimport { createGrepToolDefinition } from \"./grep.ts\";\nimport type { CoordinationLeaseCheckV1 } from \"./lease-check.ts\";\nimport { createLsToolDefinition } from \"./ls.ts\";\nimport { createPowerShellToolDefinition } from \"./powershell.ts\";\nimport { createReadToolDefinition } from \"./read.ts\";\nimport { createWriteToolDefinition } from \"./write.ts\";\n\n/** Capability id of the advisory intent-lease provider (design §8.6). */\nconst FILE_COORDINATION_CAPABILITY_ID = \"file.coordination\";\n\n/**\n * Optional `file.coordination@1` consumer face. Absent provider (plugin\n * disabled/replaced by something that is not loaded) = fast path, no advisory\n * checks; the L0 lock and L1 version check are unaffected.\n */\nfunction requireCoordination(api: CapabilityAPI): CoordinationLeaseCheckV1 | undefined {\n\ttry {\n\t\tconst handle = api.capabilities.require(FILE_COORDINATION_CAPABILITY_ID, { version: 1 });\n\t\treturn { call: (request) => handle.call(request) };\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\n/**\n * Plugin manifest config shape (`agent-forge.json` config face). Every field\n * is optional; absent fields keep each tool's existing defaults.\n */\nexport interface CodingToolsPluginConfigV1 {\n\tversion: 1;\n\t/** Working directory handed to every tool definition. */\n\tcwd?: string;\n\t/** Relative write-path globs for edit/write (child task profile); absent = unrestricted. */\n\twriteScope?: readonly string[];\n\tread?: { autoResizeImages?: boolean };\n\tbash?: { commandPrefix?: string; shellPath?: string };\n}\n\nfunction optionalString(value: PluginConfigValue | undefined, field: string): string | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"string\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a string`);\n\t}\n\treturn value;\n}\n\nfunction optionalBoolean(value: PluginConfigValue | undefined, field: string): boolean | undefined {\n\tif (value === undefined) return undefined;\n\tif (typeof value !== \"boolean\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be a boolean`);\n\t}\n\treturn value;\n}\n\nfunction optionalWriteScope(value: PluginConfigValue | undefined, field: string): readonly string[] | undefined {\n\tif (value === undefined) return undefined;\n\tif (!Array.isArray(value)) throw new Error(`coding-tools: config.${field} must be an array of strings`);\n\tconst patterns: string[] = [];\n\tfor (const entry of value) {\n\t\tif (typeof entry !== \"string\" || entry.trim() === \"\") {\n\t\t\tthrow new Error(`coding-tools: config.${field} entries must be non-empty strings`);\n\t\t}\n\t\tpatterns.push(entry.trim().replace(/\\\\/g, \"/\"));\n\t}\n\treturn patterns;\n}\n\nfunction configObject(\n\tvalue: PluginConfigValue | undefined,\n\tfield: string,\n): Readonly<Record<string, PluginConfigValue>> {\n\tif (value === undefined) return {};\n\tif (value === null || Array.isArray(value) || typeof value !== \"object\") {\n\t\tthrow new Error(`coding-tools: config.${field} must be an object`);\n\t}\n\t// The array member of the PluginConfigValue union cannot be excluded by\n\t// narrowing, so project the validated object into a typed record instead\n\t// of casting.\n\tconst record: Record<string, PluginConfigValue> = {};\n\tfor (const [key, entry] of Object.entries(value)) {\n\t\trecord[key] = entry;\n\t}\n\treturn record;\n}\n\nfunction readPluginConfig(config: PluginConfig): CodingToolsPluginConfigV1 {\n\tconst version = config.version ?? 1;\n\tif (version !== 1) {\n\t\tthrow new Error(`coding-tools: unsupported config version ${JSON.stringify(version)}, expected 1`);\n\t}\n\tconst read = configObject(config.read, \"read\");\n\tconst bash = configObject(config.bash, \"bash\");\n\treturn {\n\t\tversion: 1,\n\t\tcwd: optionalString(config.cwd, \"cwd\"),\n\t\twriteScope: optionalWriteScope(config.writeScope, \"writeScope\"),\n\t\tread: { autoResizeImages: optionalBoolean(read.autoResizeImages, \"read.autoResizeImages\") },\n\t\tbash: {\n\t\t\tcommandPrefix: optionalString(bash.commandPrefix, \"bash.commandPrefix\"),\n\t\t\tshellPath: optionalString(bash.shellPath, \"bash.shellPath\"),\n\t\t},\n\t};\n}\n\nfunction createToolDefinitions(\n\tcwd: string,\n\toptions: CodingToolsPluginConfigV1,\n\treadonlyMode: boolean,\n\tcoordination: CoordinationLeaseCheckV1 | undefined,\n): readonly ToolDefinition[] {\n\tif (readonlyMode) {\n\t\t// Equivalent of the host's createReadOnlyToolDefinitions: only read\n\t\t// carries per-tool options there.\n\t\treturn [\n\t\t\tcreateReadToolDefinition(cwd, options.read),\n\t\t\tcreateGrepToolDefinition(cwd),\n\t\t\tcreateFindToolDefinition(cwd),\n\t\t\tcreateLsToolDefinition(cwd),\n\t\t];\n\t}\n\t// Equivalent of the host's createAllToolDefinitions: read and bash carry\n\t// per-tool options, the rest keep defaults.\n\treturn [\n\t\tcreateReadToolDefinition(cwd, options.read),\n\t\tcreateBashToolDefinition(cwd, options.bash),\n\t\tcreatePowerShellToolDefinition(cwd),\n\t\tcreateEditToolDefinition(cwd, {\n\t\t\t...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),\n\t\t\t...(coordination === undefined ? {} : { coordination }),\n\t\t}),\n\t\tcreateWriteToolDefinition(cwd, {\n\t\t\t...(options.writeScope === undefined ? {} : { writeScope: options.writeScope }),\n\t\t\t...(coordination === undefined ? {} : { coordination }),\n\t\t}),\n\t\tcreateGrepToolDefinition(cwd),\n\t\tcreateFindToolDefinition(cwd),\n\t\tcreateLsToolDefinition(cwd),\n\t];\n}\n\nexport function createCodingToolsCapability(api: CapabilityAPI): readonly DisposableRegistration[] {\n\tconst config = readPluginConfig(api.config);\n\tif (config.writeScope !== undefined) {\n\t\t// Verification face for a profile field the session cannot read back: the\n\t\t// runner log shows the exact scope the plugin received.\n\t\tapi.logger?.info(\"coding-tools write scope active\", { patterns: [...config.writeScope] });\n\t}\n\tconst cwd = config.cwd ?? process.cwd();\n\tconst readonlyMode = api.host.mode === \"readonly\";\n\tconst coordination = readonlyMode ? undefined : requireCoordination(api);\n\tconst definitions = createToolDefinitions(cwd, config, readonlyMode, coordination);\n\tconst registrations: DisposableRegistration[] = [];\n\tfor (const definition of definitions) {\n\t\tregistrations.push(api.registerTool(definition));\n\t}\n\treturn registrations;\n}\n"]}
package/dist/edit.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  import { type EditDiffError, type EditDiffResult } from "@agent-forge/host/edit-diff";
2
+ import { type StatVersionV1 } from "@agent-forge/host/file-lock";
2
3
  import type { ToolDefinition } from "@agent-forge/host/tool-definition";
3
4
  import { Box } from "@agent-forge/tui";
4
5
  import { type Static, Type } from "typebox";
6
+ import { type CoordinationLeaseCheckV1 } from "./lease-check.ts";
5
7
  import { type AgentTool } from "./types.ts";
6
8
  type EditPreview = EditDiffResult | EditDiffError;
7
9
  type EditRenderState = {
@@ -26,6 +28,14 @@ export interface EditToolDetails {
26
28
  patch: string;
27
29
  /** Line number of the first change in the new file (for editor navigation) */
28
30
  firstChangedLine?: number;
31
+ /** Lines added by the edit (empty text is 0 lines; "a" and "a\n" are both 1 line). */
32
+ addedLines: number;
33
+ /** Lines removed by the edit (same counting semantics as addedLines). */
34
+ deletedLines: number;
35
+ /** Present only when the operations could not commit atomically (design §13.2 tier B). */
36
+ coordination?: {
37
+ atomic: false;
38
+ };
29
39
  }
30
40
  /**
31
41
  * Pluggable operations for the edit tool.
@@ -38,11 +48,26 @@ export interface EditOperations {
38
48
  writeFile: (absolutePath: string, content: string) => Promise<void>;
39
49
  /** Check if file is readable and writable (throw if not) */
40
50
  access: (absolutePath: string) => Promise<void>;
51
+ /**
52
+ * Atomic-commit tier (design §13.2). All three must be present to enter it:
53
+ * version facts, a complete temp file, and a synchronous replace. Remote
54
+ * operations that cannot supply a synchronous primitive stay on the
55
+ * local-mutex tier and are reported as `atomic: false`.
56
+ */
57
+ statVersion?: (absolutePath: string) => Promise<StatVersionV1 | undefined>;
58
+ writeTemp?: (absolutePath: string, content: string) => Promise<string>;
59
+ commitSync?: (tempPath: string, targetPath: string) => void;
41
60
  }
42
61
  export interface EditToolOptions {
43
62
  /** Custom operations for file editing. Default: local filesystem */
44
63
  operations?: EditOperations;
64
+ /** Relative write-path globs from the child task profile; absent = unrestricted. */
65
+ writeScope?: readonly string[];
66
+ /** `file.coordination@1` consumer face; absent = no advisory lease checks. */
67
+ coordination?: CoordinationLeaseCheckV1;
45
68
  }
69
+ /** True when the operations can commit atomically (design §13.2 tier A). */
70
+ export declare function hasAtomicCommitOperations(ops: EditOperations): ops is EditOperations & Required<Pick<EditOperations, "statVersion" | "writeTemp" | "commitSync">>;
46
71
  type EditCallRenderComponent = Box & {
47
72
  preview?: EditPreview;
48
73
  previewArgsKey?: string;
@@ -1 +1 @@
1
- {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../src/edit.ts"],"names":[],"mappings":"AAMA,OAAO,EAKN,KAAK,aAAa,EAClB,KAAK,cAAc,EAKnB,MAAM,6BAA6B,CAAC;AAOrC,OAAO,KAAK,EAAE,cAAc,EAAsB,MAAM,mCAAmC,CAAC;AAC5F,OAAO,EAAE,GAAG,EAA2B,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,KAAK,SAAS,EAAsB,MAAM,YAAY,CAAC;AAEhE,KAAK,WAAW,GAAG,cAAc,GAAG,aAAa,CAAC;AAElD,KAAK,eAAe,GAAG;IACtB,aAAa,CAAC,EAAE,uBAAuB,CAAC;CACxC,CAAC;AAaF,QAAA,MAAM,UAAU;;;;;;EASf,CAAC;AAEF,eAAO,MAAM,gCAAgC;;;CAQnC,CAAC;AAEX,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,UAAU,CAAC,CAAC;AAiBtD,MAAM,WAAW,eAAe;IAC/B,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd,8EAA8E;IAC9E,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc;IAC9B,qCAAqC;IACrC,QAAQ,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACpD,8BAA8B;IAC9B,SAAS,EAAE,CAAC,YAAY,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpE,4DAA4D;IAC5D,MAAM,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAQD,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,UAAU,CAAC,EAAE,cAAc,CAAC;CAC5B;AAuDD,KAAK,uBAAuB,GAAG,GAAG,GAAG;IACpC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,YAAY,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAmJF,wBAAgB,wBAAwB,CACvC,GAAG,EAAE,MAAM,EACX,OAAO,CAAC,EAAE,eAAe,GACvB,cAAc,CAAC,OAAO,UAAU,EAAE,eAAe,GAAG,SAAS,EAAE,eAAe,CAAC,CA2IjF;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,eAAe,GAAG,SAAS,CAAC,OAAO,UAAU,CAAC,CAEnG","sourcesContent":["// Migrated from packages/agent-forge/src/core/tools/edit (batch 1, copy-then-delete);\n// host modules are imported through supply keys (@agent-forge/host/*).\n\nimport { constants } from \"node:fs\";\nimport { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from \"node:fs/promises\";\nimport { renderDiff } from \"@agent-forge/host/diff\";\nimport {\n\tapplyEditsToNormalizedContent,\n\tcomputeEditsDiff,\n\tdetectLineEnding,\n\ttype Edit,\n\ttype EditDiffError,\n\ttype EditDiffResult,\n\tgenerateDiffString,\n\tgenerateUnifiedPatch,\n\tnormalizeToLF,\n\trestoreLineEndings,\n} from \"@agent-forge/host/edit-diff\";\nimport { withFileMutationQueue } from \"@agent-forge/host/file-mutation-queue\";\nimport { t } from \"@agent-forge/host/i18n\";\nimport { resolveToCwd } from \"@agent-forge/host/path-utils\";\nimport { renderToolPath, str } from \"@agent-forge/host/render-utils\";\nimport { splitBom } from \"@agent-forge/host/text\";\nimport type { Theme } from \"@agent-forge/host/theme\";\nimport type { ToolDefinition, ToolExecuteContext } from \"@agent-forge/host/tool-definition\";\nimport { Box, Container, Spacer, Text } from \"@agent-forge/tui\";\nimport { type Static, Type } from \"typebox\";\nimport { type AgentTool, wrapToolDefinition } from \"./types.ts\";\n\ntype EditPreview = EditDiffResult | EditDiffError;\n\ntype EditRenderState = {\n\tcallComponent?: EditCallRenderComponent;\n};\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{},\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.\",\n\t\t}),\n\t},\n\t{},\n);\n\nexport const editToolSystemPromptContribution = {\n\tsnippet: \"Make precise file edits with exact text replacement, including multiple disjoint edits in one call\",\n\tguidelines: [\n\t\t\"Use edit for precise changes (edits[].oldText must match exactly)\",\n\t\t\"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\",\n\t\t\"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\",\n\t\t\"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\",\n\t],\n} as const;\n\nexport type EditToolInput = Static<typeof editSchema>;\ntype LegacyEditToolInput = EditToolInput & {\n\toldText?: unknown;\n\tnewText?: unknown;\n};\n\ntype SingleEditInput = { oldText: string; newText: string };\n\nfunction isSingleEditInput(value: unknown): value is SingleEditInput {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn false;\n\t}\n\n\tconst edit = value as Record<string, unknown>;\n\treturn typeof edit.oldText === \"string\" && typeof edit.newText === \"string\";\n}\n\nexport interface EditToolDetails {\n\t/** Display-oriented diff of the changes made */\n\tdiff: string;\n\t/** Standard unified patch of the changes made */\n\tpatch: string;\n\t/** Line number of the first change in the new file (for editor navigation) */\n\tfirstChangedLine?: number;\n}\n\n/**\n * Pluggable operations for the edit tool.\n * Override these to delegate file editing to remote systems (for example SSH).\n */\nexport interface EditOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Check if file is readable and writable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n}\n\nconst defaultEditOperations: EditOperations = {\n\treadFile: (path) => fsReadFile(path),\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\taccess: (path) => fsAccess(path, constants.R_OK | constants.W_OK),\n};\n\nexport interface EditToolOptions {\n\t/** Custom operations for file editing. Default: local filesystem */\n\toperations?: EditOperations;\n}\n\nfunction prepareEditArguments(input: unknown): EditToolInput {\n\tif (!input || typeof input !== \"object\") {\n\t\treturn input as EditToolInput;\n\t}\n\n\tconst args = input as Record<string, unknown>;\n\n\t// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array.\n\t// Others send a single edit object instead of a one-element edits array.\n\tif (typeof args.edits === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(args.edits);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\targs.edits = parsed;\n\t\t\t} else if (isSingleEditInput(parsed)) {\n\t\t\t\targs.edits = [parsed];\n\t\t\t}\n\t\t} catch {}\n\t} else if (isSingleEditInput(args.edits)) {\n\t\targs.edits = [args.edits];\n\t}\n\n\tconst legacy = args as LegacyEditToolInput;\n\tif (typeof legacy.oldText !== \"string\" || typeof legacy.newText !== \"string\") {\n\t\treturn args as EditToolInput;\n\t}\n\n\tconst edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];\n\tedits.push({ oldText: legacy.oldText, newText: legacy.newText });\n\tconst { oldText: _oldText, newText: _newText, ...rest } = legacy;\n\treturn { ...rest, edits } as EditToolInput;\n}\n\nfunction validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {\n\tif (!Array.isArray(input.edits) || input.edits.length === 0) {\n\t\tthrow new Error(t(\"Edit tool input is invalid. edits must contain at least one replacement.\"));\n\t}\n\treturn { path: input.path, edits: input.edits };\n}\n\ntype RenderableEditArgs = {\n\tpath?: string;\n\tfile_path?: string;\n\tedits?: Edit[];\n\toldText?: string;\n\tnewText?: string;\n};\n\ntype EditToolResultLike = {\n\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\tdetails?: EditToolDetails;\n};\n\ntype EditCallRenderComponent = Box & {\n\tpreview?: EditPreview;\n\tpreviewArgsKey?: string;\n\tpreviewPending?: boolean;\n\tsettledError?: boolean;\n};\n\nfunction createEditCallRenderComponent(): EditCallRenderComponent {\n\treturn Object.assign(new Box(1, 1, (text: string) => text), {\n\t\tpreview: undefined as EditPreview | undefined,\n\t\tpreviewArgsKey: undefined as string | undefined,\n\t\tpreviewPending: false,\n\t\tsettledError: false,\n\t});\n}\n\nfunction getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {\n\tif (lastComponent instanceof Box) {\n\t\tconst component = lastComponent as EditCallRenderComponent;\n\t\tstate.callComponent = component;\n\t\treturn component;\n\t}\n\tif (state.callComponent) {\n\t\treturn state.callComponent;\n\t}\n\tconst component = createEditCallRenderComponent();\n\tstate.callComponent = component;\n\treturn component;\n}\n\nfunction getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path: string; edits: Edit[] } | null {\n\tif (!args) {\n\t\treturn null;\n\t}\n\n\tconst path = typeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\tif (!path) {\n\t\treturn null;\n\t}\n\n\tif (\n\t\tArray.isArray(args.edits) &&\n\t\targs.edits.length > 0 &&\n\t\targs.edits.every((edit) => typeof edit?.oldText === \"string\" && typeof edit?.newText === \"string\")\n\t) {\n\t\treturn { path, edits: args.edits };\n\t}\n\n\tif (typeof args.oldText === \"string\" && typeof args.newText === \"string\") {\n\t\treturn { path, edits: [{ oldText: args.oldText, newText: args.newText }] };\n\t}\n\n\treturn null;\n}\n\nfunction formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {\n\tconst pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n}\n\nfunction formatEditResult(\n\targs: RenderableEditArgs | undefined,\n\tpreview: EditPreview | undefined,\n\tresult: EditToolResultLike,\n\ttheme: Theme,\n\tisError: boolean,\n): string | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst previewDiff = preview && !(\"error\" in preview) ? preview.diff : undefined;\n\tconst previewError = preview && \"error\" in preview ? preview.error : undefined;\n\tif (isError) {\n\t\tconst errorText = result.content\n\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t.map((c) => c.text || \"\")\n\t\t\t.join(\"\\n\");\n\t\tif (!errorText || errorText === previewError) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn theme.fg(\"error\", errorText);\n\t}\n\n\tconst resultDiff = result.details?.diff;\n\tif (resultDiff && resultDiff !== previewDiff) {\n\t\treturn renderDiff(resultDiff, { filePath: rawPath ?? undefined });\n\t}\n\n\treturn undefined;\n}\n\nfunction getEditHeaderBg(\n\tpreview: EditPreview | undefined,\n\tsettledError: boolean | undefined,\n\ttheme: Theme,\n): (text: string) => string {\n\tif (preview) {\n\t\tif (\"error\" in preview) {\n\t\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t\t}\n\t\treturn (text: string) => theme.bg(\"toolSuccessBg\", text);\n\t}\n\tif (settledError) {\n\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t}\n\treturn (text: string) => theme.bg(\"toolPendingBg\", text);\n}\n\nfunction buildEditCallComponent(\n\tcomponent: EditCallRenderComponent,\n\targs: RenderableEditArgs | undefined,\n\ttheme: Theme,\n\tcwd: string,\n\toutputPad: number,\n): EditCallRenderComponent {\n\t// renderShell:\"self\" tools consume ToolRenderContext.outputPad themselves\n\t// (see tool-definition.ts); components are reused across renders, so the\n\t// padding must be refreshed on every build instead of only at construction.\n\tcomponent.setPaddingX(outputPad);\n\tcomponent.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));\n\tcomponent.clear();\n\tcomponent.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));\n\n\tif (!component.preview) {\n\t\treturn component;\n\t}\n\n\tconst body =\n\t\t\"error\" in component.preview ? theme.fg(\"error\", component.preview.error) : renderDiff(component.preview.diff);\n\tcomponent.addChild(new Spacer(1));\n\tcomponent.addChild(new Text(body, 0, 0));\n\treturn component;\n}\n\nfunction setEditPreview(\n\tcomponent: EditCallRenderComponent,\n\tpreview: EditPreview,\n\targsKey: string | undefined,\n): boolean {\n\tconst current = component.preview;\n\tconst changed =\n\t\tcurrent === undefined ||\n\t\t(\"error\" in current && \"error\" in preview\n\t\t\t? current.error !== preview.error\n\t\t\t: \"error\" in current !== \"error\" in preview) ||\n\t\t(!(\"error\" in current) &&\n\t\t\t!(\"error\" in preview) &&\n\t\t\t(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));\n\tcomponent.preview = preview;\n\tcomponent.previewArgsKey = argsKey;\n\tcomponent.previewPending = false;\n\treturn changed;\n}\n\nexport function createEditToolDefinition(\n\tcwd: string,\n\toptions?: EditToolOptions,\n): ToolDefinition<typeof editSchema, EditToolDetails | undefined, EditRenderState> {\n\tconst ops = options?.operations ?? defaultEditOperations;\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.\",\n\t\tpromptSnippet: editToolSystemPromptContribution.snippet,\n\t\tpromptGuidelines: [...editToolSystemPromptContribution.guidelines],\n\t\tparameters: editSchema,\n\t\tconstrainedSampling: { type: \"json_schema\", strict: \"prefer\" },\n\t\trenderShell: \"self\",\n\t\tprepareArguments: prepareEditArguments,\n\t\tasync execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, ctx?: ToolExecuteContext) {\n\t\t\tconst { path, edits } = validateEditInput(input);\n\t\t\tconst absolutePath = resolveToCwd(path, ctx?.cwd || cwd);\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(t(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Check if file exists.\n\t\t\t\ttry {\n\t\t\t\t\tawait ops.access(absolutePath);\n\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\tthrowIfAborted();\n\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\terror instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\t\t\tthrow new Error(t(\"Could not edit file: {path}. {errorMessage}.\", { path, errorMessage }));\n\t\t\t\t}\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Read the file.\n\t\t\t\tconst buffer = await ops.readFile(absolutePath);\n\t\t\t\tconst rawContent = buffer.toString(\"utf-8\");\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\t// Strip BOM before matching. The model will not include an invisible BOM in oldText.\n\t\t\t\tconst { bom, text: content } = splitBom(rawContent);\n\t\t\t\tconst originalEnding = detectLineEnding(content);\n\t\t\t\tconst normalizedContent = normalizeToLF(content);\n\t\t\t\tconst { baseContent, newContent } = applyEditsToNormalizedContent(normalizedContent, edits, path);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst finalContent = bom + restoreLineEndings(newContent, originalEnding);\n\t\t\t\tawait ops.writeFile(absolutePath, finalContent);\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst diffResult = generateDiffString(baseContent, newContent);\n\t\t\t\tconst patch = generateUnifiedPatch(path, baseContent, newContent);\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\ttext: `Successfully replaced ${edits.length} block(s) in ${path}.`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t\tdetails: { diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },\n\t\t\t\t};\n\t\t\t});\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst component = getEditCallRenderComponent(context.state, context.lastComponent);\n\t\t\tconst previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\n\t\t\tif (component.previewArgsKey !== argsKey) {\n\t\t\t\tcomponent.preview = undefined;\n\t\t\t\tcomponent.previewArgsKey = argsKey;\n\t\t\t\tcomponent.previewPending = false;\n\t\t\t\tcomponent.settledError = false;\n\t\t\t}\n\n\t\t\tif (context.argsComplete && previewInput && !component.preview && !component.previewPending) {\n\t\t\t\tcomponent.previewPending = true;\n\t\t\t\tconst requestKey = argsKey;\n\t\t\t\tvoid computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {\n\t\t\t\t\tif (component.previewArgsKey === requestKey) {\n\t\t\t\t\t\tsetEditPreview(component, preview, requestKey);\n\t\t\t\t\t\tcontext.invalidate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn buildEditCallComponent(component, args, theme, context.cwd, context.outputPad);\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst callComponent = context.state.callComponent;\n\t\t\tconst previewInput = getRenderablePreviewInput(context.args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\t\t\tconst typedResult = result as EditToolResultLike;\n\t\t\tconst resultDiff = !context.isError ? typedResult.details?.diff : undefined;\n\t\t\tlet changed = false;\n\t\t\tif (callComponent) {\n\t\t\t\tif (typeof resultDiff === \"string\") {\n\t\t\t\t\tchanged =\n\t\t\t\t\t\tsetEditPreview(\n\t\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\t\t{ diff: resultDiff, firstChangedLine: typedResult.details?.firstChangedLine },\n\t\t\t\t\t\t\targsKey,\n\t\t\t\t\t\t) || changed;\n\t\t\t\t}\n\t\t\t\tif (callComponent.settledError !== context.isError) {\n\t\t\t\t\tcallComponent.settledError = context.isError;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tif (changed) {\n\t\t\t\t\tbuildEditCallComponent(\n\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\tcontext.args as RenderableEditArgs | undefined,\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t\tcontext.outputPad,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output = formatEditResult(context.args, callComponent?.preview, typedResult, theme, context.isError);\n\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\tcomponent.clear();\n\t\t\tif (!output) {\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tcomponent.addChild(new Spacer(1));\n\t\t\tcomponent.addChild(new Text(output, context.outputPad, 0));\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {\n\treturn wrapToolDefinition(createEditToolDefinition(cwd, options));\n}\n"]}
1
+ {"version":3,"file":"edit.d.ts","sourceRoot":"","sources":["../src/edit.ts"],"names":[],"mappings":"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","sourcesContent":["// Migrated from packages/agent-forge/src/core/tools/edit (batch 1, copy-then-delete);\n// host modules are imported through supply keys (@agent-forge/host/*).\n\nimport { randomUUID } from \"node:crypto\";\nimport { constants, renameSync } from \"node:fs\";\nimport { access as fsAccess, readFile as fsReadFile, writeFile as fsWriteFile } from \"node:fs/promises\";\nimport { renderDiff } from \"@agent-forge/host/diff\";\nimport {\n\tapplyEditsToNormalizedContent,\n\tcomputeEditsDiff,\n\tdetectLineEnding,\n\ttype Edit,\n\ttype EditDiffError,\n\ttype EditDiffResult,\n\tgenerateDiffString,\n\tgenerateUnifiedPatch,\n\tnormalizeToLF,\n\trestoreLineEndings,\n} from \"@agent-forge/host/edit-diff\";\nimport {\n\tFileCoordinationError,\n\ttype FileMutationLockHandleV1,\n\ttype StatVersionV1,\n\tsha256Hex,\n\tstatLooksChanged,\n\tstatVersionOf,\n\twithFileMutationLock,\n} from \"@agent-forge/host/file-lock\";\nimport { withFileMutationQueue } from \"@agent-forge/host/file-mutation-queue\";\nimport { t } from \"@agent-forge/host/i18n\";\nimport { resolveToCwd } from \"@agent-forge/host/path-utils\";\nimport { formatPathRelativeToCwdOrAbsolute } from \"@agent-forge/host/paths\";\nimport { renderToolPath, str } from \"@agent-forge/host/render-utils\";\nimport { splitBom } from \"@agent-forge/host/text\";\nimport type { Theme } from \"@agent-forge/host/theme\";\nimport type { ToolDefinition, ToolExecuteContext } from \"@agent-forge/host/tool-definition\";\nimport { Box, Container, Spacer, Text } from \"@agent-forge/tui\";\nimport { type Static, Type } from \"typebox\";\nimport { commitPreparedContent } from \"./atomic-ops.ts\";\nimport { type CoordinationLeaseCheckV1, checkIntentLease, editHitRange } from \"./lease-check.ts\";\nimport { type FileSnapshotV1, readSnapshot } from \"./snapshot-ops.ts\";\nimport { type AgentTool, wrapToolDefinition } from \"./types.ts\";\nimport { recordVersion } from \"./version-registry.ts\";\nimport { assertWithinWriteScope } from \"./write-scope.ts\";\n\ntype EditPreview = EditDiffResult | EditDiffError;\n\ntype EditRenderState = {\n\tcallComponent?: EditCallRenderComponent;\n};\n\nconst replaceEditSchema = Type.Object(\n\t{\n\t\toldText: Type.String({\n\t\t\tdescription:\n\t\t\t\t\"Exact text for one targeted replacement. It must be unique in the original file and must not overlap with any other edits[].oldText in the same call.\",\n\t\t}),\n\t\tnewText: Type.String({ description: \"Replacement text for this targeted edit.\" }),\n\t},\n\t{},\n);\n\nconst editSchema = Type.Object(\n\t{\n\t\tpath: Type.String({ description: \"Path to the file to edit (relative or absolute)\" }),\n\t\tedits: Type.Array(replaceEditSchema, {\n\t\t\tdescription:\n\t\t\t\t\"One or more targeted replacements. Each edit is matched against the original file, not incrementally. Do not include overlapping or nested edits. If two changes touch the same block or nearby lines, merge them into one edit instead.\",\n\t\t}),\n\t},\n\t{},\n);\n\nexport const editToolSystemPromptContribution = {\n\tsnippet: \"Make precise file edits with exact text replacement, including multiple disjoint edits in one call\",\n\tguidelines: [\n\t\t\"Use edit for precise changes (edits[].oldText must match exactly)\",\n\t\t\"When changing multiple separate locations in one file, use one edit call with multiple entries in edits[] instead of multiple edit calls\",\n\t\t\"Each edits[].oldText is matched against the original file, not after earlier edits are applied. Do not emit overlapping or nested edits. Merge nearby changes into one edit.\",\n\t\t\"Keep edits[].oldText as small as possible while still being unique in the file. Do not pad with large unchanged regions.\",\n\t],\n} as const;\n\nexport type EditToolInput = Static<typeof editSchema>;\ntype LegacyEditToolInput = EditToolInput & {\n\toldText?: unknown;\n\tnewText?: unknown;\n};\n\ntype SingleEditInput = { oldText: string; newText: string };\n\nfunction isSingleEditInput(value: unknown): value is SingleEditInput {\n\tif (!value || typeof value !== \"object\" || Array.isArray(value)) {\n\t\treturn false;\n\t}\n\n\tconst edit = value as Record<string, unknown>;\n\treturn typeof edit.oldText === \"string\" && typeof edit.newText === \"string\";\n}\n\nexport interface EditToolDetails {\n\t/** Display-oriented diff of the changes made */\n\tdiff: string;\n\t/** Standard unified patch of the changes made */\n\tpatch: string;\n\t/** Line number of the first change in the new file (for editor navigation) */\n\tfirstChangedLine?: number;\n\t/** Lines added by the edit (empty text is 0 lines; \"a\" and \"a\\n\" are both 1 line). */\n\taddedLines: number;\n\t/** Lines removed by the edit (same counting semantics as addedLines). */\n\tdeletedLines: number;\n\t/** Present only when the operations could not commit atomically (design §13.2 tier B). */\n\tcoordination?: { atomic: false };\n}\n\n/**\n * Pluggable operations for the edit tool.\n * Override these to delegate file editing to remote systems (for example SSH).\n */\nexport interface EditOperations {\n\t/** Read file contents as a Buffer */\n\treadFile: (absolutePath: string) => Promise<Buffer>;\n\t/** Write content to a file */\n\twriteFile: (absolutePath: string, content: string) => Promise<void>;\n\t/** Check if file is readable and writable (throw if not) */\n\taccess: (absolutePath: string) => Promise<void>;\n\t/**\n\t * Atomic-commit tier (design §13.2). All three must be present to enter it:\n\t * version facts, a complete temp file, and a synchronous replace. Remote\n\t * operations that cannot supply a synchronous primitive stay on the\n\t * local-mutex tier and are reported as `atomic: false`.\n\t */\n\tstatVersion?: (absolutePath: string) => Promise<StatVersionV1 | undefined>;\n\twriteTemp?: (absolutePath: string, content: string) => Promise<string>;\n\tcommitSync?: (tempPath: string, targetPath: string) => void;\n}\n\nconst defaultEditOperations: EditOperations = {\n\treadFile: (path) => fsReadFile(path),\n\twriteFile: (path, content) => fsWriteFile(path, content, \"utf-8\"),\n\taccess: (path) => fsAccess(path, constants.R_OK | constants.W_OK),\n\tstatVersion: (path) => statVersionOf(path),\n\twriteTemp: async (path, content) => {\n\t\tconst tempPath = `${path}.${randomUUID()}.agent-forge-tmp`;\n\t\tawait fsWriteFile(tempPath, content, { encoding: \"utf-8\", mode: 0o600 });\n\t\treturn tempPath;\n\t},\n\tcommitSync: (tempPath, targetPath) => renameSync(tempPath, targetPath),\n};\n\nexport interface EditToolOptions {\n\t/** Custom operations for file editing. Default: local filesystem */\n\toperations?: EditOperations;\n\t/** Relative write-path globs from the child task profile; absent = unrestricted. */\n\twriteScope?: readonly string[];\n\t/** `file.coordination@1` consumer face; absent = no advisory lease checks. */\n\tcoordination?: CoordinationLeaseCheckV1;\n}\n\n/** True when the operations can commit atomically (design §13.2 tier A). */\nexport function hasAtomicCommitOperations(\n\tops: EditOperations,\n): ops is EditOperations & Required<Pick<EditOperations, \"statVersion\" | \"writeTemp\" | \"commitSync\">> {\n\treturn ops.statVersion !== undefined && ops.writeTemp !== undefined && ops.commitSync !== undefined;\n}\n\nfunction prepareEditArguments(input: unknown): EditToolInput {\n\tif (!input || typeof input !== \"object\") {\n\t\treturn input as EditToolInput;\n\t}\n\n\tconst args = input as Record<string, unknown>;\n\n\t// Some models (Opus 4.6, GLM-5.1) send edits as a JSON string instead of an array.\n\t// Others send a single edit object instead of a one-element edits array.\n\tif (typeof args.edits === \"string\") {\n\t\ttry {\n\t\t\tconst parsed = JSON.parse(args.edits);\n\t\t\tif (Array.isArray(parsed)) {\n\t\t\t\targs.edits = parsed;\n\t\t\t} else if (isSingleEditInput(parsed)) {\n\t\t\t\targs.edits = [parsed];\n\t\t\t}\n\t\t} catch {}\n\t} else if (isSingleEditInput(args.edits)) {\n\t\targs.edits = [args.edits];\n\t}\n\n\tconst legacy = args as LegacyEditToolInput;\n\tif (typeof legacy.oldText !== \"string\" || typeof legacy.newText !== \"string\") {\n\t\treturn args as EditToolInput;\n\t}\n\n\tconst edits = Array.isArray(legacy.edits) ? [...legacy.edits] : [];\n\tedits.push({ oldText: legacy.oldText, newText: legacy.newText });\n\tconst { oldText: _oldText, newText: _newText, ...rest } = legacy;\n\treturn { ...rest, edits } as EditToolInput;\n}\n\nfunction validateEditInput(input: EditToolInput): { path: string; edits: Edit[] } {\n\tif (!Array.isArray(input.edits) || input.edits.length === 0) {\n\t\tthrow new Error(t(\"Edit tool input is invalid. edits must contain at least one replacement.\"));\n\t}\n\treturn { path: input.path, edits: input.edits };\n}\n\ntype RenderableEditArgs = {\n\tpath?: string;\n\tfile_path?: string;\n\tedits?: Edit[];\n\toldText?: string;\n\tnewText?: string;\n};\n\ntype EditToolResultLike = {\n\tcontent: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;\n\tdetails?: EditToolDetails;\n};\n\ntype EditCallRenderComponent = Box & {\n\tpreview?: EditPreview;\n\tpreviewArgsKey?: string;\n\tpreviewPending?: boolean;\n\tsettledError?: boolean;\n};\n\nfunction createEditCallRenderComponent(): EditCallRenderComponent {\n\treturn Object.assign(new Box(1, 1, (text: string) => text), {\n\t\tpreview: undefined as EditPreview | undefined,\n\t\tpreviewArgsKey: undefined as string | undefined,\n\t\tpreviewPending: false,\n\t\tsettledError: false,\n\t});\n}\n\nfunction getEditCallRenderComponent(state: EditRenderState, lastComponent: unknown): EditCallRenderComponent {\n\tif (lastComponent instanceof Box) {\n\t\tconst component = lastComponent as EditCallRenderComponent;\n\t\tstate.callComponent = component;\n\t\treturn component;\n\t}\n\tif (state.callComponent) {\n\t\treturn state.callComponent;\n\t}\n\tconst component = createEditCallRenderComponent();\n\tstate.callComponent = component;\n\treturn component;\n}\n\nfunction getRenderablePreviewInput(args: RenderableEditArgs | undefined): { path: string; edits: Edit[] } | null {\n\tif (!args) {\n\t\treturn null;\n\t}\n\n\tconst path = typeof args.path === \"string\" ? args.path : typeof args.file_path === \"string\" ? args.file_path : null;\n\tif (!path) {\n\t\treturn null;\n\t}\n\n\tif (\n\t\tArray.isArray(args.edits) &&\n\t\targs.edits.length > 0 &&\n\t\targs.edits.every((edit) => typeof edit?.oldText === \"string\" && typeof edit?.newText === \"string\")\n\t) {\n\t\treturn { path, edits: args.edits };\n\t}\n\n\tif (typeof args.oldText === \"string\" && typeof args.newText === \"string\") {\n\t\treturn { path, edits: [{ oldText: args.oldText, newText: args.newText }] };\n\t}\n\n\treturn null;\n}\n\nfunction formatEditCall(args: RenderableEditArgs | undefined, theme: Theme, cwd: string): string {\n\tconst pathDisplay = renderToolPath(str(args?.file_path ?? args?.path), theme, cwd);\n\treturn `${theme.fg(\"toolTitle\", theme.bold(\"edit\"))} ${pathDisplay}`;\n}\n\nfunction formatEditResult(\n\targs: RenderableEditArgs | undefined,\n\tpreview: EditPreview | undefined,\n\tresult: EditToolResultLike,\n\ttheme: Theme,\n\tisError: boolean,\n): string | undefined {\n\tconst rawPath = str(args?.file_path ?? args?.path);\n\tconst previewDiff = preview && !(\"error\" in preview) ? preview.diff : undefined;\n\tconst previewError = preview && \"error\" in preview ? preview.error : undefined;\n\tif (isError) {\n\t\tconst errorText = result.content\n\t\t\t.filter((c) => c.type === \"text\")\n\t\t\t.map((c) => c.text || \"\")\n\t\t\t.join(\"\\n\");\n\t\tif (!errorText || errorText === previewError) {\n\t\t\treturn undefined;\n\t\t}\n\t\treturn theme.fg(\"error\", errorText);\n\t}\n\n\tconst resultDiff = result.details?.diff;\n\tif (resultDiff && resultDiff !== previewDiff) {\n\t\treturn renderDiff(resultDiff, { filePath: rawPath ?? undefined });\n\t}\n\n\treturn undefined;\n}\n\nfunction getEditHeaderBg(\n\tpreview: EditPreview | undefined,\n\tsettledError: boolean | undefined,\n\ttheme: Theme,\n): (text: string) => string {\n\tif (preview) {\n\t\tif (\"error\" in preview) {\n\t\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t\t}\n\t\treturn (text: string) => theme.bg(\"toolSuccessBg\", text);\n\t}\n\tif (settledError) {\n\t\treturn (text: string) => theme.bg(\"toolErrorBg\", text);\n\t}\n\treturn (text: string) => theme.bg(\"toolPendingBg\", text);\n}\n\nfunction buildEditCallComponent(\n\tcomponent: EditCallRenderComponent,\n\targs: RenderableEditArgs | undefined,\n\ttheme: Theme,\n\tcwd: string,\n\toutputPad: number,\n): EditCallRenderComponent {\n\t// renderShell:\"self\" tools consume ToolRenderContext.outputPad themselves\n\t// (see tool-definition.ts); components are reused across renders, so the\n\t// padding must be refreshed on every build instead of only at construction.\n\tcomponent.setPaddingX(outputPad);\n\tcomponent.setBgFn(getEditHeaderBg(component.preview, component.settledError, theme));\n\tcomponent.clear();\n\tcomponent.addChild(new Text(formatEditCall(args, theme, cwd), 0, 0));\n\n\tif (!component.preview) {\n\t\treturn component;\n\t}\n\n\tconst body =\n\t\t\"error\" in component.preview ? theme.fg(\"error\", component.preview.error) : renderDiff(component.preview.diff);\n\tcomponent.addChild(new Spacer(1));\n\tcomponent.addChild(new Text(body, 0, 0));\n\treturn component;\n}\n\nfunction setEditPreview(\n\tcomponent: EditCallRenderComponent,\n\tpreview: EditPreview,\n\targsKey: string | undefined,\n): boolean {\n\tconst current = component.preview;\n\tconst changed =\n\t\tcurrent === undefined ||\n\t\t(\"error\" in current && \"error\" in preview\n\t\t\t? current.error !== preview.error\n\t\t\t: \"error\" in current !== \"error\" in preview) ||\n\t\t(!(\"error\" in current) &&\n\t\t\t!(\"error\" in preview) &&\n\t\t\t(current.diff !== preview.diff || current.firstChangedLine !== preview.firstChangedLine));\n\tcomponent.preview = preview;\n\tcomponent.previewArgsKey = argsKey;\n\tcomponent.previewPending = false;\n\treturn changed;\n}\n\ntype Replacement = { baseContent: string; newContent: string };\n\n/**\n * Run the edit against the freshest content we can justify, then commit.\n *\n * The lock excludes cooperative writers, so a change seen here is either a bypass\n * writer or a lock that was recovered out from under us. In both cases the only\n * safe move is to re-derive from the new content: if the anchors still match\n * uniquely we replay once, otherwise the caller gets an explicit conflict.\n */\nasync function commitEditContent(input: {\n\treadonly ops: EditOperations;\n\treadonly lock: FileMutationLockHandleV1;\n\treadonly path: string;\n\treadonly base: FileSnapshotV1;\n\treadonly applyEdits: (normalizedText: string) => Replacement;\n\treadonly renderFinal: (newContent: string) => string;\n}): Promise<{ hash: string; baseContent: string; newContent: string; atomic: boolean }> {\n\tconst { ops, lock, path, base, applyEdits, renderFinal } = input;\n\tlet current = base;\n\tlet replacement = applyEdits(splitBom(base.content.toString(\"utf-8\")).text);\n\n\t// Tier A can skip the re-read when stat is unchanged; tier B always re-reads\n\t// because it has no stat to compare.\n\tconst unchanged =\n\t\tbase.stat !== undefined && !statLooksChanged(await ops.statVersion?.(lock.canonicalPath), base.stat);\n\tif (!unchanged) {\n\t\tconst reread = await readSnapshot(ops, lock.canonicalPath);\n\t\tif (reread === undefined) {\n\t\t\tthrow new FileCoordinationError(\"file_missing\", `${path} disappeared during the edit.`, { path });\n\t\t}\n\t\tif (reread.hash !== base.hash) {\n\t\t\ttry {\n\t\t\t\treplacement = applyEdits(splitBom(reread.content.toString(\"utf-8\")).text);\n\t\t\t} catch {\n\t\t\t\tthrow new FileCoordinationError(\n\t\t\t\t\t\"file_version_conflict\",\n\t\t\t\t\t`${path} changed on disk and the requested text no longer matches. Re-read the file and retry.`,\n\t\t\t\t\t{ path, currentVersion: reread.hash, expectedVersion: base.hash },\n\t\t\t\t);\n\t\t\t}\n\t\t}\n\t\tcurrent = reread;\n\t}\n\n\tconst finalContent = renderFinal(replacement.newContent);\n\tconst nextHash = await sha256Hex(Buffer.from(finalContent, \"utf-8\"));\n\tconst committed = await commitPreparedContent({\n\t\tops,\n\t\tlock,\n\t\ttargetPath: lock.canonicalPath,\n\t\tcontent: finalContent,\n\t\texpectedStat: current.stat,\n\t\texpectedHash: current.hash,\n\t\tmakeMismatchError: () =>\n\t\t\tnew FileCoordinationError(\n\t\t\t\t\"file_version_conflict\",\n\t\t\t\t`${path} changed while the edit was being committed. Re-read the file and retry.`,\n\t\t\t\t{ path, currentVersion: nextHash, expectedVersion: base.hash },\n\t\t\t),\n\t});\n\treturn {\n\t\thash: nextHash,\n\t\tbaseContent: replacement.baseContent,\n\t\tnewContent: replacement.newContent,\n\t\tatomic: committed.atomic,\n\t};\n}\n\nexport function createEditToolDefinition(\n\tcwd: string,\n\toptions?: EditToolOptions,\n): ToolDefinition<typeof editSchema, EditToolDetails | undefined, EditRenderState> {\n\tconst ops = options?.operations ?? defaultEditOperations;\n\tconst writeScope = options?.writeScope;\n\tconst coordination = options?.coordination;\n\treturn {\n\t\tname: \"edit\",\n\t\tlabel: \"edit\",\n\t\tdescription:\n\t\t\t\"Edit a single file using exact text replacement. Every edits[].oldText must match a unique, non-overlapping region of the original file. If two changes affect the same block or nearby lines, merge them into one edit instead of emitting overlapping edits. Do not include large unchanged regions just to connect distant changes.\",\n\t\tpromptSnippet: editToolSystemPromptContribution.snippet,\n\t\tpromptGuidelines: [...editToolSystemPromptContribution.guidelines],\n\t\tparameters: editSchema,\n\t\tconstrainedSampling: { type: \"json_schema\", strict: \"prefer\" },\n\t\trenderShell: \"self\",\n\t\tprepareArguments: prepareEditArguments,\n\t\tasync execute(_toolCallId, input: EditToolInput, signal?: AbortSignal, _onUpdate?, ctx?: ToolExecuteContext) {\n\t\t\tconst { path, edits } = validateEditInput(input);\n\t\t\tconst absolutePath = resolveToCwd(path, ctx?.cwd || cwd);\n\t\t\tconst sessionId = ctx?.sessionManager?.getSessionId();\n\n\t\t\treturn withFileMutationQueue(absolutePath, async () => {\n\t\t\t\t// Do not reject from an abort event listener here: that would release the\n\t\t\t\t// mutation queue while an in-flight filesystem operation may still finish.\n\t\t\t\t// Checking signal.aborted after each await observes the same aborts while\n\t\t\t\t// keeping the queue locked until the current operation has settled.\n\t\t\t\tconst throwIfAborted = (): void => {\n\t\t\t\t\tif (signal?.aborted) throw new Error(t(\"Operation aborted\"));\n\t\t\t\t};\n\n\t\t\t\tthrowIfAborted();\n\n\t\t\t\tconst lockOptions = {\n\t\t\t\t\t...(sessionId === undefined ? {} : { sessionId }),\n\t\t\t\t\t...(signal === undefined ? {} : { signal }),\n\t\t\t\t};\n\t\t\t\treturn withFileMutationLock(absolutePath, lockOptions, async (lock) => {\n\t\t\t\t\tassertWithinWriteScope(lock.canonicalPath, ctx?.cwd || cwd, writeScope);\n\t\t\t\t\t// Check if file exists and is writable. Kept before the read so a\n\t\t\t\t\t// read-only file keeps failing instead of being replaced atomically.\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait ops.access(lock.canonicalPath);\n\t\t\t\t\t} catch (error: unknown) {\n\t\t\t\t\t\tthrowIfAborted();\n\t\t\t\t\t\tconst errorMessage =\n\t\t\t\t\t\t\terror instanceof Error && \"code\" in error ? `Error code: ${error.code}` : String(error);\n\t\t\t\t\t\tthrow new Error(t(\"Could not edit file: {path}. {errorMessage}.\", { path, errorMessage }));\n\t\t\t\t\t}\n\t\t\t\t\tthrowIfAborted();\n\n\t\t\t\t\tconst base = await readSnapshot(ops, lock.canonicalPath);\n\t\t\t\t\tif (base === undefined) {\n\t\t\t\t\t\tthrow new FileCoordinationError(\"file_missing\", `File no longer exists: ${path}`, { path });\n\t\t\t\t\t}\n\t\t\t\t\tthrowIfAborted();\n\n\t\t\t\t\t// Strip BOM before matching. The model will not include an invisible BOM in oldText.\n\t\t\t\t\tconst { bom, text: content } = splitBom(base.content.toString(\"utf-8\"));\n\t\t\t\t\t// Advisory lease check (design §8.8): inside the lock, before any\n\t\t\t\t\t// write. Own leases are clear; foreign overlaps fail here.\n\t\t\t\t\tawait checkIntentLease({\n\t\t\t\t\t\tcoordination,\n\t\t\t\t\t\townerSessionId: sessionId,\n\t\t\t\t\t\tcanonicalPath: lock.canonicalPath,\n\t\t\t\t\t\tlockToken: lock.token,\n\t\t\t\t\t\tdisplayPath: path,\n\t\t\t\t\t\trange: editHitRange(normalizeToLF(content), edits),\n\t\t\t\t\t});\n\t\t\t\t\tconst originalEnding = detectLineEnding(content);\n\t\t\t\t\t// The matcher works on LF-normalized text (BOM already stripped);\n\t\t\t\t\t// normalize here so a re-read after a conflict takes the same path.\n\t\t\t\t\tconst applyEdits = (text: string): Replacement =>\n\t\t\t\t\t\tapplyEditsToNormalizedContent(normalizeToLF(text), edits, path);\n\t\t\t\t\tconst renderFinal = (newContent: string): string => bom + restoreLineEndings(newContent, originalEnding);\n\n\t\t\t\t\tconst committed = await commitEditContent({\n\t\t\t\t\t\tops,\n\t\t\t\t\t\tlock,\n\t\t\t\t\t\tpath,\n\t\t\t\t\t\tbase,\n\t\t\t\t\t\tapplyEdits,\n\t\t\t\t\t\trenderFinal,\n\t\t\t\t\t});\n\t\t\t\t\tthrowIfAborted();\n\n\t\t\t\t\tif (sessionId !== undefined) {\n\t\t\t\t\t\tconst after = await ops.statVersion?.(lock.canonicalPath);\n\t\t\t\t\t\trecordVersion(sessionId, lock.canonicalPath, {\n\t\t\t\t\t\t\thash: committed.hash,\n\t\t\t\t\t\t\t...(after === undefined ? {} : { stat: after }),\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\tconst diffResult = generateDiffString(committed.baseContent, committed.newContent);\n\t\t\t\t\tconst patch = generateUnifiedPatch(path, committed.baseContent, committed.newContent);\n\t\t\t\t\treturn {\n\t\t\t\t\t\tcontent: [\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\ttype: \"text\",\n\t\t\t\t\t\t\t\ttext: `Successfully replaced ${edits.length} block(s) in ${path}.`,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t],\n\t\t\t\t\t\tdetails: {\n\t\t\t\t\t\t\tdiff: diffResult.diff,\n\t\t\t\t\t\t\tpatch,\n\t\t\t\t\t\t\tfirstChangedLine: diffResult.firstChangedLine,\n\t\t\t\t\t\t\taddedLines: diffResult.addedLines,\n\t\t\t\t\t\t\tdeletedLines: diffResult.deletedLines,\n\t\t\t\t\t\t\t...(committed.atomic ? {} : { coordination: { atomic: false } }),\n\t\t\t\t\t\t},\n\t\t\t\t\t};\n\t\t\t\t});\n\t\t\t});\n\t\t},\n\t\tquietSummary(\n\t\t\targs: EditToolInput,\n\t\t\tcontext: { cwd: string; isError: boolean; isPartial: boolean; details: EditToolDetails | undefined },\n\t\t): string | undefined {\n\t\t\tconst rawPath = str(args?.path);\n\t\t\tif (!rawPath) return undefined;\n\t\t\tconst pathDisplay = formatPathRelativeToCwdOrAbsolute(rawPath, context.cwd);\n\t\t\tlet text = `edit ${pathDisplay}`;\n\t\t\tconst details = context.details;\n\t\t\tif (details && !context.isError) {\n\t\t\t\ttext += ` +${details.addedLines} -${details.deletedLines}`;\n\t\t\t}\n\t\t\treturn text;\n\t\t},\n\t\trenderCall(args, theme, context) {\n\t\t\tconst component = getEditCallRenderComponent(context.state, context.lastComponent);\n\t\t\tconst previewInput = getRenderablePreviewInput(args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\n\t\t\tif (component.previewArgsKey !== argsKey) {\n\t\t\t\tcomponent.preview = undefined;\n\t\t\t\tcomponent.previewArgsKey = argsKey;\n\t\t\t\tcomponent.previewPending = false;\n\t\t\t\tcomponent.settledError = false;\n\t\t\t}\n\n\t\t\tif (context.argsComplete && previewInput && !component.preview && !component.previewPending) {\n\t\t\t\tcomponent.previewPending = true;\n\t\t\t\tconst requestKey = argsKey;\n\t\t\t\tvoid computeEditsDiff(previewInput.path, previewInput.edits, context.cwd).then((preview) => {\n\t\t\t\t\tif (component.previewArgsKey === requestKey) {\n\t\t\t\t\t\tsetEditPreview(component, preview, requestKey);\n\t\t\t\t\t\tcontext.invalidate();\n\t\t\t\t\t}\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn buildEditCallComponent(component, args, theme, context.cwd, context.outputPad);\n\t\t},\n\t\trenderResult(result, _options, theme, context) {\n\t\t\tconst callComponent = context.state.callComponent;\n\t\t\tconst previewInput = getRenderablePreviewInput(context.args as RenderableEditArgs | undefined);\n\t\t\tconst argsKey = previewInput\n\t\t\t\t? JSON.stringify({ path: previewInput.path, edits: previewInput.edits })\n\t\t\t\t: undefined;\n\t\t\tconst typedResult = result as EditToolResultLike;\n\t\t\tconst resultDiff = !context.isError ? typedResult.details?.diff : undefined;\n\t\t\tlet changed = false;\n\t\t\tif (callComponent) {\n\t\t\t\tif (typeof resultDiff === \"string\" && typedResult.details) {\n\t\t\t\t\tchanged =\n\t\t\t\t\t\tsetEditPreview(\n\t\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tdiff: resultDiff,\n\t\t\t\t\t\t\t\tfirstChangedLine: typedResult.details.firstChangedLine,\n\t\t\t\t\t\t\t\taddedLines: typedResult.details.addedLines,\n\t\t\t\t\t\t\t\tdeletedLines: typedResult.details.deletedLines,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\targsKey,\n\t\t\t\t\t\t) || changed;\n\t\t\t\t}\n\t\t\t\tif (callComponent.settledError !== context.isError) {\n\t\t\t\t\tcallComponent.settledError = context.isError;\n\t\t\t\t\tchanged = true;\n\t\t\t\t}\n\t\t\t\tif (changed) {\n\t\t\t\t\tbuildEditCallComponent(\n\t\t\t\t\t\tcallComponent,\n\t\t\t\t\t\tcontext.args as RenderableEditArgs | undefined,\n\t\t\t\t\t\ttheme,\n\t\t\t\t\t\tcontext.cwd,\n\t\t\t\t\t\tcontext.outputPad,\n\t\t\t\t\t);\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst output = formatEditResult(context.args, callComponent?.preview, typedResult, theme, context.isError);\n\t\t\tconst component = (context.lastComponent as Container | undefined) ?? new Container();\n\t\t\tcomponent.clear();\n\t\t\tif (!output) {\n\t\t\t\treturn component;\n\t\t\t}\n\t\t\tcomponent.addChild(new Spacer(1));\n\t\t\tcomponent.addChild(new Text(output, context.outputPad, 0));\n\t\t\treturn component;\n\t\t},\n\t};\n}\n\nexport function createEditTool(cwd: string, options?: EditToolOptions): AgentTool<typeof editSchema> {\n\treturn wrapToolDefinition(createEditToolDefinition(cwd, options));\n}\n"]}