min-agent 0.1.8 → 0.2.0

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
@@ -3,7 +3,7 @@ import { readFileSync, existsSync } from "fs";
3
3
  import path from "path";
4
4
  import { resolveModel } from "./provider.js";
5
5
  import { createChatTools, createCodeTools } from "./tools/index.js";
6
- import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "./mcp.js";
6
+ import { initMcp, shutdownMcp, getMcpTools } from "./mcp.js";
7
7
  import { discoverSkills, getSkillsTool, getSkillsSystemPrompt, getSkills } from "./skills.js";
8
8
  import { loadInstructions } from "./instructions.js";
9
9
  import { getMemorySystemPrompt, getMemoryTools } from "./memory.js";
@@ -11,12 +11,8 @@ import { needsCompaction, compactMessages, estimateTokens, TokenTracker } from "
11
11
  import { loadPluginTools } from "./plugins.js";
12
12
  import { MarkdownRenderer } from "./markdown.js";
13
13
  import { DoomLoopDetector } from "./doom-loop.js";
14
- import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
15
14
  import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
16
15
  import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
17
- import { killActiveProcesses } from "./tools/bash.js";
18
- import { setConfirmReadline } from "./confirm.js";
19
- import readline from "readline";
20
16
  const MAX_STEPS = 30;
