min-agent 0.1.9 → 0.2.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.
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,245 +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 via keypress events
165
- if (process.stdin.isTTY) {
166
- readline.emitKeypressEvents(process.stdin, rl);
167
- process.stdin.on("keypress", (_ch, key) => {
168
- if (key && key.name === "escape" && abortController) {
169
- killActiveProcesses();
170
- abortController.abort();
171
- abortController = null;
172
- console.log("\n\x1b[90m(esc cancelled)\x1b[0m\n");
173
- rl.prompt();
174
- }
175
- });
176
- }
177
- console.log("\x1b[90m输入消息开始对话,输入 /help 查看命令,Esc 取消运行,/exit 退出\x1b[0m\n");
178
- rl.prompt();
179
- // Multi-line paste detection: collect rapid successive lines
180
- let pasteBuffer = [];
181
- let pasteTimer = null;
182
- const PASTE_DEBOUNCE_MS = 50;
183
- const processInput = async (text) => {
184
- const input = text.trim();
185
- if (!input) {
186
- rl.prompt();
187
- return;
188
- }
189
- if (input.startsWith("/")) {
190
- const handled = await handleSlashCommand(input, messages, instructions, modelId, rl, tracker);
191
- if (handled === "exit") {
192
- rl.close();
193
- return;
194
- }
195
- if (handled === "paste") {
196
- console.log();
197
- abortController = new AbortController();
198
- await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
199
- abortController = null;
200
- console.log();
201
- }
202
- rl.prompt();
203
- return;
204
- }
205
- // Show paste feedback for large inputs
206
- const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
207
- const pasteResult = processPastedInput(input);
208
- printPasteFeedback(pasteResult);
209
- console.log();
210
- messages.push({ role: "user", content: pasteResult.fullText });
211
- abortController = new AbortController();
212
- await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
213
- abortController = null;
214
- console.log();
215
- rl.prompt();
216
- };
217
- rl.on("line", (line) => {
218
- pasteBuffer.push(line);
219
- if (pasteTimer)
220
- clearTimeout(pasteTimer);
221
- pasteTimer = setTimeout(() => {
222
- const combined = pasteBuffer.join("\n");
223
- pasteBuffer = [];
224
- pasteTimer = null;
225
- processInput(combined);
226
- }, PASTE_DEBOUNCE_MS);
227
- });
228
- // Wait for close
229
- await new Promise((resolve) => rl.on("close", resolve));
230
- // Auto-save session on exit
231
- if (messages.length > 0) {
232
- const { saveSession } = await import("./sessions.js");
233
- sessionId = saveSession(messages, sessionId);
234
- console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
235
- }
236
- printDivider();
237
- console.log("\x1b[90mBye!\x1b[0m");
238
- await shutdownMcp();
239
- }
240
- /** AI Coding mode: project-aware interactive session */
241
- export async function runCode(modelId, resumeSessionId) {
242
- printHeader(modelId);
243
- console.log("\x1b[90m Mode: code\x1b[0m");
244
- printDivider();
245
- // Scan project
246
- console.log("\x1b[90m⟳ Scanning project...\x1b[0m");
247
- const project = scanProject();
248
- console.log(`\x1b[90m ${project.languages.join(", ") || "Unknown language"}${project.framework ? ` (${project.framework})` : ""} | ${project.isGitRepo ? `git:${project.branch}` : "no git"}\x1b[0m`);
249
- await initMcp();
250
- discoverSkills();
251
- const instructions = await loadInstructions();
252
- const codePrompt = buildCodeSystemPrompt(project, instructions);
253
- console.log("\x1b[90m✓ Ready\x1b[0m");
254
- let messages = [];
255
- let sessionId = resumeSessionId;
256
- const tracker = new TokenTracker();
257
- if (resumeSessionId) {
258
- const { loadSession } = await import("./sessions.js");
259
- const session = loadSession(resumeSessionId);
260
- if (session) {
261
- messages = session.messages;
262
- sessionId = resumeSessionId;
263
- console.log(`\x1b[90m Resumed: ${session.meta.title} (${messages.length} msgs)\x1b[0m`);
264
- }
265
- }
266
- const rl = readline.createInterface({
267
- input: process.stdin,
268
- output: process.stdout,
269
- prompt: "\x1b[32m❯ \x1b[0m",
270
- });
271
- setConfirmReadline(rl);
272
- let abortController = null;
273
- process.on("SIGINT", () => {
274
- if (abortController) {
275
- killActiveProcesses();
276
- abortController.abort();
277
- abortController = null;
278
- console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
279
- rl.prompt();
280
- }
281
- else {
282
- console.log();
283
- rl.close();
284
- }
285
- });
286
- // Listen for ESC key to cancel running agent task via keypress events
287
- if (process.stdin.isTTY) {
288
- readline.emitKeypressEvents(process.stdin, rl);
289
- process.stdin.on("keypress", (_ch, key) => {
290
- if (key && key.name === "escape" && abortController) {
291
- killActiveProcesses();
292
- abortController.abort();
293
- abortController = null;
294
- console.log("\n\x1b[90m(esc cancelled)\x1b[0m\n");
295
- rl.prompt();
296
- }
297
- });
298
- }
299
- console.log("\x1b[90m输入任务开始编码,/help 查看命令,Esc 取消运行,Ctrl+C 中断\x1b[0m\n");
300
- rl.prompt();
301
- let pasteBuffer = [];
302
- let pasteTimer = null;
303
- const PASTE_DEBOUNCE_MS = 50;
304
- const processCodeInput = async (text) => {
305
- const input = text.trim();
306
- if (!input) {
307
- rl.prompt();
308
- return;
309
- }
310
- if (input.startsWith("/")) {
311
- const handled = await handleSlashCommand(input, messages, [codePrompt], modelId, rl, tracker);
312
- if (handled === "exit") {
313
- rl.close();
314
- return;
315
- }
316
- if (handled === "paste") {
317
- console.log();
318
- abortController = new AbortController();
319
- await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
320
- abortController = null;
321
- console.log();
322
- }
323
- rl.prompt();
324
- return;
325
- }
326
- const { processPastedInput, printPasteFeedback } = await import("./paste-handler.js");
327
- const pasteResult = processPastedInput(input);
328
- printPasteFeedback(pasteResult);
329
- console.log();
330
- messages.push({ role: "user", content: pasteResult.fullText });
331
- abortController = new AbortController();
332
- await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
333
- abortController = null;
334
- console.log();
335
- rl.prompt();
336
- };
337
- rl.on("line", (line) => {
338
- pasteBuffer.push(line);
339
- if (pasteTimer)
340
- clearTimeout(pasteTimer);
341
- pasteTimer = setTimeout(() => {
342
- const combined = pasteBuffer.join("\n");
343
- pasteBuffer = [];
344
- pasteTimer = null;
345
- processCodeInput(combined);
346
- }, PASTE_DEBOUNCE_MS);
347
- });
348
- await new Promise((resolve) => rl.on("close", resolve));
349
- if (messages.length > 0) {
350
- const { saveSession } = await import("./sessions.js");
351
- sessionId = saveSession(messages, sessionId);
352
- console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
353
- }
354
- rl.close();
355
- printDivider();
356
- console.log("\x1b[90mBye!\x1b[0m");
357
- await shutdownMcp();
358
- }
359
116
  /** runOnce variant that accepts a pre-built system prompt (for code mode) */
