min-agent 0.1.4 → 0.1.5

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
@@ -7,9 +7,11 @@ import { initMcp, shutdownMcp, getMcpTools, loadMcpConfig, getMcpStatus } from "
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";
10
- import { needsCompaction, compactMessages, estimateTokens } from "./compaction.js";
10
+ import { needsCompaction, compactMessages, estimateTokens, TokenTracker } from "./compaction.js";
11
11
  import { loadPluginTools } from "./plugins.js";
12
12
  import { MarkdownRenderer } from "./markdown.js";
13
+ import { DoomLoopDetector } from "./doom-loop.js";
14
+ import { scanProject, buildCodeSystemPrompt } from "./code-mode.js";
13
15
  import { ThinkingBodySplitter, stripThinkingFromAssistantText } from "./assistant-stream.js";
14
16
  import { printHeader, printDivider, printToolCall, printToolResult, printDone } from "./output.js";
15
17
  import readline from "readline";
@@ -109,7 +111,8 @@ export async function runAgent(message, modelId, imagePaths) {
109
111
  console.log(`\x1b[36m> ${message}\x1b[0m\n`);
110
112
  const content = await buildUserContent(message, imagePaths);
111
113
  const messages = [{ role: "user", content }];
112
- await runOnce(messages, instructions, modelId);
114
+ const tracker = new TokenTracker();
115
+ await runOnce(messages, instructions, modelId, undefined, undefined, tracker);
113
116
  await shutdownMcp();
114
117
  }
115
118
  /** Interactive multi-turn chat session */
