@aaroncql/pim-agent 0.0.1

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +212 -0
  3. package/bin/pim.ts +109 -0
  4. package/package.json +49 -0
  5. package/src/extensions/_init/index.ts +109 -0
  6. package/src/extensions/bash/capture.test.ts +126 -0
  7. package/src/extensions/bash/capture.ts +80 -0
  8. package/src/extensions/bash/format.test.ts +240 -0
  9. package/src/extensions/bash/format.ts +76 -0
  10. package/src/extensions/bash/index.ts +86 -0
  11. package/src/extensions/bash/run.test.ts +262 -0
  12. package/src/extensions/bash/run.ts +207 -0
  13. package/src/extensions/bash/schema.ts +54 -0
  14. package/src/extensions/command-picker/index.ts +52 -0
  15. package/src/extensions/command-picker/ranker.test.ts +46 -0
  16. package/src/extensions/command-picker/ranker.ts +17 -0
  17. package/src/extensions/edit/edit.test.ts +285 -0
  18. package/src/extensions/edit/edit.ts +382 -0
  19. package/src/extensions/edit/index.ts +54 -0
  20. package/src/extensions/edit/schema.ts +37 -0
  21. package/src/extensions/file-picker/catalog.test.ts +263 -0
  22. package/src/extensions/file-picker/catalog.ts +219 -0
  23. package/src/extensions/file-picker/index.test.ts +168 -0
  24. package/src/extensions/file-picker/index.ts +119 -0
  25. package/src/extensions/file-picker/ranker.test.ts +94 -0
  26. package/src/extensions/file-picker/ranker.ts +76 -0
  27. package/src/extensions/footer/git.test.ts +76 -0
  28. package/src/extensions/footer/git.ts +87 -0
  29. package/src/extensions/footer/index.test.ts +161 -0
  30. package/src/extensions/footer/index.ts +148 -0
  31. package/src/extensions/footer/powerline.ts +87 -0
  32. package/src/extensions/footer/segments.test.ts +164 -0
  33. package/src/extensions/footer/segments.ts +234 -0
  34. package/src/extensions/glob/glob.test.ts +171 -0
  35. package/src/extensions/glob/glob.ts +34 -0
  36. package/src/extensions/glob/index.test.ts +68 -0
  37. package/src/extensions/glob/index.ts +136 -0
  38. package/src/extensions/glob/render.test.ts +126 -0
  39. package/src/extensions/glob/render.ts +74 -0
  40. package/src/extensions/glob/schema.ts +52 -0
  41. package/src/extensions/grep/grep.test.ts +387 -0
  42. package/src/extensions/grep/grep.ts +215 -0
  43. package/src/extensions/grep/index.test.ts +68 -0
  44. package/src/extensions/grep/index.ts +158 -0
  45. package/src/extensions/grep/render.test.ts +269 -0
  46. package/src/extensions/grep/render.ts +243 -0
  47. package/src/extensions/grep/schema.ts +92 -0
  48. package/src/extensions/read/index.ts +84 -0
  49. package/src/extensions/read/read.test.ts +177 -0
  50. package/src/extensions/read/read.ts +206 -0
  51. package/src/extensions/read/render.test.ts +61 -0
  52. package/src/extensions/read/render.ts +33 -0
  53. package/src/extensions/read/schema.ts +27 -0
  54. package/src/extensions/subagent/index.test.ts +44 -0
  55. package/src/extensions/subagent/index.ts +30 -0
  56. package/src/extensions/subagent/render.test.ts +292 -0
  57. package/src/extensions/subagent/render.ts +359 -0
  58. package/src/extensions/subagent/schema.ts +9 -0
  59. package/src/extensions/subagent/subagent.test.ts +315 -0
  60. package/src/extensions/subagent/subagent.ts +418 -0
  61. package/src/extensions/system-prompt/index.ts +28 -0
  62. package/src/extensions/system-prompt/prompt.test.ts +64 -0
  63. package/src/extensions/system-prompt/prompt.ts +213 -0
  64. package/src/extensions/todo/index.test.ts +244 -0
  65. package/src/extensions/todo/index.ts +122 -0
  66. package/src/extensions/todo/render.test.ts +180 -0
  67. package/src/extensions/todo/render.ts +172 -0
  68. package/src/extensions/todo/schema.ts +24 -0
  69. package/src/extensions/todo/todo.test.ts +222 -0
  70. package/src/extensions/todo/todo.ts +188 -0
  71. package/src/extensions/tps/index.test.ts +254 -0
  72. package/src/extensions/tps/index.ts +136 -0
  73. package/src/extensions/web-fetch/JinaReaderClient.ts +230 -0
  74. package/src/extensions/web-fetch/WebViewFetchClient.ts +186 -0
  75. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.test.ts +119 -0
  76. package/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +511 -0
  77. package/src/extensions/web-fetch/fetch.test.ts +244 -0
  78. package/src/extensions/web-fetch/fetch.ts +249 -0
  79. package/src/extensions/web-fetch/index.ts +107 -0
  80. package/src/extensions/web-fetch/render.test.ts +56 -0
  81. package/src/extensions/web-fetch/render.ts +39 -0
  82. package/src/extensions/web-fetch/schema.ts +23 -0
  83. package/src/extensions/web-search/ExaMcpClient.test.ts +143 -0
  84. package/src/extensions/web-search/ExaMcpClient.ts +258 -0
  85. package/src/extensions/web-search/index.ts +118 -0
  86. package/src/extensions/web-search/render.test.ts +21 -0
  87. package/src/extensions/web-search/render.ts +9 -0
  88. package/src/extensions/web-search/schema.ts +21 -0
  89. package/src/extensions/web-search/search.test.ts +53 -0
  90. package/src/extensions/web-search/search.ts +23 -0
  91. package/src/extensions/working-indicator/index.test.ts +21 -0
  92. package/src/extensions/working-indicator/index.ts +77 -0
  93. package/src/extensions/write/index.ts +76 -0
  94. package/src/extensions/write/render.test.ts +64 -0
  95. package/src/extensions/write/schema.ts +14 -0
  96. package/src/extensions/write/write.test.ts +108 -0
  97. package/src/extensions/write/write.ts +104 -0
  98. package/src/shared/DiffLines.test.ts +193 -0
  99. package/src/shared/DiffLines.ts +307 -0
  100. package/src/shared/DiffRenderer.test.ts +206 -0
  101. package/src/shared/DiffRenderer.ts +396 -0
  102. package/src/shared/DiffView.ts +199 -0
  103. package/src/shared/EditMatcher.test.ts +123 -0
  104. package/src/shared/EditMatcher.ts +826 -0
  105. package/src/shared/FileScanner.test.ts +158 -0
  106. package/src/shared/FileScanner.ts +41 -0
  107. package/src/shared/Fs.ts +46 -0
  108. package/src/shared/FsErrors.ts +72 -0
  109. package/src/shared/FuzzyMatcher.test.ts +114 -0
  110. package/src/shared/FuzzyMatcher.ts +73 -0
  111. package/src/shared/GitignoreFilter.test.ts +64 -0
  112. package/src/shared/GitignoreFilter.ts +142 -0
  113. package/src/shared/GlobExclusions.ts +23 -0
  114. package/src/shared/Levenshtein.ts +33 -0
  115. package/src/shared/Lines.test.ts +25 -0
  116. package/src/shared/Lines.ts +77 -0
  117. package/src/shared/McpClient.test.ts +235 -0
  118. package/src/shared/McpClient.ts +406 -0
  119. package/src/shared/OutputBudget.test.ts +99 -0
  120. package/src/shared/OutputBudget.ts +79 -0
  121. package/src/shared/Paths.test.ts +51 -0
  122. package/src/shared/Paths.ts +52 -0
  123. package/src/shared/PimSettings.test.ts +90 -0
  124. package/src/shared/PimSettings.ts +124 -0
  125. package/src/shared/Renderer.test.ts +190 -0
  126. package/src/shared/Renderer.ts +256 -0
  127. package/src/shared/SpillCache.test.ts +94 -0
  128. package/src/shared/SpillCache.ts +89 -0
  129. package/src/shared/Tools.test.ts +392 -0
  130. package/src/shared/Tools.ts +636 -0
  131. package/src/telegram/Bot.ts +198 -0
  132. package/src/telegram/Commands.ts +721 -0
  133. package/src/telegram/Config.test.ts +275 -0
  134. package/src/telegram/Config.ts +162 -0
  135. package/src/telegram/Markdown.test.ts +143 -0
  136. package/src/telegram/Markdown.ts +177 -0
  137. package/src/telegram/Message.ts +211 -0
  138. package/src/telegram/Renderer.test.ts +216 -0
  139. package/src/telegram/Renderer.ts +713 -0
  140. package/src/telegram/SendFileSchema.ts +19 -0
  141. package/src/telegram/SendFileTool.ts +94 -0
  142. package/src/telegram/Session.ts +579 -0
  143. package/src/telegram/SessionRegistry.test.ts +89 -0
  144. package/src/telegram/SessionRegistry.ts +170 -0
  145. package/src/telegram/Supervisor.ts +357 -0
  146. package/src/telegram/TaskScheduler.test.ts +278 -0
  147. package/src/telegram/TaskScheduler.ts +293 -0
  148. package/src/telegram/TaskSchema.ts +88 -0
  149. package/src/telegram/TaskStore.ts +73 -0
  150. package/src/telegram/TaskTool.test.ts +179 -0
  151. package/src/telegram/TaskTool.ts +159 -0
  152. package/src/telegram/TypingIndicator.ts +43 -0
  153. package/src/telegram/index.ts +32 -0
  154. package/src/themes/pim-dark.json +84 -0
  155. package/src/themes/pim-light.json +84 -0