21
17
  function dimStyle() {
22
18
  if ("NO_COLOR" in process.env)
@@ -117,243 +113,6 @@ export async function runAgent(message, modelId, imagePaths) {
117
113
  await runOnce(messages, instructions, modelId, undefined, undefined, tracker);
118
114
  await shutdownMcp();
119
115
  }
120
- /** Interactive multi-turn chat session */
121
- export async function runChat(modelId, resumeSessionId) {
122
- printHeader(modelId);
123
- printDivider();
124
- printInitLoading();
125
- await initMcp();
126
- discoverSkills();
127
- const instructions = await loadInstructions();
128
- printInitReady();
129
- let messages = [];
130
- let sessionId = resumeSessionId;
131
- const tracker = new TokenTracker();
132
- // Resume existing session
133
- if (resumeSessionId) {
134
- const { loadSession } = await import("./sessions.js");
135
- const session = loadSession(resumeSessionId);
136
- if (session) {
137
- messages = session.messages;
138
- sessionId = resumeSessionId;
139
- console.log(`\x1b[90m Resumed session: ${session.meta.title} (${messages.length} messages)\x1b[0m`);
140
- }
141
- }
142
- const rl = readline.createInterface({
143
- input: process.stdin,
144
- output: process.stdout,
145
- prompt: "\x1b[36m> \x1b[0m",
146
- });
147
- setConfirmReadline(rl);
148
- // Handle Ctrl+C: abort current generation, don't exit
149
- let abortController = null;
150
- process.on("SIGINT", () => {
151
- if (abortController) {
152
- killActiveProcesses();
153
- abortController.abort();
154
- abortController = null;
155
- console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
156
- rl.prompt();
157
- }
158
- else {
159
- // No active generation, exit
160
- console.log();
161
- rl.close();
162
- }
163
- });
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");
177
- rl.prompt();
178
- // Multi-line paste detection: collect rapid successive lines
179
- let pasteBuffer = [];
180
- let pasteTimer = null;
181
- const PASTE_DEBOUNCE_MS = 50;
182
- const processInput = async (text) => {
183
- const input = text.trim();
184
- if (!input) {
185
- rl.prompt();
186
- return;
187
- }
188
- if (input.startsWith("/")) {
189
- const handled = await handleSlashCommand(input, messages, instructions, modelId, rl, tracker);
190
- if (handled === "exit") {
191
- rl.close();
192
- return;
193
- }
194
- if (handled === "paste") {
195
- console.log();
196
- abortController = new AbortController();
197
- await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
198
- abortController = null;
199
- console.log();
200
- }
201
- rl.prompt();
202
- return;
203
- }
204
- // Show paste feedback for large inputs
205
- const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
206
- const pasteResult = processPastedInput(input);
207
- printPasteFeedback(pasteResult);
208
- console.log();
209
- messages.push({ role: "user", content: pasteResult.fullText });
210
- abortController = new AbortController();
211
- await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
212
- abortController = null;
213
- console.log();
214
- rl.prompt();
215
- };
216
- rl.on("line", (line) => {
217
- pasteBuffer.push(line);
218
- if (pasteTimer)
219
- clearTimeout(pasteTimer);
220
- pasteTimer = setTimeout(() => {
221
- const combined = pasteBuffer.join("\n");
222
- pasteBuffer = [];
223
- pasteTimer = null;
224
- processInput(combined);
225
- }, PASTE_DEBOUNCE_MS);
226
- });
227
- // Wait for close
228
- await new Promise((resolve) => rl.on("close", resolve));
229
- // Auto-save session on exit
230
- if (messages.length > 0) {
231
- const { saveSession } = await import("./sessions.js");
232
- sessionId = saveSession(messages, sessionId);
233
- console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
234
- }
235
- printDivider();
236
- console.log("\x1b[90mBye!\x1b[0m");
237
- await shutdownMcp();
238
- }
239
- /** AI Coding mode: project-aware interactive session */
240
- export async function runCode(modelId, resumeSessionId) {
241
- printHeader(modelId);
242
- console.log("\x1b[90m Mode: code\x1b[0m");
243
- printDivider();
244
- // Scan project
245
- console.log("\x1b[90m⟳ Scanning project...\x1b[0m");
246
- const project = scanProject();
247
- console.log(`\x1b[90m ${project.languages.join(", ") || "Unknown language"}${project.framework ? ` (${project.framework})` : ""} | ${project.isGitRepo ? `git:${project.branch}` : "no git"}\x1b[0m`);
248
- await initMcp();
249
- discoverSkills();
250
- const instructions = await loadInstructions();
251
- const codePrompt = buildCodeSystemPrompt(project, instructions);
252
- console.log("\x1b[90m✓ Ready\x1b[0m");
253
- let messages = [];
254
- let sessionId = resumeSessionId;
255
- const tracker = new TokenTracker();
256
- if (resumeSessionId) {
257
- const { loadSession } = await import("./sessions.js");
258
- const session = loadSession(resumeSessionId);
259
- if (session) {
260
- messages = session.messages;
261
- sessionId = resumeSessionId;
262
- console.log(`\x1b[90m Resumed: ${session.meta.title} (${messages.length} msgs)\x1b[0m`);
263
- }
264
- }
265
- const rl = readline.createInterface({
266
- input: process.stdin,
267
- output: process.stdout,
268
- prompt: "\x1b[32m❯ \x1b[0m",
269
- });
270
- setConfirmReadline(rl);
271
- let abortController = null;
272
- process.on("SIGINT", () => {
273
- if (abortController) {
274
- killActiveProcesses();
275
- abortController.abort();
276
- abortController = null;
277
- console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
278
- rl.prompt();
279
- }
280
- else {
281
- console.log();
282
- rl.close();
283
- }
284
- });
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");
298
- rl.prompt();
299
- let pasteBuffer = [];
300
- let pasteTimer = null;
301
- const PASTE_DEBOUNCE_MS = 50;
302
- const processCodeInput = async (text) => {
303
- const input = text.trim();
304
- if (!input) {
305
- rl.prompt();
306
- return;
307
- }
308
- if (input.startsWith("/")) {
309
- const handled = await handleSlashCommand(input, messages, [codePrompt], modelId, rl, tracker);
310
- if (handled === "exit") {
311
- rl.close();
312
- return;
313
- }
314
- if (handled === "paste") {
315
- console.log();
316
- abortController = new AbortController();
317
- await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
318
- abortController = null;
319
- console.log();
320
- }
321
- rl.prompt();
322
- return;
323
- }
324
- const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
325
- const pasteResult = processPastedInput(input);
326
- printPasteFeedback(pasteResult);
327
- console.log();
328
- messages.push({ role: "user", content: pasteResult.fullText });
329
- abortController = new AbortController();
330
- await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
331
- abortController = null;
332
- console.log();
333
- rl.prompt();
334
- };
335
- rl.on("line", (line) => {
336
- pasteBuffer.push(line);
337
- if (pasteTimer)
338
- clearTimeout(pasteTimer);
339
- pasteTimer = setTimeout(() => {
340
- const combined = pasteBuffer.join("\n");
341
- pasteBuffer = [];
342
- pasteTimer = null;
343
- processCodeInput(combined);
344
- }, PASTE_DEBOUNCE_MS);
345
- });
346
- await new Promise((resolve) => rl.on("close", resolve));
347
- if (messages.length > 0) {
348
- const { saveSession } = await import("./sessions.js");
349
- sessionId = saveSession(messages, sessionId);
350
- console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
351
- }
352
- rl.close();
353
- printDivider();
354
- console.log("\x1b[90mBye!\x1b[0m");
355
- await shutdownMcp();
356
- }
357
116
  /** runOnce variant that accepts a pre-built system prompt (for code mode) */
358
117
  export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker) {
359
118
  const model = resolveModel(modelId);
@@ -365,7 +124,8 @@ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSi
365
124
  messages.length = 0;
366
125
  messages.push(...result.messages);
367
126
  if (result.shouldContinue) {
368
- messages.push({ role: "user", content: "Continue with your task." });
127
+ const continueText = result.replayText || "Continue with your task.";
128
+ messages.push({ role: "user", content: continueText });
369
129
  }
370
130
  if (tracker)
371
131
  tracker.resetContext();
@@ -556,180 +316,6 @@ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSi
556
316
  }
