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/README.md +269 -164
- package/bin/min-agent.js +0 -0
- package/dist/agent.js +383 -16
- package/dist/cli.js +20 -1
- package/dist/clipboard.js +106 -0
- package/dist/code-mode.js +166 -0
- package/dist/compaction.js +243 -48
- package/dist/config.js +34 -7
- package/dist/context-window.js +185 -0
- package/dist/doom-loop.js +36 -0
- package/dist/instructions.js +42 -0
- package/dist/mcp.js +28 -16
- package/dist/output.js +15 -2
- package/dist/serve.js +351 -3
- package/dist/sessions.js +13 -4
- package/dist/structured-output.js +29 -0
- package/dist/title-gen.js +48 -0
- package/dist/tools/bash.js +81 -74
- package/dist/tools/code_search.js +91 -0
- package/dist/tools/explore.js +104 -0
- package/dist/tools/index.js +10 -1
- package/dist/tools/question.js +53 -0
- package/dist/tools/read.js +14 -3
- package/dist/tools/task.js +98 -0
- package/dist/tools/todo.js +88 -0
- package/docs/API.md +298 -111
- package/package.json +1 -1
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
|
-
|
|
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(
|
|
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 {
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|