@@ -0,0 +1,80 @@
1
+ import { Lines } from "../../shared/Lines";
2
+ import {
3
+ type CapturedStream,
4
+ STREAM_HEAD_BYTES,
5
+ STREAM_TAIL_BYTES,
6
+ } from "./schema";
7
+
8
+ export function concat(parts: Uint8Array[], total: number): Uint8Array {
9
+ const out = new Uint8Array(total);
10
+ let off = 0;
11
+ for (const p of parts) {
12
+ out.set(p, off);
13
+ off += p.byteLength;
14
+ }
15
+ return out;
16
+ }
17
+
18
+ export class StreamCapture {
19
+ private chunks: Uint8Array[] = [];
20
+ private totalBytesAccum = 0;
21
+ private fullBytes: Uint8Array | null = null;
22
+
23
+ push(chunk: Uint8Array): void {
24
+ if (chunk.byteLength === 0) {
25
+ return;
26
+ }
27
+ this.chunks.push(chunk);
28
+ this.totalBytesAccum += chunk.byteLength;
29
+ }
30
+
31
+ get totalBytes(): number {
32
+ return this.totalBytesAccum;
33
+ }
34
+
35
+ get truncated(): boolean {
36
+ return this.totalBytesAccum > STREAM_HEAD_BYTES + STREAM_TAIL_BYTES;
37
+ }
38
+
39
+ full(): Uint8Array {
40
+ if (!this.fullBytes) {
41
+ this.fullBytes = concat(this.chunks, this.totalBytesAccum);
42
+ }
43
+ return this.fullBytes;
44
+ }
45
+
46
+ snapshot(): CapturedStream {
47
+ if (this.totalBytesAccum === 0) {
48
+ return {
49
+ text: "",
50
+ totalBytes: 0,
51
+ truncated: false,
52
+ path: null,
53
+ nextStart: null,
54
+ };
55
+ }
56
+ const dec = new TextDecoder();
57
+ if (!this.truncated) {
58
+ return {
59
+ text: dec.decode(this.full()),
60
+ totalBytes: this.totalBytesAccum,
61
+ truncated: false,
62
+ path: null,
63
+ nextStart: null,
64
+ };
65
+ }
66
+ const all = this.full();
67
+ const headText = dec.decode(all.subarray(0, STREAM_HEAD_BYTES));
68
+ const tailText = dec.decode(
69
+ all.subarray(all.byteLength - STREAM_TAIL_BYTES)
70
+ );
71
+ const middle = this.totalBytesAccum - STREAM_HEAD_BYTES - STREAM_TAIL_BYTES;
72
+ return {
73
+ text: `${headText}\n... ${middle} bytes truncated ...\n${tailText}`,
74
+ totalBytes: this.totalBytesAccum,
75
+ truncated: true,
76
+ path: null,
77
+ nextStart: Lines.continuationLine(headText),
78
+ };
79
+ }
80
+ }
@@ -0,0 +1,240 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ detailsOf,
4
+ formatResult,
5
+ formatTruncationAffordance,
6
+ isErrorResult,
7
+ stripTrailingNewline,
8
+ } from "./format";
9
+ import {
10
+ type BashCommandResult,
11
+ STREAM_HEAD_BYTES,
12
+ STREAM_TAIL_BYTES,
13
+ } from "./schema";
14
+
15
+ function makeResult(
16
+ overrides: Partial<BashCommandResult> = {}
17
+ ): BashCommandResult {
18
+ return {
19
+ exitCode: 0,
20
+ signal: null,
21
+ stdout: {
22
+ text: "",
23
+ totalBytes: 0,
24
+ truncated: false,
25
+ path: null,
26
+ nextStart: null,
27
+ },
28
+ stderr: {
29
+ text: "",
30
+ totalBytes: 0,
31
+ truncated: false,
32
+ path: null,
33
+ nextStart: null,
34
+ },
35
+ timedOut: false,
36
+ aborted: false,
37
+ durationMs: 1,
38
+ ...overrides,
39
+ };
40
+ }
41
+
42
+ describe("stripTrailingNewline", () => {
43
+ test("removes one trailing newline", () => {
44
+ expect(stripTrailingNewline("foo\n")).toBe("foo");
45
+ });
46
+ test("leaves no-newline strings alone", () => {
47
+ expect(stripTrailingNewline("foo")).toBe("foo");
48
+ });
49
+ test("only strips one", () => {
50
+ expect(stripTrailingNewline("foo\n\n")).toBe("foo\n");
51
+ });
52
+ });
53
+
54
+ describe("formatTruncationAffordance", () => {
55
+ test("emits bracketed affordance with byte counts and next-step", () => {
56
+ const out = formatTruncationAffordance("stderr", {
57
+ text: "x",
58
+ totalBytes: 12345,
59
+ truncated: true,
60
+ path: null,
61
+ nextStart: 1,
62
+ });
63
+ expect(out.startsWith("[bash tool:")).toBe(true);
64
+ expect(out.endsWith("]")).toBe(true);
65
+ expect(out).toContain("stderr showing first");
66
+ expect(out).toContain(`first ${STREAM_HEAD_BYTES} bytes`);
67
+ expect(out).toContain(`last ${STREAM_TAIL_BYTES} bytes`);
68
+ expect(out).toContain("of 12345");
69
+ expect(out).toContain("redirect to a file");
70
+ expect(out).toContain("read");
71
+ });
72
+
73
+ test("points to spill path with a resume line when one is provided", () => {
74
+ const out = formatTruncationAffordance("stdout", {
75
+ text: "x",
76
+ totalBytes: 99999,
77
+ truncated: true,
78
+ path: "/tmp/pim-bash-abc.out",
79
+ nextStart: 42,
80
+ });
81
+ expect(out).toContain(
82
+ "use read with path=/tmp/pim-bash-abc.out and start=42 for the rest."
83
+ );
84
+ expect(out).not.toContain("redirect to a file");
85
+ });
86
+ });
87
+
88
+ describe("formatResult", () => {
89
+ test("happy path with stdout only", () => {
90
+ const out = formatResult(
91
+ makeResult({
92
+ stdout: {
93
+ text: "hello\n",
94
+ totalBytes: 6,
95
+ truncated: false,
96
+ path: null,
97
+ nextStart: null,
98
+ },
99
+ }),
100
+ 30_000
101
+ );
102
+ expect(out).toBe("Exit code: 0\nstdout:\nhello");
103
+ });
104
+
105
+ test("includes signal line when signal present", () => {
106
+ const out = formatResult(
107
+ makeResult({ exitCode: null, signal: "SIGTERM" }),
108
+ 30_000
109
+ );
110
+ expect(out).toContain("Exit code: none");
111
+ expect(out).toContain("Signal: SIGTERM");
112
+ });
113
+
114
+ test("aborted overrides timed out message", () => {
115
+ const out = formatResult(
116
+ makeResult({ aborted: true, timedOut: true }),
117
+ 30_000
118
+ );
119
+ expect(out).toContain("Aborted.");
120
+ expect(out).not.toContain("Timed out");
121
+ });
122
+
123
+ test("timed out adds duration message", () => {
124
+ const out = formatResult(makeResult({ timedOut: true }), 5000);
125
+ expect(out).toContain("Timed out after 5000 ms.");
126
+ });
127
+
128
+ test("includes both stdout and stderr when both have bytes", () => {
129
+ const out = formatResult(
130
+ makeResult({
131
+ exitCode: 1,
132
+ stdout: {
133
+ text: "out",
134
+ totalBytes: 3,
135
+ truncated: false,
136
+ path: null,
137
+ nextStart: null,
138
+ },
139
+ stderr: {
140
+ text: "err",
141
+ totalBytes: 3,
142
+ truncated: false,
143
+ path: null,
144
+ nextStart: null,
145
+ },
146
+ }),
147
+ 30_000
148
+ );
149
+ expect(out).toBe("Exit code: 1\nstdout:\nout\nstderr:\nerr");
150
+ });
151
+
152
+ test("appends bracket affordance after a truncated stream body", () => {
153
+ const out = formatResult(
154
+ makeResult({
155
+ stdout: {
156
+ text: "head…tail",
157
+ totalBytes: 99999,
158
+ truncated: true,
159
+ path: null,
160
+ nextStart: 1,
161
+ },
162
+ }),
163
+ 30_000
164
+ );
165
+ const lines = out.split("\n");
166
+ expect(lines[0]).toBe("Exit code: 0");
167
+ expect(lines[1]).toBe("stdout:");
168
+ expect(lines[2]).toBe("head…tail");
169
+ expect(lines[3]?.startsWith("[bash tool: stdout showing first")).toBe(true);
170
+ expect(lines[3]?.endsWith("]")).toBe(true);
171
+ });
172
+
173
+ test("does not append affordance when stream is not truncated", () => {
174
+ const out = formatResult(
175
+ makeResult({
176
+ stdout: {
177
+ text: "ok",
178
+ totalBytes: 2,
179
+ truncated: false,
180
+ path: null,
181
+ nextStart: null,
182
+ },
183
+ }),
184
+ 30_000
185
+ );
186
+ expect(out).not.toContain("[bash tool:");
187
+ });
188
+ });
189
+
190
+ describe("detailsOf", () => {
191
+ test("mirrors per-stream truncation and byte counts", () => {
192
+ const details = detailsOf(
193
+ makeResult({
194
+ exitCode: 1,
195
+ durationMs: 42,
196
+ stdout: {
197
+ text: "x",
198
+ totalBytes: 99999,
199
+ truncated: true,
200
+ path: null,
201
+ nextStart: 1,
202
+ },
203
+ stderr: {
204
+ text: "y",
205
+ totalBytes: 5,
206
+ truncated: false,
207
+ path: null,
208
+ nextStart: null,
209
+ },
210
+ })
211
+ );
212
+ expect(details).toEqual({
213
+ exitCode: 1,
214
+ signal: null,
215
+ durationMs: 42,
216
+ timedOut: false,
217
+ aborted: false,
218
+ stdout: { totalBytes: 99999, truncated: true, path: null },
219
+ stderr: { totalBytes: 5, truncated: false, path: null },
220
+ });
221
+ });
222
+ });
223
+
224
+ describe("isErrorResult", () => {
225
+ test("zero exit code is not an error", () => {
226
+ expect(isErrorResult(makeResult({ exitCode: 0 }))).toBe(false);
227
+ });
228
+ test("non-zero exit code is an error", () => {
229
+ expect(isErrorResult(makeResult({ exitCode: 1 }))).toBe(true);
230
+ });
231
+ test("null exit code is an error", () => {
232
+ expect(isErrorResult(makeResult({ exitCode: null }))).toBe(true);
233
+ });
234
+ test("aborted is an error", () => {
235
+ expect(isErrorResult(makeResult({ aborted: true }))).toBe(true);
236
+ });
237
+ test("timed out is an error", () => {
238
+ expect(isErrorResult(makeResult({ timedOut: true }))).toBe(true);
239
+ });
240
+ });
@@ -0,0 +1,76 @@
1
+ import {
2
+ type BashCommandResult,
3
+ type BashDetails,
4
+ type CapturedStream,
5
+ STREAM_HEAD_BYTES,
6
+ STREAM_TAIL_BYTES,
7
+ } from "./schema";
8
+
9
+ export function stripTrailingNewline(s: string): string {
10
+ return s.endsWith("\n") ? s.slice(0, -1) : s;
11
+ }
12
+
13
+ export function formatTruncationAffordance(
14
+ label: string,
15
+ s: CapturedStream
16
+ ): string {
17
+ const base = `[bash tool: ${label} showing first ${STREAM_HEAD_BYTES} bytes + last ${STREAM_TAIL_BYTES} bytes of ${s.totalBytes}`;
18
+ if (s.path) {
19
+ return `${base}; use read with path=${s.path} and start=${s.nextStart} for the rest.]`;
20
+ }
21
+ return `${base}; redirect to a file (e.g. \`cmd > /tmp/out.log\`) and use read for the full output.]`;
22
+ }
23
+
24
+ export function formatResult(
25
+ result: BashCommandResult,
26
+ timeoutMs: number
27
+ ): string {
28
+ const lines: string[] = [`Exit code: ${result.exitCode ?? "none"}`];
29
+ if (result.signal !== null) {
30
+ lines.push(`Signal: ${result.signal}`);
31
+ }
32
+ if (result.aborted) {
33
+ lines.push("Aborted.");
34
+ } else if (result.timedOut) {
35
+ lines.push(`Timed out after ${timeoutMs} ms.`);
36
+ }
37
+ if (result.stdout.totalBytes > 0) {
38
+ lines.push("stdout:");
39
+ lines.push(stripTrailingNewline(result.stdout.text));
40
+ if (result.stdout.truncated) {
41
+ lines.push(formatTruncationAffordance("stdout", result.stdout));
42
+ }
43
+ }
44
+ if (result.stderr.totalBytes > 0) {
45
+ lines.push("stderr:");
46
+ lines.push(stripTrailingNewline(result.stderr.text));
47
+ if (result.stderr.truncated) {
48
+ lines.push(formatTruncationAffordance("stderr", result.stderr));
49
+ }
50
+ }
51
+ return lines.join("\n");
52
+ }
53
+
54
+ export function isErrorResult(result: BashCommandResult): boolean {
55
+ return result.aborted || result.timedOut || result.exitCode !== 0;
56
+ }
57
+
58
+ export function detailsOf(result: BashCommandResult): BashDetails {
59
+ return {
60
+ exitCode: result.exitCode,
61
+ signal: result.signal,
62
+ durationMs: result.durationMs,
63
+ timedOut: result.timedOut,
64
+ aborted: result.aborted,
65
+ stdout: {
66
+ totalBytes: result.stdout.totalBytes,
67
+ truncated: result.stdout.truncated,
68
+ path: result.stdout.path,
69
+ },
70
+ stderr: {
71
+ totalBytes: result.stderr.totalBytes,
72
+ truncated: result.stderr.truncated,
73
+ path: result.stderr.path,
74
+ },
75
+ };
76
+ }
@@ -0,0 +1,86 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import { Renderer } from "../../shared/Renderer";
3
+ import { SpillCache } from "../../shared/SpillCache";
4
+ import { Tools } from "../../shared/Tools";
5
+ import { detailsOf, formatResult, isErrorResult } from "./format";
6
+ import { killAllActiveBashGroups, runBashCommand } from "./run";
7
+ import { type BashInput, bashSchema, DEFAULT_TIMEOUT_MS } from "./schema";
8
+
9
+ const PREVIEW_LINES = 5;
10
+
11
+ let lifecycleHandlersInstalled = false;
12
+
13
+ function installLifecycleHandlers(): void {
14
+ if (lifecycleHandlersInstalled) {
15
+ return;
16
+ }
17
+ lifecycleHandlersInstalled = true;
18
+
19
+ // Sweep bash subtrees that escaped our process group (double-forked
20
+ // daemons via their own setsid) or that the parent harness is about to
21
+ // strand by signalling us. Re-raise the signal so the default handler
22
+ // still runs.
23
+ for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"] as const) {
24
+ process.once(sig, () => {
25
+ try {
26
+ killAllActiveBashGroups(sig);
27
+ } catch {}
28
+ process.kill(process.pid, sig);
29
+ });
30
+ }
31
+ }
32
+
33
+ export default function (pi: ExtensionAPI): void {
34
+ SpillCache.installSweeper();
35
+ installLifecycleHandlers();
36
+ Tools.register(pi, {
37
+ name: "bash",
38
+ label: "bash",
39
+ description:
40
+ `Execute a bash command in the cwd. ` +
41
+ `Returns exit code, signal (if any), and stdout/stderr captured separately. ` +
42
+ `Prefer commands that emit only what you need; keep output as small as possible.`,
43
+ parameters: bashSchema,
44
+ renderShell: "self",
45
+ executionMode: "sequential",
46
+ async execute(_id, params, signal, _onUpdate, ctx) {
47
+ const { command, timeoutMs: requestedTimeoutMs } = params as BashInput;
48
+ const timeoutMs = requestedTimeoutMs ?? DEFAULT_TIMEOUT_MS;
49
+
50
+ if (signal?.aborted) {
51
+ throw new Error("Command aborted before execution.");
52
+ }
53
+
54
+ const result = await runBashCommand(command, timeoutMs, signal, ctx.cwd);
55
+ const text = formatResult(result, timeoutMs);
56
+ if (isErrorResult(result)) {
57
+ throw new Error(text);
58
+ }
59
+ return {
60
+ content: [{ type: "text", text }],
61
+ details: detailsOf(result),
62
+ };
63
+ },
64
+ renderCall(args, theme, context) {
65
+ const cmd =
66
+ typeof args?.command === "string" && args.command
67
+ ? args.command
68
+ : "...";
69
+ return Renderer.renderToolCallTitle({
70
+ label: "Bash",
71
+ title: cmd,
72
+ theme,
73
+ context,
74
+ });
75
+ },
76
+ renderResult(result, options, theme, context) {
77
+ return Renderer.renderBorderedResult({
78
+ result,
79
+ options,
80
+ theme,
81
+ context,
82
+ previewLines: PREVIEW_LINES,
83
+ });
84
+ },
85
+ });
86
+ }