@caupulican/pi-adaptative 0.84.1 → 0.85.0

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 (145) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
  3. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +14 -8
  4. package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
  5. package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +14 -8
  6. package/dist/bundled-resources/skills/deduplicate-by-evidence/SKILL.md +7 -5
  7. package/dist/bundled-resources/skills/worker-profile-writer/SKILL.md +63 -0
  8. package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
  9. package/dist/core/autonomy/lane-tool-surface.js +4 -2
  10. package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
  11. package/dist/core/background-lane-controller.d.ts +3 -0
  12. package/dist/core/background-lane-controller.d.ts.map +1 -1
  13. package/dist/core/background-lane-controller.js +6 -0
  14. package/dist/core/background-lane-controller.js.map +1 -1
  15. package/dist/core/default-tool-surface.d.ts.map +1 -1
  16. package/dist/core/default-tool-surface.js +1 -0
  17. package/dist/core/default-tool-surface.js.map +1 -1
  18. package/dist/core/delegation/worker-delegation-controller.d.ts +7 -0
  19. package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
  20. package/dist/core/delegation/worker-delegation-controller.js +28 -0
  21. package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
  22. package/dist/core/delegation/worker-profile-resolver.d.ts +2 -0
  23. package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -1
  24. package/dist/core/delegation/worker-profile-resolver.js +46 -11
  25. package/dist/core/delegation/worker-profile-resolver.js.map +1 -1
  26. package/dist/core/doctor.d.ts +1 -1
  27. package/dist/core/doctor.d.ts.map +1 -1
  28. package/dist/core/doctor.js +5 -4
  29. package/dist/core/doctor.js.map +1 -1
  30. package/dist/core/model-capability.d.ts.map +1 -1
  31. package/dist/core/model-capability.js +1 -0
  32. package/dist/core/model-capability.js.map +1 -1
  33. package/dist/core/orchestration/lane-tool-manifests.d.ts +1 -1
  34. package/dist/core/orchestration/lane-tool-manifests.d.ts.map +1 -1
  35. package/dist/core/orchestration/lane-tool-manifests.js +6 -1
  36. package/dist/core/orchestration/lane-tool-manifests.js.map +1 -1
  37. package/dist/core/orchestration/session-task-profile-store.d.ts +31 -0
  38. package/dist/core/orchestration/session-task-profile-store.d.ts.map +1 -0
  39. package/dist/core/orchestration/session-task-profile-store.js +138 -0
  40. package/dist/core/orchestration/session-task-profile-store.js.map +1 -0
  41. package/dist/core/orchestration/task-profile-writer.d.ts +58 -0
  42. package/dist/core/orchestration/task-profile-writer.d.ts.map +1 -0
  43. package/dist/core/orchestration/task-profile-writer.js +165 -0
  44. package/dist/core/orchestration/task-profile-writer.js.map +1 -0
  45. package/dist/core/runtime-builder.d.ts +3 -1
  46. package/dist/core/runtime-builder.d.ts.map +1 -1
  47. package/dist/core/runtime-builder.js +14 -0
  48. package/dist/core/runtime-builder.js.map +1 -1
  49. package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
  50. package/dist/core/security/untrusted-boundary.js +4 -6
  51. package/dist/core/security/untrusted-boundary.js.map +1 -1
  52. package/dist/core/session-role.d.ts.map +1 -1
  53. package/dist/core/session-role.js +1 -0
  54. package/dist/core/session-role.js.map +1 -1
  55. package/dist/core/system-prompt.d.ts.map +1 -1
  56. package/dist/core/system-prompt.js +21 -23
  57. package/dist/core/system-prompt.js.map +1 -1
  58. package/dist/core/tool-capability-policy.js +1 -1
  59. package/dist/core/tool-capability-policy.js.map +1 -1
  60. package/dist/core/tools/bash.d.ts +1 -6
  61. package/dist/core/tools/bash.d.ts.map +1 -1
  62. package/dist/core/tools/bash.js +2 -26
  63. package/dist/core/tools/bash.js.map +1 -1
  64. package/dist/core/tools/delegate.js +4 -4
  65. package/dist/core/tools/delegate.js.map +1 -1
  66. package/dist/core/tools/edit-diff.d.ts +32 -5
  67. package/dist/core/tools/edit-diff.d.ts.map +1 -1
  68. package/dist/core/tools/edit-diff.js +137 -69
  69. package/dist/core/tools/edit-diff.js.map +1 -1
  70. package/dist/core/tools/edit.d.ts +22 -7
  71. package/dist/core/tools/edit.d.ts.map +1 -1
  72. package/dist/core/tools/edit.js +128 -55
  73. package/dist/core/tools/edit.js.map +1 -1
  74. package/dist/core/tools/file-mutation-intent.d.ts +70 -0
  75. package/dist/core/tools/file-mutation-intent.d.ts.map +1 -0
  76. package/dist/core/tools/file-mutation-intent.js +325 -0
  77. package/dist/core/tools/file-mutation-intent.js.map +1 -0
  78. package/dist/core/tools/goal.d.ts.map +1 -1
  79. package/dist/core/tools/goal.js +41 -18
  80. package/dist/core/tools/goal.js.map +1 -1
  81. package/dist/core/tools/index.d.ts +3 -1
  82. package/dist/core/tools/index.d.ts.map +1 -1
  83. package/dist/core/tools/index.js +31 -8
  84. package/dist/core/tools/index.js.map +1 -1
  85. package/dist/core/tools/profile-writer.d.ts +33 -0
  86. package/dist/core/tools/profile-writer.d.ts.map +1 -0
  87. package/dist/core/tools/profile-writer.js +67 -0
  88. package/dist/core/tools/profile-writer.js.map +1 -0
  89. package/dist/core/tools/task-steps.d.ts.map +1 -1
  90. package/dist/core/tools/task-steps.js +6 -11
  91. package/dist/core/tools/task-steps.js.map +1 -1
  92. package/dist/core/tools/tmux-dispatch.d.ts +2 -0
  93. package/dist/core/tools/tmux-dispatch.d.ts.map +1 -1
  94. package/dist/core/tools/tmux-dispatch.js +16 -2
  95. package/dist/core/tools/tmux-dispatch.js.map +1 -1
  96. package/dist/core/tools/write.d.ts +24 -7
  97. package/dist/core/tools/write.d.ts.map +1 -1
  98. package/dist/core/tools/write.js +182 -107
  99. package/dist/core/tools/write.js.map +1 -1
  100. package/dist/index.d.ts +1 -1
  101. package/dist/index.d.ts.map +1 -1
  102. package/dist/index.js +1 -1
  103. package/dist/index.js.map +1 -1
  104. package/dist/jscpd-cli.d.ts +3 -0
  105. package/dist/jscpd-cli.d.ts.map +1 -0
  106. package/dist/jscpd-cli.js +22 -0
  107. package/dist/jscpd-cli.js.map +1 -0
  108. package/dist/modes/interactive/components/profile-resource-editor.js +2 -2
  109. package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
  110. package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
  111. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  112. package/dist/modes/interactive/components/tool-execution.js +85 -3
  113. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  114. package/dist/modes/interactive/interactive-mode.js +1 -1
  115. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  116. package/dist/utils/bundled-jscpd.d.ts +20 -0
  117. package/dist/utils/bundled-jscpd.d.ts.map +1 -0
  118. package/dist/utils/bundled-jscpd.js +154 -0
  119. package/dist/utils/bundled-jscpd.js.map +1 -0
  120. package/dist/utils/shell.d.ts.map +1 -1
  121. package/dist/utils/shell.js +11 -0
  122. package/dist/utils/shell.js.map +1 -1
  123. package/dist/utils/tools-manager.d.ts +1 -1
  124. package/dist/utils/tools-manager.d.ts.map +1 -1
  125. package/dist/utils/tools-manager.js +22 -0
  126. package/dist/utils/tools-manager.js.map +1 -1
  127. package/docs/index.md +3 -0
  128. package/docs/managed-data-tools.md +15 -0
  129. package/docs/quickstart.md +2 -2
  130. package/docs/tool-boundary-performance-roadmap-2026-08-02.md +166 -0
  131. package/docs/tool-repair.md +1 -1
  132. package/docs/worker-profiles.md +16 -0
  133. package/examples/extensions/built-in-tool-renderer.ts +12 -5
  134. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  135. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  136. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  137. package/examples/extensions/minimal-mode.ts +8 -7
  138. package/examples/extensions/prompt-customizer.ts +4 -2
  139. package/examples/extensions/sandbox/package-lock.json +2 -2
  140. package/examples/extensions/sandbox/package.json +1 -1
  141. package/examples/extensions/ssh.ts +113 -24
  142. package/examples/extensions/with-deps/package-lock.json +2 -2
  143. package/examples/extensions/with-deps/package.json +1 -1
  144. package/npm-shrinkwrap.json +112 -12
  145. package/package.json +7 -5
