min-agent 0.1.6 → 0.1.8
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.js +41 -15
- package/dist/assistant-stream.js +14 -1
- package/dist/confirm.js +24 -6
- package/dist/markdown.js +147 -3
- package/dist/tools/bash.js +12 -0
- package/dist/tools/index.js +21 -1
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -2,7 +2,7 @@ import { streamText, stepCountIs } from "ai";
|
|
|
2
2
|
import { readFileSync, existsSync } from "fs";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { resolveModel } from "./provider.js";
|
|
5
|
-
import {
|
|
5
|
+
import { createChatTools, createCodeTools } from "./tools/index.js";
|
|
6
6
|
import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "./mcp.js";
|
|
7
7
|
import { discoverSkills, getSkillsTool, getSkillsSystemPrompt, getSkills } from "./skills.js";
|
|
8
8
|
import { loadInstructions } from "./instructions.js";
|
|
@@ -14,6 +14,8 @@ import { DoomLoopDetector } from "./doom-loop.js";
|
|
|
14
14
|
import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
|
|
15
15
|
import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
|
|
16
16
|
import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
|
|
17
|
+
import { killActiveProcesses } from "./tools/bash.js";
|
|
18
|
+
import { setConfirmReadline } from "./confirm.js";
|
|
17
19
|
import readline from "readline";
|
|
18
20
|
const MAX_STEPS = 30;
|
|
19
21
|
function dimStyle() {
|
|
@@ -142,10 +144,12 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
142
144
|
output: process.stdout,
|
|
143
145
|
prompt: "\x1b[36m> \x1b[0m",
|
|
144
146
|
});
|
|
147
|
+
setConfirmReadline(rl);
|
|
145
148
|
// Handle Ctrl+C: abort current generation, don't exit
|
|
146
149
|
let abortController = null;
|
|
147
150
|
process.on("SIGINT", () => {
|
|
148
151
|
if (abortController) {
|
|
152
|
+
killActiveProcesses();
|
|
149
153
|
abortController.abort();
|
|
150
154
|
abortController = null;
|
|
151
155
|
console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
|
|
@@ -157,7 +161,19 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
157
161
|
rl.close();
|
|
158
162
|
}
|
|
159
163
|
});
|
|
160
|
-
|
|
164
|
+
// Listen for ESC key to cancel running agent task
|
|
165
|
+
if (process.stdin.isTTY) {
|
|
166
|
+
process.stdin.on("data", (buf) => {
|
|
167
|
+
if (buf.length === 1 && buf[0] === 0x1b && abortController) {
|
|
168
|
+
killActiveProcesses();
|
|
169
|
+
abortController.abort();
|
|
170
|
+
abortController = null;
|
|
171
|
+
console.log("\n\x1b[90m(esc cancelled)\x1b[0m\n");
|
|
172
|
+
rl.prompt();
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,Esc 取消运行,/exit 退出\x1b[0m\n");
|
|
161
177
|
rl.prompt();
|
|
162
178
|
// Multi-line paste detection: collect rapid successive lines
|
|
163
179
|
let pasteBuffer = [];
|
|
@@ -210,11 +226,10 @@ export async function runChat(modelId, resumeSessionId) {
|
|
|
210
226
|
});
|
|
211
227
|
// Wait for close
|
|
212
228
|
await new Promise((resolve) => rl.on("close", resolve));
|
|
213
|
-
// Auto-save session on exit
|
|
229
|
+
// Auto-save session on exit
|
|
214
230
|
if (messages.length > 0) {
|
|
215
|
-
const {
|
|
216
|
-
|
|
217
|
-
sessionId = await saveSessionWithTitle(messages, model, sessionId);
|
|
231
|
+
const { saveSession } = await import("./sessions.js");
|
|
232
|
+
sessionId = saveSession(messages, sessionId);
|
|
218
233
|
console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
|
|
219
234
|
}
|
|
220
235
|
printDivider();
|
|
@@ -252,9 +267,11 @@ export async function runCode(modelId, resumeSessionId) {
|
|
|
252
267
|
output: process.stdout,
|
|
253
268
|
prompt: "\x1b[32m❯ \x1b[0m",
|
|
254
269
|
});
|
|
270
|
+
setConfirmReadline(rl);
|
|
255
271
|
let abortController = null;
|
|
256
272
|
process.on("SIGINT", () => {
|
|
257
273
|
if (abortController) {
|
|
274
|
+
killActiveProcesses();
|
|
258
275
|
abortController.abort();
|
|
259
276
|
abortController = null;
|
|
260
277
|
console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
|
|
@@ -265,7 +282,19 @@ export async function runCode(modelId, resumeSessionId) {
|
|
|
265
282
|
rl.close();
|
|
266
283
|
}
|
|
267
284
|
});
|
|
268
|
-
|
|
285
|
+
// Listen for ESC key to cancel running agent task
|
|
286
|
+
if (process.stdin.isTTY) {
|
|
287
|
+
process.stdin.on("data", (buf) => {
|
|
288
|
+
if (buf.length === 1 && buf[0] === 0x1b && abortController) {
|
|
289
|
+
killActiveProcesses();
|
|
290
|
+
abortController.abort();
|
|
291
|
+
abortController = null;
|
|
292
|
+
console.log("\n\x1b[90m(esc cancelled)\x1b[0m\n");
|
|
293
|
+
rl.prompt();
|
|
294
|
+
}
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
console.log("\x1b[90m输入任务开始编码,/help 查看命令,Esc 取消运行,Ctrl+C 中断\x1b[0m\n");
|
|
269
298
|
rl.prompt();
|
|
270
299
|
let pasteBuffer = [];
|
|
271
300
|
let pasteTimer = null;
|
|
@@ -316,9 +345,8 @@ export async function runCode(modelId, resumeSessionId) {
|
|
|
316
345
|
});
|
|
317
346
|
await new Promise((resolve) => rl.on("close", resolve));
|
|
318
347
|
if (messages.length > 0) {
|
|
319
|
-
const {
|
|
320
|
-
|
|
321
|
-
sessionId = await saveSessionWithTitle(messages, model, sessionId);
|
|
348
|
+
const { saveSession } = await import("./sessions.js");
|
|
349
|
+
sessionId = saveSession(messages, sessionId);
|
|
322
350
|
console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
|
|
323
351
|
}
|
|
324
352
|
rl.close();
|
|
@@ -344,7 +372,7 @@ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSi
|
|
|
344
372
|
console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
|
|
345
373
|
}
|
|
346
374
|
}
|
|
347
|
-
const builtinTools =
|
|
375
|
+
const builtinTools = createCodeTools();
|
|
348
376
|
const mcpTools = getMcpTools();
|
|
349
377
|
const memoryTools = getMemoryTools();
|
|
350
378
|
const pluginTools = await loadPluginTools();
|
|
@@ -734,12 +762,11 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
734
762
|
}
|
|
735
763
|
}
|
|
736
764
|
}
|
|
737
|
-
// Merge
|
|
738
|
-
const builtinTools =
|
|
765
|
+
// Merge tools: chat builtin + MCP + skill + memory + plugins (no task/explore in chat mode)
|
|
766
|
+
const builtinTools = createChatTools();
|
|
739
767
|
const mcpTools = getMcpTools();
|
|
740
768
|
const memoryTools = getMemoryTools();
|
|
741
769
|
const pluginTools = await loadPluginTools();
|
|
742
|
-
const { createTaskTool } = await import("./tools/task.js");
|
|
743
770
|
const skills = getSkills();
|
|
744
771
|
const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
|
|
745
772
|
for (const [id, t] of Object.entries(mcpTools)) {
|
|
@@ -748,7 +775,6 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
|
|
|
748
775
|
if (skills.length > 0) {
|
|
749
776
|
allTools["skill"] = getSkillsTool();
|
|
750
777
|
}
|
|
751
|
-
allTools["task"] = createTaskTool(modelId);
|
|
752
778
|
let stepCount = 0;
|
|
753
779
|
let hasError = false;
|
|
754
780
|
const doomLoop = new DoomLoopDetector();
|
package/dist/assistant-stream.js
CHANGED
|
@@ -41,6 +41,7 @@ export class ThinkingBodySplitter {
|
|
|
41
41
|
drain(isFinal) {
|
|
42
42
|
let display = "";
|
|
43
43
|
let thinking = "";
|
|
44
|
+
let hadThinking = false;
|
|
44
45
|
while (this.buf.length > 0) {
|
|
45
46
|
const open = findFirstOpen(this.buf);
|
|
46
47
|
if (!open) {
|
|
@@ -62,7 +63,11 @@ export class ThinkingBodySplitter {
|
|
|
62
63
|
break;
|
|
63
64
|
}
|
|
64
65
|
if (open.index > 0) {
|
|
65
|
-
|
|
66
|
+
// Strip trailing newlines before thinking block
|
|
67
|
+
let pre = this.buf.slice(0, open.index);
|
|
68
|
+
pre = pre.replace(/\n+$/, "");
|
|
69
|
+
if (pre)
|
|
70
|
+
display += pre;
|
|
66
71
|
this.buf = this.buf.slice(open.index);
|
|
67
72
|
}
|
|
68
73
|
const low = lower(this.buf);
|
|
@@ -81,8 +86,16 @@ export class ThinkingBodySplitter {
|
|
|
81
86
|
}
|
|
82
87
|
const inner = this.buf.slice(afterOpen, closeRel);
|
|
83
88
|
thinking += inner;
|
|
89
|
+
hadThinking = true;
|
|
84
90
|
this.buf = this.buf.slice(closeRel + open.tag.close.length);
|
|
85
91
|
}
|
|
92
|
+
// Strip leading newlines from display that follow a thinking block
|
|
93
|
+
if (hadThinking && display.length === 0 && this.buf.startsWith("\n")) {
|
|
94
|
+
// Will be handled on next feed
|
|
95
|
+
}
|
|
96
|
+
if (hadThinking) {
|
|
97
|
+
display = display.replace(/^\n+/, "");
|
|
98
|
+
}
|
|
86
99
|
return { display, thinking };
|
|
87
100
|
}
|
|
88
101
|
}
|
package/dist/confirm.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import readline from "readline";
|
|
2
1
|
let autoApprove = false;
|
|
3
2
|
export function setAutoApprove(value) {
|
|
4
3
|
autoApprove = value;
|
|
@@ -6,16 +5,35 @@ export function setAutoApprove(value) {
|
|
|
6
5
|
export function isAutoApprove() {
|
|
7
6
|
return autoApprove;
|
|
8
7
|
}
|
|
8
|
+
/** Optional readline interface to pause/resume during confirmation prompts. */
|
|
9
|
+
let _rl = null;
|
|
10
|
+
export function setConfirmReadline(rl) {
|
|
11
|
+
_rl = rl;
|
|
12
|
+
}
|
|
9
13
|
/** Ask user for confirmation. Returns true if approved. */
|
|
10
14
|
export async function confirm(message) {
|
|
11
15
|
if (autoApprove)
|
|
12
16
|
return true;
|
|
13
|
-
|
|
17
|
+
// Pause readline so it doesn't consume/echo the keystroke
|
|
18
|
+
_rl?.pause();
|
|
19
|
+
process.stdout.write(`\n\n\x1b[1;34m⚠ ${message} [y/N] \x1b[0m\n\n`);
|
|
14
20
|
return new Promise((resolve) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
const wasRaw = process.stdin.isRaw;
|
|
22
|
+
if (process.stdin.isTTY)
|
|
23
|
+
process.stdin.setRawMode(true);
|
|
24
|
+
const onData = (buf) => {
|
|
25
|
+
const ch = buf.toString();
|
|
26
|
+
process.stdin.removeListener("data", onData);
|
|
27
|
+
if (process.stdin.isTTY)
|
|
28
|
+
process.stdin.setRawMode(wasRaw ?? false);
|
|
29
|
+
// Echo the character and newline
|
|
30
|
+
process.stdout.write(ch === "\r" || ch === "\n" ? "\n" : `${ch}\n`);
|
|
31
|
+
// Resume readline after confirmation
|
|
32
|
+
_rl?.resume();
|
|
33
|
+
const answer = ch.trim().toLowerCase();
|
|
34
|
+
resolve(answer === "y");
|
|
35
|
+
};
|
|
36
|
+
process.stdin.on("data", onData);
|
|
19
37
|
});
|
|
20
38
|
}
|
|
21
39
|
/** Check if a shell command is potentially dangerous */
|
package/dist/markdown.js
CHANGED
|
@@ -37,6 +37,8 @@ export class MarkdownRenderer {
|
|
|
37
37
|
buffer = "";
|
|
38
38
|
inCodeBlock = false;
|
|
39
39
|
codeLang = "";
|
|
40
|
+
tableRows = [];
|
|
41
|
+
inTable = false;
|
|
40
42
|
/** Process a text delta and return formatted output */
|
|
41
43
|
write(text) {
|
|
42
44
|
this.buffer += text;
|
|
@@ -49,19 +51,161 @@ export class MarkdownRenderer {
|
|
|
49
51
|
break;
|
|
50
52
|
const line = this.buffer.slice(0, nlIdx);
|
|
51
53
|
this.buffer = this.buffer.slice(nlIdx + 1);
|
|
54
|
+
// Table handling: collect rows, render when table ends
|
|
55
|
+
if (this.isTableRow(line)) {
|
|
56
|
+
if (this.isTableSeparator(line)) {
|
|
57
|
+
this.inTable = true;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
this.inTable = true;
|
|
61
|
+
this.tableRows.push(this.parseTableRow(line));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// Table just ended — flush it
|
|
65
|
+
if (this.inTable) {
|
|
66
|
+
output += this.renderTable(c);
|
|
67
|
+
this.tableRows = [];
|
|
68
|
+
this.inTable = false;
|
|
69
|
+
}
|
|
52
70
|
output += this.formatLine(line, c) + "\n";
|
|
53
71
|
}
|
|
72
|
+
// Stream partial line immediately for real-time feel
|
|
73
|
+
// Hold back only if it could be start of table or code fence
|
|
74
|
+
if (!this.inTable && !this.inCodeBlock && this.buffer.length > 0) {
|
|
75
|
+
if (!this.buffer.startsWith("|") && !this.buffer.startsWith("`")) {
|
|
76
|
+
const partial = this.buffer;
|
|
77
|
+
this.buffer = "";
|
|
78
|
+
output += this.formatInline(partial, c);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// If we're in a table and buffer starts with "|", hold it (waiting for \n)
|
|
82
|
+
// If we're in a table and buffer does NOT start with "|", the table ended mid-stream
|
|
83
|
+
if (this.inTable && this.buffer.length > 0 && !this.buffer.startsWith("|")) {
|
|
84
|
+
output += this.renderTable(c);
|
|
85
|
+
this.tableRows = [];
|
|
86
|
+
this.inTable = false;
|
|
87
|
+
// Now output the non-table buffer content
|
|
88
|
+
if (this.buffer.length > 0 && !this.buffer.startsWith("`")) {
|
|
89
|
+
const partial = this.buffer;
|
|
90
|
+
this.buffer = "";
|
|
91
|
+
output += this.formatInline(partial, c);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
54
94
|
return output;
|
|
55
95
|
}
|
|
56
96
|
/** Flush remaining buffer */
|
|
57
97
|
flush() {
|
|
58
|
-
if (!this.buffer)
|
|
59
|
-
return "";
|
|
60
98
|
const c = useColor() ? C : Z;
|
|
61
|
-
|
|
99
|
+
let out = "";
|
|
100
|
+
// If buffer has a pending table row, add it
|
|
101
|
+
if (this.inTable && this.buffer.length > 0) {
|
|
102
|
+
if (this.isTableRow(this.buffer)) {
|
|
103
|
+
if (!this.isTableSeparator(this.buffer)) {
|
|
104
|
+
this.tableRows.push(this.parseTableRow(this.buffer));
|
|
105
|
+
}
|
|
106
|
+
this.buffer = "";
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Flush pending table
|
|
110
|
+
if (this.inTable && this.tableRows.length > 0) {
|
|
111
|
+
out += this.renderTable(c);
|
|
112
|
+
this.tableRows = [];
|
|
113
|
+
this.inTable = false;
|
|
114
|
+
}
|
|
115
|
+
if (!this.buffer)
|
|
116
|
+
return out;
|
|
117
|
+
out += this.formatLine(this.buffer, c);
|
|
62
118
|
this.buffer = "";
|
|
63
119
|
return out;
|
|
64
120
|
}
|
|
121
|
+
isTableRow(line) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
return trimmed.startsWith("|") && trimmed.endsWith("|") && trimmed.includes("|", 1);
|
|
124
|
+
}
|
|
125
|
+
isTableSeparator(line) {
|
|
126
|
+
return /^\s*\|[\s:]*-+[\s:|-]*\|\s*$/.test(line);
|
|
127
|
+
}
|
|
128
|
+
parseTableRow(line) {
|
|
129
|
+
return line.trim().slice(1, -1).split("|").map((cell) => cell.trim());
|
|
130
|
+
}
|
|
131
|
+
/** Get display width of a string (CJK/emoji chars = 2, others = 1) */
|
|
132
|
+
displayWidth(str) {
|
|
133
|
+
let width = 0;
|
|
134
|
+
for (const ch of str) {
|
|
135
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
136
|
+
if (
|
|
137
|
+
// CJK
|
|
138
|
+
(code >= 0x1100 && code <= 0x115f) ||
|
|
139
|
+
(code >= 0x2e80 && code <= 0x303e) ||
|
|
140
|
+
(code >= 0x3040 && code <= 0x33bf) ||
|
|
141
|
+
(code >= 0x3400 && code <= 0x4dbf) ||
|
|
142
|
+
(code >= 0x4e00 && code <= 0x9fff) ||
|
|
143
|
+
(code >= 0xa000 && code <= 0xa4cf) ||
|
|
144
|
+
(code >= 0xac00 && code <= 0xd7af) ||
|
|
145
|
+
(code >= 0xf900 && code <= 0xfaff) ||
|
|
146
|
+
(code >= 0xfe30 && code <= 0xfe6f) ||
|
|
147
|
+
(code >= 0xff01 && code <= 0xff60) ||
|
|
148
|
+
(code >= 0xffe0 && code <= 0xffe6) ||
|
|
149
|
+
(code >= 0x20000 && code <= 0x2fffd) ||
|
|
150
|
+
(code >= 0x30000 && code <= 0x3fffd) ||
|
|
151
|
+
// Emoji
|
|
152
|
+
(code >= 0x1f300 && code <= 0x1f9ff) || // Misc Symbols, Emoticons, Dingbats, etc.
|
|
153
|
+
(code >= 0x1fa00 && code <= 0x1faff) || // Chess, Extended-A
|
|
154
|
+
(code >= 0x2600 && code <= 0x27bf) || // Misc Symbols, Dingbats
|
|
155
|
+
(code >= 0xfe00 && code <= 0xfe0f) || // Variation Selectors (skip width)
|
|
156
|
+
(code >= 0x200d && code <= 0x200d) || // ZWJ (skip width)
|
|
157
|
+
(code >= 0x1f1e0 && code <= 0x1f1ff) // Regional Indicators (flags)
|
|
158
|
+
) {
|
|
159
|
+
// Variation selectors and ZWJ are zero-width joiners
|
|
160
|
+
if ((code >= 0xfe00 && code <= 0xfe0f) || code === 0x200d) {
|
|
161
|
+
width += 0;
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
width += 2;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
width += 1;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return width;
|
|
172
|
+
}
|
|
173
|
+
/** Pad string to target display width */
|
|
174
|
+
padToWidth(str, targetWidth) {
|
|
175
|
+
const currentWidth = this.displayWidth(str);
|
|
176
|
+
const padding = targetWidth - currentWidth;
|
|
177
|
+
return padding > 0 ? str + " ".repeat(padding) : str;
|
|
178
|
+
}
|
|
179
|
+
renderTable(c) {
|
|
180
|
+
if (this.tableRows.length === 0)
|
|
181
|
+
return "";
|
|
182
|
+
// Calculate column widths based on display width (CJK-aware)
|
|
183
|
+
const colCount = Math.max(...this.tableRows.map((r) => r.length));
|
|
184
|
+
const widths = Array(colCount).fill(0);
|
|
185
|
+
for (const row of this.tableRows) {
|
|
186
|
+
for (let i = 0; i < row.length; i++) {
|
|
187
|
+
widths[i] = Math.max(widths[i], this.displayWidth(row[i] ?? ""));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const lines = [];
|
|
191
|
+
const top = `${c.dim}┌${widths.map((w) => "─".repeat(w + 2)).join("┬")}┐${c.reset}`;
|
|
192
|
+
const mid = `${c.dim}├${widths.map((w) => "─".repeat(w + 2)).join("┼")}┤${c.reset}`;
|
|
193
|
+
const bot = `${c.dim}└${widths.map((w) => "─".repeat(w + 2)).join("┴")}┘${c.reset}`;
|
|
194
|
+
lines.push(top);
|
|
195
|
+
for (let r = 0; r < this.tableRows.length; r++) {
|
|
196
|
+
const row = this.tableRows[r];
|
|
197
|
+
const cells = widths.map((w, i) => {
|
|
198
|
+
const cell = row[i] ?? "";
|
|
199
|
+
const padded = this.padToWidth(cell, w);
|
|
200
|
+
return r === 0 ? `${c.bold}${padded}${c.reset}` : padded;
|
|
201
|
+
});
|
|
202
|
+
lines.push(`${c.dim}│${c.reset} ${cells.join(` ${c.dim}│${c.reset} `)} ${c.dim}│${c.reset}`);
|
|
203
|
+
if (r === 0)
|
|
204
|
+
lines.push(mid);
|
|
205
|
+
}
|
|
206
|
+
lines.push(bot);
|
|
207
|
+
return lines.join("\n") + "\n";
|
|
208
|
+
}
|
|
65
209
|
formatInline(line, c) {
|
|
66
210
|
// Split by inline code spans; format outside segments only
|
|
67
211
|
const parts = line.split(/(`[^`]*`)/g);
|
package/dist/tools/bash.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { tool, jsonSchema } from "ai";
|
|
2
2
|
import { spawn } from "child_process";
|
|
3
3
|
import { confirm, isDangerousCommand, isAutoApprove } from "../confirm.js";
|
|
4
|
+
/** Track active child processes so they can be killed on abort (e.g. ESC). */
|
|
5
|
+
const activeProcesses = new Set();
|
|
6
|
+
/** Kill all active child processes spawned by the bash tool. */
|
|
7
|
+
export function killActiveProcesses() {
|
|
8
|
+
for (const proc of activeProcesses) {
|
|
9
|
+
killProcess(proc.pid);
|
|
10
|
+
}
|
|
11
|
+
activeProcesses.clear();
|
|
12
|
+
}
|
|
4
13
|
export const bashTool = tool({
|
|
5
14
|
description: "Run a shell command. Use this for system operations, running builds, tests, git commands, etc. The command runs in the current working directory. You SHOULD set a timeout based on how long you expect the command to take. If no timeout is set, the command runs until it finishes or the user manually interrupts (Ctrl+C).",
|
|
6
15
|
inputSchema: jsonSchema({
|
|
@@ -31,6 +40,7 @@ export const bashTool = tool({
|
|
|
31
40
|
detached: process.platform !== "win32",
|
|
32
41
|
env: { ...process.env, ...(isWin ? { PYTHONIOENCODING: "utf-8" } : {}) },
|
|
33
42
|
});
|
|
43
|
+
activeProcesses.add(proc);
|
|
34
44
|
proc.stdout?.on("data", (chunk) => chunks.push(chunk));
|
|
35
45
|
proc.stderr?.on("data", (chunk) => chunks.push(chunk));
|
|
36
46
|
// Timeout kill (only if timeout is specified)
|
|
@@ -52,6 +62,7 @@ export const bashTool = tool({
|
|
|
52
62
|
};
|
|
53
63
|
process.on("SIGINT", sigintHandler);
|
|
54
64
|
proc.on("close", (code) => {
|
|
65
|
+
activeProcesses.delete(proc);
|
|
55
66
|
process.removeListener("SIGINT", sigintHandler);
|
|
56
67
|
if (timer)
|
|
57
68
|
clearTimeout(timer);
|
|
@@ -66,6 +77,7 @@ export const bashTool = tool({
|
|
|
66
77
|
}
|
|
67
78
|
});
|
|
68
79
|
proc.on("error", (err) => {
|
|
80
|
+
activeProcesses.delete(proc);
|
|
69
81
|
process.removeListener("SIGINT", sigintHandler);
|
|
70
82
|
if (timer)
|
|
71
83
|
clearTimeout(timer);
|
package/dist/tools/index.js
CHANGED
|
@@ -9,7 +9,23 @@ import { webFetchTool } from "./web_fetch.js";
|
|
|
9
9
|
import { todoTool } from "./todo.js";
|
|
10
10
|
import { questionTool } from "./question.js";
|
|
11
11
|
import { codeSearchTool } from "./code_search.js";
|
|
12
|
-
|
|
12
|
+
/** Chat mode: general assistant tools (fewer tools = less token overhead) */
|
|
13
|
+
export function createChatTools() {
|
|
14
|
+
const tools = {
|
|
15
|
+
bash: bashTool,
|
|
16
|
+
read: readTool,
|
|
17
|
+
write: writeTool,
|
|
18
|
+
edit: editTool,
|
|
19
|
+
glob: globTool,
|
|
20
|
+
grep: grepTool,
|
|
21
|
+
web_search: webSearchTool,
|
|
22
|
+
web_fetch: webFetchTool,
|
|
23
|
+
question: questionTool,
|
|
24
|
+
};
|
|
25
|
+
return tools;
|
|
26
|
+
}
|
|
27
|
+
/** Code mode: full tool set including task tracking and code search */
|
|
28
|
+
export function createCodeTools() {
|
|
13
29
|
const tools = {
|
|
14
30
|
bash: bashTool,
|
|
15
31
|
read: readTool,
|
|
@@ -27,3 +43,7 @@ export function createTools() {
|
|
|
27
43
|
}
|
|
28
44
|
return tools;
|
|
29
45
|
}
|
|
46
|
+
/** @deprecated Use createChatTools or createCodeTools */
|
|
47
|
+
export function createTools() {
|
|
48
|
+
return createChatTools();
|
|
49
|
+
}
|