@caupulican/pi-adaptative 0.81.43 → 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.
- package/CHANGELOG.md +48 -0
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.d.ts.map +1 -1
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js +14 -8
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.js.map +1 -1
- package/dist/bundled-resources/extensions/tmux-agent-manager/index.ts +14 -8
- package/dist/bundled-resources/skills/deduplicate-by-evidence/SKILL.md +7 -5
- package/dist/bundled-resources/skills/worker-profile-writer/SKILL.md +63 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/agent-paths.d.ts +1 -1
- package/dist/core/agent-paths.d.ts.map +1 -1
- package/dist/core/agent-paths.js +1 -1
- package/dist/core/agent-paths.js.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.d.ts.map +1 -1
- package/dist/core/autonomy/lane-tool-surface.js +4 -2
- package/dist/core/autonomy/lane-tool-surface.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts +3 -0
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +6 -0
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts.map +1 -1
- package/dist/core/default-tool-surface.js +1 -0
- package/dist/core/default-tool-surface.js.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.d.ts +7 -0
- package/dist/core/delegation/worker-delegation-controller.d.ts.map +1 -1
- package/dist/core/delegation/worker-delegation-controller.js +28 -0
- package/dist/core/delegation/worker-delegation-controller.js.map +1 -1
- package/dist/core/delegation/worker-profile-resolver.d.ts +2 -0
- package/dist/core/delegation/worker-profile-resolver.d.ts.map +1 -1
- package/dist/core/delegation/worker-profile-resolver.js +46 -11
- package/dist/core/delegation/worker-profile-resolver.js.map +1 -1
- package/dist/core/doctor.d.ts +5 -5
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +17 -20
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/model-capability.d.ts.map +1 -1
- package/dist/core/model-capability.js +1 -0
- package/dist/core/model-capability.js.map +1 -1
- package/dist/core/orchestration/lane-tool-manifests.d.ts +1 -1
- package/dist/core/orchestration/lane-tool-manifests.d.ts.map +1 -1
- package/dist/core/orchestration/lane-tool-manifests.js +6 -1
- package/dist/core/orchestration/lane-tool-manifests.js.map +1 -1
- package/dist/core/orchestration/session-task-profile-store.d.ts +31 -0
- package/dist/core/orchestration/session-task-profile-store.d.ts.map +1 -0
- package/dist/core/orchestration/session-task-profile-store.js +138 -0
- package/dist/core/orchestration/session-task-profile-store.js.map +1 -0
- package/dist/core/orchestration/task-profile-writer.d.ts +58 -0
- package/dist/core/orchestration/task-profile-writer.d.ts.map +1 -0
- package/dist/core/orchestration/task-profile-writer.js +165 -0
- package/dist/core/orchestration/task-profile-writer.js.map +1 -0
- package/dist/core/runtime-builder.d.ts +3 -1
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +14 -0
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/secrets/credential-exposure-guard.d.ts.map +1 -1
- package/dist/core/secrets/credential-exposure-guard.js +32 -0
- package/dist/core/secrets/credential-exposure-guard.js.map +1 -1
- package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
- package/dist/core/security/untrusted-boundary.js +4 -6
- package/dist/core/security/untrusted-boundary.js.map +1 -1
- package/dist/core/session-role.d.ts.map +1 -1
- package/dist/core/session-role.js +1 -0
- package/dist/core/session-role.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +21 -22
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tool-capability-policy.js +1 -1
- package/dist/core/tool-capability-policy.js.map +1 -1
- package/dist/core/tools/bash.d.ts +1 -6
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +2 -26
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/delegate.js +4 -4
- package/dist/core/tools/delegate.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts +32 -5
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +137 -69
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts +22 -7
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +128 -55
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/file-mutation-intent.d.ts +70 -0
- package/dist/core/tools/file-mutation-intent.d.ts.map +1 -0
- package/dist/core/tools/file-mutation-intent.js +325 -0
- package/dist/core/tools/file-mutation-intent.js.map +1 -0
- package/dist/core/tools/goal.d.ts.map +1 -1
- package/dist/core/tools/goal.js +41 -18
- package/dist/core/tools/goal.js.map +1 -1
- package/dist/core/tools/index.d.ts +3 -1
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +31 -8
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/profile-writer.d.ts +33 -0
- package/dist/core/tools/profile-writer.d.ts.map +1 -0
- package/dist/core/tools/profile-writer.js +67 -0
- package/dist/core/tools/profile-writer.js.map +1 -0
- package/dist/core/tools/task-steps.d.ts.map +1 -1
- package/dist/core/tools/task-steps.js +6 -11
- package/dist/core/tools/task-steps.js.map +1 -1
- package/dist/core/tools/tmux-dispatch.d.ts +2 -0
- package/dist/core/tools/tmux-dispatch.d.ts.map +1 -1
- package/dist/core/tools/tmux-dispatch.js +16 -2
- package/dist/core/tools/tmux-dispatch.js.map +1 -1
- package/dist/core/tools/write.d.ts +24 -7
- package/dist/core/tools/write.d.ts.map +1 -1
- package/dist/core/tools/write.js +182 -107
- package/dist/core/tools/write.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/jscpd-cli.d.ts +3 -0
- package/dist/jscpd-cli.d.ts.map +1 -0
- package/dist/jscpd-cli.js +22 -0
- package/dist/jscpd-cli.js.map +1 -0
- package/dist/modes/interactive/components/profile-resource-editor.js +2 -2
- package/dist/modes/interactive/components/profile-resource-editor.js.map +1 -1
- package/dist/modes/interactive/components/tool-execution.d.ts +1 -0
- package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
- package/dist/modes/interactive/components/tool-execution.js +85 -3
- package/dist/modes/interactive/components/tool-execution.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +3 -3
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/utils/bundled-jscpd.d.ts +20 -0
- package/dist/utils/bundled-jscpd.d.ts.map +1 -0
- package/dist/utils/bundled-jscpd.js +154 -0
- package/dist/utils/bundled-jscpd.js.map +1 -0
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +11 -0
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +6 -1
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +81 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/development.md +1 -1
- package/docs/index.md +3 -0
- package/docs/managed-data-tools.md +15 -0
- package/docs/quickstart.md +2 -2
- package/docs/tool-boundary-performance-roadmap-2026-08-02.md +166 -0
- package/docs/tool-repair.md +1 -1
- package/docs/windows.md +2 -2
- package/docs/work-directory.md +1 -1
- package/docs/worker-profiles.md +16 -0
- package/examples/extensions/built-in-tool-renderer.ts +12 -5
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/minimal-mode.ts +8 -7
- package/examples/extensions/prompt-customizer.ts +4 -2
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/ssh.ts +113 -24
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +123 -23
- package/package.json +8 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Task worker profiles
|
|
2
|
+
|
|
3
|
+
`profile_writer` lets the foreground orchestrator create the smallest worker profile needed for one bounded task. It does not edit user or project profile files.
|
|
4
|
+
|
|
5
|
+
Task profiles are:
|
|
6
|
+
|
|
7
|
+
- Derived only from an owner-authorized base profile.
|
|
8
|
+
- Immutable and assigned a `task-...` ID by Pi.
|
|
9
|
+
- Stored on the owning session branch, never shared across independent sessions.
|
|
10
|
+
- Limited to 32 profiles per session.
|
|
11
|
+
- Unable to add tools, resources, or budget beyond the base profile.
|
|
12
|
+
- Limited to configured, authenticated, non-exhausted models and a thinking level that model supports.
|
|
13
|
+
|
|
14
|
+
Use `profile_writer { action: "inspect" }` when the authorized bases or model combinations are unknown. Then call `create` with a concise task and only the fields that need narrowing. Pass the returned profile ID unchanged to `delegate`; never invent one.
|
|
15
|
+
|
|
16
|
+
Workers cannot invoke `profile_writer`, recursively delegate, or expand their admitted contract. A resumed worker retains the exact profile, model, resources, and transcript admitted for that task.
|
|
@@ -26,7 +26,13 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import type { BashToolDetails, EditToolDetails, ExtensionAPI, ReadToolDetails } from "@caupulican/pi-adaptative";
|
|
29
|
-
import {
|
|
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
|
|
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
|
|
234
|
-
text += theme.fg("dim", ` (${
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.85.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.52.0"
|
|
12
12
|
}
|
|
@@ -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
|
|
183
|
-
const
|
|
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}${
|
|
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
|
|
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(
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.85.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sandbox-runtime": "^0.0.26"
|
|
12
12
|
}
|
|
@@ -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
|
|
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: ["
|
|
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 ${
|
|
51
|
-
access: (p) => sshExec(remote, `test -r ${
|
|
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 ${
|
|
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
|
-
|
|
68
|
-
|
|
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
|
|
78
|
-
return {
|
|
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 ${
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
9
|
+
"version": "0.85.0",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"ms": "^2.1.3"
|
|
12
12
|
},
|