@dyyz1993/pi-coding-agent 0.74.45 → 0.74.47
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/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +13 -0
- package/dist/core/agent-session.js.map +1 -1
- package/dist/extensions/auto-memory/__tests__/extract-result.test.ts +42 -0
- package/dist/extensions/auto-memory/__tests__/prefetch-history.test.ts +136 -0
- package/dist/extensions/auto-memory/__tests__/prompts.test.ts +29 -0
- package/dist/extensions/auto-memory/__tests__/skip-rules.test.ts +366 -0
- package/dist/extensions/auto-memory/contract.d.ts +16 -0
- package/dist/extensions/auto-memory/contract.d.ts.map +1 -1
- package/dist/extensions/auto-memory/contract.js.map +1 -1
- package/dist/extensions/auto-memory/contract.ts +16 -0
- package/dist/extensions/auto-memory/index.ts +134 -13
- package/dist/extensions/auto-memory/prompts.ts +10 -0
- package/dist/extensions/auto-memory/skip-rules.ts +2 -0
- package/dist/extensions/bash-ext/index.ts +855 -845
- package/dist/extensions/claude-hooks-compat/index.ts +12 -7
- package/dist/extensions/coordinator/handler.test.ts +388 -123
- package/dist/extensions/coordinator/handler.ts +78 -12
- package/dist/extensions/coordinator/index.ts +267 -198
- package/dist/extensions/coordinator/types.d.ts +16 -0
- package/dist/extensions/coordinator/types.d.ts.map +1 -1
- package/dist/extensions/coordinator/types.js.map +1 -1
- package/dist/extensions/coordinator/types.ts +57 -49
- package/dist/extensions/lsp/lsp/index.ts +15 -9
- package/dist/extensions/lsp/lsp/lsp-clangd-e2e.test.ts +229 -0
- package/dist/extensions/message-bridge/index.ts +14 -11
- package/dist/extensions/session-supervisor/index.ts +14 -8
- package/dist/extensions/subagent-v2/index.ts +58 -42
- package/dist/extensions/todo-ext/index.ts +7 -3
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +9 -1
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/package.json +1 -1
|
@@ -32,44 +32,44 @@ import type { ChildProcess } from "child_process";
|
|
|
32
32
|
import { Type } from "typebox";
|
|
33
33
|
import type { BashToolDetails as _BashToolDetails, ExtensionAPI, ExtensionContext } from "@dyyz1993/pi-coding-agent";
|
|
34
34
|
import {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
35
|
+
DEFAULT_MAX_BYTES,
|
|
36
|
+
DEFAULT_MAX_LINES,
|
|
37
|
+
OutputCollector,
|
|
38
|
+
ServerChannel,
|
|
39
|
+
createTypedChannel,
|
|
40
|
+
killProcessTree,
|
|
41
|
+
sanitizeBinaryOutput,
|
|
42
|
+
spawnManagedProcess,
|
|
43
|
+
waitForChildProcess,
|
|
44
44
|
} from "@dyyz1993/pi-coding-agent";
|
|
45
45
|
import { BASH_CHANNEL_NAME, type BashChannelContract, type BashProcess } from "./contract.js";
|
|
46
46
|
export type { BashProcess, BashChannelEvent } from "./contract.js";
|
|
47
47
|
|
|
48
48
|
interface TerminatedDetails {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
reason: string;
|
|
50
|
+
pid?: number;
|
|
51
|
+
command: string;
|
|
52
|
+
startedAt: number;
|
|
53
|
+
endedAt?: number;
|
|
54
|
+
durationMs: number;
|
|
55
|
+
logPath?: string;
|
|
56
|
+
exitCode?: number | null;
|
|
57
|
+
timeoutSecs?: number;
|
|
58
|
+
error?: string;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
interface BackgroundDetails {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
62
|
+
pid?: number;
|
|
63
|
+
command: string;
|
|
64
|
+
startedAt: number;
|
|
65
|
+
durationMs: number;
|
|
66
|
+
logPath?: string;
|
|
67
|
+
detached: boolean;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
type BashToolDetails = _BashToolDetails & {
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
terminated?: TerminatedDetails;
|
|
72
|
+
background?: BackgroundDetails;
|
|
73
73
|
};
|
|
74
74
|
|
|
75
75
|
const DEFAULT_TIMEOUT_SECONDS = 300;
|
|
@@ -77,40 +77,40 @@ const MAX_TIMEOUT_SECONDS = 14400; // 4 hours — foreground commands shouldn't
|
|
|
77
77
|
const DEFAULT_BACKGROUND_AFTER_SECONDS = 600;
|
|
78
78
|
|
|
79
79
|
const bashSchema = Type.Object({
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
80
|
+
command: Type.String({ description: "Bash command to execute" }),
|
|
81
|
+
description: Type.String({ description: "Clear, concise description of what this command does in 5-10 words" }),
|
|
82
|
+
timeout: Type.Optional(
|
|
83
|
+
Type.Number({
|
|
84
|
+
description: `Hard timeout in seconds (max ${MAX_TIMEOUT_SECONDS}s = 4h). Process is killed if still running after this duration. Defaults to ${DEFAULT_TIMEOUT_SECONDS}s (5 minutes). Acts as a safety net to prevent zombie processes.`,
|
|
85
|
+
}),
|
|
86
|
+
),
|
|
87
|
+
backgroundAfter: Type.Optional(
|
|
88
|
+
Type.Number({
|
|
89
|
+
description:
|
|
90
|
+
`Soft limit in seconds. If the command runs longer than this, it is automatically moved to background instead of blocking the agent. Default: ${DEFAULT_BACKGROUND_AFTER_SECONDS}s (10 min). Must be less than timeout if set. Use for long-running tasks where the agent should stay productive.`,
|
|
91
|
+
}),
|
|
92
|
+
),
|
|
93
|
+
cwd: Type.Optional(
|
|
94
|
+
Type.String({
|
|
95
|
+
description:
|
|
96
|
+
"Working directory for the command. Defaults to the agent's current working directory. Use this to run commands in a specific project or directory without cd.",
|
|
97
|
+
}),
|
|
98
|
+
),
|
|
99
99
|
});
|
|
100
100
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
interface ManagedBash {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
104
|
+
proc: BashProcess;
|
|
105
|
+
resolve: (result: AgentToolResult<BashToolDetails>) => void;
|
|
106
|
+
reject: (error: Error) => void;
|
|
107
|
+
child: ChildProcess;
|
|
108
|
+
resolved: boolean;
|
|
109
|
+
backgrounded: boolean;
|
|
110
|
+
killedByUser?: boolean;
|
|
111
|
+
logStream: ReturnType<typeof createWriteStream> | undefined;
|
|
112
|
+
outputSubscribed: boolean;
|
|
113
|
+
stdin: ChildProcess["stdin"];
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
const managed = new Map<string, ManagedBash>();
|
|
@@ -118,809 +118,819 @@ const history: BashProcess[] = [];
|
|
|
118
118
|
const deletedIds = new Set<string>();
|
|
119
119
|
|
|
120
120
|
function generateBashId(): string {
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
const id = randomBytes(3).toString("hex");
|
|
122
|
+
return `bash-${id}`;
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
function getLogPath(bashId: string): string {
|
|
126
|
-
|
|
126
|
+
return join(tmpdir(), `pi-${bashId}.log`);
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
const BG_PREVIEW_LINES = 20;
|
|
130
130
|
|
|
131
131
|
function takeLastLines(text: string, n: number): string {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
132
|
+
const lines = text.split("\n");
|
|
133
|
+
if (lines.length <= n) return text;
|
|
134
|
+
return `... (${lines.length - n} earlier lines)\n${lines.slice(-n).join("\n")}`;
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
function grepLines(text: string, pattern: string): string {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
const lines = text.split("\n");
|
|
139
|
+
const matched = lines.filter((l) => l.toLowerCase().includes(pattern.toLowerCase()));
|
|
140
|
+
if (matched.length === 0) return `(no lines matching "${pattern}")`;
|
|
141
|
+
return matched.join("\n");
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
function formatDuration(ms: number): string {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
145
|
+
const s = Math.floor(ms / 1000);
|
|
146
|
+
if (s < 60) return `${s}s`;
|
|
147
|
+
const m = Math.floor(s / 60);
|
|
148
|
+
return `${m}m${s % 60}s`;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
export default function
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
151
|
+
export default function(pi: ExtensionAPI) {
|
|
152
|
+
let channel: ServerChannel<BashChannelContract> | null = null;
|
|
153
|
+
|
|
154
|
+
function createLogStream(m: ManagedBash): void {
|
|
155
|
+
if (m.logStream) return;
|
|
156
|
+
const logPath = getLogPath(m.proc.bashId);
|
|
157
|
+
const logStream = createWriteStream(logPath);
|
|
158
|
+
if (m.proc.output) logStream.write(m.proc.output);
|
|
159
|
+
m.proc.logPath = logPath;
|
|
160
|
+
m.logStream = logStream;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
pi.on("session_start", async () => {
|
|
164
|
+
// Kill all managed processes from previous session before clearing references.
|
|
165
|
+
// Without this, background processes become orphans when the session switches.
|
|
166
|
+
for (const m of managed.values()) {
|
|
167
|
+
if (!m.resolved && m.proc.pid) {
|
|
168
|
+
try {
|
|
169
|
+
killProcessTree(m.proc.pid);
|
|
170
|
+
} catch {
|
|
171
|
+
// Process may have already exited — ignore
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (m.logStream) m.logStream.end();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const rawChannel = pi.registerChannel(BASH_CHANNEL_NAME);
|
|
179
|
+
channel = createTypedChannel<BashChannelContract>(rawChannel).server;
|
|
180
|
+
} catch {
|
|
181
|
+
// registerChannel only available in RPC mode — skip in interactive mode
|
|
182
|
+
}
|
|
183
|
+
managed.clear();
|
|
184
|
+
history.length = 0;
|
|
185
|
+
deletedIds.clear();
|
|
186
|
+
channel?.emit("list", { type: "list", processes: [], timestamp: Date.now() });
|
|
187
|
+
|
|
188
|
+
channel?.handle("list", () => {
|
|
189
|
+
const activeBg = Array.from(managed.values())
|
|
190
|
+
.filter((m) => m.backgrounded)
|
|
191
|
+
.map((m) => m.proc);
|
|
192
|
+
const hist = history.filter((p) => !deletedIds.has(p.toolCallId));
|
|
193
|
+
return {
|
|
194
|
+
type: "list" as const,
|
|
195
|
+
processes: [...activeBg, ...hist],
|
|
196
|
+
timestamp: Date.now(),
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
channel?.handle("kill", ({ toolCallId }) => {
|
|
201
|
+
if (!toolCallId) return { ok: false, reason: "not_found" };
|
|
202
|
+
const m = managed.get(toolCallId);
|
|
203
|
+
if (!m) {
|
|
204
|
+
// Process already exited — emit terminated event so frontend can sync state
|
|
205
|
+
channel?.emit("terminated", {
|
|
206
|
+
type: "terminated",
|
|
207
|
+
toolCallId,
|
|
208
|
+
pid: undefined,
|
|
209
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
210
|
+
timestamp: Date.now(),
|
|
211
|
+
});
|
|
212
|
+
return { ok: true, alreadyExited: true };
|
|
213
|
+
}
|
|
214
|
+
if (m.proc.pid) {
|
|
215
|
+
killProcessTree(m.proc.pid);
|
|
216
|
+
}
|
|
217
|
+
m.proc.status = "terminated";
|
|
218
|
+
m.proc.endedAt = Date.now();
|
|
219
|
+
m.resolved = true;
|
|
220
|
+
m.killedByUser = true;
|
|
221
|
+
const durationMs = m.proc.endedAt - m.proc.startedAt;
|
|
222
|
+
if (m.logStream) m.logStream.end();
|
|
223
|
+
channel?.emit("terminated", {
|
|
224
|
+
type: "terminated",
|
|
225
|
+
toolCallId,
|
|
226
|
+
pid: m.proc.pid,
|
|
227
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
});
|
|
230
|
+
m.resolve({
|
|
231
|
+
content: [
|
|
232
|
+
{
|
|
233
|
+
type: "text",
|
|
234
|
+
text: `${m.proc.output || "(no output)"}\n\n[User cancelled after ${formatDuration(durationMs)}, PID: ${m.proc.pid ?? "unknown"}${m.proc.logPath ? `. Log: ${m.proc.logPath}` : ""}]`,
|
|
235
|
+
},
|
|
236
|
+
],
|
|
237
|
+
details: {
|
|
238
|
+
terminated: {
|
|
239
|
+
reason: "user_cancel",
|
|
240
|
+
pid: m.proc.pid,
|
|
241
|
+
command: m.proc.command,
|
|
242
|
+
startedAt: m.proc.startedAt,
|
|
243
|
+
endedAt: m.proc.endedAt,
|
|
244
|
+
durationMs,
|
|
245
|
+
logPath: m.proc.logPath,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
return { ok: true };
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
channel?.handle("background", ({ toolCallId }) => {
|
|
253
|
+
if (!toolCallId) return { ok: false, reason: "not_found" };
|
|
254
|
+
const m = managed.get(toolCallId);
|
|
255
|
+
if (!m) {
|
|
256
|
+
// Process already exited — emit terminated event so frontend can sync state
|
|
257
|
+
channel?.emit("terminated", {
|
|
258
|
+
type: "terminated",
|
|
259
|
+
toolCallId,
|
|
260
|
+
pid: undefined,
|
|
261
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
262
|
+
timestamp: Date.now(),
|
|
263
|
+
});
|
|
264
|
+
return { ok: true, alreadyExited: true };
|
|
265
|
+
}
|
|
266
|
+
m.proc.status = "background";
|
|
267
|
+
m.resolved = true;
|
|
268
|
+
m.backgrounded = true;
|
|
269
|
+
m.outputSubscribed = false;
|
|
270
|
+
createLogStream(m);
|
|
271
|
+
const durationMs = Date.now() - m.proc.startedAt;
|
|
272
|
+
channel?.emit("background", {
|
|
273
|
+
type: "background",
|
|
274
|
+
toolCallId,
|
|
275
|
+
pid: m.proc.pid,
|
|
276
|
+
data: m.proc.output.slice(-2000),
|
|
277
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
278
|
+
timestamp: Date.now(),
|
|
279
|
+
});
|
|
280
|
+
const outputPreview = m.proc.output ? takeLastLines(m.proc.output, BG_PREVIEW_LINES) : "(no output yet)";
|
|
281
|
+
m.resolve({
|
|
282
|
+
content: [
|
|
283
|
+
{
|
|
284
|
+
type: "text",
|
|
285
|
+
text: `${outputPreview}\n\n[Moved to background after ${formatDuration(durationMs)}, PID: ${m.proc.pid ?? "unknown"}. <bashId>${m.proc.bashId}</bashId>. Log: ${m.proc.logPath}. Use get_background_process with <bashId>${m.proc.bashId}</bashId> to check progress.]`,
|
|
286
|
+
},
|
|
287
|
+
],
|
|
288
|
+
details: {
|
|
289
|
+
background: {
|
|
290
|
+
pid: m.proc.pid,
|
|
291
|
+
command: m.proc.command,
|
|
292
|
+
startedAt: m.proc.startedAt,
|
|
293
|
+
durationMs,
|
|
294
|
+
logPath: m.proc.logPath,
|
|
295
|
+
detached: false,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
return { ok: true };
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
channel?.handle("subscribe_output", ({ toolCallId }) => {
|
|
303
|
+
if (!toolCallId) return;
|
|
304
|
+
const m = managed.get(toolCallId);
|
|
305
|
+
if (m?.backgrounded) m.outputSubscribed = true;
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
channel?.handle("unsubscribe_output", ({ toolCallId }) => {
|
|
309
|
+
if (!toolCallId) return;
|
|
310
|
+
const m = managed.get(toolCallId);
|
|
311
|
+
if (m) m.outputSubscribed = false;
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
channel?.handle("remove", ({ toolCallId }) => {
|
|
315
|
+
if (!toolCallId) return;
|
|
316
|
+
deletedIds.add(toolCallId);
|
|
317
|
+
managed.delete(toolCallId);
|
|
318
|
+
const idx = history.findIndex((p) => p.toolCallId === toolCallId);
|
|
319
|
+
if (idx >= 0) history.splice(idx, 1);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
channel?.handle("write_stdin", ({ toolCallId, data }) => {
|
|
323
|
+
if (!toolCallId || !data) return;
|
|
324
|
+
const m = managed.get(toolCallId);
|
|
325
|
+
if (m?.stdin && !m.stdin.destroyed) {
|
|
326
|
+
m.stdin.write(data);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
pi.registerTool({
|
|
332
|
+
name: "bash",
|
|
333
|
+
label: "bash",
|
|
334
|
+
description: [
|
|
335
|
+
`Execute a bash command. 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.`,
|
|
336
|
+
"",
|
|
337
|
+
"Timeout and background behavior:",
|
|
338
|
+
`- timeout: Hard limit in seconds. Process is killed after this duration. Default: ${DEFAULT_TIMEOUT_SECONDS}s (5 min). Max: ${MAX_TIMEOUT_SECONDS}s (4h). This is a safety net — always present.`,
|
|
339
|
+
"- backgroundAfter: Soft limit in seconds. If the command runs longer, it is automatically moved to background. Default: ${DEFAULT_BACKGROUND_AFTER_SECONDS}s (10 min). The process keeps running, the agent receives a notification and can continue other work.",
|
|
340
|
+
"- If backgroundAfter < timeout: command goes to background first, then gets killed if it reaches timeout.",
|
|
341
|
+
"- If backgroundAfter >= timeout (or not set): command runs until timeout, then gets killed.",
|
|
342
|
+
"",
|
|
343
|
+
"When to use backgroundAfter:",
|
|
344
|
+
"- Long builds (npm install, cargo build, docker build): set backgroundAfter to a reasonable time so the agent stays productive.",
|
|
345
|
+
"- Quick commands (ls, grep, echo): no need for backgroundAfter, they finish fast.",
|
|
346
|
+
"",
|
|
347
|
+
"Rules:",
|
|
348
|
+
"- ALWAYS provide a description (5-10 words explaining what the command does).",
|
|
349
|
+
"- Use cwd to run commands in a specific directory instead of cd.",
|
|
350
|
+
"- When a command is moved to background, the result includes a <bashId>. Use get_background_process with that ID to poll progress before running dependent commands.",
|
|
351
|
+
].join("\n"),
|
|
352
|
+
promptSnippet: "Execute bash commands (ls, grep, find, etc.)",
|
|
353
|
+
parameters: bashSchema,
|
|
354
|
+
async execute(
|
|
355
|
+
toolCallId: string,
|
|
356
|
+
{ command, description, timeout, backgroundAfter, cwd: cwdParam }: { command: string; description: string; timeout?: number; backgroundAfter?: number; cwd?: string },
|
|
357
|
+
signal?: AbortSignal,
|
|
358
|
+
onUpdate?: AgentToolUpdateCallback<BashToolDetails>,
|
|
359
|
+
_ctx?: ExtensionContext,
|
|
360
|
+
): Promise<AgentToolResult<BashToolDetails>> {
|
|
361
|
+
return new Promise((resolve, reject) => {
|
|
362
|
+
const effectiveTimeout = Math.min(timeout ?? DEFAULT_TIMEOUT_SECONDS, MAX_TIMEOUT_SECONDS);
|
|
363
|
+
const rawBackgroundAfter = backgroundAfter ?? DEFAULT_BACKGROUND_AFTER_SECONDS;
|
|
364
|
+
const effectiveBackgroundAfter = rawBackgroundAfter < effectiveTimeout ? rawBackgroundAfter : undefined;
|
|
365
|
+
const cwd = cwdParam ?? _ctx?.cwd ?? process.cwd();
|
|
366
|
+
const bashId = generateBashId();
|
|
367
|
+
|
|
368
|
+
const spawnResult = spawnManagedProcess({
|
|
369
|
+
command,
|
|
370
|
+
cwd,
|
|
371
|
+
timeout: effectiveTimeout,
|
|
372
|
+
signal,
|
|
373
|
+
stdin: "pipe",
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
if (spawnResult instanceof Error) {
|
|
377
|
+
reject(spawnResult);
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const { child, cleanup: spawnCleanup, isTimedOut } = spawnResult;
|
|
382
|
+
|
|
383
|
+
// Immediately send EOF on stdin so CLI tools that read stdin (e.g. xbrowser readStdin())
|
|
384
|
+
// don't hang forever waiting for input. Interactive stdin is handled via write_stdin channel.
|
|
385
|
+
if (child.stdin && !child.stdin.destroyed) {
|
|
386
|
+
child.stdin.end();
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const proc: BashProcess = {
|
|
390
|
+
bashId,
|
|
391
|
+
toolCallId,
|
|
392
|
+
command,
|
|
393
|
+
cwd,
|
|
394
|
+
pid: child.pid ?? undefined,
|
|
395
|
+
startedAt: Date.now(),
|
|
396
|
+
output: "",
|
|
397
|
+
status: "running",
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
managed.set(toolCallId, {
|
|
401
|
+
proc,
|
|
402
|
+
resolve,
|
|
403
|
+
reject,
|
|
404
|
+
child,
|
|
405
|
+
resolved: false,
|
|
406
|
+
backgrounded: false,
|
|
407
|
+
logStream: undefined,
|
|
408
|
+
outputSubscribed: false,
|
|
409
|
+
stdin: child.stdin,
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
const logPath = getLogPath(bashId);
|
|
413
|
+
const logStream = createWriteStream(logPath);
|
|
414
|
+
proc.logPath = logPath;
|
|
415
|
+
const m = managed.get(toolCallId)!;
|
|
416
|
+
m.logStream = logStream;
|
|
417
|
+
|
|
418
|
+
channel?.emit("start", {
|
|
419
|
+
type: "start",
|
|
420
|
+
toolCallId,
|
|
421
|
+
pid: child.pid ?? undefined,
|
|
422
|
+
data: command,
|
|
423
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
424
|
+
timestamp: proc.startedAt,
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const collector = new OutputCollector();
|
|
428
|
+
|
|
429
|
+
const handleData = (data: Buffer) => {
|
|
430
|
+
const m = managed.get(toolCallId);
|
|
431
|
+
if (m?.logStream) m.logStream.write(data);
|
|
432
|
+
|
|
433
|
+
if (m?.backgrounded) {
|
|
434
|
+
if (m.outputSubscribed) {
|
|
435
|
+
const text = sanitizeBinaryOutput(stripAnsi(data.toString("utf-8"))).replace(/\r/g, "");
|
|
436
|
+
channel?.emit("output", {
|
|
437
|
+
type: "output",
|
|
438
|
+
toolCallId,
|
|
439
|
+
data: text,
|
|
440
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
441
|
+
timestamp: Date.now(),
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
collector.push(data);
|
|
448
|
+
|
|
449
|
+
const rawText = data.toString("utf-8");
|
|
450
|
+
const text = sanitizeBinaryOutput(stripAnsi(rawText)).replace(/\r/g, "");
|
|
451
|
+
proc.output += text;
|
|
452
|
+
|
|
453
|
+
channel?.emit("output", {
|
|
454
|
+
type: "output",
|
|
455
|
+
toolCallId,
|
|
456
|
+
data: text,
|
|
457
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
458
|
+
timestamp: Date.now(),
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
if (onUpdate) {
|
|
462
|
+
const truncation = collector.getTruncation();
|
|
463
|
+
onUpdate({
|
|
464
|
+
content: [{ type: "text", text: truncation.content || "" }],
|
|
465
|
+
details: {
|
|
466
|
+
truncation: truncation.truncated ? truncation : undefined,
|
|
467
|
+
fullOutputPath: collector.fullOutputPath,
|
|
468
|
+
},
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
child.stdout?.on("data", handleData);
|
|
474
|
+
child.stderr?.on("data", handleData);
|
|
475
|
+
|
|
476
|
+
let backgroundAfterHandle: NodeJS.Timeout | undefined;
|
|
477
|
+
if (effectiveBackgroundAfter !== undefined) {
|
|
478
|
+
backgroundAfterHandle = setTimeout(() => {
|
|
479
|
+
const m = managed.get(toolCallId);
|
|
480
|
+
if (!m || m.resolved || m.backgrounded) return;
|
|
481
|
+
m.proc.status = "background";
|
|
482
|
+
m.resolved = true;
|
|
483
|
+
m.backgrounded = true;
|
|
484
|
+
m.outputSubscribed = false;
|
|
485
|
+
createLogStream(m);
|
|
486
|
+
const durationMs = Date.now() - m.proc.startedAt;
|
|
487
|
+
channel?.emit("background", {
|
|
488
|
+
type: "background",
|
|
489
|
+
toolCallId,
|
|
490
|
+
pid: m.proc.pid,
|
|
491
|
+
data: m.proc.output.slice(-2000),
|
|
492
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
493
|
+
timestamp: Date.now(),
|
|
494
|
+
});
|
|
495
|
+
const outputPreview = m.proc.output ? takeLastLines(m.proc.output, BG_PREVIEW_LINES) : "(no output yet)";
|
|
496
|
+
m.resolve({
|
|
497
|
+
content: [
|
|
498
|
+
{
|
|
499
|
+
type: "text",
|
|
500
|
+
text: `${outputPreview}\n\n[Automatically moved to background after ${formatDuration(durationMs)} (backgroundAfter=${effectiveBackgroundAfter}s), PID: ${m.proc.pid ?? "unknown"}. <bashId>${m.proc.bashId}</bashId>. Log: ${m.proc.logPath}. Use get_background_process with <bashId>${m.proc.bashId}</bashId> to check progress.]`,
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
details: {
|
|
504
|
+
background: {
|
|
505
|
+
pid: m.proc.pid,
|
|
506
|
+
command: m.proc.command,
|
|
507
|
+
startedAt: m.proc.startedAt,
|
|
508
|
+
durationMs,
|
|
509
|
+
logPath: m.proc.logPath,
|
|
510
|
+
detached: false,
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}, effectiveBackgroundAfter * 1000);
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
waitForChildProcess(child)
|
|
518
|
+
.then((code) => {
|
|
519
|
+
spawnCleanup();
|
|
520
|
+
if (backgroundAfterHandle) clearTimeout(backgroundAfterHandle);
|
|
521
|
+
collector.close();
|
|
522
|
+
|
|
523
|
+
const m = managed.get(toolCallId);
|
|
524
|
+
if (m?.resolved) {
|
|
525
|
+
if (m.logStream) m.logStream.end();
|
|
526
|
+
proc.exitCode = code;
|
|
527
|
+
proc.endedAt = Date.now();
|
|
528
|
+
proc.status = code === 0 ? "done" : "error";
|
|
529
|
+
if (m.killedByUser) {
|
|
530
|
+
if (!deletedIds.has(toolCallId)) history.push({ ...proc });
|
|
531
|
+
managed.delete(toolCallId);
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
channel?.emit(proc.status === "done" ? "end" : "error", {
|
|
535
|
+
type: proc.status === "done" ? "end" : "error",
|
|
536
|
+
toolCallId,
|
|
537
|
+
data: proc.output.slice(-2000),
|
|
538
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
539
|
+
timestamp: Date.now(),
|
|
540
|
+
});
|
|
541
|
+
if (!deletedIds.has(toolCallId)) history.push({ ...proc });
|
|
542
|
+
managed.delete(toolCallId);
|
|
543
|
+
try {
|
|
544
|
+
pi.sendUserMessage(
|
|
545
|
+
`[system] Background process "${proc.command}" (PID: ${proc.pid ?? "unknown"}) exited with code ${code ?? "unknown"} after ${formatDuration((proc.endedAt ?? Date.now()) - proc.startedAt)}.${proc.logPath ? ` Log: ${proc.logPath}` : ""}. Use get_background_process with <bashId>${proc.bashId}</bashId> to retrieve the output and continue your task.`,
|
|
546
|
+
{ deliverAs: "followUp" },
|
|
547
|
+
);
|
|
548
|
+
} catch (err) {
|
|
549
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
550
|
+
if (msg.includes("stale")) return;
|
|
551
|
+
console.debug("[bash-ext] background exit notification failed:", msg);
|
|
552
|
+
}
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (signal?.aborted) {
|
|
557
|
+
proc.status = "terminated";
|
|
558
|
+
proc.endedAt = Date.now();
|
|
559
|
+
const durationMs = proc.endedAt - proc.startedAt;
|
|
560
|
+
const outputText = proc.output || "(no output)";
|
|
561
|
+
channel?.emit("terminated", {
|
|
562
|
+
type: "terminated",
|
|
563
|
+
toolCallId,
|
|
564
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
565
|
+
timestamp: Date.now(),
|
|
566
|
+
});
|
|
567
|
+
managed.delete(toolCallId);
|
|
568
|
+
resolve({
|
|
569
|
+
content: [
|
|
570
|
+
{
|
|
571
|
+
type: "text",
|
|
572
|
+
text: `${outputText}\n\n[Aborted after ${formatDuration(durationMs)}, PID: ${proc.pid ?? "unknown"}]`,
|
|
573
|
+
},
|
|
574
|
+
],
|
|
575
|
+
details: {
|
|
576
|
+
terminated: {
|
|
577
|
+
reason: "signal",
|
|
578
|
+
pid: proc.pid,
|
|
579
|
+
command: proc.command,
|
|
580
|
+
startedAt: proc.startedAt,
|
|
581
|
+
endedAt: proc.endedAt,
|
|
582
|
+
durationMs,
|
|
583
|
+
logPath: collector.fullOutputPath,
|
|
584
|
+
},
|
|
585
|
+
},
|
|
586
|
+
});
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
if (isTimedOut()) {
|
|
590
|
+
proc.status = "error";
|
|
591
|
+
proc.endedAt = Date.now();
|
|
592
|
+
const durationMs = proc.endedAt - proc.startedAt;
|
|
593
|
+
const outputText = proc.output || "(no output)";
|
|
594
|
+
channel?.emit("error", {
|
|
595
|
+
type: "error",
|
|
596
|
+
toolCallId,
|
|
597
|
+
data: `Timed out after ${effectiveTimeout}s`,
|
|
598
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
599
|
+
timestamp: Date.now(),
|
|
600
|
+
});
|
|
601
|
+
managed.delete(toolCallId);
|
|
602
|
+
resolve({
|
|
603
|
+
content: [
|
|
604
|
+
{
|
|
605
|
+
type: "text",
|
|
606
|
+
text: `${outputText}\n\n[Timed out after ${effectiveTimeout}s, PID: ${proc.pid ?? "unknown"}]`,
|
|
607
|
+
},
|
|
608
|
+
],
|
|
609
|
+
details: {
|
|
610
|
+
terminated: {
|
|
611
|
+
reason: "timeout",
|
|
612
|
+
pid: proc.pid,
|
|
613
|
+
command: proc.command,
|
|
614
|
+
startedAt: proc.startedAt,
|
|
615
|
+
endedAt: proc.endedAt,
|
|
616
|
+
durationMs,
|
|
617
|
+
timeoutSecs: effectiveTimeout,
|
|
618
|
+
logPath: collector.fullOutputPath,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
proc.exitCode = code;
|
|
626
|
+
proc.endedAt = Date.now();
|
|
627
|
+
|
|
628
|
+
const truncation = collector.finalize();
|
|
629
|
+
|
|
630
|
+
let outputText = truncation.content || "(no output)";
|
|
631
|
+
let details: BashToolDetails | undefined;
|
|
632
|
+
if (truncation.truncated) {
|
|
633
|
+
details = { truncation, fullOutputPath: collector.fullOutputPath };
|
|
634
|
+
const startLine = truncation.totalLines - truncation.outputLines + 1;
|
|
635
|
+
const endLine = truncation.totalLines;
|
|
636
|
+
outputText += `\n\n[Showing lines ${startLine}-${endLine} of ${truncation.totalLines}. Full output: ${collector.fullOutputPath}]`;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (code !== 0 && code !== null) {
|
|
640
|
+
proc.status = "error";
|
|
641
|
+
const durationMs = proc.endedAt - proc.startedAt;
|
|
642
|
+
outputText += `\n\n[Command failed with exit code ${code} after ${formatDuration(durationMs)}, PID: ${proc.pid ?? "unknown"}]`;
|
|
643
|
+
channel?.emit("error", {
|
|
644
|
+
type: "error",
|
|
645
|
+
toolCallId,
|
|
646
|
+
data: outputText,
|
|
647
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
648
|
+
timestamp: Date.now(),
|
|
649
|
+
});
|
|
650
|
+
managed.delete(toolCallId);
|
|
651
|
+
resolve({
|
|
652
|
+
content: [{ type: "text", text: outputText }],
|
|
653
|
+
details: {
|
|
654
|
+
terminated: {
|
|
655
|
+
reason: "error",
|
|
656
|
+
pid: proc.pid,
|
|
657
|
+
command: proc.command,
|
|
658
|
+
startedAt: proc.startedAt,
|
|
659
|
+
endedAt: proc.endedAt,
|
|
660
|
+
durationMs,
|
|
661
|
+
exitCode: code,
|
|
662
|
+
logPath: collector.fullOutputPath,
|
|
663
|
+
},
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
} else {
|
|
667
|
+
proc.status = "done";
|
|
668
|
+
channel?.emit("end", {
|
|
669
|
+
type: "end",
|
|
670
|
+
toolCallId,
|
|
671
|
+
data: outputText,
|
|
672
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
673
|
+
timestamp: Date.now(),
|
|
674
|
+
});
|
|
675
|
+
managed.delete(toolCallId);
|
|
676
|
+
resolve({
|
|
677
|
+
content: [{ type: "text", text: outputText }],
|
|
678
|
+
details: details as BashToolDetails,
|
|
679
|
+
} as AgentToolResult<BashToolDetails>);
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
.catch((err: Error) => {
|
|
683
|
+
spawnCleanup();
|
|
684
|
+
if (backgroundAfterHandle) clearTimeout(backgroundAfterHandle);
|
|
685
|
+
collector.close();
|
|
686
|
+
|
|
687
|
+
const m = managed.get(toolCallId);
|
|
688
|
+
if (m?.resolved) {
|
|
689
|
+
if (m.logStream) m.logStream.end();
|
|
690
|
+
proc.status = "error";
|
|
691
|
+
proc.endedAt = Date.now();
|
|
692
|
+
proc.exitCode = null;
|
|
693
|
+
proc.error = err.message;
|
|
694
|
+
if (m.killedByUser) {
|
|
695
|
+
if (!deletedIds.has(toolCallId)) history.push({ ...proc });
|
|
696
|
+
managed.delete(toolCallId);
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
channel?.emit("error", {
|
|
700
|
+
type: "error",
|
|
701
|
+
toolCallId,
|
|
702
|
+
data: proc.output.slice(-2000),
|
|
703
|
+
processes: Array.from(managed.values()).map((x) => x.proc),
|
|
704
|
+
timestamp: Date.now(),
|
|
705
|
+
});
|
|
706
|
+
if (!deletedIds.has(toolCallId)) history.push({ ...proc });
|
|
707
|
+
managed.delete(toolCallId);
|
|
708
|
+
try {
|
|
709
|
+
pi.sendUserMessage(
|
|
710
|
+
`[system] Background process "${proc.command}" (PID: ${proc.pid ?? "unknown"}) crashed: ${err.message}${proc.logPath ? `. Log: ${proc.logPath}` : ""}. Use get_background_process with <bashId>${proc.bashId}</bashId> to retrieve the output and continue your task.`,
|
|
711
|
+
{ deliverAs: "followUp" },
|
|
712
|
+
);
|
|
713
|
+
} catch (innerErr) {
|
|
714
|
+
const msg = innerErr instanceof Error ? innerErr.message : String(innerErr);
|
|
715
|
+
if (msg.includes("stale")) return;
|
|
716
|
+
console.debug("[bash-ext] background crash notification failed:", msg);
|
|
717
|
+
}
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const durationMs = (proc.endedAt || Date.now()) - proc.startedAt;
|
|
722
|
+
const outputText = proc.output || "(no output)";
|
|
723
|
+
|
|
724
|
+
if (err.message === "aborted") {
|
|
725
|
+
proc.status = "terminated";
|
|
726
|
+
channel?.emit("terminated", {
|
|
727
|
+
type: "terminated",
|
|
728
|
+
toolCallId,
|
|
729
|
+
data: outputText,
|
|
730
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
731
|
+
timestamp: Date.now(),
|
|
732
|
+
});
|
|
733
|
+
managed.delete(toolCallId);
|
|
734
|
+
resolve({
|
|
735
|
+
content: [
|
|
736
|
+
{
|
|
737
|
+
type: "text",
|
|
738
|
+
text: `${outputText}\n\n[Aborted after ${formatDuration(durationMs)}, PID: ${proc.pid ?? "unknown"}]`,
|
|
739
|
+
},
|
|
740
|
+
],
|
|
741
|
+
details: {
|
|
742
|
+
terminated: {
|
|
743
|
+
reason: "signal",
|
|
744
|
+
pid: proc.pid,
|
|
745
|
+
command: proc.command,
|
|
746
|
+
startedAt: proc.startedAt,
|
|
747
|
+
endedAt: proc.endedAt,
|
|
748
|
+
durationMs,
|
|
749
|
+
logPath: collector.fullOutputPath,
|
|
750
|
+
},
|
|
751
|
+
},
|
|
752
|
+
});
|
|
753
|
+
} else if (err.message.startsWith("timeout:")) {
|
|
754
|
+
const timeoutSecs = Number(err.message.split(":")[1]);
|
|
755
|
+
channel?.emit("error", {
|
|
756
|
+
type: "error",
|
|
757
|
+
toolCallId,
|
|
758
|
+
data: outputText,
|
|
759
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
760
|
+
timestamp: Date.now(),
|
|
761
|
+
});
|
|
762
|
+
managed.delete(toolCallId);
|
|
763
|
+
resolve({
|
|
764
|
+
content: [
|
|
765
|
+
{
|
|
766
|
+
type: "text",
|
|
767
|
+
text: `${outputText}\n\n[Timed out after ${timeoutSecs}s, PID: ${proc.pid ?? "unknown"}]`,
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
details: {
|
|
771
|
+
terminated: {
|
|
772
|
+
reason: "timeout",
|
|
773
|
+
pid: proc.pid,
|
|
774
|
+
command: proc.command,
|
|
775
|
+
startedAt: proc.startedAt,
|
|
776
|
+
endedAt: proc.endedAt,
|
|
777
|
+
durationMs,
|
|
778
|
+
timeoutSecs,
|
|
779
|
+
logPath: collector.fullOutputPath,
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
} else {
|
|
784
|
+
channel?.emit("error", {
|
|
785
|
+
type: "error",
|
|
786
|
+
toolCallId,
|
|
787
|
+
data: outputText,
|
|
788
|
+
processes: Array.from(managed.values()).map((m) => m.proc),
|
|
789
|
+
timestamp: Date.now(),
|
|
790
|
+
});
|
|
791
|
+
managed.delete(toolCallId);
|
|
792
|
+
resolve({
|
|
793
|
+
content: [
|
|
794
|
+
{
|
|
795
|
+
type: "text",
|
|
796
|
+
text: `${outputText}\n\n[Command crashed after ${formatDuration(durationMs)}, PID: ${proc.pid ?? "unknown"}: ${err.message}]`,
|
|
797
|
+
},
|
|
798
|
+
],
|
|
799
|
+
details: {
|
|
800
|
+
terminated: {
|
|
801
|
+
reason: "error",
|
|
802
|
+
pid: proc.pid,
|
|
803
|
+
command: proc.command,
|
|
804
|
+
startedAt: proc.startedAt,
|
|
805
|
+
endedAt: proc.endedAt,
|
|
806
|
+
durationMs,
|
|
807
|
+
error: err.message,
|
|
808
|
+
logPath: collector.fullOutputPath,
|
|
809
|
+
},
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
});
|
|
815
|
+
},
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
const bashStatusSchema = Type.Object({
|
|
819
|
+
bashId: Type.String({ description: "The bashId returned when a command was moved to background. Example: bash-abc123" }),
|
|
820
|
+
lastLines: Type.Optional(Type.Number({ description: "Only show the last N lines of output. Useful for checking tail of long-running builds. Default: show last 2000 chars." })),
|
|
821
|
+
grep: Type.Optional(Type.String({ description: "Filter output to only lines containing this keyword (case-insensitive). Useful for finding errors, warnings, or specific patterns in build output." })),
|
|
822
|
+
});
|
|
823
|
+
|
|
824
|
+
function findProcess(bashId: string): { proc: BashProcess; isLive: boolean } | null {
|
|
825
|
+
for (const m of managed.values()) {
|
|
826
|
+
if (m.proc.bashId === bashId) return { proc: m.proc, isLive: !m.proc.endedAt };
|
|
827
|
+
}
|
|
828
|
+
const histProc = history.find((p) => p.bashId === bashId);
|
|
829
|
+
if (histProc) return { proc: histProc, isLive: false };
|
|
830
|
+
return null;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
pi.registerTool({
|
|
834
|
+
name: "get_background_process",
|
|
835
|
+
label: "get_background_process",
|
|
836
|
+
description: [
|
|
837
|
+
"Query the status and output of a backgrounded bash process by its bashId.",
|
|
838
|
+
"",
|
|
839
|
+
"When a bash command is moved to background (manually or via backgroundAfter), it returns a <bashId>. Use this tool to:",
|
|
840
|
+
"- Check if the process is still running, finished, or errored",
|
|
841
|
+
"- Get the accumulated output (filtered if needed)",
|
|
842
|
+
"- Get the exit code (if finished)",
|
|
843
|
+
"",
|
|
844
|
+
"Filtering options:",
|
|
845
|
+
"- lastLines: show only the last N lines (e.g. lastLines=5 for quick status check)",
|
|
846
|
+
"- grep: filter output to lines containing a keyword (e.g. grep='error' to find failures)",
|
|
847
|
+
"- Both can be combined: lastLines=10 + grep='warning'",
|
|
848
|
+
"",
|
|
849
|
+
"Typical flow:",
|
|
850
|
+
"1. Start long command: bash({ command: 'npm install', backgroundAfter: 60 })",
|
|
851
|
+
"2. Do other work while it runs",
|
|
852
|
+
"3. Poll: get_background_process({ bashId: 'bash-abc123' })",
|
|
853
|
+
"4. If status='done', proceed. If status='running', poll again later.",
|
|
854
|
+
].join("\n"),
|
|
855
|
+
promptSnippet: "Check status of a backgrounded bash command",
|
|
856
|
+
parameters: bashStatusSchema,
|
|
857
|
+
async execute(
|
|
858
|
+
_toolCallId: string,
|
|
859
|
+
{ bashId, lastLines, grep: grepPattern }: { bashId: string; lastLines?: number; grep?: string },
|
|
860
|
+
): Promise<AgentToolResult<BashToolDetails>> {
|
|
861
|
+
const result = findProcess(bashId);
|
|
862
|
+
|
|
863
|
+
if (!result) {
|
|
864
|
+
return {
|
|
865
|
+
content: [
|
|
866
|
+
{
|
|
867
|
+
type: "text",
|
|
868
|
+
text: `No process found with <bashId>${bashId}</bashId>. It may have never existed, been removed, or the session has been reset.`,
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
details: undefined as unknown as BashToolDetails,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const { proc, isLive } = result;
|
|
876
|
+
const durationMs = (proc.endedAt ?? Date.now()) - proc.startedAt;
|
|
877
|
+
|
|
878
|
+
const rawOutput = proc.output || "(no output yet)";
|
|
879
|
+
const allLines = rawOutput.split("\n");
|
|
880
|
+
const totalLines = allLines.length;
|
|
881
|
+
|
|
882
|
+
let displayLines: string[];
|
|
883
|
+
let startLine: number;
|
|
884
|
+
let endLine: number;
|
|
885
|
+
|
|
886
|
+
if (grepPattern) {
|
|
887
|
+
// Grep mode: filter matching lines, keep line numbers
|
|
888
|
+
const matched = allLines
|
|
889
|
+
.map((line, i) => ({ line, num: i + 1 }))
|
|
890
|
+
.filter((e) => e.line.toLowerCase().includes(grepPattern.toLowerCase()));
|
|
891
|
+
if (matched.length === 0) {
|
|
892
|
+
displayLines = [`(no lines matching "${grepPattern}")`];
|
|
893
|
+
startLine = 0;
|
|
894
|
+
endLine = 0;
|
|
895
|
+
} else {
|
|
896
|
+
// Apply lastLines to grep results if specified
|
|
897
|
+
const sliced = lastLines && lastLines > 0 ? matched.slice(-lastLines) : matched;
|
|
898
|
+
displayLines = sliced.map((e) => `L${e.num}: ${e.line}`);
|
|
899
|
+
startLine = sliced[0].num;
|
|
900
|
+
endLine = sliced[sliced.length - 1].num;
|
|
901
|
+
}
|
|
902
|
+
} else {
|
|
903
|
+
// Normal mode: take last N lines with line numbers
|
|
904
|
+
const n = lastLines && lastLines > 0 ? lastLines : 50;
|
|
905
|
+
startLine = Math.max(1, totalLines - n + 1);
|
|
906
|
+
endLine = totalLines;
|
|
907
|
+
const selected = allLines.slice(-n);
|
|
908
|
+
displayLines = selected.map((line, i) => `L${startLine + i}: ${line}`);
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const output = displayLines.join("\n");
|
|
912
|
+
|
|
913
|
+
const header = [
|
|
914
|
+
`Process: ${proc.command}`,
|
|
915
|
+
`<bashId>${proc.bashId}</bashId>`,
|
|
916
|
+
`Status: ${proc.status}${isLive ? " (still running)" : ""}`,
|
|
917
|
+
`PID: ${proc.pid ?? "unknown"}`,
|
|
918
|
+
`Duration: ${formatDuration(durationMs)}`,
|
|
919
|
+
proc.exitCode !== undefined ? `Exit code: ${proc.exitCode}` : null,
|
|
920
|
+
proc.logPath ? `Log: ${proc.logPath}` : null,
|
|
921
|
+
proc.error ? `Error: ${proc.error}` : null,
|
|
922
|
+
totalLines > 0 ? `Lines: ${startLine}-${endLine} of ${totalLines} total` : null,
|
|
923
|
+
grepPattern ? `Filtered by: "${grepPattern}"` : null,
|
|
924
|
+
"",
|
|
925
|
+
isLive ? "Output so far:" : "Output:",
|
|
926
|
+
]
|
|
927
|
+
.filter(Boolean)
|
|
928
|
+
.join("\n");
|
|
929
|
+
|
|
930
|
+
return {
|
|
931
|
+
content: [{ type: "text", text: `${header}\n${output}` }],
|
|
932
|
+
details: undefined as unknown as BashToolDetails,
|
|
933
|
+
};
|
|
934
|
+
},
|
|
935
|
+
});
|
|
926
936
|
}
|