@link-assistant/agent 0.26.10 → 0.26.11
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/package.json +1 -1
- package/src/tool/bash.ts +81 -101
package/package.json
CHANGED
package/src/tool/bash.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import z from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { ProcessRunner } from 'command-stream/process-runner';
|
|
3
3
|
import { Tool } from './tool';
|
|
4
4
|
import DESCRIPTION from './bash.txt';
|
|
5
5
|
import { Branding } from '../branding';
|
|
@@ -7,8 +7,6 @@ import { Log } from '../util/log';
|
|
|
7
7
|
import { Instance } from '../project/instance';
|
|
8
8
|
import { lazy } from '../util/lazy';
|
|
9
9
|
import { Language } from 'web-tree-sitter';
|
|
10
|
-
import { $ } from 'bun';
|
|
11
|
-
import { Filesystem } from '../util/filesystem';
|
|
12
10
|
import { Permission } from '../permission';
|
|
13
11
|
import { fileURLToPath } from 'url';
|
|
14
12
|
import { processResult } from './process-result';
|
|
@@ -16,10 +14,22 @@ import { processResult } from './process-result';
|
|
|
16
14
|
const MAX_OUTPUT_LENGTH = 30_000;
|
|
17
15
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000;
|
|
18
16
|
const MAX_TIMEOUT = 10 * 60 * 1000;
|
|
19
|
-
const
|
|
20
|
-
|
|
17
|
+
const KILL_SIGNAL = 'SIGTERM';
|
|
21
18
|
export const log = Log.create({ service: 'bash-tool' });
|
|
22
19
|
|
|
20
|
+
// command-stream 1.3.0 signals only the shell on Windows, which has no process
|
|
21
|
+
// groups, so the shell's descendants keep running
|
|
22
|
+
// (link-foundation/command-stream#205). taskkill /t walks the tree from the
|
|
23
|
+
// shell, so it must run before the shell itself is stopped.
|
|
24
|
+
const killWindowsTree = async (pid: number | undefined) => {
|
|
25
|
+
if (!pid) return;
|
|
26
|
+
// A failed taskkill resolves with its exit code; the runner is stopped next.
|
|
27
|
+
await new ProcessRunner(
|
|
28
|
+
{ mode: 'exec', file: 'taskkill', args: ['/pid', String(pid), '/f', '/t'] },
|
|
29
|
+
{ mirror: false, capture: false, stdin: 'ignore' }
|
|
30
|
+
).catch(() => {});
|
|
31
|
+
};
|
|
32
|
+
|
|
23
33
|
const resolveWasm = (asset: string) => {
|
|
24
34
|
if (asset.startsWith('file://')) return fileURLToPath(asset);
|
|
25
35
|
if (asset.startsWith('/') || /^[a-z]:/i.test(asset)) return asset;
|
|
@@ -119,15 +129,8 @@ export const BashTool = Tool.define('bash', {
|
|
|
119
129
|
});
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
cwd: Instance.directory,
|
|
125
|
-
env: {
|
|
126
|
-
...process.env,
|
|
127
|
-
},
|
|
128
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
129
|
-
detached: process.platform !== 'win32',
|
|
130
|
-
});
|
|
132
|
+
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
133
|
+
const cancel = AbortSignal.any([ctx.abort, timeoutSignal]);
|
|
131
134
|
|
|
132
135
|
let output = '';
|
|
133
136
|
|
|
@@ -139,97 +142,74 @@ export const BashTool = Tool.define('bash', {
|
|
|
139
142
|
},
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
ctx.metadata({
|
|
145
|
-
metadata: {
|
|
146
|
-
output,
|
|
147
|
-
description: params.description,
|
|
148
|
-
},
|
|
149
|
-
});
|
|
150
|
-
};
|
|
145
|
+
let exit: number | null = null;
|
|
146
|
+
let signal: string | null = null;
|
|
151
147
|
|
|
152
|
-
|
|
153
|
-
|
|
148
|
+
// command-stream 1.3.0 finishes a runner whose signal is already aborted
|
|
149
|
+
// before stream() subscribes to its end event, so the iterator never ends
|
|
150
|
+
// (link-foundation/command-stream#207).
|
|
151
|
+
// A cancelled command does not need to start at all.
|
|
152
|
+
if (!cancel.aborted) {
|
|
153
|
+
// On Windows the runner is stopped only after taskkill has stopped the
|
|
154
|
+
// shell's process tree.
|
|
155
|
+
const stop =
|
|
156
|
+
process.platform === 'win32' ? new AbortController() : undefined;
|
|
154
157
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
// The file/args shell form spawns the command string through Node's
|
|
159
|
+
// platform shell, as `spawn(command, { shell: true })` does. A command
|
|
160
|
+
// string spec would route simple commands and pipelines through
|
|
161
|
+
// command-stream's JavaScript builtins (`ls`, `exit`, `cd`, ...) instead.
|
|
162
|
+
const command = new ProcessRunner(
|
|
163
|
+
{ mode: 'shell', file: params.command, args: [] },
|
|
164
|
+
{
|
|
165
|
+
cwd: Instance.directory,
|
|
166
|
+
env: { ...process.env },
|
|
167
|
+
signal: stop?.signal ?? cancel,
|
|
168
|
+
killSignal: KILL_SIGNAL,
|
|
169
|
+
killGrace: 200,
|
|
170
|
+
mirror: false,
|
|
171
|
+
capture: true,
|
|
172
|
+
stdin: 'ignore',
|
|
173
|
+
}
|
|
174
|
+
);
|
|
159
175
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
176
|
+
const stopTree = () => {
|
|
177
|
+
void killWindowsTree(command.child?.pid).finally(() => stop?.abort());
|
|
178
|
+
};
|
|
179
|
+
if (stop) cancel.addEventListener('abort', stopTree, { once: true });
|
|
165
180
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
181
|
+
for await (const chunk of command.stream()) {
|
|
182
|
+
if (chunk.type === 'exit') {
|
|
183
|
+
exit = chunk.code;
|
|
184
|
+
} else {
|
|
185
|
+
output += chunk.data.toString();
|
|
186
|
+
ctx.metadata({
|
|
187
|
+
metadata: {
|
|
188
|
+
output,
|
|
189
|
+
description: params.description,
|
|
190
|
+
},
|
|
170
191
|
});
|
|
171
|
-
killer.once('exit', resolve);
|
|
172
|
-
killer.once('error', resolve);
|
|
173
|
-
});
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
try {
|
|
178
|
-
process.kill(-pid, 'SIGTERM');
|
|
179
|
-
await Bun.sleep(SIGKILL_TIMEOUT_MS);
|
|
180
|
-
// The shell may exit before a child that ignores SIGTERM. The
|
|
181
|
-
// process group still needs SIGKILL until its output pipes close.
|
|
182
|
-
if (!closed) {
|
|
183
|
-
process.kill(-pid, 'SIGKILL');
|
|
184
|
-
}
|
|
185
|
-
} catch (_e) {
|
|
186
|
-
proc.kill('SIGTERM');
|
|
187
|
-
await Bun.sleep(SIGKILL_TIMEOUT_MS);
|
|
188
|
-
if (!exited) {
|
|
189
|
-
proc.kill('SIGKILL');
|
|
190
192
|
}
|
|
191
193
|
}
|
|
192
|
-
};
|
|
193
|
-
|
|
194
|
-
const abortHandler = () => {
|
|
195
|
-
aborted = true;
|
|
196
|
-
void killTree();
|
|
197
|
-
};
|
|
198
|
-
|
|
199
|
-
ctx.abort.addEventListener('abort', abortHandler, { once: true });
|
|
200
194
|
|
|
201
|
-
|
|
202
|
-
timedOut = true;
|
|
203
|
-
void killTree();
|
|
204
|
-
}, timeout);
|
|
195
|
+
cancel.removeEventListener('abort', stopTree);
|
|
205
196
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// close follows exit after stdout/stderr have drained. Install these
|
|
217
|
-
// listeners before killing an already-aborted process.
|
|
218
|
-
proc.once('close', () => {
|
|
219
|
-
closed = true;
|
|
220
|
-
cleanup();
|
|
221
|
-
resolve();
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
proc.once('error', (error) => {
|
|
225
|
-
exited = true;
|
|
226
|
-
cleanup();
|
|
227
|
-
reject(error);
|
|
228
|
-
});
|
|
229
|
-
});
|
|
197
|
+
// command-stream reports a shell that died from a signal as exit 0
|
|
198
|
+
// (link-foundation/command-stream#208).
|
|
199
|
+
// Its result keeps the native child, which has the signal name.
|
|
200
|
+
const finished = command.result as {
|
|
201
|
+
child?: { signalCode?: string | null };
|
|
202
|
+
} | null;
|
|
203
|
+
signal = finished?.child?.signalCode ?? null;
|
|
204
|
+
}
|
|
230
205
|
|
|
231
|
-
|
|
232
|
-
|
|
206
|
+
const timedOut = timeoutSignal.aborted;
|
|
207
|
+
const aborted = ctx.abort.aborted;
|
|
208
|
+
// command-stream settles a cancelled command as soon as it sends the kill
|
|
209
|
+
// signal and reports a synthesized 128 + signal code. The real exit status
|
|
210
|
+
// is unavailable, as it is when the shell itself dies from a signal.
|
|
211
|
+
if (timedOut || aborted) signal = KILL_SIGNAL;
|
|
212
|
+
if (signal) exit = null;
|
|
233
213
|
|
|
234
214
|
if (output.length > MAX_OUTPUT_LENGTH) {
|
|
235
215
|
output = output.slice(0, MAX_OUTPUT_LENGTH);
|
|
@@ -246,16 +226,16 @@ export const BashTool = Tool.define('bash', {
|
|
|
246
226
|
|
|
247
227
|
const result = processResult({
|
|
248
228
|
output,
|
|
249
|
-
exit
|
|
250
|
-
signal
|
|
229
|
+
exit,
|
|
230
|
+
signal,
|
|
251
231
|
timedOut,
|
|
252
232
|
aborted,
|
|
253
233
|
});
|
|
254
234
|
|
|
255
235
|
log.debug(() => ({
|
|
256
236
|
message: 'command finished',
|
|
257
|
-
exit
|
|
258
|
-
signal
|
|
237
|
+
exit,
|
|
238
|
+
signal,
|
|
259
239
|
timedOut,
|
|
260
240
|
aborted,
|
|
261
241
|
isError: result.isError,
|
|
@@ -265,8 +245,8 @@ export const BashTool = Tool.define('bash', {
|
|
|
265
245
|
title: params.command,
|
|
266
246
|
metadata: {
|
|
267
247
|
output,
|
|
268
|
-
exit
|
|
269
|
-
signal
|
|
248
|
+
exit,
|
|
249
|
+
signal,
|
|
270
250
|
timedOut,
|
|
271
251
|
aborted,
|
|
272
252
|
description: params.description,
|