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 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 { createTools } from "./tools/index.js";
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
- console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,/exit 退出\x1b[0m\n");
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 with LLM-generated title
229
+ // Auto-save session on exit
214
230
  if (messages.length > 0) {
215
- const { saveSessionWithTitle } = await import("./sessions.js");
216
- const model = resolveModel(modelId);
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
- console.log("\x1b[90m输入任务开始编码,/help 查看命令,Ctrl+C 中断\x1b[0m\n");
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 { saveSessionWithTitle } = await import("./sessions.js");
320
- const model = resolveModel(modelId);
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 = createTools();
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 all tools: builtin + MCP + skill + memory + plugins + task
738
- const builtinTools = createTools();
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();
@@ -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
- display += this.buf.slice(0, open.index);
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
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
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
- rl.question(`\x1b[33m⚠ ${message} [y/N] \x1b[0m`, (answer) => {
16
- rl.close();
17
- resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
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
- const out = this.formatLine(this.buffer, c);
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);
@@ -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);
@@ -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
- export function createTools() {
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "min-agent",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "Minimal AI coding agent with tool use, MCP, and skills support",
6
6
  "license": "MIT",