agent-sh 0.12.8 → 0.12.9

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.
@@ -15,6 +15,7 @@ import { getSettings, updateSettings } from "../settings.js";
15
15
  import { createToolProtocol } from "./tool-protocol.js";
16
16
  // Core tool factories
17
17
  import { createBashTool } from "./tools/bash.js";
18
+ import { createPwshTool } from "./tools/pwsh.js";
18
19
  import { createReadFileTool } from "./tools/read-file.js";
19
20
  import { createWriteFileTool } from "./tools/write-file.js";
20
21
  import { createEditFileTool } from "./tools/edit-file.js";
@@ -591,6 +592,9 @@ export class AgentLoop {
591
592
  return env;
592
593
  };
593
594
  this.toolRegistry.register(createBashTool({ getCwd, getEnv, bus: this.bus }));
595
+ if (process.platform === "win32") {
596
+ this.toolRegistry.register(createPwshTool({ getCwd, getEnv, bus: this.bus }));
597
+ }
594
598
  this.toolRegistry.register(createReadFileTool(getCwd, this.fileReadCache));
595
599
  this.toolRegistry.register(createWriteFileTool(getCwd));
596
600
  this.toolRegistry.register(createEditFileTool(getCwd));
@@ -50,7 +50,7 @@ export function createGlobTool(getCwd) {
50
50
  timeout: 10_000,
51
51
  });
52
52
  await done;