@@ -26,7 +26,13 @@
26
26
  */
27
27
 
28
28
  import type { BashToolDetails, EditToolDetails, ExtensionAPI, ReadToolDetails } from "@caupulican/pi-adaptative";
29
- import { createBashTool, createEditTool, createReadTool, createWriteTool } from "@caupulican/pi-adaptative";
29
+ import {
30
+ createBashTool,
31
+ createEditTool,
32
+ createReadTool,
33
+ createWriteTool,
34
+ FileMutationIntentController,
35
+ } from "@caupulican/pi-adaptative";
30
36
  import { Text } from "@caupulican/pi-tui";
31
37
 
32
38
  export default function (pi: ExtensionAPI) {
@@ -151,7 +157,8 @@ export default function (pi: ExtensionAPI) {
151
157
  });
152
158
 
153
159
  // --- Edit tool: show path and diff stats ---
154
- const originalEdit = createEditTool(cwd);
160
+ const fileMutationIntents = new FileMutationIntentController();
161
+ const originalEdit = createEditTool(cwd, { intentController: fileMutationIntents });
155
162
  pi.registerTool({
156
163
  name: "edit",
157
164
  label: "edit",
@@ -216,7 +223,7 @@ export default function (pi: ExtensionAPI) {
216
223
  });
217
224
 
218
225
  // --- Write tool: show path and size ---
219
- const originalWrite = createWriteTool(cwd);
226
+ const originalWrite = createWriteTool(cwd, { intentController: fileMutationIntents });
220
227
  pi.registerTool({
221
228
  name: "write",
222
229
  label: "write",
@@ -230,8 +237,8 @@ export default function (pi: ExtensionAPI) {
230
237
  renderCall(args, theme, _context) {
231
238
  let text = theme.fg("toolTitle", theme.bold("write "));
232
239
  text += theme.fg("accent", args.path);
233
- const lineCount = args.content.split("\n").length;
234
- text += theme.fg("dim", ` (${lineCount} lines)`);
240
+ const writeContent = args.action === "commit" && "content" in args ? args.content : undefined;
241
+ if (writeContent) text += theme.fg("dim", ` (${writeContent.length} characters)`);
235
242
  return new Text(text, 0, 0);
236
243
  },
237
244
 
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider",
3
- "version": "0.84.1",
3
+ "version": "0.85.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-custom-provider",
9
- "version": "0.84.1",
9
+ "version": "0.85.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.52.0"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-anthropic",
3
3
  "private": true,
4
- "version": "0.84.1",
4
+ "version": "0.85.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-custom-provider-gitlab-duo",
3
3
  "private": true,
4
- "version": "0.84.1",
4
+ "version": "0.85.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -25,6 +25,7 @@ import {
25
25
  createLsTool,
26
26
  createReadTool,
27
27
  createWriteTool,
28
+ FileMutationIntentController,
28
29
  } from "@caupulican/pi-adaptative";
29
30
  import { Text } from "@caupulican/pi-tui";
30
31
  import { homedir } from "os";
@@ -44,11 +45,12 @@ function shortenPath(path: string): string {
44
45
  const toolCache = new Map<string, ReturnType<typeof createBuiltInTools>>();
45
46
 
46
47
  function createBuiltInTools(cwd: string) {
48
+ const fileMutationIntents = new FileMutationIntentController();
47
49
  return {
48
50
  read: createReadTool(cwd),
49
51
  bash: createBashTool(cwd),
50
- edit: createEditTool(cwd),
51
- write: createWriteTool(cwd),
52
+ edit: createEditTool(cwd, { intentController: fileMutationIntents }),
53
+ write: createWriteTool(cwd, { intentController: fileMutationIntents }),
52
54
  find: createFindTool(cwd),
53
55
  grep: createGrepTool(cwd),
54
56
  ls: createLsTool(cwd),
@@ -167,8 +169,7 @@ export default function (pi: ExtensionAPI) {
167
169
  pi.registerTool({
168
170
  name: "write",
169
171
  label: "write",
170
- description:
171
- "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
172
+ description: "Create a new file through path-only preflight. Existing paths are never overwritten.",
172
173
  parameters: getBuiltInTools(process.cwd()).write.parameters,
173
174
 
174
175
  async execute(toolCallId, params, signal, onUpdate, ctx) {
@@ -179,10 +180,10 @@ export default function (pi: ExtensionAPI) {
179
180
  renderCall(args, theme, _context) {
180
181
  const path = shortenPath(args.path || "");
181
182
  const pathDisplay = path ? theme.fg("accent", path) : theme.fg("toolOutput", "...");
182
- const lineCount = args.content ? args.content.split("\n").length : 0;
183
- const lineInfo = lineCount > 0 ? theme.fg("muted", ` (${lineCount} lines)`) : "";
183
+ const writeContent = args.action === "commit" && "content" in args ? args.content : undefined;
184
+ const sizeInfo = writeContent ? theme.fg("muted", ` (${writeContent.length} characters)`) : "";
184
185
 
185
- return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${lineInfo}`, 0, 0);
186
+ return new Text(`${theme.fg("toolTitle", theme.bold("write"))} ${pathDisplay}${sizeInfo}`, 0, 0);
186
187
  },
187
188
 
188
189
  renderResult(result, { expanded }, theme, _context) {
@@ -37,12 +37,14 @@ function addToolGuidance(options: BuildSystemPromptOptions, basePrompt: string):
37
37
 
38
38
  if (hasTool("edit")) {
39
39
  parts.push(
40
- "• Use the `edit` tool for precise text replacements in files. Match exact content including whitespace.",
40
+ "• Use `edit` action=prepare before generating precise replacements, then commit with the returned intent. Match exact content including whitespace.",
41
41
  );
42
42
  }
43
43
 
44
44
  if (hasTool("write")) {
45
- parts.push("• Use the `write` tool to create new files or overwrite existing ones completely.");
45
+ parts.push(
46
+ "• Use `write` action=prepare before generating content; commit creates a new file and never overwrites.",
47
+ );
46
48
  }
47
49
 
48
50
  if (options.skills && options.skills.length > 0) {
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
- "version": "0.84.1",
3
+ "version": "0.85.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-sandbox",
9
- "version": "0.84.1",
9
+ "version": "0.85.0",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sandbox-runtime": "^0.0.26"
12
12
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-sandbox",
3
3
  "private": true,
4
- "version": "0.84.1",
4
+ "version": "0.85.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -10,10 +10,11 @@
10
10
  *
11
11
  * Requirements:
12
12
  * - SSH key-based auth (no password prompts)
13
- * - bash on remote
13
+ * - bash and GNU coreutils (sha256sum, stat) on remote
14
14
  */
15
15
 
16
16
  import { spawn } from "node:child_process";
17
+ import { constants } from "node:fs";
17
18
  import type { ExtensionAPI } from "@caupulican/pi-adaptative";
18
19
  import {
19
20
  type BashOperations,
@@ -22,13 +23,20 @@ import {
22
23
  createReadTool,
23
24
  createWriteTool,
24
25
  type EditOperations,
26
+ FileMutationIntentController,
27
+ type FileMutationIntentOperations,
28
+ type FilePathInspection,
25
29
  type ReadOperations,
26
30
  type WriteOperations,
27
31
  } from "@caupulican/pi-adaptative";
28
32
 
29
- function sshExec(remote: string, command: string): Promise<Buffer> {
33
+ function shellQuote(value: string): string {
34
+ return `'${value.replaceAll("'", `'"'"'`)}'`;
35
+ }
36
+
37
+ function sshExec(remote: string, command: string, stdin?: string | Buffer): Promise<Buffer> {
30
38
  return new Promise((resolve, reject) => {
31
- const child = spawn("ssh", [remote, command], { stdio: ["ignore", "pipe", "pipe"] });
39
+ const child = spawn("ssh", [remote, command], { stdio: ["pipe", "pipe", "pipe"] });
32
40
  const chunks: Buffer[] = [];
33
41
  const errChunks: Buffer[] = [];
34
42
  child.stdout.on("data", (data) => chunks.push(data));
@@ -41,17 +49,18 @@ function sshExec(remote: string, command: string): Promise<Buffer> {
41
49
  resolve(Buffer.concat(chunks));
42
50
  }
43
51
  });
52
+ child.stdin.end(stdin);
44
53
  });
45
54
  }
46
55
 
47
56
  function createRemoteReadOps(remote: string, remoteCwd: string, localCwd: string): ReadOperations {
48
57
  const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
49
58
  return {
50
- readFile: (p) => sshExec(remote, `cat ${JSON.stringify(toRemote(p))}`),
51
- access: (p) => sshExec(remote, `test -r ${JSON.stringify(toRemote(p))}`).then(() => {}),
59
+ readFile: (p) => sshExec(remote, `cat -- ${shellQuote(toRemote(p))}`),
60
+ access: (p) => sshExec(remote, `test -r ${shellQuote(toRemote(p))}`).then(() => {}),
52
61
  detectImageMimeType: async (p) => {
53
62
  try {
54
- const r = await sshExec(remote, `file --mime-type -b ${JSON.stringify(toRemote(p))}`);
63
+ const r = await sshExec(remote, `file --mime-type -b -- ${shellQuote(toRemote(p))}`);
55
64
  const m = r.toString().trim();
56
65
  return ["image/jpeg", "image/png", "image/gif", "image/webp"].includes(m) ? m : null;
57
66
  } catch {
@@ -61,21 +70,86 @@ function createRemoteReadOps(remote: string, remoteCwd: string, localCwd: string
61
70
  };
62
71
  }
63
72
 
73
+ async function writeRemoteContent(remote: string, path: string, content: string, exclusive: boolean): Promise<void> {
74
+ const noClobber = exclusive ? "set -o noclobber; " : "";
75
+ await sshExec(remote, `${noClobber}cat > ${shellQuote(path)}`, content);
76
+ }
77
+
64
78
  function createRemoteWriteOps(remote: string, remoteCwd: string, localCwd: string): WriteOperations {
65
79
  const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
66
80
  return {
67
- writeFile: async (p, content) => {
68
- const b64 = Buffer.from(content).toString("base64");
69
- await sshExec(remote, `echo ${JSON.stringify(b64)} | base64 -d > ${JSON.stringify(toRemote(p))}`);
70
- },
71
- mkdir: (dir) => sshExec(remote, `mkdir -p ${JSON.stringify(toRemote(dir))}`).then(() => {}),
81
+ createFile: (p, content) => writeRemoteContent(remote, toRemote(p), content, true),
82
+ mkdir: (dir) => sshExec(remote, `mkdir -p -- ${shellQuote(toRemote(dir))}`).then(() => {}),
72
83
  };
73
84
  }
74
85
 
75
86
  function createRemoteEditOps(remote: string, remoteCwd: string, localCwd: string): EditOperations {
76
87
  const r = createRemoteReadOps(remote, remoteCwd, localCwd);
77
- const w = createRemoteWriteOps(remote, remoteCwd, localCwd);
78
- return { readFile: r.readFile, access: r.access, writeFile: w.writeFile };
88
+ const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
89
+ return {
90
+ readFile: r.readFile,
91
+ writeFile: (p, content) => writeRemoteContent(remote, toRemote(p), content, false),
92
+ };
93
+ }
94
+
95
+ function createRemoteIntentOps(remote: string, remoteCwd: string, localCwd: string): FileMutationIntentOperations {
96
+ const toRemote = (p: string) => p.replace(localCwd, remoteCwd);
97
+ return {
98
+ async inspect(p, followSymlinks): Promise<FilePathInspection | undefined> {
99
+ const path = shellQuote(toRemote(p));
100
+ const kind = followSymlinks
101
+ ? `if [ -f ${path} ]; then printf 'file\\t'; elif [ -d ${path} ]; then printf 'directory\\t'; elif [ -e ${path} ]; then printf 'other\\t'; else exit 44; fi`
102
+ : `if [ -L ${path} ]; then printf 'other\\t'; elif [ -f ${path} ]; then printf 'file\\t'; elif [ -d ${path} ]; then printf 'directory\\t'; elif [ -e ${path} ]; then printf 'other\\t'; else exit 44; fi`;
103
+ const follow = followSymlinks ? "-L " : "";
104
+ try {
105
+ const output = (
106
+ await sshExec(
107
+ remote,
108
+ `${kind}; stat ${follow}--printf ${shellQuote("%d\\t%i\\t%f\\t%s\\t%y\\t%z")} -- ${path}`,
109
+ )
110
+ )
111
+ .toString("utf8")
112
+ .trimEnd();
113
+ const [entryKind, dev, ino, mode, size, mtimeMs, ctimeMs] = output.split("\t");
114
+ if (
115
+ (entryKind !== "file" && entryKind !== "directory" && entryKind !== "other") ||
116
+ !dev ||
117
+ !ino ||
118
+ !mode ||
119
+ !size ||
120
+ !mtimeMs ||
121
+ !ctimeMs
122
+ ) {
123
+ throw new Error(`Unexpected remote stat response for ${p}`);
124
+ }
125
+ return { kind: entryKind, identity: { dev, ino, mode, size, mtimeMs, ctimeMs } };
126
+ } catch (error) {
127
+ if (String(error).includes("SSH failed (44)")) return undefined;
128
+ throw error;
129
+ }
130
+ },
131
+ access(p, mode) {
132
+ const path = shellQuote(toRemote(p));
133
+ const checks = [
134
+ mode & constants.R_OK ? `test -r ${path}` : undefined,
135
+ mode & constants.W_OK ? `test -w ${path}` : undefined,
136
+ ].filter((check): check is string => check !== undefined);
137
+ return sshExec(remote, checks.length > 0 ? checks.join(" && ") : `test -e ${path}`).then(() => {});
138
+ },
139
+ copyFileExclusive(sourcePath, targetPath) {
140
+ return sshExec(
141
+ remote,
142
+ `set -o noclobber; cat -- ${shellQuote(toRemote(sourcePath))} > ${shellQuote(toRemote(targetPath))}`,
143
+ ).then(() => {});
144
+ },
145
+ async hashFile(p) {
146
+ const output = await sshExec(remote, `sha256sum -- ${shellQuote(toRemote(p))}`);
147
+ const digest = output.toString("utf8").trim().split(/\s+/, 1)[0];
148
+ if (!digest || !/^[0-9a-f]{64}$/i.test(digest)) throw new Error(`Invalid remote sha256 for ${p}`);
149
+ return digest.toLowerCase();
150
+ },
151
+ removeFile: (p) => sshExec(remote, `rm -- ${shellQuote(toRemote(p))}`).then(() => {}),
152
+ };
79
153
  }
80
154
 
81
155
  function createRemoteBashOps(remote: string, remoteCwd: string, localCwd: string): BashOperations {
@@ -83,7 +157,7 @@ function createRemoteBashOps(remote: string, remoteCwd: string, localCwd: string
83
157
  return {
84
158
  exec: (command, cwd, { onData, signal, timeout }) =>
85
159
  new Promise((resolve, reject) => {
86
- const cmd = `cd ${JSON.stringify(toRemote(cwd))} && ${command}`;
160
+ const cmd = `cd ${shellQuote(toRemote(cwd))} && ${command}`;
87
161
  const child = spawn("ssh", [remote, cmd], { stdio: ["ignore", "pipe", "pipe"] });
88
162
  let timedOut = false;
89
163
  const timer = timeout
@@ -115,15 +189,35 @@ export default function (pi: ExtensionAPI) {
115
189
  pi.registerFlag("ssh", { description: "SSH remote: user@host or user@host:/path", type: "string" });
116
190
 
117
191
  const localCwd = process.cwd();
192
+ const localFileMutationIntents = new FileMutationIntentController();
118
193
  const localRead = createReadTool(localCwd);
119
- const localWrite = createWriteTool(localCwd);
120
- const localEdit = createEditTool(localCwd);
194
+ const localWrite = createWriteTool(localCwd, { intentController: localFileMutationIntents });
195
+ const localEdit = createEditTool(localCwd, { intentController: localFileMutationIntents });
121
196
  const localBash = createBashTool(localCwd);
122
197
 
123
198
  // Resolved lazily on session_start (CLI flags not available during factory)
124
199
  let resolvedSsh: { remote: string; remoteCwd: string } | null = null;
200
+ let remoteFileTools: { write: ReturnType<typeof createWriteTool>; edit: ReturnType<typeof createEditTool> } | null =
201
+ null;
125
202
 
126
203
  const getSsh = () => resolvedSsh;
204
+ const getRemoteFileTools = (ssh: { remote: string; remoteCwd: string }) => {
205
+ if (remoteFileTools) return remoteFileTools;
206
+ const intentController = new FileMutationIntentController({
207
+ operations: createRemoteIntentOps(ssh.remote, ssh.remoteCwd, localCwd),
208
+ });
209
+ remoteFileTools = {
210
+ write: createWriteTool(localCwd, {
211
+ operations: createRemoteWriteOps(ssh.remote, ssh.remoteCwd, localCwd),
212
+ intentController,
213
+ }),
214
+ edit: createEditTool(localCwd, {
215
+ operations: createRemoteEditOps(ssh.remote, ssh.remoteCwd, localCwd),
216
+ intentController,
217
+ }),
218
+ };
219
+ return remoteFileTools;
220
+ };
127
221
 
128
222
  pi.registerTool({
129
223
  ...localRead,
@@ -144,10 +238,7 @@ export default function (pi: ExtensionAPI) {
144
238
  async execute(id, params, signal, onUpdate, _ctx) {
145
239
  const ssh = getSsh();
146
240
  if (ssh) {
147
- const tool = createWriteTool(localCwd, {
148
- operations: createRemoteWriteOps(ssh.remote, ssh.remoteCwd, localCwd),
149
- });
150
- return tool.execute(id, params, signal, onUpdate);
241
+ return getRemoteFileTools(ssh).write.execute(id, params, signal, onUpdate);
151
242
  }
152
243
  return localWrite.execute(id, params, signal, onUpdate);
153
244
  },
@@ -158,10 +249,7 @@ export default function (pi: ExtensionAPI) {
158
249
  async execute(id, params, signal, onUpdate, _ctx) {
159
250
  const ssh = getSsh();
160
251
  if (ssh) {
161
- const tool = createEditTool(localCwd, {
162
- operations: createRemoteEditOps(ssh.remote, ssh.remoteCwd, localCwd),
163
- });
164
- return tool.execute(id, params, signal, onUpdate);
252
+ return getRemoteFileTools(ssh).edit.execute(id, params, signal, onUpdate);
165
253
  }
166
254
  return localEdit.execute(id, params, signal, onUpdate);
167
255
  },
@@ -185,6 +273,7 @@ export default function (pi: ExtensionAPI) {
185
273
  // Resolve SSH config now that CLI flags are available
186
274
  const arg = pi.getFlag("ssh") as string | undefined;
187
275
  if (arg) {
276
+ remoteFileTools = null;
188
277
  if (arg.includes(":")) {
189
278
  const [remote, path] = arg.split(":");
190
279
  resolvedSsh = { remote, remoteCwd: path };
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
- "version": "0.84.1",
3
+ "version": "0.85.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "pi-extension-with-deps",
9
- "version": "0.84.1",
9
+ "version": "0.85.0",
10
10
  "dependencies": {
11
11
  "ms": "^2.1.3"
12
12
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pi-extension-with-deps",
3
3
  "private": true,
4
- "version": "0.84.1",
4
+ "version": "0.85.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "clean": "echo 'nothing to clean'",
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@caupulican/pi-adaptative",
3
- "version": "0.84.1",
3
+ "version": "0.85.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@caupulican/pi-adaptative",
9
- "version": "0.84.1",
9
+ "version": "0.85.0",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
- "@caupulican/pi-agent-core": "^0.84.1",
13
- "@caupulican/pi-ai": "^0.84.1",
14
- "@caupulican/pi-tui": "^0.84.1",
12
+ "@caupulican/pi-agent-core": "^0.85.0",
13
+ "@caupulican/pi-ai": "^0.85.0",
14
+ "@caupulican/pi-tui": "^0.85.0",
15
15
  "@silvia-odwyer/photon-node": "0.3.4",
16
16
  "chalk": "5.6.2",
17
17
  "cross-spawn": "7.0.6",
@@ -20,6 +20,7 @@
20
20
  "highlight.js": "10.7.3",
21
21
  "hosted-git-info": "9.0.3",
22
22
  "ignore": "7.0.5",
23
+ "jscpd": "5.0.14",
23
24
  "jiti": "2.7.0",
24
25
  "minimatch": "10.2.5",
25
26
  "proper-lockfile": "4.1.2",
@@ -32,6 +33,7 @@
32
33
  "@mariozechner/clipboard": "0.3.9"
33
34
  },
34
35
  "bin": {
36
+ "jscpd": "dist/jscpd-cli.js",
35
37
  "pi": "dist/cli.js",
36
38
  "pi-adaptative": "dist/cli.js"
37
39
  },
@@ -475,11 +477,11 @@
475
477
  }
476
478
  },
477
479
  "node_modules/@caupulican/pi-agent-core": {
478
- "version": "0.84.1",
479
- "resolved": "https://registry.npmjs.org/@caupulican/pi-agent-core/-/pi-agent-core-0.84.1.tgz",
480
+ "version": "0.85.0",
481
+ "resolved": "https://registry.npmjs.org/@caupulican/pi-agent-core/-/pi-agent-core-0.85.0.tgz",
480
482
  "license": "MIT",
481
483
  "dependencies": {
482
- "@caupulican/pi-ai": "^0.84.1",
484
+ "@caupulican/pi-ai": "^0.85.0",
483
485
  "ignore": "7.0.5",
484
486
  "typebox": "1.1.38",
485
487
  "yaml": "2.9.0"
@@ -489,8 +491,8 @@
489
491
  }
490
492
  },
491
493
  "node_modules/@caupulican/pi-ai": {
492
- "version": "0.84.1",
493
- "resolved": "https://registry.npmjs.org/@caupulican/pi-ai/-/pi-ai-0.84.1.tgz",
494
+ "version": "0.85.0",
495
+ "resolved": "https://registry.npmjs.org/@caupulican/pi-ai/-/pi-ai-0.85.0.tgz",
494
496
  "license": "MIT",
495
497
  "dependencies": {
496
498
  "@anthropic-ai/sdk": "0.91.1",
@@ -542,8 +544,8 @@
542
544
  }
543
545
  },
544
546
  "node_modules/@caupulican/pi-tui": {
545
- "version": "0.84.1",
546
- "resolved": "https://registry.npmjs.org/@caupulican/pi-tui/-/pi-tui-0.84.1.tgz",
547
+ "version": "0.85.0",
548
+ "resolved": "https://registry.npmjs.org/@caupulican/pi-tui/-/pi-tui-0.85.0.tgz",
547
549
  "license": "MIT",
548
550
  "dependencies": {
549
551
  "get-east-asian-width": "1.6.0",
@@ -1683,6 +1685,104 @@
1683
1685
  "jiti": "lib/jiti-cli.mjs"
1684
1686
  }
1685
1687
  },
1688
+ "node_modules/jscpd": {
1689
+ "version": "5.0.14",
1690
+ "resolved": "https://registry.npmjs.org/jscpd/-/jscpd-5.0.14.tgz",
1691
+ "integrity": "sha512-zge+FPZZAymt2Do5Z0+QHyIn4/XcUhrO/W7of9HcHZfx2AK8++dYhLA1uWtwXj47ml3Of8PbcUW4wUWvYMCc3w==",
1692
+ "license": "MIT",
1693
+ "optionalDependencies": {
1694
+ "jscpd-darwin-arm64": "5.0.14",
1695
+ "jscpd-darwin-x64": "5.0.14",
1696
+ "jscpd-linux-arm64-gnu": "5.0.14",
1697
+ "jscpd-linux-x64-gnu": "5.0.14",
1698
+ "jscpd-linux-x64-musl": "5.0.14",
1699
+ "jscpd-windows-x64-msvc": "5.0.14"
1700
+ },
1701
+ "bin": {
1702
+ "jscpd": "run-jscpd.js"
1703
+ },
1704
+ "engines": {
1705
+ "node": ">=18"
1706
+ }
1707
+ },
1708
+ "node_modules/jscpd-darwin-arm64": {
1709
+ "version": "5.0.14",
1710
+ "resolved": "https://registry.npmjs.org/jscpd-darwin-arm64/-/jscpd-darwin-arm64-5.0.14.tgz",
1711
+ "integrity": "sha512-Ojjl79SBuj9tEW6WbjZ1a/1ZOR89dneH9yLQYQu8WyWaQownttnx7RYFEHU6aGhS4jIvwUEbr+1wxzFTb37cwg==",
1712
+ "license": "MIT",
1713
+ "os": [
1714
+ "darwin"
1715
+ ],
1716
+ "cpu": [
1717
+ "arm64"
1718
+ ],
1719
+ "optional": true
1720
+ },
1721
+ "node_modules/jscpd-darwin-x64": {
1722
+ "version": "5.0.14",
1723
+ "resolved": "https://registry.npmjs.org/jscpd-darwin-x64/-/jscpd-darwin-x64-5.0.14.tgz",
1724
+ "integrity": "sha512-DxFg5XvjMZ81iVeqillnM5apqcGCfNTbroNF+mPLr7RkHLGH6mudLgtO+ILL/hfpZXy1bF9oIY5BSudPmN/k9A==",
1725
+ "license": "MIT",
1726
+ "os": [
1727
+ "darwin"
1728
+ ],
1729
+ "cpu": [
1730
+ "x64"
1731
+ ],
1732
+ "optional": true
1733
+ },
1734
+ "node_modules/jscpd-linux-arm64-gnu": {
1735
+ "version": "5.0.14",
1736
+ "resolved": "https://registry.npmjs.org/jscpd-linux-arm64-gnu/-/jscpd-linux-arm64-gnu-5.0.14.tgz",
1737
+ "integrity": "sha512-1uw+XBHEt9pONXNICSp5HpaVWPjG6mQ6deDXaq9Yb0xCNJkX4/8gmn0vhzekIyZD2DspRYKPUolbDsqm/HEdYg==",
1738
+ "license": "MIT",
1739
+ "os": [
1740
+ "linux"
1741
+ ],
1742
+ "cpu": [
1743
+ "arm64"
1744
+ ],
1745
+ "optional": true
1746
+ },
1747
+ "node_modules/jscpd-linux-x64-gnu": {
1748
+ "version": "5.0.14",
1749
+ "resolved": "https://registry.npmjs.org/jscpd-linux-x64-gnu/-/jscpd-linux-x64-gnu-5.0.14.tgz",
1750
+ "integrity": "sha512-dFTbyyrm+Z9pcXIVzJQCw8QAgiNqIiO69sm4AfA7/wFdPoizoVzjhaXsYXcSV4bs0aoPiWbNazg0J0HgslT/5A==",
1751
+ "license": "MIT",
1752
+ "os": [
1753
+ "linux"
1754
+ ],
1755
+ "cpu": [
1756
+ "x64"
1757
+ ],
1758
+ "optional": true
1759
+ },
1760
+ "node_modules/jscpd-linux-x64-musl": {
1761
+ "version": "5.0.14",
1762
+ "resolved": "https://registry.npmjs.org/jscpd-linux-x64-musl/-/jscpd-linux-x64-musl-5.0.14.tgz",
1763
+ "integrity": "sha512-SayS7qQJvixyy9eR0+UjepkTsUUwqvlsiuSxfIdHgG2qzqoh/thnkgiu4By8fsiiDpQONsQrRrZDwHRQ3GDrBQ==",
1764
+ "license": "MIT",
1765
+ "os": [
1766
+ "linux"
1767
+ ],
1768
+ "cpu": [
1769
+ "x64"
1770
+ ],
1771
+ "optional": true
1772
+ },
1773
+ "node_modules/jscpd-windows-x64-msvc": {
1774
+ "version": "5.0.14",
1775
+ "resolved": "https://registry.npmjs.org/jscpd-windows-x64-msvc/-/jscpd-windows-x64-msvc-5.0.14.tgz",
1776
+ "integrity": "sha512-DqjxlVkUanlahGgY2lY7Zkrau4BUTI+AwWky+bPGK4kSK2AIOaUziY9Q19u8b58idXmJA9FKK98Fuu4ajNXVjQ==",
1777
+ "license": "MIT",
1778
+ "os": [
1779
+ "win32"
1780
+ ],
1781
+ "cpu": [
1782
+ "x64"
1783
+ ],
1784
+ "optional": true
1785
+ },
1686
1786
  "node_modules/json-bigint": {
1687
1787
  "version": "1.0.0",
1688
1788
  "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caupulican/pi-adaptative",
3
- "version": "0.84.1",
3
+ "version": "0.85.0",
4
4
  "description": "Adaptive fork of Pi coding agent for self-evolving agent harness experiments",
5
5
  "type": "module",
6
6
  "piConfig": {
@@ -8,6 +8,7 @@
8
8
  "configDir": ".pi"
9
9
  },
10
10
  "bin": {
11
+ "jscpd": "dist/jscpd-cli.js",
11
12
  "pi": "dist/cli.js",
12
13
  "pi-adaptative": "dist/cli.js"
13
14
  },
@@ -71,7 +72,7 @@
71
72
  ],
72
73
  "scripts": {
73
74
  "clean": "shx rm -rf dist",
74
- "build": "node ../../scripts/run-tsc.mjs -p tsconfig.build.json && shx chmod +x dist/cli.js && npm run copy-assets",
75
+ "build": "node ../../scripts/run-tsc.mjs -p tsconfig.build.json && shx chmod +x dist/cli.js dist/jscpd-cli.js && npm run copy-assets",
75
76
  "build:binary": "npm --prefix ../tui run build && npm --prefix ../ai run build && npm --prefix ../agent run build && npm run build && bun build --compile ./dist/bun/cli.js ./src/utils/image-resize-worker.ts ./src/core/memory/providers/transcript-recall-worker.ts --outfile dist/pi && npm run copy-binary-assets",
76
77
  "copy-assets": "shx mkdir -p dist/modes/interactive/theme && shx cp src/modes/interactive/theme/*.json dist/modes/interactive/theme/ && shx mkdir -p dist/modes/interactive/assets && shx cp src/modes/interactive/assets/*.png dist/modes/interactive/assets/ && shx mkdir -p dist/core/export-html/vendor && shx cp src/core/skill-block.mjs src/core/skill-block.d.mts dist/core/ && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js src/core/export-html/session-tree-foundations.mjs src/core/export-html/session-tree-foundations.d.mts dist/core/export-html/ && shx cp src/core/export-html/vendor/*.js dist/core/export-html/vendor/ && shx mkdir -p dist/bundled-resources && shx cp -r src/bundled-resources/* dist/bundled-resources/",
77
78
  "copy-binary-assets": "shx cp package.json dist/ && shx cp README.md dist/ && shx cp CHANGELOG.md dist/ && shx mkdir -p dist/theme && shx cp src/modes/interactive/theme/*.json dist/theme/ && shx mkdir -p dist/assets && shx cp src/modes/interactive/assets/*.png dist/assets/ && shx mkdir -p dist/export-html/vendor && shx cp src/core/skill-block.mjs dist/ && shx cp src/core/export-html/template.html src/core/export-html/template.css src/core/export-html/template.js src/core/export-html/session-tree-foundations.mjs dist/export-html/ && shx cp src/core/export-html/vendor/*.js dist/export-html/vendor/ && shx mkdir -p dist/bundled-resources && shx cp -r src/bundled-resources/* dist/bundled-resources/ && shx cp -r docs dist/ && shx cp -r examples dist/ && shx cp ../../node_modules/@silvia-odwyer/photon-node/photon_rs_bg.wasm dist/",
@@ -81,9 +82,9 @@
81
82
  "postinstall": "node scripts/postinstall-python-runtime.mjs"
82
83
  },
83
84
  "dependencies": {
84
- "@caupulican/pi-agent-core": "^0.84.1",
85
- "@caupulican/pi-ai": "^0.84.1",
86
- "@caupulican/pi-tui": "^0.84.1",
85
+ "@caupulican/pi-agent-core": "^0.85.0",
86
+ "@caupulican/pi-ai": "^0.85.0",
87
+ "@caupulican/pi-tui": "^0.85.0",
87
88
  "@silvia-odwyer/photon-node": "0.3.4",
88
89
  "chalk": "5.6.2",
89
90
  "cross-spawn": "7.0.6",
@@ -92,6 +93,7 @@
92
93
  "highlight.js": "10.7.3",
93
94
  "hosted-git-info": "9.0.3",
94
95
  "ignore": "7.0.5",
96
+ "jscpd": "5.0.14",
95
97
  "jiti": "2.7.0",
96
98
  "minimatch": "10.2.5",
97
99
  "proper-lockfile": "4.1.2",