360
117
  export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker) {
361
118
  const model = resolveModel(modelId);
@@ -367,7 +124,8 @@ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSi
367
124
  messages.length = 0;
368
125
  messages.push(...result.messages);
369
126
  if (result.shouldContinue) {
370
- 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 });
371
129
  }
372
130
  if (tracker)
373
131
  tracker.resetContext();
@@ -558,180 +316,6 @@ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSi
558
316
  }
559
317
  }
560
318
  }
561
- async function handleSlashCommand(input, messages, instructions, modelId, rl, tracker) {
562
- const [cmd, ...rest] = input.slice(1).split(/\s+/);
563
- const arg = rest.join(" ");
564
- switch (cmd) {
565
- case "exit":
566
- case "quit":
567
- case "q":
568
- console.log("\x1b[90m⟳ Exiting...\x1b[0m");
569
- return "exit";
570
- case "clear":
571
- messages.length = 0;
572
- console.log("\x1b[90m ✓ Conversation cleared\x1b[0m");
573
- return "handled";
574
- case "compact":
575
- if (messages.length < 4) {
576
- console.log("\x1b[90m Not enough messages to compact\x1b[0m");
577
- }
578
- else {
579
- console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
580
- const model = resolveModel(modelId);
581
- const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
582
- messages.length = 0;
583
- messages.push(...result.messages);
584
- if (tracker)
585
- tracker.resetContext();
586
- console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
587
- }
588
- return "handled";
589
- case "model":
590
- if (arg) {
591
- const config = await import("./config.js");
592
- const cfg = config.loadConfig();
593
- if (cfg.provider) {
594
- cfg.provider.defaultModel = arg;
595
- config.saveConfig(cfg);
596
- console.log(`\x1b[90m ✓ Default model set to: ${arg}\x1b[0m`);
597
- }
598
- }
599
- else {
600
- const config = await import("./config.js");
601
- const cfg = config.loadConfig();
602
- console.log(`\x1b[90m Current model: ${cfg.provider?.defaultModel ?? "not set"}\x1b[0m`);
603
- }
604
- return "handled";
605
- case "models": {
606
- const config = await import("./config.js");
607
- const cfg = config.loadConfig();
608
- if (!cfg.provider?.baseURL || !cfg.provider?.apiKey) {
609
- console.log("\x1b[90m Not configured. Run: min-agent setup\x1b[0m");
610
- return "handled";
611
- }
612
- console.log("\x1b[90m Fetching models...\x1b[0m");
613
- const models = await config.fetchModels(cfg.provider.baseURL, cfg.provider.apiKey);
614
- if (models.length === 0) {
615
- console.log("\x1b[90m No models found or unable to fetch model list\x1b[0m");
616
- }
617
- else {
618
- console.log(`\x1b[90m Available models (${models.length}):\x1b[0m`);
619
- for (const m of models) {
620
- const marker = m === cfg.provider.defaultModel ? " ← default" : "";
621
- console.log(`\x1b[90m - ${m}${marker}\x1b[0m`);
622
- }
623
- }
624
- return "handled";
625
- }
626
- case "memory":
627
- if (arg) {
628
- const { addMemory } = await import("./memory.js");
629
- addMemory(arg);
630
- console.log(`\x1b[90m ✓ Memory saved: "${arg}"\x1b[0m`);
631
- }
632
- else {
633
- const { loadMemories } = await import("./memory.js");
634
- const memories = loadMemories();
635
- if (memories.length === 0) {
636
- console.log("\x1b[90m No memories stored\x1b[0m");
637
- }
638
- else {
639
- for (let i = 0; i < memories.length; i++) {
640
- const m = memories[i];
641
- const tags = m.tags.length > 0 ? ` [${m.tags.join(", ")}]` : "";
642
- console.log(`\x1b[90m #${i + 1}: ${m.content}${tags}\x1b[0m`);
643
- }
644
- }
645
- }
646
- return "handled";
647
- case "skills": {
648
- discoverSkills();
649
- const skills = getSkills();
650
- if (skills.length === 0) {
651
- console.log("\x1b[90m No skills available\x1b[0m");
652
- }
653
- else {
654
- for (const skill of skills) {
655
- console.log(`\x1b[90m ${skill.name} enabled\x1b[0m`);
656
- }
657
- }
658
- return "handled";
659
- }
660
- case "mcp": {
661
- const config = loadMcpConfig();
662
- const servers = Object.entries(config.mcpServers);
663
- if (servers.length === 0) {
664
- console.log("\x1b[90m No MCP servers configured\x1b[0m");
665
- }
666
- else {
667
- const status = getMcpStatus();
668
- console.log(`\x1b[90m MCP servers (${servers.length}):\x1b[0m`);
669
- for (const [name, cfg] of servers) {
670
- const disabled = cfg.enabled === false;
671
- const connected = status[name]?.connected ?? false;
672
- const toolCount = status[name]?.tools.length ?? 0;
673
- const state = disabled ? "disabled" : connected ? "connected" : "disconnected";
674
- const toolsText = toolCount > 0 ? `, ${toolCount} tools` : "";
675
- console.log(`\x1b[90m - ${name}: ${state}${toolsText}\x1b[0m`);
676
- }
677
- }
678
- return "handled";
679
- }
680
- case "tokens": {
681
- if (tracker && tracker.lastInputTokens > 0) {
682
- const { getContextWindow } = await import("./context-window.js");
683
- const ctxWindow = await getContextWindow(modelId);
684
- const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
685
- console.log(`\x1b[90m Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)\x1b[0m`);
686
- console.log(`\x1b[90m Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out\x1b[0m`);
687
- }
688
- else {
689
- console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
690
- }
691
- console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
692
- return "handled";
693
- }
694
- case "path":
695
- case "pwd":
696
- console.log(`\x1b[90m ${process.cwd()}\x1b[0m`);
697
- return "handled";
698
- case "paste": {
699
- const { getClipboardImage } = await import("./clipboard.js");
700
- const img = getClipboardImage();
701
- if (!img) {
702
- console.log("\x1b[90m No image found in clipboard\x1b[0m");
703
- return "handled";
704
- }
705
- console.log(`\x1b[90m 📎 Clipboard image (${(img.data.length / 1024).toFixed(1)} KB)\x1b[0m`);
706
- const text = arg || "What's in this image?";
707
- const content = [
708
- { type: "text", text },
709
- { type: "image", image: img.data, mimeType: img.mimeType },
710
- ];
711
- messages.push({ role: "user", content });
712
- // Return a special signal to trigger runOnce
713
- return "paste";
714
- }
715
- case "help":
716
- console.log(`\x1b[90m Slash commands:
717
- /clear Clear conversation history
718
- /compact Force context compaction
719
- /model [name] Show or change current model
720
- /models List available models from provider
721
- /memory [text] List memories or save a new one
722
- /skills List discovered skills
723
- /mcp List MCP servers and connection status
724
- /tokens Show estimated token usage
725
- /path Show current working directory
726
- /paste [text] Paste clipboard image + optional prompt
727
- /help Show this help
728
- /exit Exit the chat\x1b[0m`);
729
- return "handled";
730
- default:
731
- console.log(`\x1b[90m Unknown command: /${cmd}. Type /help for available commands.\x1b[0m`);
732
- return "handled";
733
- }
734
- }
735
319
  export async function runOnce(messages, instructions, modelId, abortSignal, callbacks, tracker) {
736
320
  const model = resolveModel(modelId);
737
321
  const api = !!callbacks;
@@ -749,10 +333,9 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
749
333
  messages.push(...result.messages);
750
334
  // Auto-continue: inject a message so the agent keeps working
751
335
  if (result.shouldContinue) {
752
- messages.push({
753
- role: "user",
754
- content: "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.",
755
- });
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 });
756
339
  }