53
- if (session.exitCode === -1 && session.output.startsWith("Failed to spawn")) {
53
+ if (session.spawnFailed) {
54
54
  return {
55
55
  content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
56
56
  exitCode: 1,
@@ -127,7 +127,7 @@ export function createGrepTool(getCwd) {
127
127
  maxOutputBytes: 64 * 1024,
128
128
  });
129
129
  await done;
130
- if (session.exitCode === -1 && session.output.startsWith("Failed to spawn")) {
130
+ if (session.spawnFailed) {
131
131
  return {
132
132
  content: "ripgrep not available — the bundled binary failed to load and `rg` is not on PATH. Reinstall agent-sh, or install ripgrep manually (https://github.com/BurntSushi/ripgrep#installation).",
133
133
  exitCode: 1,
@@ -0,0 +1,7 @@
1
+ import type { EventBus } from "../../event-bus.js";
2
+ import type { ToolDefinition } from "../types.js";
3
+ export declare function createPwshTool(opts: {
4
+ getCwd: () => string;
5
+ getEnv: () => Record<string, string>;
6
+ bus: EventBus;
7
+ }): ToolDefinition;
@@ -0,0 +1,90 @@
1
+ import { executeArgv, killSession } from "../../executor.js";
2
+ // Targets PowerShell 7+ (`pwsh`). Legacy `powershell.exe` is intentionally
3
+ // not auto-fallback — its tool surface diverges enough that compatibility
4
+ // shims aren't worth the maintenance.
5
+ export function createPwshTool(opts) {
6
+ return {
7
+ name: "pwsh",
8
+ description: "Execute a PowerShell command in an isolated subprocess. " +
9
+ "Use this on Windows when the `bash` tool fails (no /bin/bash available). " +
10
+ "Use PowerShell syntax — e.g. `Get-ChildItem`, `Select-String`, `$env:HOME`. " +
11
+ "Does not affect the user's shell state. " +
12
+ "cwd is set to the working directory from the shell context. " +
13
+ "Do NOT use pwsh for file searching — use grep/glob instead. " +
14
+ "Do NOT use pwsh for reading files — use read_file instead.",
15
+ input_schema: {
16
+ type: "object",
17
+ properties: {
18
+ command: {
19
+ type: "string",
20
+ description: "The PowerShell command to execute",
21
+ },
22
+ timeout: {
23
+ type: "number",
24
+ description: "Timeout in seconds (default: 60)",
25
+ },
26
+ description: {
27
+ type: "string",
28
+ description: "Short description of what this command does (e.g., 'Install dependencies', 'Run test suite')",
29
+ },
30
+ },
31
+ required: ["command"],
32
+ },
33
+ showOutput: true,
34
+ modifiesFiles: true,
35
+ requiresPermission: true,
36
+ getDisplayInfo: () => ({
37
+ kind: "execute",
38
+ icon: "▶",
39
+ locations: [],
40
+ }),
41
+ async execute(args, onChunk, ctx) {
42
+ const command = args.command;
43
+ const timeout = (args.timeout ?? 60) * 1000;
44
+ const intercepted = opts.bus.emitPipe("agent:terminal-intercept", {
45
+ command,
46
+ cwd: opts.getCwd(),
47
+ intercepted: false,
48
+ output: "",
49
+ });
50
+ if (intercepted.intercepted) {
51
+ return {
52
+ content: intercepted.output,
53
+ exitCode: 0,
54
+ isError: false,
55
+ };
56
+ }
57
+ const { session, done } = executeArgv({
58
+ file: "pwsh",
59
+ args: ["-NoProfile", "-NonInteractive", "-Command", command],
60
+ cwd: opts.getCwd(),
61
+ env: opts.getEnv(),
62
+ timeout,
63
+ onOutput: onChunk,
64
+ });
65
+ const onAbort = () => killSession(session);
66
+ ctx?.signal?.addEventListener("abort", onAbort, { once: true });
67
+ try {
68
+ await done;
69
+ }
70
+ finally {
71
+ ctx?.signal?.removeEventListener("abort", onAbort);
72
+ }
73
+ if (session.spawnFailed) {
74
+ return {
75
+ content: "PowerShell (pwsh) not found on PATH. Install PowerShell 7: winget install Microsoft.PowerShell.",
76
+ exitCode: 1,
77
+ isError: true,
78
+ };
79
+ }
80
+ const content = session.truncated
81
+ ? `[output truncated, showing last portion]\n${session.output}`
82
+ : session.output;
83
+ return {
84
+ content: content || "(no output)",
85
+ exitCode: session.exitCode,
86
+ isError: session.exitCode !== 0,
87
+ };
88
+ },
89
+ };
90
+ }
@@ -6,6 +6,9 @@ export interface ExecutorSession {
6
6
  exitCode: number | null;
7
7
  done: boolean;
8
8
  truncated: boolean;
9
+ /** True when the binary couldn't be launched (ENOENT, EACCES). Lets callers
10
+ * distinguish "tool missing" from "tool ran and exited with -1". */
11
+ spawnFailed: boolean;
9
12
  process: ChildProcess | null;
10
13
  resolve?: () => void;
11
14
  }
package/dist/executor.js CHANGED
@@ -16,6 +16,7 @@ export function executeCommand(opts) {
16
16
  exitCode: null,
17
17
  done: false,
18
18
  truncated: false,
19
+ spawnFailed: false,
19
20
  process: null,
20
21
  };
21
22
  const done = new Promise((resolve) => {
@@ -39,6 +40,7 @@ export function executeCommand(opts) {
39
40
  }
40
41
  catch (err) {
41
42
  session.exitCode = -1;
43
+ session.spawnFailed = true;
42
44
  session.output = `Failed to spawn: ${err instanceof Error ? err.message : String(err)}`;
43
45
  session.done = true;
44
46
  session.resolve?.();
@@ -79,6 +81,9 @@ export function executeCommand(opts) {
79
81
  cancelKill?.();
80
82
  if (!session.done) {
81
83
  session.exitCode = -1;
84
+ const code = err.code;
85
+ if (code === "ENOENT" || code === "EACCES")
86
+ session.spawnFailed = true;
82
87
  session.output += `\nProcess error: ${err.message}`;
83
88
  session.done = true;
84
89
  session.process = null;
@@ -102,6 +107,7 @@ export function executeArgv(opts) {
102
107
  exitCode: null,
103
108
  done: false,
104
109
  truncated: false,
110
+ spawnFailed: false,
105
111
  process: null,
106
112
  };
107
113
  const done = new Promise((resolve) => {
@@ -123,6 +129,7 @@ export function executeArgv(opts) {
123
129
  }
124
130
  catch (err) {
125
131
  session.exitCode = -1;
132
+ session.spawnFailed = true;
126
133
  session.output = `Failed to spawn ${opts.file}: ${err instanceof Error ? err.message : String(err)}`;
127
134
  session.done = true;
128
135
  session.resolve?.();
@@ -168,6 +175,9 @@ export function executeArgv(opts) {
168
175
  clearTimeout(timer);
169
176
  if (!session.done) {
170
177
  session.exitCode = -1;
178
+ const code = err.code;
179
+ if (code === "ENOENT" || code === "EACCES")
180
+ session.spawnFailed = true;
171
181
  session.output += `\nProcess error: ${err.message}`;
172
182
  session.done = true;
173
183
  session.process = null;
@@ -185,10 +195,17 @@ export function killSession(session) {
185
195
  const proc = session.process;
186
196
  if (!proc || !proc.pid)
187
197
  return () => { };
198
+ // Try process-group kill first (works for executeCommand's detached bash
199
+ // children); fall back to direct kill (executeArgv's non-detached spawn,
200
+ // and Windows where negative pids aren't supported).
188
201
  try {
189
202
  process.kill(-proc.pid, "SIGTERM");
190
203
  }
191
204
  catch { }
205
+ try {
206
+ proc.kill("SIGTERM");
207
+ }
208
+ catch { }
192
209
  let settled = false;
193
210
  const fallback = setTimeout(() => {
194
211
  if (!settled && !session.done && proc.pid) {
@@ -196,6 +213,10 @@ export function killSession(session) {
196
213
  process.kill(-proc.pid, "SIGKILL");
197
214
  }
198
215
  catch { }
216
+ try {
217
+ proc.kill("SIGKILL");
218
+ }
219
+ catch { }
199
220
  }
200
221
  }, 5000);
201
222
  fallback.unref();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-sh",
3
- "version": "0.12.8",
3
+ "version": "0.12.9",
4
4
  "description": "A shell-first terminal where AI is one keystroke away",
5
5
  "type": "module",
6
6
  "main": "dist/core.js",