@caupulican/pi-adaptative 0.81.35 → 0.81.38
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 +36 -0
- package/README.md +1 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +10 -8
- package/dist/cli/args.js.map +1 -1
- package/dist/core/agent-session.d.ts +5 -0
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +17 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
- package/dist/core/autonomy/approval-gate.js +2 -0
- package/dist/core/autonomy/approval-gate.js.map +1 -1
- package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
- package/dist/core/autonomy/foreground-envelope.js +2 -0
- package/dist/core/autonomy/foreground-envelope.js.map +1 -1
- package/dist/core/autonomy/gates.d.ts.map +1 -1
- package/dist/core/autonomy/gates.js +9 -2
- package/dist/core/autonomy/gates.js.map +1 -1
- package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
- package/dist/core/autonomy/risk-assessment.js +2 -0
- package/dist/core/autonomy/risk-assessment.js.map +1 -1
- package/dist/core/background-lane-controller.d.ts.map +1 -1
- package/dist/core/background-lane-controller.js +1 -1
- package/dist/core/background-lane-controller.js.map +1 -1
- package/dist/core/bash-execution-controller.d.ts +9 -4
- package/dist/core/bash-execution-controller.d.ts.map +1 -1
- package/dist/core/bash-execution-controller.js +7 -6
- package/dist/core/bash-execution-controller.js.map +1 -1
- package/dist/core/bash-executor.d.ts +2 -0
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +3 -2
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/context/sqlite-runtime-index.d.ts.map +1 -1
- package/dist/core/context/sqlite-runtime-index.js +13 -7
- package/dist/core/context/sqlite-runtime-index.js.map +1 -1
- package/dist/core/default-tool-surface.d.ts +7 -0
- package/dist/core/default-tool-surface.d.ts.map +1 -1
- package/dist/core/default-tool-surface.js +29 -10
- package/dist/core/default-tool-surface.js.map +1 -1
- package/dist/core/delegation/worker-actions.d.ts.map +1 -1
- package/dist/core/delegation/worker-actions.js +1 -1
- package/dist/core/delegation/worker-actions.js.map +1 -1
- package/dist/core/doctor.d.ts +10 -10
- package/dist/core/doctor.d.ts.map +1 -1
- package/dist/core/doctor.js +30 -20
- package/dist/core/doctor.js.map +1 -1
- package/dist/core/exec.d.ts +2 -0
- package/dist/core/exec.d.ts.map +1 -1
- package/dist/core/exec.js +1 -0
- package/dist/core/exec.js.map +1 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +3 -7
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/types.d.ts +13 -3
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +3 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/memory/memory-manager.d.ts.map +1 -1
- package/dist/core/memory/memory-manager.js +1 -0
- package/dist/core/memory/memory-manager.js.map +1 -1
- package/dist/core/model-capability.d.ts.map +1 -1
- package/dist/core/model-capability.js +2 -0
- package/dist/core/model-capability.js.map +1 -1
- package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
- package/dist/core/model-router/tool-escalation.js +17 -3
- package/dist/core/model-router/tool-escalation.js.map +1 -1
- package/dist/core/models/local-runtime.d.ts.map +1 -1
- package/dist/core/models/local-runtime.js +4 -3
- package/dist/core/models/local-runtime.js.map +1 -1
- package/dist/core/python-runtime.d.ts +46 -0
- package/dist/core/python-runtime.d.ts.map +1 -0
- package/dist/core/python-runtime.js +157 -0
- package/dist/core/python-runtime.js.map +1 -0
- package/dist/core/runtime-builder.d.ts +4 -0
- package/dist/core/runtime-builder.d.ts.map +1 -1
- package/dist/core/runtime-builder.js +31 -9
- package/dist/core/runtime-builder.js.map +1 -1
- package/dist/core/security/untrusted-boundary.d.ts +2 -2
- package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
- package/dist/core/security/untrusted-boundary.js +4 -3
- package/dist/core/security/untrusted-boundary.js.map +1 -1
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +2 -0
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +8 -0
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +13 -2
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/tasks/session-task-state.d.ts +10 -0
- package/dist/core/tasks/session-task-state.d.ts.map +1 -0
- package/dist/core/tasks/session-task-state.js +29 -0
- package/dist/core/tasks/session-task-state.js.map +1 -0
- package/dist/core/tasks/task-command.d.ts +30 -0
- package/dist/core/tasks/task-command.d.ts.map +1 -0
- package/dist/core/tasks/task-command.js +79 -0
- package/dist/core/tasks/task-command.js.map +1 -0
- package/dist/core/tasks/task-state.d.ts +71 -0
- package/dist/core/tasks/task-state.d.ts.map +1 -0
- package/dist/core/tasks/task-state.js +372 -0
- package/dist/core/tasks/task-state.js.map +1 -0
- package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
- package/dist/core/tool-selection/tool-selection-controller.js +1 -1
- package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
- package/dist/core/tools/bash.d.ts +20 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +124 -199
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +55 -44
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +8 -4
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/index.d.ts +9 -6
- package/dist/core/tools/index.d.ts.map +1 -1
- package/dist/core/tools/index.js +24 -9
- package/dist/core/tools/index.js.map +1 -1
- package/dist/core/tools/python.d.ts +64 -0
- package/dist/core/tools/python.d.ts.map +1 -0
- package/dist/core/tools/python.js +241 -0
- package/dist/core/tools/python.js.map +1 -0
- package/dist/core/tools/render-utils.d.ts.map +1 -1
- package/dist/core/tools/render-utils.js +1 -1
- package/dist/core/tools/render-utils.js.map +1 -1
- package/dist/core/tools/shell-contract-router.d.ts +13 -0
- package/dist/core/tools/shell-contract-router.d.ts.map +1 -0
- package/dist/core/tools/shell-contract-router.js +330 -0
- package/dist/core/tools/shell-contract-router.js.map +1 -0
- package/dist/core/tools/task-steps.d.ts +46 -0
- package/dist/core/tools/task-steps.d.ts.map +1 -0
- package/dist/core/tools/task-steps.js +160 -0
- package/dist/core/tools/task-steps.js.map +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts +1 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +12 -0
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/session-flow-commands.d.ts +10 -0
- package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
- package/dist/modes/interactive/session-flow-commands.js +43 -0
- package/dist/modes/interactive/session-flow-commands.js.map +1 -1
- package/dist/utils/shell.d.ts +6 -8
- package/dist/utils/shell.d.ts.map +1 -1
- package/dist/utils/shell.js +75 -69
- package/dist/utils/shell.js.map +1 -1
- package/dist/utils/tools-manager.d.ts +23 -3
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +94 -11
- package/dist/utils/tools-manager.js.map +1 -1
- package/docs/development.md +2 -0
- package/docs/index.md +2 -0
- package/docs/packages.md +1 -1
- package/docs/python.md +43 -0
- package/docs/settings.md +9 -5
- package/docs/task-steps.md +90 -0
- package/docs/termux.md +7 -0
- package/docs/usage.md +5 -6
- package/docs/windows.md +15 -12
- 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/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +32 -37
- package/package.json +9 -7
- package/scripts/postinstall-python-runtime.mjs +20 -0
package/dist/core/tools/bash.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
|
-
import { access as fsAccess
|
|
3
|
-
import { relative as relativePath, resolve as resolvePath } from "node:path";
|
|
2
|
+
import { access as fsAccess } from "node:fs/promises";
|
|
4
3
|
import { createSilenceWatchdog } from "@caupulican/pi-agent-core";
|
|
5
4
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, } from "@caupulican/pi-agent-core/node";
|
|
6
5
|
import { Container, Text, truncateToWidth } from "@caupulican/pi-tui";
|
|
@@ -10,44 +9,53 @@ import { keyHint } from "../../modes/interactive/components/keybinding-hints.js"
|
|
|
10
9
|
import { truncateToVisualLines } from "../../modes/interactive/components/visual-truncate.js";
|
|
11
10
|
import { theme } from "../../modes/interactive/theme/theme.js";
|
|
12
11
|
import { waitForChildProcessWithTermination } from "../../utils/child-process.js";
|
|
13
|
-
import { getShellConfig, getShellEnv, trackDetachedChildPid, untrackDetachedChildPid } from "../../utils/shell.js";
|
|
12
|
+
import { getPlatformShellToolName, getShellConfig, getShellEnv, prefixPowerShellCommand, trackDetachedChildPid, untrackDetachedChildPid, } from "../../utils/shell.js";
|
|
14
13
|
import { withExclusiveMutationBarrier } from "./file-mutation-queue.js";
|
|
15
14
|
import { classifyGitCommand, executeFilteredGit } from "./git-filter.js";
|
|
16
15
|
import { OutputAccumulator } from "./output-accumulator.js";
|
|
17
16
|
import { getTextOutput, invalidArgText, str } from "./render-utils.js";
|
|
17
|
+
import { routeShellContract } from "./shell-contract-router.js";
|
|
18
18
|
import { wrapToolDefinition } from "./tool-definition-wrapper.js";
|
|
19
|
-
/**
|
|
19
|
+
/** Low-level silence bound retained for direct shell-operation consumers. Agent tool calls always pass a wall-clock bound. */
|
|
20
20
|
const DEFAULT_COMMAND_SILENCE_MS = 600_000;
|
|
21
|
+
/** Agent-facing wall-clock bound: continuously producing output must not make a command immortal. */
|
|
22
|
+
export const DEFAULT_COMMAND_TIMEOUT_SECONDS = 120;
|
|
23
|
+
export const MAX_COMMAND_TIMEOUT_SECONDS = 3600;
|
|
24
|
+
const MIN_COMMAND_TIMEOUT_SECONDS = 0.1;
|
|
21
25
|
let commandSilenceMsOverride;
|
|
22
|
-
|
|
26
|
+
let commandTimeoutMsOverride;
|
|
27
|
+
/** Test hook: override the low-level silence threshold. Pass undefined to restore the default. */
|
|
23
28
|
export function setCommandSilenceMsForTests(ms) {
|
|
24
29
|
commandSilenceMsOverride = ms;
|
|
25
30
|
}
|
|
31
|
+
/** Test hook: override the agent tool's default wall-clock bound. Pass undefined to restore it. */
|
|
32
|
+
export function setCommandTimeoutMsForTests(ms) {
|
|
33
|
+
commandTimeoutMsOverride = ms;
|
|
34
|
+
}
|
|
35
|
+
export function resolveCommandTimeoutSeconds(timeout) {
|
|
36
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout) || timeout <= 0) {
|
|
37
|
+
return DEFAULT_COMMAND_TIMEOUT_SECONDS;
|
|
38
|
+
}
|
|
39
|
+
return Math.max(MIN_COMMAND_TIMEOUT_SECONDS, Math.min(timeout, MAX_COMMAND_TIMEOUT_SECONDS));
|
|
40
|
+
}
|
|
26
41
|
const bashSchema = Type.Object({
|
|
27
|
-
command: Type.String({ description: "
|
|
42
|
+
command: Type.String({ description: "Shell command to execute" }),
|
|
28
43
|
timeout: Type.Optional(Type.Number({
|
|
29
|
-
description:
|
|
44
|
+
description: `Wall-clock timeout in seconds. Defaults to ${DEFAULT_COMMAND_TIMEOUT_SECONDS}; positive overrides are capped at ${MAX_COMMAND_TIMEOUT_SECONDS}. Zero or negative values use the default.`,
|
|
30
45
|
})),
|
|
31
46
|
});
|
|
32
|
-
|
|
33
|
-
* Create bash operations using pi's built-in local shell execution backend.
|
|
34
|
-
*
|
|
35
|
-
* This is useful for extensions that intercept user_bash and still want pi's
|
|
36
|
-
* standard local shell behavior while wrapping or rewriting commands.
|
|
37
|
-
*/
|
|
38
|
-
export function createLocalBashOperations(options) {
|
|
47
|
+
function createLocalShellOperations(shellName, options) {
|
|
39
48
|
return {
|
|
40
49
|
exec: async (command, cwd, { onData, signal, timeout, env }) => {
|
|
41
|
-
const { shell, args } = getShellConfig(options?.shellPath);
|
|
50
|
+
const { shell, args } = getShellConfig(options?.shellPath, shellName);
|
|
42
51
|
try {
|
|
43
52
|
await fsAccess(cwd, constants.F_OK);
|
|
44
53
|
}
|
|
45
54
|
catch {
|
|
46
|
-
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute
|
|
55
|
+
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute ${shellName} commands.`);
|
|
47
56
|
}
|
|
48
|
-
if (signal?.aborted)
|
|
57
|
+
if (signal?.aborted)
|
|
49
58
|
throw new Error("aborted");
|
|
50
|
-
}
|
|
51
59
|
const child = spawn(shell, [...args, command], {
|
|
52
60
|
cwd,
|
|
53
61
|
detached: process.platform !== "win32",
|
|
@@ -59,10 +67,6 @@ export function createLocalBashOperations(options) {
|
|
|
59
67
|
trackDetachedChildPid(child.pid);
|
|
60
68
|
const terminationController = new AbortController();
|
|
61
69
|
const onAbort = () => terminationController.abort();
|
|
62
|
-
// A command that keeps producing output must never be killed by this mechanism:
|
|
63
|
-
// silence bounds mute-ness, not duration. It arms when the caller left `timeout`
|
|
64
|
-
// unset or non-positive (treated as unset) — an explicit positive timeout means
|
|
65
|
-
// the model took responsibility for the wall clock.
|
|
66
70
|
let silenceKilled = false;
|
|
67
71
|
const silenceMs = commandSilenceMsOverride ?? DEFAULT_COMMAND_SILENCE_MS;
|
|
68
72
|
const silenceWatchdog = (timeout === undefined || timeout <= 0) && silenceMs > 0
|
|
@@ -79,10 +83,8 @@ export function createLocalBashOperations(options) {
|
|
|
79
83
|
onData(data);
|
|
80
84
|
};
|
|
81
85
|
try {
|
|
82
|
-
// Stream stdout and stderr.
|
|
83
86
|
child.stdout?.on("data", onChunk);
|
|
84
87
|
child.stderr?.on("data", onChunk);
|
|
85
|
-
// Forward caller cancellation into the shared bounded termination path.
|
|
86
88
|
if (signal) {
|
|
87
89
|
if (signal.aborted)
|
|
88
90
|
onAbort();
|
|
@@ -94,15 +96,12 @@ export function createLocalBashOperations(options) {
|
|
|
94
96
|
timeoutMs: timeout !== undefined && timeout > 0 ? timeout * 1000 : undefined,
|
|
95
97
|
killGraceMs: 2_000,
|
|
96
98
|
});
|
|
97
|
-
if (signal?.aborted)
|
|
99
|
+
if (signal?.aborted)
|
|
98
100
|
throw new Error("aborted");
|
|
99
|
-
|
|
100
|
-
if (terminal.reason === "timeout") {
|
|
101
|
+
if (terminal.reason === "timeout")
|
|
101
102
|
throw new Error(`timeout:${timeout}`);
|
|
102
|
-
|
|
103
|
-
if (silenceKilled) {
|
|
103
|
+
if (silenceKilled)
|
|
104
104
|
throw new Error(`silence:${silenceMs / 1000}`);
|
|
105
|
-
}
|
|
106
105
|
return { exitCode: terminal.code };
|
|
107
106
|
}
|
|
108
107
|
finally {
|
|
@@ -115,6 +114,41 @@ export function createLocalBashOperations(options) {
|
|
|
115
114
|
},
|
|
116
115
|
};
|
|
117
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Create bash operations using pi's built-in local shell execution backend.
|
|
119
|
+
*
|
|
120
|
+
* This is useful for extensions that intercept user_bash and still want pi's
|
|
121
|
+
* standard local shell behavior while wrapping or rewriting commands.
|
|
122
|
+
*/
|
|
123
|
+
export function createLocalBashOperations(options) {
|
|
124
|
+
return createLocalShellOperations("bash", options);
|
|
125
|
+
}
|
|
126
|
+
/** Create PowerShell operations using pi's built-in local execution backend. */
|
|
127
|
+
export function createLocalPowerShellOperations(options) {
|
|
128
|
+
return createLocalShellOperations("powershell", options);
|
|
129
|
+
}
|
|
130
|
+
/** Create the platform shell backend without requiring callers or the model to choose a shell. */
|
|
131
|
+
export function createLocalPlatformShellOperations(options = {}, platform = process.platform) {
|
|
132
|
+
const operations = options.operations ??
|
|
133
|
+
createLocalShellOperations(getPlatformShellToolName(platform), { shellPath: options.shellPath });
|
|
134
|
+
return {
|
|
135
|
+
async exec(command, cwd, execOptions) {
|
|
136
|
+
let resolvedCommand = command;
|
|
137
|
+
if (platform === "win32") {
|
|
138
|
+
const route = routeShellContract(command, platform);
|
|
139
|
+
if (route.kind === "unsupported")
|
|
140
|
+
throw new Error(route.error);
|
|
141
|
+
if (route.kind === "powershell")
|
|
142
|
+
resolvedCommand = route.command;
|
|
143
|
+
}
|
|
144
|
+
if (options.commandPrefix)
|
|
145
|
+
resolvedCommand = `${options.commandPrefix}\n${resolvedCommand}`;
|
|
146
|
+
if (platform === "win32")
|
|
147
|
+
resolvedCommand = prefixPowerShellCommand(resolvedCommand);
|
|
148
|
+
return operations.exec(resolvedCommand, cwd, execOptions);
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
118
152
|
function resolveSpawnContext(command, cwd, spawnHook) {
|
|
119
153
|
const baseContext = { command, cwd, env: { ...getShellEnv() } };
|
|
120
154
|
return spawnHook ? spawnHook(baseContext) : baseContext;
|
|
@@ -132,12 +166,13 @@ class BashResultRenderComponent extends Container {
|
|
|
132
166
|
function formatDuration(ms) {
|
|
133
167
|
return `${(ms / 1000).toFixed(1)}s`;
|
|
134
168
|
}
|
|
135
|
-
function formatBashCall(args) {
|
|
169
|
+
function formatBashCall(args, shellName) {
|
|
136
170
|
const command = str(args?.command);
|
|
137
171
|
const timeout = args?.timeout;
|
|
138
172
|
const timeoutSuffix = timeout ? theme.fg("muted", ` (timeout ${timeout}s)`) : "";
|
|
139
173
|
const commandDisplay = command === null ? invalidArgText(theme) : command ? command : theme.fg("toolOutput", "...");
|
|
140
|
-
|
|
174
|
+
const prompt = shellName === "powershell" ? "PS>" : "$";
|
|
175
|
+
return theme.fg("toolTitle", theme.bold(`${prompt} ${commandDisplay}`)) + timeoutSuffix;
|
|
141
176
|
}
|
|
142
177
|
function rebuildBashResultRenderComponent(component, result, options, showImages, startedAt, endedAt) {
|
|
143
178
|
const state = component.state;
|
|
@@ -209,158 +244,40 @@ function rebuildBashResultRenderComponent(component, result, options, showImages
|
|
|
209
244
|
component.addChild(new Text(`\n${theme.fg("muted", `${label} ${formatDuration(endTime - startedAt)}`)}`, 0, 0));
|
|
210
245
|
}
|
|
211
246
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
return { optimized: false };
|
|
219
|
-
}
|
|
220
|
-
// Reject if there are shell operators or pipes/redirects
|
|
221
|
-
const shellOperators = ["|", ">", "<", "&", ";", "\n", "\r", "$", "`", "(", ")", "*", "?", "[", "]"];
|
|
222
|
-
if (shellOperators.some((op) => trimmed.includes(op))) {
|
|
223
|
-
return { optimized: false };
|
|
224
|
-
}
|
|
225
|
-
// Simple tokenizer split by whitespace
|
|
226
|
-
const args = trimmed.split(/\s+/);
|
|
227
|
-
if (args.length === 0) {
|
|
228
|
-
return { optimized: false };
|
|
229
|
-
}
|
|
230
|
-
const cmd = args[0];
|
|
231
|
-
const unquote = (s) => {
|
|
232
|
-
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith("'") && s.endsWith("'"))) {
|
|
233
|
-
return s.slice(1, -1);
|
|
234
|
-
}
|
|
235
|
-
return s;
|
|
236
|
-
};
|
|
237
|
-
const resolveToCwd = (p, base) => {
|
|
238
|
-
const clean = unquote(p);
|
|
239
|
-
if (clean.startsWith("/"))
|
|
240
|
-
return clean;
|
|
241
|
-
return resolvePath(base, clean);
|
|
242
|
-
};
|
|
243
|
-
try {
|
|
244
|
-
if (cmd === "cat") {
|
|
245
|
-
if (args.length === 2 && !args[1].startsWith("-")) {
|
|
246
|
-
const filePath = resolveToCwd(args[1], cwd);
|
|
247
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
248
|
-
return { optimized: true, output: content, exitCode: 0 };
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
else if (cmd === "head") {
|
|
252
|
-
if (args.length === 2 && !args[1].startsWith("-")) {
|
|
253
|
-
const filePath = resolveToCwd(args[1], cwd);
|
|
254
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
255
|
-
const lines = content.split("\n").slice(0, 10).join("\n");
|
|
256
|
-
return { optimized: true, output: lines, exitCode: 0 };
|
|
257
|
-
}
|
|
258
|
-
else if (args.length === 4 && args[1] === "-n" && !args[3].startsWith("-")) {
|
|
259
|
-
const count = parseInt(args[2], 10);
|
|
260
|
-
if (!Number.isNaN(count) && count >= 0) {
|
|
261
|
-
const filePath = resolveToCwd(args[3], cwd);
|
|
262
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
263
|
-
const lines = content.split("\n").slice(0, count).join("\n");
|
|
264
|
-
return { optimized: true, output: lines, exitCode: 0 };
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
else if (cmd === "tail") {
|
|
269
|
-
if (args.length === 2 && !args[1].startsWith("-")) {
|
|
270
|
-
const filePath = resolveToCwd(args[1], cwd);
|
|
271
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
272
|
-
const allLines = content.split("\n");
|
|
273
|
-
const lines = allLines.slice(Math.max(0, allLines.length - 10)).join("\n");
|
|
274
|
-
return { optimized: true, output: lines, exitCode: 0 };
|
|
275
|
-
}
|
|
276
|
-
else if (args.length === 4 && args[1] === "-n" && !args[3].startsWith("-")) {
|
|
277
|
-
const count = parseInt(args[2], 10);
|
|
278
|
-
if (!Number.isNaN(count) && count >= 0) {
|
|
279
|
-
const filePath = resolveToCwd(args[3], cwd);
|
|
280
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
281
|
-
const allLines = content.split("\n");
|
|
282
|
-
const lines = allLines.slice(Math.max(0, allLines.length - count)).join("\n");
|
|
283
|
-
return { optimized: true, output: lines, exitCode: 0 };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
else if (cmd === "ls") {
|
|
288
|
-
if (args.length === 1 || (args.length === 2 && !args[1].startsWith("-"))) {
|
|
289
|
-
const targetDir = args.length === 2 ? resolveToCwd(args[1], cwd) : cwd;
|
|
290
|
-
const entries = await fsReaddir(targetDir);
|
|
291
|
-
entries.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
292
|
-
const results = [];
|
|
293
|
-
for (const entry of entries) {
|
|
294
|
-
let suffix = "";
|
|
295
|
-
try {
|
|
296
|
-
const entryStat = await fsStat(resolvePath(targetDir, entry));
|
|
297
|
-
if (entryStat.isDirectory())
|
|
298
|
-
suffix = "/";
|
|
299
|
-
}
|
|
300
|
-
catch { }
|
|
301
|
-
results.push(entry + suffix);
|
|
302
|
-
}
|
|
303
|
-
return { optimized: true, output: results.join("\n"), exitCode: 0 };
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
else if (cmd === "grep" || cmd === "rg") {
|
|
307
|
-
if (args.length === 3 && !args[1].startsWith("-") && !args[2].startsWith("-")) {
|
|
308
|
-
const patternStr = unquote(args[1]);
|
|
309
|
-
if (/[\\^$.|?*+()[\]{}]/.test(patternStr))
|
|
310
|
-
return { optimized: false };
|
|
311
|
-
const filePath = resolveToCwd(args[2], cwd);
|
|
312
|
-
const content = await fsReadFile(filePath, "utf-8");
|
|
313
|
-
const lines = content.split("\n");
|
|
314
|
-
const matches = [];
|
|
315
|
-
for (let i = 0; i < lines.length; i++) {
|
|
316
|
-
if (lines[i].includes(patternStr)) {
|
|
317
|
-
matches.push(lines[i]);
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
return { optimized: true, output: matches.join("\n"), exitCode: matches.length > 0 ? 0 : 1 };
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
else if (cmd === "find") {
|
|
324
|
-
if (args.length === 1 || (args.length === 2 && !args[1].startsWith("-"))) {
|
|
325
|
-
const searchPath = args.length === 2 ? resolveToCwd(args[1], cwd) : cwd;
|
|
326
|
-
const walk = async (dir) => {
|
|
327
|
-
let results = [];
|
|
328
|
-
const entries = await fsReaddir(dir, { withFileTypes: true });
|
|
329
|
-
for (const entry of entries) {
|
|
330
|
-
const full = resolvePath(dir, entry.name);
|
|
331
|
-
results.push(full);
|
|
332
|
-
if (entry.isDirectory()) {
|
|
333
|
-
results = results.concat(await walk(full));
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
return results;
|
|
337
|
-
};
|
|
338
|
-
const all = (await walk(searchPath)).map((p) => relativePath(searchPath, p).replace(/\\/g, "/"));
|
|
339
|
-
return { optimized: true, output: all.join("\n"), exitCode: 0 };
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
catch {
|
|
344
|
-
return { optimized: false };
|
|
345
|
-
}
|
|
346
|
-
return { optimized: false };
|
|
347
|
-
}
|
|
348
|
-
export function createBashToolDefinition(cwd, options) {
|
|
349
|
-
const ops = options?.operations ?? createLocalBashOperations({ shellPath: options?.shellPath });
|
|
247
|
+
function createShellToolDefinition(cwd, backendShell, contractPlatform, options) {
|
|
248
|
+
const toolName = "bash";
|
|
249
|
+
const ops = options?.operations ??
|
|
250
|
+
(backendShell === "powershell"
|
|
251
|
+
? createLocalPowerShellOperations({ shellPath: options?.shellPath })
|
|
252
|
+
: createLocalBashOperations({ shellPath: options?.shellPath }));
|
|
350
253
|
const commandPrefix = options?.commandPrefix;
|
|
351
254
|
const spawnHook = options?.spawnHook;
|
|
352
|
-
const
|
|
353
|
-
const canFilterCommand =
|
|
255
|
+
const hasExecutionOverrides = Boolean(options?.operations || options?.shellPath || commandPrefix || spawnHook);
|
|
256
|
+
const canFilterCommand = !hasExecutionOverrides;
|
|
257
|
+
const routesWindowsContract = contractPlatform === "win32";
|
|
258
|
+
const contractDescription = routesWindowsContract
|
|
259
|
+
? "Execute Pi's stable Bash-like command contract in the current working directory. On Windows, a finite deterministic router converts supported simple commands to PowerShell; unsupported Bash constructs fail closed instead of being guessed."
|
|
260
|
+
: "Execute a Bash command in the current working directory.";
|
|
354
261
|
return {
|
|
355
|
-
name:
|
|
356
|
-
label:
|
|
357
|
-
description:
|
|
358
|
-
promptSnippet:
|
|
262
|
+
name: toolName,
|
|
263
|
+
label: toolName,
|
|
264
|
+
description: `${contractDescription} Returns stdout and stderr. Output is truncated to last ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). If truncated, full output is saved to a temp file. Commands have a ${DEFAULT_COMMAND_TIMEOUT_SECONDS}-second wall-clock default, including commands that keep producing output; use a positive timeout only when a scoped operation justifies a larger bound (maximum ${MAX_COMMAND_TIMEOUT_SECONDS} seconds).`,
|
|
265
|
+
promptSnippet: routesWindowsContract
|
|
266
|
+
? "Execute simple Bash-like commands; Pi routes supported forms deterministically to PowerShell on Windows"
|
|
267
|
+
: "Execute Bash commands (ls, grep, find, etc.)",
|
|
268
|
+
promptGuidelines: routesWindowsContract
|
|
269
|
+
? [
|
|
270
|
+
"Use the bash tool's portable simple-command contract on Windows; do not write PowerShell or ask the user to choose a shell.",
|
|
271
|
+
"Use one simple command per call. The deterministic router rejects pipelines, redirection, expansion, shell chaining, nested shells, and unsupported Bash forms; use dedicated read/edit/search tools or separate calls instead.",
|
|
272
|
+
"Supported Bash-like file commands are converted with literal-path PowerShell operations; verify targets before recursive rm, cp, or mv calls.",
|
|
273
|
+
"Keep searches scoped and purpose-driven: discover paths first, pass an explicit root and filters, prefer rg over broad find, and increase the timeout only for a justified bounded search.",
|
|
274
|
+
]
|
|
275
|
+
: [
|
|
276
|
+
"Keep searches scoped and purpose-driven: discover paths first, pass an explicit root and filters, prefer rg over broad find, and increase the timeout only for a justified bounded search.",
|
|
277
|
+
],
|
|
359
278
|
parameters: bashSchema,
|
|
360
279
|
async execute(_toolCallId, { command, timeout }, signal, onUpdate, _ctx) {
|
|
361
|
-
const
|
|
362
|
-
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
|
363
|
-
const output = new OutputAccumulator({ tempFilePrefix: "pi-bash" });
|
|
280
|
+
const output = new OutputAccumulator({ tempFilePrefix: `pi-${toolName}` });
|
|
364
281
|
let updateTimer;
|
|
365
282
|
let updateDirty = false;
|
|
366
283
|
let lastUpdateAt = 0;
|
|
@@ -373,7 +290,7 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
373
290
|
persistIfFullTruncated: true,
|
|
374
291
|
});
|
|
375
292
|
const preview = {
|
|
376
|
-
content: snapshot.content,
|
|
293
|
+
content: snapshot.content.replace(/\r/g, ""),
|
|
377
294
|
skippedLines: Math.max(0, snapshot.truncation.totalLines - snapshot.truncation.outputLines),
|
|
378
295
|
};
|
|
379
296
|
onUpdate({
|
|
@@ -422,7 +339,7 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
422
339
|
};
|
|
423
340
|
const formatOutput = (snapshot, emptyText = "(no output)") => {
|
|
424
341
|
const truncation = snapshot.truncation;
|
|
425
|
-
let text = snapshot.content || emptyText;
|
|
342
|
+
let text = snapshot.content.replace(/\r/g, "") || emptyText;
|
|
426
343
|
let details;
|
|
427
344
|
const preview = output.preview(BASH_PREVIEW_LINES, BASH_PREVIEW_BYTES);
|
|
428
345
|
const fullOutputNotice = snapshot.fullOutputPath
|
|
@@ -463,11 +380,14 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
463
380
|
return { text, details };
|
|
464
381
|
};
|
|
465
382
|
const appendStatus = (text, status) => `${text ? `${text}\n\n` : ""}${status}`;
|
|
383
|
+
const effectiveTimeoutSeconds = typeof timeout === "number" && Number.isFinite(timeout) && timeout > 0
|
|
384
|
+
? resolveCommandTimeoutSeconds(timeout)
|
|
385
|
+
: (commandTimeoutMsOverride ?? DEFAULT_COMMAND_TIMEOUT_SECONDS * 1000) / 1000;
|
|
466
386
|
try {
|
|
467
387
|
if (canFilterCommand) {
|
|
468
|
-
const classification = classifyGitCommand(command,
|
|
388
|
+
const classification = classifyGitCommand(command, getShellEnv());
|
|
469
389
|
if (classification.eligible && classification.subcommand) {
|
|
470
|
-
const res = await executeFilteredGit(cwd, classification.subcommand, classification.globalOptions || [], classification.subcommandArgs || [], { signal, timeout });
|
|
390
|
+
const res = await executeFilteredGit(cwd, classification.subcommand, classification.globalOptions || [], classification.subcommandArgs || [], { signal, timeout: effectiveTimeoutSeconds });
|
|
471
391
|
if (res.exitCode !== -100) {
|
|
472
392
|
output.append(res.rawBytes ?? Buffer.from(res.rawOut, "utf-8"));
|
|
473
393
|
const snapshot = await finishOutput();
|
|
@@ -488,27 +408,25 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
488
408
|
}
|
|
489
409
|
}
|
|
490
410
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (optResult.exitCode !== 0 && optResult.exitCode !== undefined) {
|
|
498
|
-
throw new Error(appendStatus(outputText, `Command exited with code ${optResult.exitCode}`));
|
|
499
|
-
}
|
|
500
|
-
return { content: [{ type: "text", text: outputText }], details };
|
|
501
|
-
}
|
|
411
|
+
let backendCommand = command;
|
|
412
|
+
if (routesWindowsContract) {
|
|
413
|
+
const route = routeShellContract(command, contractPlatform);
|
|
414
|
+
if (route.kind === "unsupported")
|
|
415
|
+
throw new Error(route.error);
|
|
416
|
+
backendCommand = route.command;
|
|
502
417
|
}
|
|
418
|
+
const commandWithPrefix = commandPrefix ? `${commandPrefix}\n${backendCommand}` : backendCommand;
|
|
419
|
+
const resolvedCommand = backendShell === "powershell" ? prefixPowerShellCommand(commandWithPrefix) : commandWithPrefix;
|
|
420
|
+
const spawnContext = resolveSpawnContext(resolvedCommand, cwd, spawnHook);
|
|
503
421
|
let exitCode;
|
|
504
422
|
try {
|
|
505
|
-
//
|
|
506
|
-
// actual
|
|
423
|
+
// Shell commands cannot statically declare which files they mutate, so the
|
|
424
|
+
// actual execution takes the coarse exclusive barrier: it waits for
|
|
507
425
|
// in-flight edit/write mutations to drain and blocks new ones meanwhile.
|
|
508
426
|
const result = await withExclusiveMutationBarrier(() => ops.exec(spawnContext.command, spawnContext.cwd, {
|
|
509
427
|
onData: handleData,
|
|
510
428
|
signal,
|
|
511
|
-
timeout,
|
|
429
|
+
timeout: effectiveTimeoutSeconds,
|
|
512
430
|
env: spawnContext.env,
|
|
513
431
|
}));
|
|
514
432
|
exitCode = result.exitCode;
|
|
@@ -525,7 +443,10 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
525
443
|
}
|
|
526
444
|
if (err instanceof Error && err.message.startsWith("silence:")) {
|
|
527
445
|
const secs = err.message.split(":")[1];
|
|
528
|
-
|
|
446
|
+
const recovery = backendShell === "bash"
|
|
447
|
+
? "re-run it with an explicit timeout, or run it in the background with '&'."
|
|
448
|
+
: "re-run it with an explicit timeout.";
|
|
449
|
+
throw new Error(appendStatus(text, `Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, ${recovery}`));
|
|
529
450
|
}
|
|
530
451
|
throw err;
|
|
531
452
|
}
|
|
@@ -548,7 +469,7 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
548
469
|
state.endedAt = undefined;
|
|
549
470
|
}
|
|
550
471
|
const text = context.lastComponent ?? new Text("", 0, 0);
|
|
551
|
-
text.setText(formatBashCall(args));
|
|
472
|
+
text.setText(formatBashCall(args, toolName));
|
|
552
473
|
return text;
|
|
553
474
|
},
|
|
554
475
|
renderResult(result, options, _theme, context) {
|
|
@@ -570,6 +491,10 @@ export function createBashToolDefinition(cwd, options) {
|
|
|
570
491
|
},
|
|
571
492
|
};
|
|
572
493
|
}
|
|
494
|
+
export function createBashToolDefinition(cwd, options) {
|
|
495
|
+
const platform = options?.platform ?? process.platform;
|
|
496
|
+
return createShellToolDefinition(cwd, getPlatformShellToolName(platform), platform, options);
|
|
497
|
+
}
|
|
573
498
|
export function createBashTool(cwd, options) {
|
|
574
499
|
return wrapToolDefinition(createBashToolDefinition(cwd, options));
|
|
575
500
|
}
|