@@ -123,6 +126,7 @@ export async function runChat(modelId, resumeSessionId) {
123
126
  printInitReady();
124
127
  let messages = [];
125
128
  let sessionId = resumeSessionId;
129
+ const tracker = new TokenTracker();
126
130
  // Resume existing session
127
131
  if (resumeSessionId) {
128
132
  const { loadSession } = await import("./sessions.js");
@@ -144,7 +148,7 @@ export async function runChat(modelId, resumeSessionId) {
144
148
  if (abortController) {
145
149
  abortController.abort();
146
150
  abortController = null;
147
- console.log("\n\x1b[90m(interrupted)\x1b[0m\n");
151
+ console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
148
152
  rl.prompt();
149
153
  }
150
154
  else {
@@ -163,24 +167,34 @@ export async function runChat(modelId, resumeSessionId) {
163
167
  }
164
168
  // Handle slash commands
165
169
  if (input.startsWith("/")) {
166
- const handled = await handleSlashCommand(input, messages, instructions, modelId, rl);
170
+ const handled = await handleSlashCommand(input, messages, instructions, modelId, rl, tracker);
167
171
  if (handled === "exit")
168
172
  break;
173
+ if (handled === "paste") {
174
+ console.log();
175
+ abortController = new AbortController();
176
+ await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
177
+ abortController = null;
178
+ console.log();
179
+ rl.prompt();
180
+ continue;
181
+ }
169
182
  rl.prompt();
170
183
  continue;
171
184
  }
172
185
  console.log();
173
186
  messages.push({ role: "user", content: input });
174
187
  abortController = new AbortController();
175
- await runOnce(messages, instructions, modelId, abortController.signal);
188
+ await runOnce(messages, instructions, modelId, abortController.signal, undefined, tracker);
176
189
  abortController = null;
177
190
  console.log();
178
191
  rl.prompt();
179
192
  }
180
- // Auto-save session on exit
193
+ // Auto-save session on exit with LLM-generated title
181
194
  if (messages.length > 0) {
182
- const { saveSession } = await import("./sessions.js");
183
- sessionId = saveSession(messages, sessionId);
195
+ const { saveSessionWithTitle } = await import("./sessions.js");
196
+ const model = resolveModel(modelId);
197
+ sessionId = await saveSessionWithTitle(messages, model, sessionId);
184
198
  console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
185
199
  }
186
200
  rl.close();
@@ -188,7 +202,297 @@ export async function runChat(modelId, resumeSessionId) {
188
202
  console.log("\x1b[90mBye!\x1b[0m");
189
203
  await shutdownMcp();
190
204
  }
191
- async function handleSlashCommand(input, messages, instructions, modelId, rl) {
205
+ /** AI Coding mode: project-aware interactive session */
206
+ export async function runCode(modelId, resumeSessionId) {
207
+ printHeader(modelId);
208
+ console.log("\x1b[90m Mode: code\x1b[0m");
209
+ printDivider();
210
+ // Scan project
211
+ console.log("\x1b[90m⟳ Scanning project...\x1b[0m");
212
+ const project = scanProject();
213
+ console.log(`\x1b[90m ${project.languages.join(", ") || "Unknown language"}${project.framework ? ` (${project.framework})` : ""} | ${project.isGitRepo ? `git:${project.branch}` : "no git"}\x1b[0m`);
214
+ await initMcp();
215
+ discoverSkills();
216
+ const instructions = await loadInstructions();
217
+ const codePrompt = buildCodeSystemPrompt(project, instructions);
218
+ console.log("\x1b[90m✓ Ready\x1b[0m");
219
+ let messages = [];
220
+ let sessionId = resumeSessionId;
221
+ const tracker = new TokenTracker();
222
+ if (resumeSessionId) {
223
+ const { loadSession } = await import("./sessions.js");
224
+ const session = loadSession(resumeSessionId);
225
+ if (session) {
226
+ messages = session.messages;
227
+ sessionId = resumeSessionId;
228
+ console.log(`\x1b[90m Resumed: ${session.meta.title} (${messages.length} msgs)\x1b[0m`);
229
+ }
230
+ }
231
+ const rl = readline.createInterface({
232
+ input: process.stdin,
233
+ output: process.stdout,
234
+ prompt: "\x1b[32m❯ \x1b[0m",
235
+ });
236
+ let abortController = null;
237
+ process.on("SIGINT", () => {
238
+ if (abortController) {
239
+ abortController.abort();
240
+ abortController = null;
241
+ console.log("\n\x1b[90m(cancelled)\x1b[0m\n");
242
+ rl.prompt();
243
+ }
244
+ else {
245
+ console.log();
246
+ rl.close();
247
+ }
248
+ });
249
+ console.log("\x1b[90m输入任务开始编码,/help 查看命令,Ctrl+C 中断\x1b[0m\n");
250
+ rl.prompt();
251
+ for await (const line of rl) {
252
+ const input = line.trim();
253
+ if (!input) {
254
+ rl.prompt();
255
+ continue;
256
+ }
257
+ if (input.startsWith("/")) {
258
+ const handled = await handleSlashCommand(input, messages, [codePrompt], modelId, rl, tracker);
259
+ if (handled === "exit")
260
+ break;
261
+ if (handled === "paste") {
262
+ console.log();
263
+ abortController = new AbortController();
264
+ await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
265
+ abortController = null;
266
+ console.log();
267
+ rl.prompt();
268
+ continue;
269
+ }
270
+ rl.prompt();
271
+ continue;
272
+ }
273
+ console.log();
274
+ messages.push({ role: "user", content: input });
275
+ abortController = new AbortController();
276
+ // Use code system prompt instead of generic one
277
+ await runOnceWithSystem(messages, codePrompt, modelId, abortController.signal, undefined, tracker);
278
+ abortController = null;
279
+ console.log();
280
+ rl.prompt();
281
+ }
282
+ if (messages.length > 0) {
283
+ const { saveSessionWithTitle } = await import("./sessions.js");
284
+ const model = resolveModel(modelId);
285
+ sessionId = await saveSessionWithTitle(messages, model, sessionId);
286
+ console.log(`\x1b[90m Session saved: ${sessionId}\x1b[0m`);
287
+ }
288
+ rl.close();
289
+ printDivider();
290
+ console.log("\x1b[90mBye!\x1b[0m");
291
+ await shutdownMcp();
292
+ }
293
+ /** runOnce variant that accepts a pre-built system prompt (for code mode) */
294
+ export async function runOnceWithSystem(messages, systemPrompt, modelId, abortSignal, callbacks, tracker) {
295
+ const model = resolveModel(modelId);
296
+ const api = !!callbacks;
297
+ if (needsCompaction(messages, tracker)) {
298
+ console.log("\x1b[90m⟳ Compacting context...\x1b[0m");
299
+ const result = await compactMessages(messages, model);
300
+ if (result.compacted) {
301
+ messages.length = 0;
302
+ messages.push(...result.messages);
303
+ if (result.shouldContinue) {
304
+ messages.push({ role: "user", content: "Continue with your task." });
305
+ }
306
+ if (tracker)
307
+ tracker.resetContext();
308
+ console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
309
+ }
310
+ }
311
+ const builtinTools = createTools();
312
+ const mcpTools = getMcpTools();
313
+ const memoryTools = getMemoryTools();
314
+ const pluginTools = await loadPluginTools();
315
+ const { createTaskTool } = await import("./tools/task.js");
316
+ const { createExploreTool } = await import("./tools/explore.js");
317
+ const skills = getSkills();
318
+ const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
319
+ for (const [id, t] of Object.entries(mcpTools))
320
+ allTools[id] = t;
321
+ if (skills.length > 0)
322
+ allTools["skill"] = getSkillsTool();
323
+ allTools["task"] = createTaskTool(modelId);
324
+ allTools["explore"] = createExploreTool(modelId);
325
+ let stepCount = 0;
326
+ let hasError = false;
327
+ const doomLoop = new DoomLoopDetector();
328
+ const result = streamText({
329
+ model,
330
+ system: systemPrompt,
331
+ messages,
332
+ tools: allTools,
333
+ stopWhen: stepCountIs(MAX_STEPS),
334
+ maxRetries: 3,
335
+ abortSignal,
336
+ onStepFinish() { stepCount++; },
337
+ onError() { },
338
+ });
339
+ Promise.resolve(result.usage).catch(() => { });
340
+ let rawText = "";
341
+ let assistantText = "";
342
+ const md = new MarkdownRenderer();
343
+ const thinkingSplit = new ThinkingBodySplitter();
344
+ try {
345
+ for await (const event of result.fullStream) {
346
+ switch (event.type) {
347
+ case "text-delta": {
348
+ const { display, thinking } = thinkingSplit.feed(event.text);
349
+ if (thinking) {
350
+ if (callbacks?.onThinkingDelta)
351
+ callbacks.onThinkingDelta(thinking);
352
+ else
353
+ writeThinkingDelta(thinking);
354
+ }
355
+ if (display) {
356
+ rawText += display;
357
+ assistantText += display;
358
+ if (callbacks?.onAssistantDisplayDelta)
359
+ callbacks.onAssistantDisplayDelta(display);
360
+ else {
361
+ const formatted = md.write(display);
362
+ if (formatted)
363
+ process.stdout.write(formatted);
364
+ }
365
+ }
366
+ break;
367
+ }
368
+ case "tool-call": {
369
+ if (doomLoop.record(event.toolName, event.input)) {
370
+ const warn = `Doom loop detected: "${event.toolName}" — breaking.`;
371
+ if (callbacks?.onStreamError)
372
+ callbacks.onStreamError(warn);
373
+ else
374
+ console.log(`\n\x1b[33m⚠ ${warn}\x1b[0m`);
375
+ hasError = true;
376
+ break;
377
+ }
378
+ const flush = thinkingSplit.flush();
379
+ if (flush.thinking) {
380
+ if (callbacks?.onThinkingDelta)
381
+ callbacks.onThinkingDelta(flush.thinking);
382
+ else
383
+ writeThinkingDelta(flush.thinking);
384
+ }
385
+ if (flush.display) {
386
+ rawText += flush.display;
387
+ assistantText += flush.display;
388
+ if (callbacks?.onAssistantDisplayDelta)
389
+ callbacks.onAssistantDisplayDelta(flush.display);
390
+ else {
391
+ const extra = md.write(flush.display);
392
+ if (extra)
393
+ process.stdout.write(extra);
394
+ }
395
+ }
396
+ if (!api) {
397
+ const flushed = md.flush();
398
+ if (flushed)
399
+ process.stdout.write(flushed);
400
+ if (rawText.trim())
401
+ console.log();
402
+ }
403
+ rawText = "";
404
+ if (callbacks?.onToolCall)
405
+ callbacks.onToolCall(event.toolName, event.input);
406
+ else
407
+ printToolCall(event.toolName, event.input);
408
+ break;
409
+ }
410
+ case "tool-result":
411
+ if (callbacks?.onToolResult)
412
+ callbacks.onToolResult(event.toolName, event.output);
413
+ else
414
+ printToolResult(event.toolName, event.output);
415
+ break;
416
+ case "error":
417
+ hasError = true;
418
+ if (callbacks?.onStreamError)
419
+ callbacks.onStreamError(String(event.error));
420
+ else
421
+ console.error(`\x1b[31mError: ${event.error}\x1b[0m`);
422
+ break;
423
+ case "finish":
424
+ break;
425
+ }
426
+ }
427
+ const end = thinkingSplit.flush();
428
+ if (end.thinking) {
429
+ if (callbacks?.onThinkingDelta)
430
+ callbacks.onThinkingDelta(end.thinking);
431
+ else
432
+ writeThinkingDelta(end.thinking);
433
+ }
434
+ if (end.display) {
435
+ assistantText += end.display;
436
+ if (callbacks?.onAssistantDisplayDelta)
437
+ callbacks.onAssistantDisplayDelta(end.display);
438
+ else {
439
+ const tail = md.write(end.display);
440
+ if (tail)
441
+ process.stdout.write(tail);
442
+ }
443
+ }
444
+ if (!api) {
445
+ const remaining = md.flush();
446
+ if (remaining)
447
+ process.stdout.write(remaining);
448
+ if (rawText.trim())
449
+ console.log();
450
+ }
451
+ const cleaned = stripThinkingFromAssistantText(assistantText);
452
+ if (cleaned.trim())
453
+ messages.push({ role: "assistant", content: cleaned });
454
+ let usage;
455
+ try {
456
+ usage = await result.usage;
457
+ }
458
+ catch {
459
+ usage = undefined;
460
+ }
461
+ if (usage && tracker)
462
+ tracker.update(usage);
463
+ if (callbacks?.onRunFinish) {
464
+ callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
465
+ }
466
+ else {
467
+ if (!hasError && usage) {
468
+ printDivider();
469
+ const { getContextWindow } = await import("./context-window.js");
470
+ const ctxWindow = await getContextWindow(modelId);
471
+ printDone(stepCount, usage, ctxWindow);
472
+ }
473
+ else {
474
+ printDivider();
475
+ }
476
+ }
477
+ }
478
+ catch (err) {
479
+ if (err.name === "AbortError" || abortSignal?.aborted) {
480
+ const cleaned = stripThinkingFromAssistantText(assistantText);
481
+ if (cleaned.trim())
482
+ messages.push({ role: "assistant", content: cleaned });
483
+ if (callbacks?.onRunFinish)
484
+ callbacks.onRunFinish({ stepCount, usage: undefined, hasError: false, aborted: true });
485
+ return;
486
+ }
487
+ if (callbacks?.onStreamError)
488
+ callbacks.onStreamError(err.message);
489
+ else {
490
+ printDivider();
491
+ console.error(`\x1b[31mError: ${err.message}\x1b[0m`);
492
+ }
493
+ }
494
+ }
495
+ async function handleSlashCommand(input, messages, instructions, modelId, rl, tracker) {
192
496
  const [cmd, ...rest] = input.slice(1).split(/\s+/);
193
497
  const arg = rest.join(" ");
194
498
  switch (cmd) {
@@ -207,9 +511,11 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
207
511
  else {
208
512
  console.log("\x1b[90m ⟳ Compacting...\x1b[0m");
209
513
  const model = resolveModel(modelId);
210
- const result = await compactMessages(messages, model, { keepRecentTurns: 2 });
514
+ const result = await compactMessages(messages, model, { keepRecentTurns: 2, autoContinue: false });
211
515
  messages.length = 0;
212
516
  messages.push(...result.messages);
517
+ if (tracker)
518
+ tracker.resetContext();
213
519
  console.log(`\x1b[90m ✓ Compacted (${estimateTokens(messages)} tokens estimated)\x1b[0m`);
214
520
  }
215
521
  return "handled";
@@ -305,10 +611,41 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
305
611
  }
306
612
  return "handled";
307
613
  }
308
- case "tokens":
309
- console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
614
+ case "tokens": {
615
+ if (tracker && tracker.lastInputTokens > 0) {
616
+ const { getContextWindow } = await import("./context-window.js");
617
+ const ctxWindow = await getContextWindow(modelId);
618
+ const pct = Math.round((tracker.lastInputTokens / ctxWindow) * 100);
619
+ console.log(`\x1b[90m Context: ${tracker.lastInputTokens} / ${ctxWindow} tokens (${pct}%)\x1b[0m`);
620
+ console.log(`\x1b[90m Total: ${tracker.totalInputTokens} in / ${tracker.totalOutputTokens} out\x1b[0m`);
621
+ }
622
+ else {
623
+ console.log(`\x1b[90m Estimated tokens in context: ${estimateTokens(messages)}\x1b[0m`);
624
+ }
310
625
  console.log(`\x1b[90m Messages: ${messages.length}\x1b[0m`);
311
626
  return "handled";
627
+ }
628
+ case "path":
629
+ case "pwd":
630
+ console.log(`\x1b[90m ${process.cwd()}\x1b[0m`);
631
+ return "handled";
632
+ case "paste": {
633
+ const { getClipboardImage } = await import("./clipboard.js");
634
+ const img = getClipboardImage();
635
+ if (!img) {
636
+ console.log("\x1b[90m No image found in clipboard\x1b[0m");
637
+ return "handled";
638
+ }
639
+ console.log(`\x1b[90m 📎 Clipboard image (${(img.data.length / 1024).toFixed(1)} KB)\x1b[0m`);
640
+ const text = arg || "What's in this image?";
641
+ const content = [
642
+ { type: "text", text },
643
+ { type: "image", image: img.data, mimeType: img.mimeType },
644
+ ];
645
+ messages.push({ role: "user", content });
646
+ // Return a special signal to trigger runOnce
647
+ return "paste";
648
+ }
312
649
  case "help":
313
650
  console.log(`\x1b[90m Slash commands:
314
651
  /clear Clear conversation history
@@ -319,6 +656,8 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
319
656
  /skills List discovered skills
320
657
  /mcp List MCP servers and connection status
321
658
  /tokens Show estimated token usage
659
+ /path Show current working directory
660
+ /paste [text] Paste clipboard image + optional prompt
322
661
  /help Show this help
323
662
  /exit Exit the chat\x1b[0m`);
324
663
  return "handled";
@@ -327,11 +666,11 @@ async function handleSlashCommand(input, messages, instructions, modelId, rl) {
327
666
  return "handled";
328
667
  }
329
668
  }
330
- export async function runOnce(messages, instructions, modelId, abortSignal, callbacks) {
669
+ export async function runOnce(messages, instructions, modelId, abortSignal, callbacks, tracker) {
331
670
  const model = resolveModel(modelId);
332
671
  const api = !!callbacks;
333
672
  // Auto-compact if context is getting too large
334
- if (needsCompaction(messages)) {
673
+ if (needsCompaction(messages, tracker)) {
335
674
  if (api) {
336
675
  callbacks.onCompaction?.("compacting_start");
337
676
  }
@@ -342,6 +681,15 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
342
681
  if (result.compacted) {
343
682
  messages.length = 0;
344
683
  messages.push(...result.messages);
684
+ // Auto-continue: inject a message so the agent keeps working
685
+ if (result.shouldContinue) {
686
+ messages.push({
687
+ role: "user",
688
+ content: "Continue if you have next steps, or stop and ask for clarification if you are unsure how to proceed.",
689
+ });
690
+ }
691
+ if (tracker)
692
+ tracker.resetContext();
345
693
  if (api) {
346
694
  callbacks.onCompaction?.(`compacted_ok estimated_tokens=${estimateTokens(messages)}`);
347
695
  }
@@ -350,11 +698,12 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
350
698
  }
351
699
  }
352
700
  }
353
- // Merge all tools: builtin + MCP + skill + memory + plugins
701
+ // Merge all tools: builtin + MCP + skill + memory + plugins + task
354
702
  const builtinTools = createTools();
355
703
  const mcpTools = getMcpTools();
356
704
  const memoryTools = getMemoryTools();
357
705
  const pluginTools = await loadPluginTools();
706
+ const { createTaskTool } = await import("./tools/task.js");
358
707
  const skills = getSkills();
359
708
  const allTools = { ...builtinTools, ...memoryTools, ...pluginTools };
360
709
  for (const [id, t] of Object.entries(mcpTools)) {
@@ -363,8 +712,10 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
363
712
  if (skills.length > 0) {
364
713
  allTools["skill"] = getSkillsTool();
365
714
  }
715
+ allTools["task"] = createTaskTool(modelId);
366
716
  let stepCount = 0;
367
717
  let hasError = false;
718
+ const doomLoop = new DoomLoopDetector();
368
719
  const result = streamText({
369
720
  model,
370
721
  system: buildSystemPrompt(instructions),
@@ -411,6 +762,16 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
411
762
  break;
412
763
  }
413
764
  case "tool-call": {
765
+ // Doom loop detection
766
+ if (doomLoop.record(event.toolName, event.input)) {
767
+ const warning = `\x1b[33m⚠ Doom loop detected: "${event.toolName}" called ${3} times with same args. Breaking loop.\x1b[0m`;
768
+ if (callbacks?.onStreamError)
769
+ callbacks.onStreamError(warning);
770
+ else
771
+ console.log(`\n${warning}`);
772
+ hasError = true;
773
+ break;
774
+ }
414
775
  const splitFlush = thinkingSplit.flush();
415
776
  emitThinking(splitFlush.thinking);
416
777
  if (splitFlush.display) {
@@ -492,6 +853,10 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
492
853
  catch {
493
854
  usage = undefined;
494
855
  }
856
+ // Update token tracker with real usage from API
857
+ if (usage && tracker) {
858
+ tracker.update(usage);
859
+ }
495
860
  if (callbacks?.onRunFinish) {
496
861
  callbacks.onRunFinish({ stepCount, usage, hasError, aborted: false });
497
862
  }
@@ -501,7 +866,9 @@ export async function runOnce(messages, instructions, modelId, abortSignal, call
501
866
  return;
502
867
  }
503
868
  printDivider();
504
- printDone(stepCount, usage);
869
+ const { getContextWindow } = await import("./context-window.js");
870
+ const ctxWindow = await getContextWindow(modelId);
871
+ printDone(stepCount, usage, ctxWindow);
505
872
  }
506
873
  }
507
874
  catch (err) {
package/dist/cli.js CHANGED
@@ -24,6 +24,7 @@ Usage:
24
24
  min-agent chat <message> Send a message to the agent
25
25
  min-agent chat Start interactive multi-turn chat
26
26
  min-agent chat --resume <id> Resume a previous session
27
+ min-agent code AI coding mode (project-aware)
27
28
  min-agent setup Configure API provider (interactive)
28
29
  min-agent models List available models
29
30
  min-agent history List saved sessions
@@ -159,7 +160,6 @@ async function main() {
159
160
  }
160
161
  const message = chatArgs.join(" ");
161
162
  if (!message) {
162
- // No message provided — enter interactive multi-turn mode
163
163
  await runChat(modelOverride, resumeId);
164
164
  }
165
165
  else {
@@ -167,6 +167,25 @@ async function main() {
167
167
  }
168
168
  break;
169
169
  }
170
+ case "code": {
171
+ if (!isConfigured()) {
172
+ console.error("Not configured. Run: min-agent setup");
173
+ process.exit(1);
174
+ }
175
+ let modelOverride;
176
+ let resumeId;
177
+ for (let i = 1; i < args.length; i++) {
178
+ if ((args[i] === "--model" || args[i] === "-m") && args[i + 1]) {
179
+ modelOverride = args[++i];
180
+ }
181
+ else if (args[i] === "--resume" && args[i + 1]) {
182
+ resumeId = args[++i];
183
+ }
184
+ }
185
+ const { runCode } = await import("./agent.js");
186
+ await runCode(modelOverride, resumeId);
187
+ break;
188
+ }
170
189
  case "serve": {
171
190
  if (!isConfigured()) {
172
191
  console.error("Not configured. Run: min-agent setup");
@@ -0,0 +1,106 @@
1
+ import { execSync } from "child_process";
2
+ import path from "path";
3
+ import os from "os";
4
+ export function getClipboardImage() {
5
+ switch (process.platform) {
6
+ case "darwin":
7
+ return getClipboardImageMac();
8
+ case "linux":
9
+ return getClipboardImageLinux();
10
+ case "win32":
11
+ return getClipboardImageWindows();
12
+ default:
13
+ return null;
14
+ }
15
+ }
16
+ function getClipboardImageMac() {
17
+ const tmpFile = path.join(os.tmpdir(), `min-agent-paste-${Date.now()}.png`);
18
+ try {
19
+ // Try pngpaste first (brew install pngpaste)
20
+ execSync(`pngpaste "${tmpFile}" 2>/dev/null`, { stdio: "pipe" });
21
+ const { readFileSync } = require("fs");
22
+ const data = readFileSync(tmpFile);
23
+ try {
24
+ require("fs").unlinkSync(tmpFile);
25
+ }
26
+ catch { }
27
+ if (data.length > 0)
28
+ return { data, mimeType: "image/png" };
29
+ }
30
+ catch { }
31
+ try {
32
+ // Fallback: osascript to save clipboard image
33
+ const script = `
34
+ set tmpFile to POSIX file "${tmpFile}"
35
+ try
36
+ set imgData to the clipboard as «class PNGf»
37
+ set fp to open for access tmpFile with write permission
38
+ write imgData to fp
39
+ close access fp
40
+ return "ok"
41
+ on error
42
+ return "no_image"
43
+ end try
44
+ `;
45
+ const result = execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { encoding: "utf-8" }).trim();
46
+ if (result === "ok") {
47
+ const { readFileSync, unlinkSync } = require("fs");
48
+ const data = readFileSync(tmpFile);
49
+ try {
50
+ unlinkSync(tmpFile);
51
+ }
52
+ catch { }
53
+ if (data.length > 0)
54
+ return { data, mimeType: "image/png" };
55
+ }
56
+ }
57
+ catch { }
58
+ return null;
59
+ }
60
+ function getClipboardImageLinux() {
61
+ try {
62
+ // xclip
63
+ const data = execSync("xclip -selection clipboard -t image/png -o 2>/dev/null", {
64
+ maxBuffer: 10 * 1024 * 1024,
65
+ });
66
+ if (data.length > 0)
67
+ return { data, mimeType: "image/png" };
68
+ }
69
+ catch { }
70
+ try {
71
+ // xsel fallback
72
+ const data = execSync("xsel --clipboard --output 2>/dev/null", {
73
+ maxBuffer: 10 * 1024 * 1024,
74
+ });
75
+ // Check if it's actually image data (PNG magic bytes)
76
+ if (data.length > 8 && data[0] === 0x89 && data[1] === 0x50) {
77
+ return { data, mimeType: "image/png" };
78
+ }
79
+ }
80
+ catch { }
81
+ return null;
82
+ }
83
+ function getClipboardImageWindows() {
84
+ const tmpFile = path.join(os.tmpdir(), `min-agent-paste-${Date.now()}.png`);
85
+ try {
86
+ const ps = `
87
+ Add-Type -AssemblyName System.Windows.Forms
88
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
89
+ if ($img) { $img.Save('${tmpFile.replace(/\\/g, "\\\\")}', [System.Drawing.Imaging.ImageFormat]::Png); Write-Output 'ok' }
90
+ else { Write-Output 'no_image' }
91
+ `;
92
+ const result = execSync(`powershell -NoProfile -Command "${ps}"`, { encoding: "utf-8" }).trim();
93
+ if (result === "ok") {
94
+ const { readFileSync, unlinkSync } = require("fs");
95
+ const data = readFileSync(tmpFile);
96
+ try {
97
+ unlinkSync(tmpFile);
98
+ }
99
+ catch { }
100
+ if (data.length > 0)
101
+ return { data, mimeType: "image/png" };
102
+ }
103
+ }
104
+ catch { }
105
+ return null;
106
+ }