757
340
  if (tracker)
758
341
  tracker.resetContext();
package/dist/cli.js CHANGED
@@ -1,4 +1,4 @@
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";
@@ -21,6 +21,7 @@ function printUsage() {
21
21
  min-agent - Minimal AI coding agent
22
22
 
23
23
  Usage:
24
+ min-agent init Initialize .min-agent/ in current directory
24
25
  min-agent chat <message> Send a message to the agent
25
26
  min-agent chat Start interactive multi-turn chat
26
27
  min-agent chat --resume <id> Resume a previous session
@@ -47,6 +48,10 @@ Options:
47
48
  --image, -i <path> Attach an image (can be used multiple times)
48
49
  --yes, -y Auto-approve all confirmations (dangerous commands, file overwrites)
49
50
 
51
+ Permission modes (set in ~/.min-agent/config.json):
52
+ "permission": "ask" Prompt before dangerous operations (default)
53
+ "permission": "allow-all" Auto-approve everything (same as --yes permanently)
54
+
50
55
  Rules (loaded as system instructions):
51
56
  Global: ~/.min-agent/rules.md
52
57
  Project: ./AGENTS.md or ./RULES.md or ./.min-agent/AGENTS.md
@@ -103,7 +108,6 @@ function ensureProjectDefaults() {
103
108
  }
104
109
  }
