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.
package/dist/agent/agent-loop.js
CHANGED
|
@@ -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));
|
package/dist/agent/tools/glob.js
CHANGED
|
@@ -50,7 +50,7 @@ export function createGlobTool(getCwd) {
|
|
|
50
50
|
timeout: 10_000,
|
|
51
51
|
});
|
|
52
52
|
await done;
|
|
53
|
-
if (session.
|
|
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,
|
package/dist/agent/tools/grep.js
CHANGED
|
@@ -127,7 +127,7 @@ export function createGrepTool(getCwd) {
|
|
|
127
127
|
maxOutputBytes: 64 * 1024,
|
|
128
128
|
});
|
|
129
129
|
await done;
|
|
130
|
-
if (session.
|
|
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,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
|
+
}
|
package/dist/executor.d.ts
CHANGED
|
@@ -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();
|