@caupulican/pi-adaptative 0.81.35 → 0.81.37

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 (148) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/cli/args.d.ts.map +1 -1
  3. package/dist/cli/args.js +10 -8
  4. package/dist/cli/args.js.map +1 -1
  5. package/dist/core/agent-session.d.ts +5 -0
  6. package/dist/core/agent-session.d.ts.map +1 -1
  7. package/dist/core/agent-session.js +17 -0
  8. package/dist/core/agent-session.js.map +1 -1
  9. package/dist/core/autonomy/approval-gate.d.ts.map +1 -1
  10. package/dist/core/autonomy/approval-gate.js +2 -0
  11. package/dist/core/autonomy/approval-gate.js.map +1 -1
  12. package/dist/core/autonomy/foreground-envelope.d.ts.map +1 -1
  13. package/dist/core/autonomy/foreground-envelope.js +2 -0
  14. package/dist/core/autonomy/foreground-envelope.js.map +1 -1
  15. package/dist/core/autonomy/gates.d.ts.map +1 -1
  16. package/dist/core/autonomy/gates.js +9 -2
  17. package/dist/core/autonomy/gates.js.map +1 -1
  18. package/dist/core/autonomy/risk-assessment.d.ts.map +1 -1
  19. package/dist/core/autonomy/risk-assessment.js +2 -0
  20. package/dist/core/autonomy/risk-assessment.js.map +1 -1
  21. package/dist/core/bash-execution-controller.d.ts +9 -4
  22. package/dist/core/bash-execution-controller.d.ts.map +1 -1
  23. package/dist/core/bash-execution-controller.js +7 -6
  24. package/dist/core/bash-execution-controller.js.map +1 -1
  25. package/dist/core/bash-executor.d.ts +2 -0
  26. package/dist/core/bash-executor.d.ts.map +1 -1
  27. package/dist/core/bash-executor.js +3 -2
  28. package/dist/core/bash-executor.js.map +1 -1
  29. package/dist/core/default-tool-surface.d.ts +7 -0
  30. package/dist/core/default-tool-surface.d.ts.map +1 -1
  31. package/dist/core/default-tool-surface.js +29 -10
  32. package/dist/core/default-tool-surface.js.map +1 -1
  33. package/dist/core/doctor.d.ts +10 -10
  34. package/dist/core/doctor.d.ts.map +1 -1
  35. package/dist/core/doctor.js +30 -20
  36. package/dist/core/doctor.js.map +1 -1
  37. package/dist/core/exec.d.ts +2 -0
  38. package/dist/core/exec.d.ts.map +1 -1
  39. package/dist/core/exec.js +1 -0
  40. package/dist/core/exec.js.map +1 -1
  41. package/dist/core/extensions/types.d.ts +13 -3
  42. package/dist/core/extensions/types.d.ts.map +1 -1
  43. package/dist/core/extensions/types.js +3 -0
  44. package/dist/core/extensions/types.js.map +1 -1
  45. package/dist/core/memory/memory-manager.d.ts.map +1 -1
  46. package/dist/core/memory/memory-manager.js +1 -0
  47. package/dist/core/memory/memory-manager.js.map +1 -1
  48. package/dist/core/model-capability.d.ts.map +1 -1
  49. package/dist/core/model-capability.js +2 -0
  50. package/dist/core/model-capability.js.map +1 -1
  51. package/dist/core/model-router/tool-escalation.d.ts.map +1 -1
  52. package/dist/core/model-router/tool-escalation.js +17 -3
  53. package/dist/core/model-router/tool-escalation.js.map +1 -1
  54. package/dist/core/python-runtime.d.ts +46 -0
  55. package/dist/core/python-runtime.d.ts.map +1 -0
  56. package/dist/core/python-runtime.js +157 -0
  57. package/dist/core/python-runtime.js.map +1 -0
  58. package/dist/core/runtime-builder.d.ts +4 -0
  59. package/dist/core/runtime-builder.d.ts.map +1 -1
  60. package/dist/core/runtime-builder.js +31 -9
  61. package/dist/core/runtime-builder.js.map +1 -1
  62. package/dist/core/security/untrusted-boundary.d.ts +2 -2
  63. package/dist/core/security/untrusted-boundary.d.ts.map +1 -1
  64. package/dist/core/security/untrusted-boundary.js +4 -3
  65. package/dist/core/security/untrusted-boundary.js.map +1 -1
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +2 -0
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/slash-commands.d.ts.map +1 -1
  70. package/dist/core/slash-commands.js +8 -0
  71. package/dist/core/slash-commands.js.map +1 -1
  72. package/dist/core/system-prompt.d.ts.map +1 -1
  73. package/dist/core/system-prompt.js +13 -2
  74. package/dist/core/system-prompt.js.map +1 -1
  75. package/dist/core/tasks/session-task-state.d.ts +10 -0
  76. package/dist/core/tasks/session-task-state.d.ts.map +1 -0
  77. package/dist/core/tasks/session-task-state.js +29 -0
  78. package/dist/core/tasks/session-task-state.js.map +1 -0
  79. package/dist/core/tasks/task-command.d.ts +30 -0
  80. package/dist/core/tasks/task-command.d.ts.map +1 -0
  81. package/dist/core/tasks/task-command.js +79 -0
  82. package/dist/core/tasks/task-command.js.map +1 -0
  83. package/dist/core/tasks/task-state.d.ts +71 -0
  84. package/dist/core/tasks/task-state.d.ts.map +1 -0
  85. package/dist/core/tasks/task-state.js +372 -0
  86. package/dist/core/tasks/task-state.js.map +1 -0
  87. package/dist/core/tool-selection/tool-selection-controller.d.ts.map +1 -1
  88. package/dist/core/tool-selection/tool-selection-controller.js +1 -1
  89. package/dist/core/tool-selection/tool-selection-controller.js.map +1 -1
  90. package/dist/core/tools/bash.d.ts +20 -2
  91. package/dist/core/tools/bash.d.ts.map +1 -1
  92. package/dist/core/tools/bash.js +122 -197
  93. package/dist/core/tools/bash.js.map +1 -1
  94. package/dist/core/tools/find.d.ts.map +1 -1
  95. package/dist/core/tools/find.js +3 -0
  96. package/dist/core/tools/find.js.map +1 -1
  97. package/dist/core/tools/grep.d.ts.map +1 -1
  98. package/dist/core/tools/grep.js +3 -0
  99. package/dist/core/tools/grep.js.map +1 -1
  100. package/dist/core/tools/index.d.ts +9 -6
  101. package/dist/core/tools/index.d.ts.map +1 -1
  102. package/dist/core/tools/index.js +24 -9
  103. package/dist/core/tools/index.js.map +1 -1
  104. package/dist/core/tools/python.d.ts +64 -0
  105. package/dist/core/tools/python.d.ts.map +1 -0
  106. package/dist/core/tools/python.js +241 -0
  107. package/dist/core/tools/python.js.map +1 -0
  108. package/dist/core/tools/shell-contract-router.d.ts +13 -0
  109. package/dist/core/tools/shell-contract-router.d.ts.map +1 -0
  110. package/dist/core/tools/shell-contract-router.js +295 -0
  111. package/dist/core/tools/shell-contract-router.js.map +1 -0
  112. package/dist/core/tools/task-steps.d.ts +46 -0
  113. package/dist/core/tools/task-steps.d.ts.map +1 -0
  114. package/dist/core/tools/task-steps.js +160 -0
  115. package/dist/core/tools/task-steps.js.map +1 -0
  116. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  117. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  118. package/dist/modes/interactive/interactive-mode.js +12 -0
  119. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  120. package/dist/modes/interactive/session-flow-commands.d.ts +10 -0
  121. package/dist/modes/interactive/session-flow-commands.d.ts.map +1 -1
  122. package/dist/modes/interactive/session-flow-commands.js +43 -0
  123. package/dist/modes/interactive/session-flow-commands.js.map +1 -1
  124. package/dist/utils/shell.d.ts +6 -8
  125. package/dist/utils/shell.d.ts.map +1 -1
  126. package/dist/utils/shell.js +61 -70
  127. package/dist/utils/shell.js.map +1 -1
  128. package/dist/utils/tools-manager.d.ts +23 -3
  129. package/dist/utils/tools-manager.d.ts.map +1 -1
  130. package/dist/utils/tools-manager.js +94 -11
  131. package/dist/utils/tools-manager.js.map +1 -1
  132. package/docs/index.md +2 -0
  133. package/docs/python.md +43 -0
  134. package/docs/settings.md +7 -3
  135. package/docs/task-steps.md +90 -0
  136. package/docs/termux.md +7 -0
  137. package/docs/usage.md +5 -6
  138. package/docs/windows.md +15 -12
  139. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  140. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  141. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  142. package/examples/extensions/sandbox/package-lock.json +2 -2
  143. package/examples/extensions/sandbox/package.json +1 -1
  144. package/examples/extensions/with-deps/package-lock.json +2 -2
  145. package/examples/extensions/with-deps/package.json +1 -1
  146. package/npm-shrinkwrap.json +32 -37
  147. package/package.json +9 -7
  148. package/scripts/postinstall-python-runtime.mjs +20 -0