105
110
  async function main() {
106
- ensureProjectDefaults();
107
111
  if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
108
112
  printUsage();
109
113
  process.exit(0);
@@ -166,7 +170,8 @@ async function main() {
166
170
  }
167
171
  const message = chatArgs.join(" ");
168
172
  if (!message) {
169
- await runChat(modelOverride, resumeId);
173
+ const { runTui } = await import("./tui-chat.js");
174
+ await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "chat" });
170
175
  }
171
176
  else {
172
177
  await runAgent(message, modelOverride, images.length > 0 ? images : undefined);
@@ -188,8 +193,8 @@ async function main() {
188
193
  resumeId = args[++i];
189
194
  }
190
195
  }
191
- const { runCode } = await import("./agent.js");
192
- await runCode(modelOverride, resumeId);
196
+ const { runTui } = await import("./tui-chat.js");
197
+ await runTui({ modelId: modelOverride, resumeSessionId: resumeId, mode: "code" });
193
198
  break;
194
199
  }
195
200
  case "serve": {
@@ -580,6 +585,11 @@ Commands:
580
585
  }
581
586
  break;
582
587
  }
588
+ case "init": {
589
+ ensureProjectDefaults();
590
+ console.log(`✓ Initialized .min-agent/ in ${process.cwd()}`);
591
+ break;
592
+ }
583
593
  case "rules": {
584
594
  const subcommand = args[1];
585
595
  if (subcommand === "edit") {