557
317
  }
558
318
  }
559
- async function handleSlashCommand(input, messages, instructions, modelId, rl, tracker) {
560
- const [cmd, ...rest] = input.slice(1).split(/\s+/);
561
- const arg = rest.join(" ");
562
- switch (cmd) {
563
- case "exit":
564
- case "quit":
565
- case "q":
566
- console.log("\x1b[90m⟳ Exiting...\x1b[0m");
567
- return "exit";
568
- case "clear":
569
- messages.length = 0;
570
- console.log("\x1b[90m ✓ Conversation cleared\x1b[0m");
571
- return "handled";
572
- case "compact":
573
- if (messages.length < 4) {
574
- console.log("\x1b[90m Not enough messages to compact\x1b[0m");
575
- }
576
- else {
577
- console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
578
- const model = resolveModel(modelId);
579
- const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
580
- messages.length = 0;
581
- messages.push(...result.messages);
582
- if (tracker)
583
- tracker.resetContext();
584
- console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
585
- }
586
- return "handled";
587
- case "model":
588
- if (arg) {
589
- const config = await import("./config.js");
590
- const cfg = config.loadConfig();
591
- if (cfg.provider) {
592
- cfg.provider.defaultModel = arg;
593
- config.saveConfig(cfg);
594
- console.log(`\x1b[90m ✓ Default model set to: ${arg}\x1b[0m`);
595
- }
596
- }
597
- else {
598
- const config = await import("./config.js");
599
- const cfg = config.loadConfig();
600
- console.log(`\x1b[90m Current model: ${cfg.provider?.defaultModel ?? "not set"}\x1b[0m`);
601
- }
602
- return "handled";
603
- case "models": {
604
- const config = await import("./config.js");
605
- const cfg = config.loadConfig();
606
- if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
607
- console.log("\x1b[90m Not configured. Run: min-agent setup\x1b[0m");
608
- return "handled";
609
- }
610
- console.log("\x1b[90m Fetching models...\x1b[0m");
611
- const models = await config.fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
612
- if (models.length === 0) {
613
- console.log("\x1b[90m No models found or unable to fetch model list\x1b[0m");
614
- }
615
- else {
616
- console.log(`\x1b[90m Available models (${models.length}):\x1b[0m`);
617
- for (const m of models) {
618
- const marker = m === cfg.provider.defaultModel ? " ← default" : "";
619
- console.log(`\x1b[90m - ${m}${marker}\x1b[0m`);
620
- }
621
- }
622
- return "handled";
623
- }
624
- case "memory":
625
- if (arg) {
626
- const { addMemory } = await import("./memory.js");
627
- addMemory(arg);
628
- console.log(`\x1b[90m ✓ Memory saved: "${arg}"\x1b[0m`);
629
- }
630
- else {
631
- const { loadMemories } = await import("./memory.js");
632
- const memories = loadMemories();
633
- if (memories.length === 0) {
634
- console.log("\x1b[90m No memories stored\x1b[0m");
635
- }
636
- else {
637
- for (let i = 0; i < memories.length; i++) {
638
- const m = memories[i];
639
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
640
- console.log(`\x1b[90m #${i + 1}: ${m.content}${tags}\x1b[0m`);
641
- }
642
- }
643
- }
644
- return "handled";
645
- case "skills": {
646
- discoverSkills();
647
- const skills = getSkills();
648
- if (skills.length === 0) {
649
- console.log("\x1b[90m No skills available\x1b[0m");
650
- }
651
- else {
652
- for (const skill of skills) {
653
- console.log(`\x1b[90m ${skill.name} enabled\x1b[0m`);
654
- }
655
- }
656
- return "handled";
657
- }
658
- case "mcp": {
659
- const config = loadMcpConfig();
660
- const servers = Object.entries(config.mcpServers);
661
- if (servers.length === 0) {
662
- console.log("\x1b[90m No MCP servers configured\x1b[0m");
663
- }
664
- else {
665
- const status = getMcpStatus();
666
- console.log(`\x1b[90m MCP servers (${servers.length}):\x1b[0m`);
667
- for (const [name, cfg] of servers) {
668
- const disabled = cfg.enabled === false;
669
- const connected = status[name]?.connected ?? false;
670
- const toolCount = status[name]?.tools.length ?? 0;
671
- const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
672
- const toolsText = toolCount > 0 ? `, ${toolCount} tools` : "";
673
- console.log(`\x1b[90m - ${name}: ${state}${toolsText}\x1b[0m`);
674
- }
675
- }
676
- return "handled";
677
- }
678
- case "tokens": {
679
- if (tracker && tracker.lastInputTokens > 0) {
680
- const { getContextWindow } = await import("./context-window.js");
681
- const ctxWindow = await getContextWindow(modelId);
682
- const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
683
- console.log(`\x1b[90m Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)\x1b[0m`);
684
- console.log(`\x1b[90m Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out\x1b[0m`);
685
- }
686
- else {
687
- console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
688
- }
689
- console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
690
- return "handled";
691
- }
692
- case "path":
693
- case "pwd":
694
- console.log(`\x1b[90m ${process.cwd()}\x1b[0m`);
695
- return "handled";
696
- case "paste": {
697
- const { getClipboardImage } = await import("./clipboard.js");
698
- const img = getClipboardImage();
699
- if (!img) {
700
- console.log("\x1b[90m No image found in clipboard\x1b[0m");
701
- return "handled";
702
- }
703
- console.log(`\x1b[90m 📎 Clipboard image (${(img.data.length / 1024).toFixed(1)} KB)\x1b[0m`);
704
- const text = arg || "What's in this image?";
705
- const content = [
706
- { type: "text", text },
707
- { type: "image", image: img.data, mimeType: img.mimeType },
708
- ];
709
- messages.push({ role: "user", content });
710
- // Return a special signal to trigger runOnce
711
- return "paste";
712
- }
713
- case "help":
714
- console.log(`\x1b[90m Slash commands:
715
- /clear Clear conversation history
716
- /compact Force context compaction
717
- /model [name] Show or change current model
718
- /models List available models from provider
719
- /memory [text] List memories or save a new one
720
- /skills List discovered skills
721
- /mcp List MCP servers and connection status
722
- /tokens Show estimated token usage
723
- /path Show current working directory
724
- /paste [text] Paste clipboard image + optional prompt
725
- /help Show this help
726
- /exit Exit the chat\x1b[0m`);
727
- return "handled";
728
- default:
729
- console.log(`\x1b[90m Unknown command: /${cmd}. Type /help for available commands.\x1b[0m`);
730
- return "handled";
731
- }
732
- }
733
319
  export async function runOnce(messages, instructions, modelId, abortSignal, callbacks, tracker) {
734
320
  const model = resolveModel(modelId);
735
321
  const api = !!callbacks;
@@ -747,10 +333,9 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
747
333
  messages.push(...result.messages);
748
334
  // Auto-continue: inject a message so the agent keeps working
749
335
  if (result.shouldContinue) {
750
- messages.push({
751
- role: "user",
752
- content: "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.",
753
- });
336
+ const continueText = result.replayText ||
337
+ "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.";
338
+ messages.push({ role: "user", content: continueText });
754
339
  }
755
340
  if (tracker)
756
341
  tracker.resetContext();
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
- import { runAgent, runChat } from "./agent.js";
1
+ import { runAgent } from "./agent.js";
2
2
  import { loadMcpConfig, saveMcpConfig, checkMcpServer, checkAllMcpServers, formatMcpServerBinding, } from "./mcp.js";
3
3
  import { discoverSkills, getSkills } from "./skills.js";
4
4
  import { loadMemories, addMemory, deleteMemory, searchMemories } from "./memory.js";
5
5
  import { runSetup, isConfigured, loadConfig, saveConfig, fetchModels, getConfigDir, getRulesFile } from "./config.js";
6
6
  import { setAutoApprove } from "./confirm.js";
7
- import { existsSync, writeFileSync, mkdirSync } from "fs";
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
8
  import path from "path";
9
9
  const rawArgs = process.argv.slice(2);
10
10
  // Extract leading global flags only, so subcommands can still use "-y"
@@ -47,6 +47,10 @@ Options:
47
47
  --image, -i <path> Attach an image (can be used multiple times)
48
48
  --yes, -y Auto-approve all confirmations (dangerous commands, file overwrites)
49
49
 
50
+ Permission modes (set in ~/.min-agent/config.json):
51
+ "permission": "ask" Prompt before dangerous operations (default)
52
+ "permission": "allow-all" Auto-approve everything (same as --yes permanently)
53
+
50
54
  Rules (loaded as system instructions):
51
55
  Global: ~/.min-agent/rules.md
52
56
  Project: ./AGENTS.md or ./RULES.md or ./.min-agent/AGENTS.md
@@ -108,6 +112,12 @@ async function main() {
108
112
  printUsage();
109
113
  process.exit(0);
110
114
  }
115
+ if (args[0] === "--version" || args[0] === "-v") {
116
+ const pkgPath = path.resolve(path.dirname(new URL(import.meta.url).pathname), "../package.json");
117
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
118
+ console.log(pkg.version);
119
+ process.exit(0);
120
+ }
111
121
  const command = args[0];
112
122
  switch (command) {
113
123
  case "setup": {
@@ -160,7 +170,8 @@ async function main() {
160
170
  }
161
171
  const message = chatArgs.join(" ");
162
172
  if (!message) {
163
- await runChat(modelOverride, resumeId);
173
+ const { runTui } = await import("./tui-chat.js");
174
+ await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "chat" });
164
175
  }
165
176
  else {
166
177
  await runAgent(message, modelOverride, images.length > 0 ? images : undefined);
@@ -182,8 +193,8 @@ async function main() {
182
193
  resumeId = args[++i];
183
194
  }
184
195
  }
185
- const { runCode } = await import("./agent.js");
186
- await runCode(modelOverride, resumeId);
196
+ const { runTui } = await import("./tui-chat.js");
197
+ await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "code" });
187
198
  break;
188
199
  }
189
200
  case "serve": {