@@ -1,6 +1,5 @@
1
1
  import { constants } from "node:fs";
2
- import { access as fsAccess, readdir as fsReaddir, readFile as fsReadFile, stat as fsStat } from "node:fs/promises";
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
- /** Default silence bound for foreground commands without an explicit timeout (spec §2: ~10 min). */
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
- /** Test hook: override the silence threshold. Pass undefined to restore the default. */
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: "Bash command to execute" }),
42
+ command: Type.String({ description: "Shell command to execute" }),
28
43
  timeout: Type.Optional(Type.Number({
29
- description: "Timeout in seconds (optional). When set to a positive value, this wall-clock limit replaces the default silence watchdog. Zero or negative values are treated as unset, so the silence watchdog still applies.",
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 bash commands.`);
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
- return theme.fg("toolTitle", theme.bold(`$ ${commandDisplay}`)) + timeoutSuffix;
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
- async function tryOptimizeCommand(command, cwd) {
213
- if (process.env.PI_TOOL_OPTIMIZER_DISABLED === "1") {
214
- return { optimized: false };
215
- }
216
- const trimmed = command.trim();
217
- if (!trimmed) {
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 canOptimizeCommand = !options?.operations && !options?.shellPath && !commandPrefix && !spawnHook;
353
- const canFilterCommand = canOptimizeCommand;
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: "bash",
356
- label: "bash",
357
- description: `Execute a bash command in the current working directory. 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. Optionally provide a timeout in seconds. Without an explicit timeout, the command is killed after ${DEFAULT_COMMAND_SILENCE_MS / 1000}s of continuous silence (no stdout/stderr output) rather than being bounded by total runtime; commands that keep producing output are never killed by this. Provide an explicit timeout to replace the silence watchdog with a wall-clock limit, or background long, quiet work with '&'.`,
358
- promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
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 resolvedCommand = commandPrefix ? `${commandPrefix}\n${command}` : command;
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;
@@ -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, spawnContext.env);
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
- if (canOptimizeCommand) {
492
- const optResult = await tryOptimizeCommand(command, cwd);
493
- if (optResult.optimized) {
494
- output.append(Buffer.from(optResult.output ?? "", "utf-8"));
495
- const snapshot = await finishOutput();
496
- const { text: outputText, details } = formatOutput(snapshot);
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
- // Bash cannot statically declare which files a command mutates, so the
506
- // actual shell execution takes the coarse exclusive barrier: it waits for
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
- throw new Error(appendStatus(text, `Command killed after ${secs}s of silence (no output). If the command is legitimately quiet for long stretches, re-run it with an explicit timeout, or run it in the background with '&'.`));
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
  }