mini-coder 0.0.3

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.
Files changed (3) hide show
  1. package/README.md +140 -0
  2. package/dist/mc.js +3727 -0
  3. package/package.json +34 -0
package/dist/mc.js ADDED
@@ -0,0 +1,3727 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/index.ts
5
+ import * as c7 from "yoctocolors";
6
+
7
+ // src/agent/agent.ts
8
+ import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
9
+ import { homedir as homedir5 } from "os";
10
+ import { join as join11 } from "path";
11
+ import * as c6 from "yoctocolors";
12
+
13
+ // src/cli/commands.ts
14
+ import * as c3 from "yoctocolors";
15
+
16
+ // src/llm-api/providers.ts
17
+ import { createAnthropic } from "@ai-sdk/anthropic";
18
+ import { createGoogleGenerativeAI } from "@ai-sdk/google";
19
+ import { createOpenAI } from "@ai-sdk/openai";
20
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
21
+ import { createOllama } from "ollama-ai-provider";
22
+ var ZEN_BASE = "https://opencode.ai/zen/v1";
23
+ var ZEN_ANTHROPIC_MODELS = new Set([
24
+ "claude-opus-4-6",
25
+ "claude-opus-4-5",
26
+ "claude-opus-4-1",
27
+ "claude-sonnet-4-6",
28
+ "claude-sonnet-4-5",
29
+ "claude-sonnet-4",
30
+ "claude-haiku-4-5",
31
+ "claude-3-5-haiku"
32
+ ]);
33
+ var ZEN_OPENAI_MODELS = new Set([
34
+ "gpt-5.2",
35
+ "gpt-5.2-codex",
36
+ "gpt-5.1",
37
+ "gpt-5.1-codex",
38
+ "gpt-5.1-codex-max",
39
+ "gpt-5.1-codex-mini",
40
+ "gpt-5",
41
+ "gpt-5-codex",
42
+ "gpt-5-nano"
43
+ ]);
44
+ var ZEN_GOOGLE_MODELS = new Set([
45
+ "gemini-3.1-pro",
46
+ "gemini-3-pro",
47
+ "gemini-3-flash"
48
+ ]);
49
+ var _zenAnthropic = null;
50
+ var _zenOpenAI = null;
51
+ var _zenCompat = null;
52
+ function getZenApiKey() {
53
+ const key = process.env.OPENCODE_API_KEY;
54
+ if (!key)
55
+ throw new Error("OPENCODE_API_KEY is not set");
56
+ return key;
57
+ }
58
+ function zenAnthropic() {
59
+ if (!_zenAnthropic) {
60
+ _zenAnthropic = createAnthropic({
61
+ apiKey: getZenApiKey(),
62
+ baseURL: ZEN_BASE
63
+ });
64
+ }
65
+ return _zenAnthropic;
66
+ }
67
+ function zenOpenAI() {
68
+ if (!_zenOpenAI) {
69
+ _zenOpenAI = createOpenAI({
70
+ apiKey: getZenApiKey(),
71
+ baseURL: ZEN_BASE
72
+ });
73
+ }
74
+ return _zenOpenAI;
75
+ }
76
+ function zenGoogle(modelId) {
77
+ return createGoogleGenerativeAI({
78
+ apiKey: getZenApiKey(),
79
+ baseURL: ZEN_BASE
80
+ });
81
+ }
82
+ function zenCompat() {
83
+ if (!_zenCompat) {
84
+ _zenCompat = createOpenAICompatible({
85
+ name: "zen-compat",
86
+ apiKey: getZenApiKey(),
87
+ baseURL: ZEN_BASE
88
+ });
89
+ }
90
+ return _zenCompat;
91
+ }
92
+ var _directAnthropic = null;
93
+ var _directOpenAI = null;
94
+ var _directGoogle = null;
95
+ function directAnthropic() {
96
+ if (!_directAnthropic) {
97
+ const key = process.env.ANTHROPIC_API_KEY;
98
+ if (!key)
99
+ throw new Error("ANTHROPIC_API_KEY is not set");
100
+ _directAnthropic = createAnthropic({ apiKey: key });
101
+ }
102
+ return _directAnthropic;
103
+ }
104
+ function directOpenAI() {
105
+ if (!_directOpenAI) {
106
+ const key = process.env.OPENAI_API_KEY;
107
+ if (!key)
108
+ throw new Error("OPENAI_API_KEY is not set");
109
+ _directOpenAI = createOpenAI({ apiKey: key });
110
+ }
111
+ return _directOpenAI;
112
+ }
113
+ function directGoogle() {
114
+ if (!_directGoogle) {
115
+ const key = process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY;
116
+ if (!key)
117
+ throw new Error("GOOGLE_API_KEY or GEMINI_API_KEY is not set");
118
+ _directGoogle = createGoogleGenerativeAI({ apiKey: key });
119
+ }
120
+ return _directGoogle;
121
+ }
122
+ var CONTEXT_WINDOW_TABLE = [
123
+ [/^claude-/, 200000],
124
+ [/^gemini-/, 1e6],
125
+ [/^gpt-5/, 128000],
126
+ [/^gpt-4/, 128000],
127
+ [/^kimi-k2/, 262000],
128
+ [/^minimax-m2/, 196000],
129
+ [/^glm-/, 128000],
130
+ [/^qwen3-/, 131000]
131
+ ];
132
+ function getContextWindow(modelString) {
133
+ const modelId = modelString.includes("/") ? modelString.slice(modelString.indexOf("/") + 1) : modelString;
134
+ for (const [pattern, tokens] of CONTEXT_WINDOW_TABLE) {
135
+ if (pattern.test(modelId))
136
+ return tokens;
137
+ }
138
+ return null;
139
+ }
140
+ function resolveModel(modelString) {
141
+ const slashIdx = modelString.indexOf("/");
142
+ if (slashIdx === -1) {
143
+ throw new Error(`Invalid model string "${modelString}". Expected format: "<provider>/<model-id>"`);
144
+ }
145
+ const provider = modelString.slice(0, slashIdx);
146
+ const modelId = modelString.slice(slashIdx + 1);
147
+ switch (provider) {
148
+ case "zen": {
149
+ if (ZEN_ANTHROPIC_MODELS.has(modelId)) {
150
+ return zenAnthropic()(modelId);
151
+ }
152
+ if (ZEN_OPENAI_MODELS.has(modelId)) {
153
+ return zenOpenAI()(modelId);
154
+ }
155
+ if (ZEN_GOOGLE_MODELS.has(modelId)) {
156
+ return zenGoogle(modelId)(modelId);
157
+ }
158
+ return zenCompat()(modelId);
159
+ }
160
+ case "anthropic":
161
+ return directAnthropic()(modelId);
162
+ case "openai":
163
+ return directOpenAI()(modelId);
164
+ case "google":
165
+ return directGoogle()(modelId);
166
+ case "ollama": {
167
+ const baseURL = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
168
+ const ollamaProvider = createOllama({ baseURL });
169
+ return ollamaProvider(modelId);
170
+ }
171
+ default:
172
+ throw new Error(`Unknown provider "${provider}". Supported: zen, anthropic, openai, google, ollama`);
173
+ }
174
+ }
175
+ function autoDiscoverModel() {
176
+ if (process.env.OPENCODE_API_KEY)
177
+ return "zen/claude-sonnet-4-6";
178
+ if (process.env.ANTHROPIC_API_KEY)
179
+ return "anthropic/claude-sonnet-4-5-20250929";
180
+ if (process.env.OPENAI_API_KEY)
181
+ return "openai/gpt-4o";
182
+ if (process.env.GOOGLE_API_KEY ?? process.env.GEMINI_API_KEY)
183
+ return "google/gemini-2.0-flash";
184
+ return "ollama/llama3.2";
185
+ }
186
+ async function fetchZenModels() {
187
+ const key = process.env.OPENCODE_API_KEY;
188
+ if (!key)
189
+ return [];
190
+ try {
191
+ const res = await fetch(`${ZEN_BASE}/models`, {
192
+ headers: { Authorization: `Bearer ${key}` },
193
+ signal: AbortSignal.timeout(8000)
194
+ });
195
+ if (!res.ok)
196
+ return [];
197
+ const json = await res.json();
198
+ const models = json.data ?? [];
199
+ return models.map((m) => ({
200
+ id: `zen/${m.id}`,
201
+ displayName: m.id,
202
+ provider: "zen",
203
+ context: m.context_window,
204
+ free: m.id.endsWith("-free") || m.id === "gpt-5-nano" || m.id === "big-pickle"
205
+ }));
206
+ } catch {
207
+ return [];
208
+ }
209
+ }
210
+ async function fetchOllamaModels() {
211
+ const base = process.env.OLLAMA_BASE_URL ?? "http://localhost:11434";
212
+ try {
213
+ const res = await fetch(`${base}/api/tags`, {
214
+ signal: AbortSignal.timeout(3000)
215
+ });
216
+ if (!res.ok)
217
+ return [];
218
+ const json = await res.json();
219
+ return (json.models ?? []).map((m) => ({
220
+ id: `ollama/${m.name}`,
221
+ displayName: m.name + (m.details?.parameter_size ? ` (${m.details.parameter_size})` : ""),
222
+ provider: "ollama"
223
+ }));
224
+ } catch {
225
+ return [];
226
+ }
227
+ }
228
+ async function fetchAvailableModels() {
229
+ const [zen, ollama] = await Promise.all([
230
+ fetchZenModels(),
231
+ fetchOllamaModels()
232
+ ]);
233
+ return [...zen, ...ollama];
234
+ }
235
+
236
+ // src/session/db.ts
237
+ import { Database } from "bun:sqlite";
238
+ import { existsSync, mkdirSync, unlinkSync } from "fs";
239
+ import { homedir } from "os";
240
+ import { join } from "path";
241
+ function getConfigDir() {
242
+ return join(homedir(), ".config", "mini-coder");
243
+ }
244
+ function getDbPath() {
245
+ const dir = getConfigDir();
246
+ if (!existsSync(dir))
247
+ mkdirSync(dir, { recursive: true });
248
+ return join(dir, "sessions.db");
249
+ }
250
+ var DB_VERSION = 3;
251
+ var SCHEMA = `
252
+ CREATE TABLE IF NOT EXISTS sessions (
253
+ id TEXT PRIMARY KEY,
254
+ title TEXT NOT NULL DEFAULT '',
255
+ cwd TEXT NOT NULL,
256
+ model TEXT NOT NULL,
257
+ created_at INTEGER NOT NULL,
258
+ updated_at INTEGER NOT NULL
259
+ );
260
+
261
+ CREATE TABLE IF NOT EXISTS messages (
262
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
263
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
264
+ payload TEXT NOT NULL,
265
+ turn_index INTEGER NOT NULL DEFAULT 0,
266
+ created_at INTEGER NOT NULL
267
+ );
268
+
269
+ CREATE TABLE IF NOT EXISTS prompt_history (
270
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
271
+ text TEXT NOT NULL,
272
+ created_at INTEGER NOT NULL
273
+ );
274
+
275
+ CREATE TABLE IF NOT EXISTS mcp_servers (
276
+ name TEXT PRIMARY KEY,
277
+ transport TEXT NOT NULL,
278
+ url TEXT,
279
+ command TEXT,
280
+ args TEXT,
281
+ env TEXT,
282
+ created_at INTEGER NOT NULL
283
+ );
284
+
285
+ CREATE INDEX IF NOT EXISTS idx_messages_session
286
+ ON messages(session_id, id);
287
+
288
+ CREATE INDEX IF NOT EXISTS idx_messages_turn
289
+ ON messages(session_id, turn_index);
290
+
291
+ CREATE INDEX IF NOT EXISTS idx_sessions_updated
292
+ ON sessions(updated_at DESC);
293
+
294
+ CREATE TABLE IF NOT EXISTS settings (
295
+ key TEXT PRIMARY KEY,
296
+ value TEXT NOT NULL
297
+ );
298
+
299
+ CREATE TABLE IF NOT EXISTS snapshots (
300
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
301
+ session_id TEXT NOT NULL,
302
+ turn_index INTEGER NOT NULL,
303
+ path TEXT NOT NULL,
304
+ content BLOB,
305
+ existed INTEGER NOT NULL
306
+ );
307
+
308
+ CREATE INDEX IF NOT EXISTS idx_snapshots_turn
309
+ ON snapshots(session_id, turn_index);
310
+ `;
311
+ var _db = null;
312
+ function getDb() {
313
+ if (!_db) {
314
+ const dbPath = getDbPath();
315
+ let db = new Database(dbPath, { create: true });
316
+ db.exec("PRAGMA journal_mode=WAL;");
317
+ db.exec("PRAGMA foreign_keys=ON;");
318
+ const version = db.query("PRAGMA user_version").get()?.user_version ?? 0;
319
+ if (version !== DB_VERSION) {
320
+ try {
321
+ db.close();
322
+ } catch {}
323
+ for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
324
+ if (existsSync(path))
325
+ unlinkSync(path);
326
+ }
327
+ db = new Database(dbPath, { create: true });
328
+ db.exec("PRAGMA journal_mode=WAL;");
329
+ db.exec("PRAGMA foreign_keys=ON;");
330
+ db.exec(SCHEMA);
331
+ db.exec(`PRAGMA user_version = ${DB_VERSION};`);
332
+ } else {
333
+ db.exec(SCHEMA);
334
+ }
335
+ _db = db;
336
+ }
337
+ return _db;
338
+ }
339
+ function createSession(opts) {
340
+ const db = getDb();
341
+ const now = Date.now();
342
+ db.run(`INSERT INTO sessions (id, title, cwd, model, created_at, updated_at)
343
+ VALUES (?, ?, ?, ?, ?, ?)`, [opts.id, opts.title ?? "", opts.cwd, opts.model, now, now]);
344
+ const session = getSession(opts.id);
345
+ if (!session) {
346
+ throw new Error(`Failed to create session ${opts.id}`);
347
+ }
348
+ return session;
349
+ }
350
+ function getSession(id) {
351
+ return getDb().query("SELECT * FROM sessions WHERE id = ?").get(id) ?? null;
352
+ }
353
+ function touchSession(id, model) {
354
+ getDb().run("UPDATE sessions SET updated_at = ?, model = ? WHERE id = ?", [
355
+ Date.now(),
356
+ model,
357
+ id
358
+ ]);
359
+ }
360
+ function listSessions(limit = 20) {
361
+ return getDb().query("SELECT * FROM sessions ORDER BY updated_at DESC LIMIT ?").all(limit);
362
+ }
363
+ function saveMessages(sessionId, msgs, turnIndex = 0) {
364
+ const db = getDb();
365
+ const stmt = db.prepare(`INSERT INTO messages (session_id, payload, turn_index, created_at)
366
+ VALUES (?, ?, ?, ?)`);
367
+ const now = Date.now();
368
+ for (const msg of msgs) {
369
+ stmt.run(sessionId, JSON.stringify(msg), turnIndex, now);
370
+ }
371
+ }
372
+ function getMaxTurnIndex(sessionId) {
373
+ const row = getDb().query("SELECT MAX(turn_index) AS max_turn FROM messages WHERE session_id = ?").get(sessionId);
374
+ return row?.max_turn ?? -1;
375
+ }
376
+ function deleteLastTurn(sessionId, turnIndex) {
377
+ const target = turnIndex !== undefined ? turnIndex : getMaxTurnIndex(sessionId);
378
+ if (target < 0)
379
+ return false;
380
+ getDb().run("DELETE FROM messages WHERE session_id = ? AND turn_index = ?", [
381
+ sessionId,
382
+ target
383
+ ]);
384
+ return true;
385
+ }
386
+ function loadMessages(sessionId) {
387
+ const rows = getDb().query("SELECT payload FROM messages WHERE session_id = ? ORDER BY id ASC").all(sessionId);
388
+ return rows.map((row) => JSON.parse(row.payload));
389
+ }
390
+ function addPromptHistory(text) {
391
+ if (!text.trim())
392
+ return;
393
+ getDb().run("INSERT INTO prompt_history (text, created_at) VALUES (?, ?)", [
394
+ text.trim(),
395
+ Date.now()
396
+ ]);
397
+ }
398
+ function getPromptHistory(limit = 200) {
399
+ const rows = getDb().query("SELECT text FROM prompt_history ORDER BY id DESC LIMIT ?").all(limit);
400
+ return rows.map((r) => r.text).reverse();
401
+ }
402
+ function listMcpServers() {
403
+ return getDb().query("SELECT name, transport, url, command, args, env FROM mcp_servers ORDER BY name").all();
404
+ }
405
+ function upsertMcpServer(server) {
406
+ getDb().run(`INSERT INTO mcp_servers (name, transport, url, command, args, env, created_at)
407
+ VALUES (?, ?, ?, ?, ?, ?, ?)
408
+ ON CONFLICT(name) DO UPDATE SET
409
+ transport = excluded.transport,
410
+ url = excluded.url,
411
+ command = excluded.command,
412
+ args = excluded.args,
413
+ env = excluded.env`, [
414
+ server.name,
415
+ server.transport,
416
+ server.url ?? null,
417
+ server.command ?? null,
418
+ server.args ?? null,
419
+ server.env ?? null,
420
+ Date.now()
421
+ ]);
422
+ }
423
+ function deleteMcpServer(name) {
424
+ getDb().run("DELETE FROM mcp_servers WHERE name = ?", [name]);
425
+ }
426
+ function getSetting(key) {
427
+ const row = getDb().query("SELECT value FROM settings WHERE key = ?").get(key);
428
+ return row?.value ?? null;
429
+ }
430
+ function setSetting(key, value) {
431
+ getDb().run(`INSERT INTO settings (key, value) VALUES (?, ?)
432
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value`, [key, value]);
433
+ }
434
+ function getPreferredModel() {
435
+ return getSetting("preferred_model");
436
+ }
437
+ function setPreferredModel(model) {
438
+ setSetting("preferred_model", model);
439
+ }
440
+ function saveSnapshot(sessionId, turnIndex, files) {
441
+ const db = getDb();
442
+ const stmt = db.prepare(`INSERT INTO snapshots (session_id, turn_index, path, content, existed)
443
+ VALUES (?, ?, ?, ?, ?)`);
444
+ const insert = db.transaction(() => {
445
+ for (const f of files) {
446
+ stmt.run(sessionId, turnIndex, f.path, f.content ?? null, f.existed ? 1 : 0);
447
+ }
448
+ });
449
+ insert();
450
+ }
451
+ function loadSnapshot(sessionId, turnIndex) {
452
+ const rows = getDb().query("SELECT path, content, existed FROM snapshots WHERE session_id = ? AND turn_index = ?").all(sessionId, turnIndex);
453
+ return rows.map((r) => ({
454
+ path: r.path,
455
+ content: r.content ?? null,
456
+ existed: r.existed === 1
457
+ }));
458
+ }
459
+ function deleteSnapshot(sessionId, turnIndex) {
460
+ getDb().run("DELETE FROM snapshots WHERE session_id = ? AND turn_index = ?", [
461
+ sessionId,
462
+ turnIndex
463
+ ]);
464
+ }
465
+ function deleteAllSnapshots(sessionId) {
466
+ getDb().run("DELETE FROM snapshots WHERE session_id = ?", [sessionId]);
467
+ }
468
+ function generateSessionId() {
469
+ const ts = Date.now().toString(36);
470
+ const rand = Math.random().toString(36).slice(2, 7);
471
+ return `${ts}-${rand}`;
472
+ }
473
+
474
+ // src/cli/markdown.ts
475
+ import * as c from "yoctocolors";
476
+ function renderInline(text) {
477
+ let out = "";
478
+ let i = 0;
479
+ let prevWasBoldClose = false;
480
+ while (i < text.length) {
481
+ if (text[i] === "`") {
482
+ const end = text.indexOf("`", i + 1);
483
+ if (end !== -1) {
484
+ out += c.yellow(text.slice(i, end + 1));
485
+ i = end + 1;
486
+ prevWasBoldClose = false;
487
+ continue;
488
+ }
489
+ }
490
+ if (text.slice(i, i + 2) === "**" && text[i + 2] !== "*") {
491
+ const end = text.indexOf("**", i + 2);
492
+ if (end !== -1) {
493
+ out += c.bold(text.slice(i + 2, end));
494
+ i = end + 2;
495
+ prevWasBoldClose = true;
496
+ continue;
497
+ }
498
+ }
499
+ if (text[i] === "*" && text[i + 1] !== "*" && (prevWasBoldClose || text[i - 1] !== "*")) {
500
+ const end = text.indexOf("*", i + 1);
501
+ if (end !== -1 && text[end - 1] !== "*") {
502
+ out += c.dim(text.slice(i + 1, end));
503
+ i = end + 1;
504
+ prevWasBoldClose = false;
505
+ continue;
506
+ }
507
+ }
508
+ out += text[i];
509
+ i++;
510
+ prevWasBoldClose = false;
511
+ }
512
+ return out;
513
+ }
514
+ function renderLine(raw, inFence) {
515
+ if (/^(`{3,}|~{3,})/.test(raw)) {
516
+ return { output: c.dim(raw), inFence: !inFence };
517
+ }
518
+ if (inFence) {
519
+ return { output: c.yellow(raw), inFence: true };
520
+ }
521
+ if (/^(-{3,}|\*{3,}|={3,})\s*$/.test(raw)) {
522
+ return { output: c.dim("\u2500".repeat(40)), inFence: false };
523
+ }
524
+ const h3 = raw.match(/^(#{3,})\s+(.*)/);
525
+ if (h3)
526
+ return { output: c.bold(renderInline(h3[2] ?? "")), inFence: false };
527
+ const h2 = raw.match(/^##\s+(.*)/);
528
+ if (h2)
529
+ return {
530
+ output: c.bold(c.cyan(renderInline(h2[1] ?? ""))),
531
+ inFence: false
532
+ };
533
+ const h1 = raw.match(/^#\s+(.*)/);
534
+ if (h1)
535
+ return {
536
+ output: c.bold(c.cyan(renderInline(h1[1] ?? ""))),
537
+ inFence: false
538
+ };
539
+ const bq = raw.match(/^>\s?(.*)/);
540
+ if (bq)
541
+ return {
542
+ output: c.dim(`\u2502 ${renderInline(bq[1] ?? "")}`),
543
+ inFence: false
544
+ };
545
+ const ul = raw.match(/^(\s*)[*\-+]\s+(.*)/);
546
+ if (ul) {
547
+ const indent = ul[1] ?? "";
548
+ return {
549
+ output: `${indent}${c.dim("\xB7")} ${renderInline(ul[2] ?? "")}`,
550
+ inFence: false
551
+ };
552
+ }
553
+ const ol = raw.match(/^(\s*)(\d+)\.\s+(.*)/);
554
+ if (ol) {
555
+ const indent = ol[1] ?? "";
556
+ const num = ol[2] ?? "";
557
+ return {
558
+ output: `${indent}${c.dim(`${num}.`)} ${renderInline(ol[3] ?? "")}`,
559
+ inFence: false
560
+ };
561
+ }
562
+ return { output: renderInline(raw), inFence: false };
563
+ }
564
+ function renderMarkdown(text) {
565
+ let inFence = false;
566
+ return text.split(`
567
+ `).map((raw) => {
568
+ const r = renderLine(raw, inFence);
569
+ inFence = r.inFence;
570
+ return r.output;
571
+ }).join(`
572
+ `);
573
+ }
574
+ function renderChunk(text, inFence) {
575
+ let fence = inFence;
576
+ const output = text.split(`
577
+ `).map((raw) => {
578
+ const r = renderLine(raw, fence);
579
+ fence = r.inFence;
580
+ return r.output;
581
+ }).join(`
582
+ `);
583
+ return { output, inFence: fence };
584
+ }
585
+
586
+ // src/cli/output.ts
587
+ import { homedir as homedir2 } from "os";
588
+ import * as c2 from "yoctocolors";
589
+ var HOME = homedir2();
590
+ var PACKAGE_VERSION = "0.0.3";
591
+ function restoreTerminal() {
592
+ try {
593
+ process.stderr.write("\x1B[?25h");
594
+ process.stderr.write("\r\x1B[2K");
595
+ } catch {}
596
+ try {
597
+ if (process.stdin.isTTY && process.stdin.isRaw) {
598
+ process.stdin.setRawMode(false);
599
+ }
600
+ } catch {}
601
+ }
602
+ function registerTerminalCleanup() {
603
+ const cleanup = () => restoreTerminal();
604
+ process.on("exit", cleanup);
605
+ process.on("SIGTERM", () => {
606
+ cleanup();
607
+ process.exit(143);
608
+ });
609
+ process.on("SIGINT", () => {
610
+ if (process.listenerCount("SIGINT") > 1)
611
+ return;
612
+ cleanup();
613
+ process.exit(130);
614
+ });
615
+ process.on("uncaughtException", (err) => {
616
+ cleanup();
617
+ throw err;
618
+ });
619
+ process.on("unhandledRejection", (reason) => {
620
+ cleanup();
621
+ throw reason instanceof Error ? reason : new Error(String(reason));
622
+ });
623
+ }
624
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
625
+
626
+ class Spinner {
627
+ frame = 0;
628
+ timer = null;
629
+ label = "";
630
+ start(label = "") {
631
+ this.label = label;
632
+ if (this.timer)
633
+ return;
634
+ process.stderr.write("\x1B[?25l");
635
+ this._tick();
636
+ this.timer = setInterval(() => this._tick(), 80);
637
+ }
638
+ stop() {
639
+ if (!this.timer)
640
+ return;
641
+ clearInterval(this.timer);
642
+ this.timer = null;
643
+ process.stderr.write("\r\x1B[2K\x1B[?25h");
644
+ }
645
+ update(label) {
646
+ this.label = label;
647
+ }
648
+ _tick() {
649
+ const f = FRAMES[this.frame++ % FRAMES.length] ?? "\u280B";
650
+ const label = this.label ? c2.dim(` ${this.label}`) : "";
651
+ process.stderr.write(`\r${c2.dim(f)}${label}`);
652
+ }
653
+ }
654
+ function writeln(text = "") {
655
+ process.stdout.write(`${text}
656
+ `);
657
+ }
658
+ function write(text) {
659
+ process.stdout.write(text);
660
+ }
661
+ function stripHashlinePrefix(text) {
662
+ return text.replace(/^\d+:[0-9a-f]{2}\| /, "");
663
+ }
664
+ var G = {
665
+ prompt: c2.green("\u203A"),
666
+ reply: c2.cyan("\u25C6"),
667
+ search: c2.yellow("?"),
668
+ read: c2.dim("\u2190"),
669
+ write: c2.green("\u270E"),
670
+ run: c2.dim("$"),
671
+ agent: c2.cyan("\u21E2"),
672
+ mcp: c2.yellow("\u2699"),
673
+ ok: c2.green("\u2714"),
674
+ err: c2.red("\u2716"),
675
+ warn: c2.yellow("!"),
676
+ info: c2.dim("\xB7"),
677
+ diff_add: c2.green("+"),
678
+ diff_del: c2.red("-"),
679
+ diff_hdr: c2.cyan("@")
680
+ };
681
+ function toolGlyph(name) {
682
+ if (name === "glob" || name === "grep")
683
+ return G.search;
684
+ if (name === "read")
685
+ return G.read;
686
+ if (name === "create" || name === "replace" || name === "insert")
687
+ return G.write;
688
+ if (name === "shell")
689
+ return G.run;
690
+ if (name === "subagent")
691
+ return G.agent;
692
+ if (name.startsWith("mcp_"))
693
+ return G.mcp;
694
+ return G.info;
695
+ }
696
+ function toolCallLine(name, args) {
697
+ const a = args && typeof args === "object" ? args : {};
698
+ if (name === "glob") {
699
+ const pattern = String(a.pattern ?? "");
700
+ const cwd = a.cwd ? String(a.cwd) : "";
701
+ return `${G.search} ${c2.dim("glob")} ${c2.bold(pattern)}${cwd ? c2.dim(` in ${cwd}`) : ""}`;
702
+ }
703
+ if (name === "grep") {
704
+ const flags = [
705
+ a.include ? String(a.include) : null,
706
+ a.caseSensitive === false ? "i" : null
707
+ ].filter(Boolean).join(" ");
708
+ const pattern = String(a.pattern ?? "");
709
+ return `${G.search} ${c2.dim("grep")} ${c2.bold(pattern)}${flags ? c2.dim(` ${flags}`) : ""}`;
710
+ }
711
+ if (name === "read") {
712
+ const line = Number.isFinite(a.line) ? Number(a.line) : null;
713
+ const count = Number.isFinite(a.count) ? Number(a.count) : null;
714
+ const range = line || count ? c2.dim(`:${line ?? 1}${count ? `+${count}` : ""}`) : "";
715
+ return `${G.read} ${c2.dim("read")} ${String(a.path ?? "")}${range}`;
716
+ }
717
+ if (name === "create") {
718
+ return `${G.write} ${c2.dim("create")} ${c2.bold(String(a.path ?? ""))}`;
719
+ }
720
+ if (name === "replace") {
721
+ const range = a.endAnchor ? c2.dim(` ${a.startAnchor}\u2013${a.endAnchor}`) : c2.dim(` ${a.startAnchor}`);
722
+ const verb = a.newContent === undefined || a.newContent === "" ? "delete" : "replace";
723
+ return `${G.write} ${c2.dim(verb)} ${c2.bold(String(a.path ?? ""))}${range}`;
724
+ }
725
+ if (name === "insert") {
726
+ return `${G.write} ${c2.dim(`insert ${a.position ?? ""}`)} ${c2.bold(String(a.path ?? ""))}${c2.dim(` @ ${a.anchor}`)}`;
727
+ }
728
+ if (name === "shell") {
729
+ const cmd = String(a.command ?? "");
730
+ const shortCmd = cmd.length > 72 ? `${cmd.slice(0, 69)}\u2026` : cmd;
731
+ return `${G.run} ${c2.dim("$")} ${shortCmd}`;
732
+ }
733
+ if (name === "subagent") {
734
+ const prompt = String(a.prompt ?? "");
735
+ const shortPrompt = prompt.length > 60 ? `${prompt.slice(0, 57)}\u2026` : prompt;
736
+ return `${G.agent} ${c2.dim("subagent")} ${c2.dim(shortPrompt)}`;
737
+ }
738
+ if (name.startsWith("mcp_")) {
739
+ return `${G.mcp} ${c2.dim(name)}`;
740
+ }
741
+ return `${toolGlyph(name)} ${c2.dim(name)}`;
742
+ }
743
+ function renderToolCall(toolName, args) {
744
+ writeln(` ${toolCallLine(toolName, args)}`);
745
+ }
746
+ function renderHook(toolName, scriptPath, success) {
747
+ const short = scriptPath.replace(HOME, "~");
748
+ if (success) {
749
+ writeln(` ${G.ok} ${c2.dim(`hook post-${toolName}`)}`);
750
+ } else {
751
+ writeln(` ${G.err} ${c2.red(`hook post-${toolName} failed`)} ${c2.dim(short)}`);
752
+ }
753
+ }
754
+ function renderToolResultInline(toolName, result, isError, indent) {
755
+ if (isError) {
756
+ const msg = typeof result === "string" ? result : result instanceof Error ? result.message : JSON.stringify(result);
757
+ const oneLiner = msg.split(`
758
+ `)[0] ?? msg;
759
+ writeln(`${indent}${G.err} ${c2.red(oneLiner)}`);
760
+ return;
761
+ }
762
+ if (toolName === "glob") {
763
+ const r = result;
764
+ const n = r.files.length;
765
+ writeln(`${indent}${G.info} ${c2.dim(n === 0 ? "no matches" : `${n} file${n === 1 ? "" : "s"}${r.truncated ? " (capped)" : ""}`)}`);
766
+ return;
767
+ }
768
+ if (toolName === "grep") {
769
+ const r = result;
770
+ const n = r.matches.length;
771
+ writeln(`${indent}${G.info} ${c2.dim(n === 0 ? "no matches" : `${n} match${n === 1 ? "" : "es"}${r.truncated ? " (capped)" : ""}`)}`);
772
+ return;
773
+ }
774
+ if (toolName === "read") {
775
+ const r = result;
776
+ writeln(`${indent}${G.info} ${c2.dim(`${r.totalLines} lines${r.truncated ? " (truncated)" : ""}`)}`);
777
+ return;
778
+ }
779
+ if (toolName === "create") {
780
+ const r = result;
781
+ const verb = r.created ? c2.green("created") : c2.dim("overwritten");
782
+ writeln(`${indent}${G.ok} ${verb} ${r.path}`);
783
+ return;
784
+ }
785
+ if (toolName === "replace" || toolName === "insert") {
786
+ const r = result;
787
+ const verb = toolName === "insert" ? "inserted" : r.deleted ? "deleted" : "replaced";
788
+ writeln(`${indent}${G.ok} ${c2.dim(verb)} ${r.path}`);
789
+ return;
790
+ }
791
+ if (toolName === "shell") {
792
+ const r = result;
793
+ const badge = r.timedOut ? c2.yellow("timeout") : r.success ? c2.green(`\u2714 ${r.exitCode}`) : c2.red(`\u2716 ${r.exitCode}`);
794
+ writeln(`${indent}${badge}`);
795
+ return;
796
+ }
797
+ if (toolName === "subagent") {
798
+ const r = result;
799
+ if (r.inputTokens || r.outputTokens) {
800
+ writeln(`${indent}${G.ok} ${c2.dim(`\u2191${r.inputTokens ?? 0} \u2193${r.outputTokens ?? 0}`)}`);
801
+ }
802
+ return;
803
+ }
804
+ if (toolName.startsWith("mcp_")) {
805
+ const content = Array.isArray(result) ? result : [result];
806
+ const first = content[0];
807
+ if (first?.type === "text" && first.text) {
808
+ const oneLiner = first.text.split(`
809
+ `)[0] ?? "";
810
+ if (oneLiner)
811
+ writeln(`${indent}${G.info} ${c2.dim(oneLiner.length > 80 ? `${oneLiner.slice(0, 77)}\u2026` : oneLiner)}`);
812
+ }
813
+ return;
814
+ }
815
+ const text = JSON.stringify(result);
816
+ writeln(`${indent}${G.info} ${c2.dim(text.length > 80 ? `${text.slice(0, 77)}\u2026` : text)}`);
817
+ }
818
+ function renderSubagentActivity(activity, indent, maxDepth) {
819
+ for (const entry of activity) {
820
+ writeln(`${indent}${toolCallLine(entry.toolName, entry.args)}`);
821
+ if (entry.toolName === "subagent" && maxDepth > 0) {
822
+ const nested = entry.result;
823
+ if (nested?.activity?.length) {
824
+ renderSubagentActivity(nested.activity, `${indent} `, maxDepth - 1);
825
+ }
826
+ }
827
+ renderToolResultInline(entry.toolName, entry.result, entry.isError, `${indent} `);
828
+ }
829
+ }
830
+ function renderToolResult(toolName, result, isError) {
831
+ if (isError) {
832
+ const msg = typeof result === "string" ? result : result instanceof Error ? result.message : JSON.stringify(result);
833
+ const oneLiner = msg.split(`
834
+ `)[0] ?? msg;
835
+ writeln(` ${G.err} ${c2.red(oneLiner)}`);
836
+ return;
837
+ }
838
+ if (toolName === "glob") {
839
+ const r = result;
840
+ const files = r.files;
841
+ if (files.length === 0) {
842
+ writeln(` ${G.info} ${c2.dim("no matches")}`);
843
+ return;
844
+ }
845
+ const show = files.slice(0, 12);
846
+ const rest = files.length - show.length;
847
+ const prefix = commonPrefix(show);
848
+ for (const f of show) {
849
+ const rel = prefix && f.startsWith(prefix) ? c2.dim(prefix) + f.slice(prefix.length) : f;
850
+ writeln(` ${c2.dim("\xB7")} ${rel}`);
851
+ }
852
+ if (rest > 0)
853
+ writeln(` ${c2.dim(` +${rest} more`)}`);
854
+ if (r.truncated)
855
+ writeln(` ${c2.dim(" (results capped)")}`);
856
+ return;
857
+ }
858
+ if (toolName === "grep") {
859
+ const r = result;
860
+ if (r.matches.length === 0) {
861
+ writeln(` ${G.info} ${c2.dim("no matches")}`);
862
+ return;
863
+ }
864
+ const seen = new Set;
865
+ let shown = 0;
866
+ for (const m of r.matches) {
867
+ if (shown >= 10)
868
+ break;
869
+ const key = m.file;
870
+ const fileLabel = seen.has(key) ? c2.dim(" ".repeat(m.file.length + 1)) : c2.dim(`${m.file}:`);
871
+ seen.add(key);
872
+ writeln(` ${fileLabel}${c2.dim(String(m.line))} ${stripHashlinePrefix(m.text).trim()}`);
873
+ shown++;
874
+ }
875
+ const rest = r.matches.length - shown;
876
+ if (rest > 0)
877
+ writeln(` ${c2.dim(` +${rest} more`)}`);
878
+ if (r.truncated)
879
+ writeln(` ${c2.dim(" (results capped)")}`);
880
+ return;
881
+ }
882
+ if (toolName === "read") {
883
+ const r = result;
884
+ const linesReturned = r.content ? r.content.split(`
885
+ `).length : 0;
886
+ const endLine = linesReturned > 0 ? r.line + linesReturned - 1 : r.line;
887
+ const range = r.line === 1 && endLine === r.totalLines ? `${r.totalLines} lines` : `lines ${r.line}\u2013${endLine} of ${r.totalLines}`;
888
+ writeln(` ${G.info} ${c2.dim(`${r.path} ${range}${r.truncated ? " (truncated)" : ""}`)}`);
889
+ return;
890
+ }
891
+ if (toolName === "create") {
892
+ const r = result;
893
+ const verb = r.created ? c2.green("created") : c2.dim("overwritten");
894
+ writeln(` ${G.ok} ${verb} ${r.path}`);
895
+ renderDiff(r.diff);
896
+ return;
897
+ }
898
+ if (toolName === "replace" || toolName === "insert") {
899
+ const r = result;
900
+ const verb = toolName === "insert" ? "inserted" : r.deleted ? "deleted" : "replaced";
901
+ writeln(` ${G.ok} ${c2.dim(verb)} ${r.path}`);
902
+ renderDiff(r.diff);
903
+ return;
904
+ }
905
+ if (toolName === "shell") {
906
+ const r = result;
907
+ const badge = r.timedOut ? c2.yellow("timeout") : r.success ? c2.green(`\u2714 ${r.exitCode}`) : c2.red(`\u2716 ${r.exitCode}`);
908
+ writeln(` ${badge}`);
909
+ const outLines = r.stdout ? r.stdout.split(`
910
+ `) : [];
911
+ const errLines = r.stderr ? r.stderr.split(`
912
+ `) : [];
913
+ for (const line of outLines.slice(0, 20)) {
914
+ writeln(` ${c2.dim("\u2502")} ${line}`);
915
+ }
916
+ if (outLines.length > 20)
917
+ writeln(` ${c2.dim(`\u2502 \u2026 +${outLines.length - 20} lines`)}`);
918
+ for (const line of errLines.slice(0, 8)) {
919
+ if (line.trim())
920
+ writeln(` ${c2.red("\u2502")} ${c2.dim(line)}`);
921
+ }
922
+ if (errLines.length > 8)
923
+ writeln(` ${c2.red(`\u2502 \u2026 +${errLines.length - 8} lines`)}`);
924
+ return;
925
+ }
926
+ if (toolName === "subagent") {
927
+ const r = result;
928
+ if (r.activity?.length) {
929
+ renderSubagentActivity(r.activity, " ", 1);
930
+ }
931
+ if (r.result) {
932
+ const lines = r.result.split(`
933
+ `);
934
+ const preview = lines.slice(0, 8);
935
+ for (const line of preview)
936
+ writeln(` ${c2.dim("\u2502")} ${line}`);
937
+ if (lines.length > 8)
938
+ writeln(` ${c2.dim(`\u2502 \u2026 +${lines.length - 8} lines`)}`);
939
+ }
940
+ if (r.inputTokens || r.outputTokens) {
941
+ writeln(` ${c2.dim(`\u2191${r.inputTokens ?? 0} \u2193${r.outputTokens ?? 0}`)}`);
942
+ }
943
+ return;
944
+ }
945
+ if (toolName.startsWith("mcp_")) {
946
+ const content = Array.isArray(result) ? result : [result];
947
+ for (const block of content.slice(0, 5)) {
948
+ if (block?.type === "text" && block.text) {
949
+ const lines = block.text.split(`
950
+ `).slice(0, 6);
951
+ for (const l of lines)
952
+ writeln(` ${c2.dim("\u2502")} ${l}`);
953
+ }
954
+ }
955
+ return;
956
+ }
957
+ const text = JSON.stringify(result);
958
+ writeln(` ${c2.dim(text.length > 120 ? `${text.slice(0, 117)}\u2026` : text)}`);
959
+ }
960
+ function renderDiff(diff) {
961
+ if (!diff || diff === "(no changes)")
962
+ return;
963
+ for (const line of diff.split(`
964
+ `)) {
965
+ if (line.startsWith("+++") || line.startsWith("---")) {
966
+ writeln(` ${c2.dim(line)}`);
967
+ } else if (line.startsWith("+")) {
968
+ writeln(` ${c2.green(line)}`);
969
+ } else if (line.startsWith("-")) {
970
+ writeln(` ${c2.red(line)}`);
971
+ } else if (line.startsWith("@@")) {
972
+ writeln(` ${c2.cyan(line)}`);
973
+ } else {
974
+ writeln(` ${c2.dim(line)}`);
975
+ }
976
+ }
977
+ }
978
+ async function renderTurn(events, spinner) {
979
+ let inText = false;
980
+ let rawBuffer = "";
981
+ let inFence = false;
982
+ let printQueue = "";
983
+ let printPos = 0;
984
+ let tickerHandle = null;
985
+ let inputTokens = 0;
986
+ let outputTokens = 0;
987
+ let contextTokens = 0;
988
+ let newMessages = [];
989
+ function enqueuePrint(ansi) {
990
+ printQueue += ansi;
991
+ if (tickerHandle === null) {
992
+ scheduleTick();
993
+ }
994
+ }
995
+ function tick() {
996
+ tickerHandle = null;
997
+ if (printPos < printQueue.length) {
998
+ process.stdout.write(printQueue[printPos]);
999
+ printPos++;
1000
+ scheduleTick();
1001
+ } else {
1002
+ printQueue = "";
1003
+ printPos = 0;
1004
+ }
1005
+ }
1006
+ function scheduleTick() {
1007
+ tickerHandle = setTimeout(tick, 8);
1008
+ }
1009
+ function drainQueue() {
1010
+ if (tickerHandle !== null) {
1011
+ clearTimeout(tickerHandle);
1012
+ tickerHandle = null;
1013
+ }
1014
+ if (printPos < printQueue.length) {
1015
+ process.stdout.write(printQueue.slice(printPos));
1016
+ }
1017
+ printQueue = "";
1018
+ printPos = 0;
1019
+ }
1020
+ function renderAndTrim(end) {
1021
+ const chunk = rawBuffer.slice(0, end);
1022
+ const rendered = renderChunk(chunk, inFence);
1023
+ inFence = rendered.inFence;
1024
+ enqueuePrint(rendered.output);
1025
+ rawBuffer = rawBuffer.slice(end);
1026
+ }
1027
+ function flushChunks() {
1028
+ let boundary = rawBuffer.indexOf(`
1029
+
1030
+ `);
1031
+ if (boundary !== -1) {
1032
+ renderAndTrim(boundary + 2);
1033
+ flushChunks();
1034
+ return;
1035
+ }
1036
+ boundary = rawBuffer.lastIndexOf(`
1037
+ `);
1038
+ if (boundary !== -1) {
1039
+ renderAndTrim(boundary + 1);
1040
+ }
1041
+ }
1042
+ function flushAll() {
1043
+ if (rawBuffer) {
1044
+ renderAndTrim(rawBuffer.length);
1045
+ }
1046
+ drainQueue();
1047
+ }
1048
+ for await (const event of events) {
1049
+ switch (event.type) {
1050
+ case "text-delta": {
1051
+ if (!inText) {
1052
+ spinner.stop();
1053
+ process.stdout.write(`${G.reply} `);
1054
+ inText = true;
1055
+ }
1056
+ rawBuffer += event.delta;
1057
+ flushChunks();
1058
+ break;
1059
+ }
1060
+ case "tool-call-start": {
1061
+ if (inText) {
1062
+ flushAll();
1063
+ writeln();
1064
+ inText = false;
1065
+ }
1066
+ spinner.stop();
1067
+ renderToolCall(event.toolName, event.args);
1068
+ spinner.start(event.toolName);
1069
+ break;
1070
+ }
1071
+ case "tool-result": {
1072
+ spinner.stop();
1073
+ renderToolResult(event.toolName, event.result, event.isError);
1074
+ spinner.start("thinking");
1075
+ break;
1076
+ }
1077
+ case "turn-complete": {
1078
+ if (inText) {
1079
+ flushAll();
1080
+ writeln();
1081
+ inText = false;
1082
+ }
1083
+ spinner.stop();
1084
+ inputTokens = event.inputTokens;
1085
+ outputTokens = event.outputTokens;
1086
+ contextTokens = event.contextTokens;
1087
+ newMessages = event.messages;
1088
+ break;
1089
+ }
1090
+ case "turn-error": {
1091
+ if (inText) {
1092
+ flushAll();
1093
+ writeln();
1094
+ inText = false;
1095
+ }
1096
+ spinner.stop();
1097
+ const isAbort = event.error.name === "AbortError" || event.error.name === "Error" && event.error.message.toLowerCase().includes("abort");
1098
+ if (isAbort) {
1099
+ writeln(`${G.warn} ${c2.dim("interrupted")}`);
1100
+ } else {
1101
+ const msg = event.error.message.split(`
1102
+ `)[0] ?? event.error.message;
1103
+ writeln(`${G.err} ${c2.red(msg)}`);
1104
+ }
1105
+ break;
1106
+ }
1107
+ }
1108
+ }
1109
+ return { inputTokens, outputTokens, contextTokens, newMessages };
1110
+ }
1111
+ function renderStatusBar(opts) {
1112
+ const cols = process.stdout.columns ?? 80;
1113
+ const left = [c2.cyan(opts.model)];
1114
+ if (opts.provider && opts.provider !== "zen")
1115
+ left.push(c2.dim(opts.provider));
1116
+ left.push(c2.dim(opts.sessionId.slice(0, 8)));
1117
+ if (opts.ralphMode)
1118
+ left.push(c2.magenta("\u21BB ralph"));
1119
+ const right = [];
1120
+ if (opts.inputTokens > 0 || opts.outputTokens > 0) {
1121
+ right.push(c2.dim(`\u2191${fmtTokens(opts.inputTokens)} \u2193${fmtTokens(opts.outputTokens)}`));
1122
+ }
1123
+ if (opts.contextTokens > 0) {
1124
+ const ctxRaw = fmtTokens(opts.contextTokens);
1125
+ if (opts.contextWindow !== null) {
1126
+ const pct = Math.round(opts.contextTokens / opts.contextWindow * 100);
1127
+ const ctxMax = fmtTokens(opts.contextWindow);
1128
+ const pctStr = `${pct}%`;
1129
+ const colored = pct >= 90 ? c2.red(pctStr) : pct >= 75 ? c2.yellow(pctStr) : c2.dim(pctStr);
1130
+ right.push(c2.dim(`ctx ${ctxRaw}/${ctxMax} `) + colored);
1131
+ } else {
1132
+ right.push(c2.dim(`ctx ${ctxRaw}`));
1133
+ }
1134
+ }
1135
+ if (opts.gitBranch)
1136
+ right.push(c2.dim(`\u2387 ${opts.gitBranch}`));
1137
+ const cwdDisplay = opts.cwd;
1138
+ const middle = c2.dim(cwdDisplay);
1139
+ const sep = c2.dim(" ");
1140
+ const full = [...left, middle, ...right.reverse()].join(sep);
1141
+ const visible = stripAnsi(full);
1142
+ const out = visible.length > cols ? truncateAnsi(full, cols - 1) : full;
1143
+ process.stdout.write(`${out}
1144
+ `);
1145
+ }
1146
+ function renderBanner(model, cwd) {
1147
+ writeln();
1148
+ writeln(` ${c2.cyan("mc")} ${c2.dim(`mini-coder \xB7 v${PACKAGE_VERSION}`)}`);
1149
+ writeln(` ${c2.dim(model)} ${c2.dim("\xB7")} ${c2.dim(cwd)}`);
1150
+ writeln(` ${c2.dim("/help for commands \xB7 ctrl+d to exit")}`);
1151
+ writeln();
1152
+ }
1153
+ function renderError(err) {
1154
+ const msg = err instanceof Error ? err.message : String(err);
1155
+ writeln(`${G.err} ${c2.red(msg)}`);
1156
+ }
1157
+ function renderInfo(msg) {
1158
+ writeln(`${G.info} ${c2.dim(msg)}`);
1159
+ }
1160
+ function fmtTokens(n) {
1161
+ if (n >= 1000)
1162
+ return `${(n / 1000).toFixed(1)}k`;
1163
+ return String(n);
1164
+ }
1165
+ function commonPrefix(paths) {
1166
+ const first = paths[0];
1167
+ if (!first)
1168
+ return "";
1169
+ const parts = first.split("/");
1170
+ let prefix = "";
1171
+ for (let i = 0;i < parts.length - 1; i++) {
1172
+ const candidate = `${parts.slice(0, i + 1).join("/")}/`;
1173
+ if (paths.every((p) => p.startsWith(candidate)))
1174
+ prefix = candidate;
1175
+ else
1176
+ break;
1177
+ }
1178
+ return prefix;
1179
+ }
1180
+ var ANSI_ESCAPE = "\x1B";
1181
+ function stripAnsi(s) {
1182
+ if (!s.includes(ANSI_ESCAPE))
1183
+ return s;
1184
+ return s.split(ANSI_ESCAPE).map((chunk, idx) => idx === 0 ? chunk : chunk.replace(/^\[[0-9;]*m/, "")).join("");
1185
+ }
1186
+ function truncateAnsi(s, maxLen) {
1187
+ const plain = stripAnsi(s);
1188
+ if (plain.length <= maxLen)
1189
+ return s;
1190
+ let visible = 0;
1191
+ let i = 0;
1192
+ while (i < s.length && visible < maxLen - 1) {
1193
+ if (s[i] === "\x1B") {
1194
+ while (i < s.length && s[i] !== "m")
1195
+ i++;
1196
+ } else {
1197
+ visible++;
1198
+ }
1199
+ i++;
1200
+ }
1201
+ return s.slice(0, i) + c2.dim("\u2026");
1202
+ }
1203
+ var PREFIX = {
1204
+ user: G.prompt,
1205
+ assistant: G.reply,
1206
+ tool: G.mcp,
1207
+ error: G.err,
1208
+ info: G.info,
1209
+ success: G.ok
1210
+ };
1211
+
1212
+ // src/cli/commands.ts
1213
+ async function handleModel(ctx, args) {
1214
+ if (args) {
1215
+ let modelId = args;
1216
+ if (!args.includes("/")) {
1217
+ const models2 = await fetchAvailableModels();
1218
+ const match = models2.find((m) => m.id.split("/").slice(1).join("/") === args || m.id === args);
1219
+ if (match) {
1220
+ modelId = match.id;
1221
+ } else {
1222
+ writeln(`${PREFIX.error} unknown model ${c3.cyan(args)} ${c3.dim("\u2014 run /models for the full list")}`);
1223
+ return;
1224
+ }
1225
+ }
1226
+ ctx.setModel(modelId);
1227
+ writeln(`${PREFIX.success} model \u2192 ${c3.cyan(modelId)}`);
1228
+ return;
1229
+ }
1230
+ writeln(`${c3.dim(" fetching models\u2026")}`);
1231
+ const models = await fetchAvailableModels();
1232
+ process.stdout.write("\x1B[1A\r\x1B[2K");
1233
+ if (models.length === 0) {
1234
+ writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
1235
+ writeln(c3.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
1236
+ return;
1237
+ }
1238
+ const byProvider = new Map;
1239
+ for (const m of models) {
1240
+ const existing = byProvider.get(m.provider);
1241
+ if (existing) {
1242
+ existing.push(m);
1243
+ } else {
1244
+ byProvider.set(m.provider, [m]);
1245
+ }
1246
+ }
1247
+ writeln();
1248
+ for (const [provider, list] of byProvider) {
1249
+ writeln(c3.bold(` ${provider}`));
1250
+ for (const m of list) {
1251
+ const isCurrent = ctx.currentModel === m.id;
1252
+ const freeTag = m.free ? c3.green(" free") : "";
1253
+ const ctxTag = m.context ? c3.dim(` ${Math.round(m.context / 1000)}k`) : "";
1254
+ const cur = isCurrent ? c3.cyan(" \u25C0") : "";
1255
+ writeln(` ${c3.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}`);
1256
+ writeln(` ${c3.dim(m.id)}`);
1257
+ }
1258
+ }
1259
+ writeln();
1260
+ writeln(c3.dim(" /model <id> to switch \xB7 e.g. /model zen/claude-sonnet-4-6"));
1261
+ }
1262
+ function handlePlan(ctx) {
1263
+ ctx.setPlanMode(!ctx.planMode);
1264
+ if (ctx.planMode) {
1265
+ if (ctx.ralphMode)
1266
+ ctx.setRalphMode(false);
1267
+ writeln(`${PREFIX.info} ${c3.yellow("plan mode")} ${c3.dim("\u2014 read-only tools + MCP, no writes or shell")}`);
1268
+ } else {
1269
+ writeln(`${PREFIX.info} ${c3.dim("plan mode off")}`);
1270
+ }
1271
+ }
1272
+ function handleRalph(ctx) {
1273
+ ctx.setRalphMode(!ctx.ralphMode);
1274
+ if (ctx.ralphMode) {
1275
+ if (ctx.planMode)
1276
+ ctx.setPlanMode(false);
1277
+ writeln(`${PREFIX.info} ${c3.magenta("ralph mode")} ${c3.dim("\u2014 loops until done, fresh context each iteration")}`);
1278
+ } else {
1279
+ writeln(`${PREFIX.info} ${c3.dim("ralph mode off")}`);
1280
+ }
1281
+ }
1282
+ async function handleUndo(ctx) {
1283
+ const ok = await ctx.undoLastTurn();
1284
+ if (ok) {
1285
+ writeln(`${PREFIX.success} ${c3.dim("last turn undone \u2014 history and files restored")}`);
1286
+ } else {
1287
+ writeln(`${PREFIX.info} ${c3.dim("nothing to undo")}`);
1288
+ }
1289
+ }
1290
+ async function handleMcp(ctx, args) {
1291
+ const parts = args.trim().split(/\s+/);
1292
+ const sub = parts[0] ?? "list";
1293
+ switch (sub) {
1294
+ case "list": {
1295
+ const servers = listMcpServers();
1296
+ if (servers.length === 0) {
1297
+ writeln(c3.dim(" no MCP servers configured"));
1298
+ writeln(c3.dim(" /mcp add <name> http <url> \xB7 /mcp add <name> stdio <cmd> [args...]"));
1299
+ return;
1300
+ }
1301
+ writeln();
1302
+ for (const s of servers) {
1303
+ const detail = s.url ? c3.dim(` ${s.url}`) : s.command ? c3.dim(` ${s.command}`) : "";
1304
+ writeln(` ${c3.yellow("\u2699")} ${c3.bold(s.name)} ${c3.dim(s.transport)}${detail}`);
1305
+ }
1306
+ return;
1307
+ }
1308
+ case "add": {
1309
+ const [, name, transport, ...rest] = parts;
1310
+ if (!name || !transport || rest.length === 0) {
1311
+ writeln(c3.red(" usage: /mcp add <name> http <url>"));
1312
+ writeln(c3.red(" /mcp add <name> stdio <cmd> [args...]"));
1313
+ return;
1314
+ }
1315
+ if (transport === "http") {
1316
+ const url = rest[0];
1317
+ if (!url) {
1318
+ writeln(c3.red(" usage: /mcp add <name> http <url>"));
1319
+ return;
1320
+ }
1321
+ upsertMcpServer({
1322
+ name,
1323
+ transport,
1324
+ url,
1325
+ command: null,
1326
+ args: null,
1327
+ env: null
1328
+ });
1329
+ } else if (transport === "stdio") {
1330
+ const [command, ...cmdArgs] = rest;
1331
+ if (!command) {
1332
+ writeln(c3.red(" usage: /mcp add <name> stdio <cmd> [args...]"));
1333
+ return;
1334
+ }
1335
+ upsertMcpServer({
1336
+ name,
1337
+ transport,
1338
+ url: null,
1339
+ command,
1340
+ args: cmdArgs.length ? JSON.stringify(cmdArgs) : null,
1341
+ env: null
1342
+ });
1343
+ } else {
1344
+ writeln(c3.red(` unknown transport: ${transport} (use http or stdio)`));
1345
+ return;
1346
+ }
1347
+ try {
1348
+ await ctx.connectMcpServer(name);
1349
+ writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} added and connected`);
1350
+ } catch (e) {
1351
+ writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} saved ${c3.dim(`(connection failed: ${String(e)})`)}`);
1352
+ }
1353
+ return;
1354
+ }
1355
+ case "remove":
1356
+ case "rm": {
1357
+ const [, name] = parts;
1358
+ if (!name) {
1359
+ writeln(c3.red(" usage: /mcp remove <name>"));
1360
+ return;
1361
+ }
1362
+ deleteMcpServer(name);
1363
+ writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} removed`);
1364
+ return;
1365
+ }
1366
+ default:
1367
+ writeln(c3.red(` unknown: /mcp ${sub}`));
1368
+ writeln(c3.dim(" subcommands: list \xB7 add \xB7 remove"));
1369
+ }
1370
+ }
1371
+ var REVIEW_PROMPT = (cwd, focus) => `You are a code reviewer. Review recent changes and provide actionable feedback.
1372
+
1373
+ Working directory: ${cwd}
1374
+ ${focus ? `Focus: ${focus}` : ""}
1375
+
1376
+ ## What to review
1377
+ - No args: \`git diff\` (unstaged) + \`git diff --cached\` (staged) + \`git status --short\` (untracked)
1378
+ - Commit hash: \`git show <hash>\`
1379
+ - Branch: \`git diff <branch>...HEAD\`
1380
+ - PR number/URL: \`gh pr view\` + \`gh pr diff\`
1381
+
1382
+ ## How to review
1383
+ After getting the diff, read the full files changed \u2014 diffs alone miss context.
1384
+ Check for AGENTS.md or CONVENTIONS.md for project conventions.
1385
+
1386
+ ## What to flag (priority order)
1387
+ 1. **Bugs** \u2014 logic errors, missing edge cases, unhandled errors, race conditions, security issues. Be certain before flagging; investigate first.
1388
+ 2. **Structure** \u2014 wrong abstraction, established patterns ignored, excessive nesting.
1389
+ 3. **Performance** \u2014 only if obviously problematic (O(n\xB2) on unbounded data, N+1, blocking hot paths).
1390
+ 4. **Style** \u2014 only clear violations of project conventions. Don't be a zealot.
1391
+
1392
+ Only review the changed code, not pre-existing code.
1393
+
1394
+ ## Output
1395
+ - Be direct and specific: quote code, cite file and line number.
1396
+ - State the scenario/input that triggers a bug \u2014 severity depends on this.
1397
+ - No flattery, no filler. Matter-of-fact tone.
1398
+ - End with a short **Summary** of the most important items.
1399
+ `;
1400
+ async function handleReview(ctx, args) {
1401
+ const focus = args.trim();
1402
+ writeln(`${PREFIX.info} ${c3.cyan("review")} ${c3.dim("\u2014 spawning review subagent\u2026")}`);
1403
+ writeln();
1404
+ try {
1405
+ const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
1406
+ if (output.activity.length) {
1407
+ renderSubagentActivity(output.activity, " ", 1);
1408
+ writeln();
1409
+ }
1410
+ write(renderMarkdown(output.result));
1411
+ writeln();
1412
+ return {
1413
+ type: "inject-user-message",
1414
+ text: `Code review output:
1415
+
1416
+ ${output.result}
1417
+
1418
+ <system-message>Review the findings and summarize them to the user.</system-message>`
1419
+ };
1420
+ } catch (e) {
1421
+ writeln(`${PREFIX.error} review failed: ${String(e)}`);
1422
+ return { type: "handled" };
1423
+ }
1424
+ }
1425
+ function handleNew(ctx) {
1426
+ ctx.startNewSession();
1427
+ writeln(`${PREFIX.success} ${c3.dim("new session started \u2014 context cleared")}`);
1428
+ }
1429
+ function handleHelp() {
1430
+ writeln();
1431
+ const cmds = [
1432
+ ["/model [id]", "list or switch models (fetches live list)"],
1433
+ ["/undo", "remove the last turn from conversation history"],
1434
+ ["/plan", "toggle plan mode (read-only tools + MCP)"],
1435
+ [
1436
+ "/ralph",
1437
+ "toggle ralph mode (autonomous loop, fresh context each iteration)"
1438
+ ],
1439
+ ["/review [focus]", "run a structured code review on recent changes"],
1440
+ ["/mcp list", "list MCP servers"],
1441
+ ["/mcp add <n> <t> [u]", "add an MCP server"],
1442
+ ["/mcp remove <name>", "remove an MCP server"],
1443
+ ["/new", "start a new session with clean context"],
1444
+ ["/help", "this message"],
1445
+ ["/exit", "quit"]
1446
+ ];
1447
+ for (const [cmd, desc] of cmds) {
1448
+ writeln(` ${c3.cyan(cmd.padEnd(26))} ${c3.dim(desc)}`);
1449
+ }
1450
+ writeln();
1451
+ writeln(` ${c3.green("@file".padEnd(26))} ${c3.dim("inject file contents into prompt (Tab to complete)")}`);
1452
+ writeln(` ${c3.green("!cmd".padEnd(26))} ${c3.dim("run shell command, output added as context")}`);
1453
+ writeln();
1454
+ writeln(` ${c3.dim("ctrl+c")} cancel ${c3.dim("\xB7")} ${c3.dim("ctrl+d")} exit ${c3.dim("\xB7")} ${c3.dim("ctrl+r")} history search ${c3.dim("\xB7")} ${c3.dim("\u2191\u2193")} history`);
1455
+ writeln();
1456
+ }
1457
+ async function handleCommand(command, args, ctx) {
1458
+ switch (command.toLowerCase()) {
1459
+ case "model":
1460
+ case "models":
1461
+ await handleModel(ctx, args);
1462
+ return { type: "handled" };
1463
+ case "undo":
1464
+ await handleUndo(ctx);
1465
+ return { type: "handled" };
1466
+ case "plan":
1467
+ handlePlan(ctx);
1468
+ return { type: "handled" };
1469
+ case "ralph":
1470
+ handleRalph(ctx);
1471
+ return { type: "handled" };
1472
+ case "mcp":
1473
+ await handleMcp(ctx, args);
1474
+ return { type: "handled" };
1475
+ case "new":
1476
+ handleNew(ctx);
1477
+ return { type: "handled" };
1478
+ case "review":
1479
+ return await handleReview(ctx, args);
1480
+ case "help":
1481
+ case "?":
1482
+ handleHelp();
1483
+ return { type: "handled" };
1484
+ case "exit":
1485
+ case "quit":
1486
+ case "q":
1487
+ return { type: "exit" };
1488
+ default:
1489
+ writeln(`${PREFIX.error} unknown: /${command} ${c3.dim("\u2014 /help for commands")}`);
1490
+ return { type: "unknown", command };
1491
+ }
1492
+ }
1493
+
1494
+ // src/cli/image-types.ts
1495
+ var IMAGE_EXTENSIONS = new Set([
1496
+ "png",
1497
+ "jpg",
1498
+ "jpeg",
1499
+ "gif",
1500
+ "webp",
1501
+ "bmp",
1502
+ "tiff",
1503
+ "tif",
1504
+ "avif",
1505
+ "heic"
1506
+ ]);
1507
+ function isImageFilename(s) {
1508
+ const dotIdx = s.lastIndexOf(".");
1509
+ if (dotIdx === -1 || dotIdx === 0)
1510
+ return false;
1511
+ const ext = s.slice(dotIdx + 1).toLowerCase();
1512
+ return IMAGE_EXTENSIONS.has(ext);
1513
+ }
1514
+ async function loadImageFile(filePath) {
1515
+ try {
1516
+ const file = Bun.file(filePath);
1517
+ if (!await file.exists())
1518
+ return null;
1519
+ const buf = await file.arrayBuffer();
1520
+ const b64 = Buffer.from(buf).toString("base64");
1521
+ const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
1522
+ const mediaType = file.type || `image/${ext === "jpg" ? "jpeg" : ext}` || "image/png";
1523
+ return { data: b64, mediaType };
1524
+ } catch {
1525
+ return null;
1526
+ }
1527
+ }
1528
+
1529
+ // src/cli/input.ts
1530
+ import { join as join2, relative } from "path";
1531
+ import * as c4 from "yoctocolors";
1532
+ var ESC = "\x1B";
1533
+ var CSI = `${ESC}[`;
1534
+ var CLEAR_LINE = `\r${CSI}2K`;
1535
+ var CURSOR_LEFT = `${CSI}D`;
1536
+ var CURSOR_RIGHT = `${CSI}C`;
1537
+ var CURSOR_UP = `${CSI}A`;
1538
+ var CURSOR_DOWN = `${CSI}B`;
1539
+ var SAVE_CURSOR = `${CSI}s`;
1540
+ var RESTORE_CURSOR = `${CSI}u`;
1541
+ var BPASTE_ENABLE = `${ESC}[?2004h`;
1542
+ var BPASTE_DISABLE = `${ESC}[?2004l`;
1543
+ var BPASTE_START = `${ESC}[200~`;
1544
+ var BPASTE_END = `${ESC}[201~`;
1545
+ var ENTER = "\r";
1546
+ var NEWLINE = `
1547
+ `;
1548
+ var BACKSPACE = "\x7F";
1549
+ var CTRL_C = "\x03";
1550
+ var CTRL_D = "\x04";
1551
+ var CTRL_A = "\x01";
1552
+ var CTRL_E = "\x05";
1553
+ var CTRL_W = "\x17";
1554
+ var CTRL_U = "\x15";
1555
+ var CTRL_K = "\v";
1556
+ var CTRL_L = "\f";
1557
+ var CTRL_R = "\x12";
1558
+ var TAB = "\t";
1559
+ async function getFileCompletions(prefix, cwd) {
1560
+ const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
1561
+ const glob = new Bun.Glob(`**/*${query}*`);
1562
+ const results = [];
1563
+ for await (const file of glob.scan({ cwd, onlyFiles: true })) {
1564
+ if (file.includes("node_modules") || file.includes(".git"))
1565
+ continue;
1566
+ results.push(`@${relative(cwd, join2(cwd, file))}`);
1567
+ if (results.length >= 10)
1568
+ break;
1569
+ }
1570
+ return results;
1571
+ }
1572
+ async function tryExtractImageFromPaste(pasted, cwd) {
1573
+ const trimmed = pasted.trim();
1574
+ if (trimmed.startsWith("data:image/")) {
1575
+ const commaIdx = trimmed.indexOf(",");
1576
+ if (commaIdx !== -1 && trimmed.slice(0, commaIdx).includes(";base64")) {
1577
+ const header = trimmed.slice(0, commaIdx);
1578
+ const b64 = trimmed.slice(commaIdx + 1);
1579
+ const mediaType = header.split(";")[0]?.slice(5) ?? "image/png";
1580
+ return {
1581
+ attachment: { data: b64, mediaType },
1582
+ label: `[image: ${mediaType}]`
1583
+ };
1584
+ }
1585
+ }
1586
+ if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
1587
+ const filePath = trimmed.startsWith("/") ? trimmed : join2(cwd, trimmed);
1588
+ const attachment = await loadImageFile(filePath);
1589
+ if (attachment) {
1590
+ const name = filePath.split("/").pop() ?? trimmed;
1591
+ return { attachment, label: `[image: ${name}]` };
1592
+ }
1593
+ }
1594
+ return null;
1595
+ }
1596
+ var _stdinReader = null;
1597
+ function getStdinReader() {
1598
+ if (!_stdinReader) {
1599
+ const stream = new ReadableStream({
1600
+ start(controller) {
1601
+ process.stdin.on("data", (chunk) => {
1602
+ try {
1603
+ controller.enqueue(new Uint8Array(chunk));
1604
+ } catch {}
1605
+ });
1606
+ process.stdin.once("end", () => {
1607
+ try {
1608
+ controller.close();
1609
+ } catch {}
1610
+ _stdinReader = null;
1611
+ });
1612
+ }
1613
+ });
1614
+ _stdinReader = stream.getReader();
1615
+ }
1616
+ return _stdinReader;
1617
+ }
1618
+ async function readKey(reader) {
1619
+ const { value, done } = await reader.read();
1620
+ if (done || !value)
1621
+ return "";
1622
+ return new TextDecoder().decode(value);
1623
+ }
1624
+ var PASTE_SENTINEL = "\x00PASTE\x00";
1625
+ var PASTE_SENTINEL_LEN = PASTE_SENTINEL.length;
1626
+ function pasteLabel(text) {
1627
+ const lines = text.split(`
1628
+ `);
1629
+ const first = lines[0] ?? "";
1630
+ const preview = first.length > 40 ? `${first.slice(0, 40)}\u2026` : first;
1631
+ const extra = lines.length - 1;
1632
+ const more = extra > 0 ? ` +${extra} more line${extra === 1 ? "" : "s"}` : "";
1633
+ return `[pasted: "${preview}"${more}]`;
1634
+ }
1635
+ var PROMPT = c4.green("\u25B6 ");
1636
+ var PROMPT_PLAN = c4.yellow("\u2B22 ");
1637
+ var PROMPT_RALPH = c4.magenta("\u21BB ");
1638
+ var PROMPT_RAW_LEN = 2;
1639
+ async function readline(opts) {
1640
+ const cwd = opts.cwd ?? process.cwd();
1641
+ const history = getPromptHistory(200);
1642
+ let histIdx = history.length;
1643
+ let buf = "";
1644
+ let cursor = 0;
1645
+ let savedInput = "";
1646
+ let searchMode = false;
1647
+ let searchQuery = "";
1648
+ let pasteBuffer = null;
1649
+ const imageAttachments = [];
1650
+ process.stdin.setRawMode(true);
1651
+ process.stdin.resume();
1652
+ process.stdout.write(BPASTE_ENABLE);
1653
+ const reader = getStdinReader();
1654
+ function renderPrompt() {
1655
+ const cols = process.stdout.columns ?? 80;
1656
+ const visualBuf = (pasteBuffer ? buf.replace(PASTE_SENTINEL, c4.dim(pasteLabel(pasteBuffer))) : buf).replace(/\[image: [^\]]+\]/g, (m) => c4.dim(c4.cyan(m)));
1657
+ const visualCursor = pasteBuffer ? (() => {
1658
+ const sentinelPos = buf.indexOf(PASTE_SENTINEL);
1659
+ if (sentinelPos === -1 || cursor <= sentinelPos)
1660
+ return cursor;
1661
+ return cursor - PASTE_SENTINEL_LEN + pasteLabel(pasteBuffer).length;
1662
+ })() : cursor;
1663
+ const display = visualBuf.length > cols - PROMPT_RAW_LEN - 2 ? `\u2026${visualBuf.slice(-(cols - PROMPT_RAW_LEN - 3))}` : visualBuf;
1664
+ const prompt = opts.planMode ? PROMPT_PLAN : opts.ralphMode ? PROMPT_RALPH : PROMPT;
1665
+ process.stdout.write(`${CLEAR_LINE}${prompt}${display}${CSI}${PROMPT_RAW_LEN + visualCursor + 1}G`);
1666
+ }
1667
+ function renderSearchPrompt() {
1668
+ process.stdout.write(`${CLEAR_LINE}${c4.cyan("search:")} ${searchQuery}\u2588`);
1669
+ }
1670
+ function applyHistory() {
1671
+ if (histIdx < history.length) {
1672
+ buf = history[histIdx] ?? "";
1673
+ } else {
1674
+ buf = savedInput;
1675
+ }
1676
+ cursor = buf.length;
1677
+ renderPrompt();
1678
+ }
1679
+ renderPrompt();
1680
+ try {
1681
+ while (true) {
1682
+ const raw = await readKey(reader);
1683
+ if (!raw)
1684
+ continue;
1685
+ if (searchMode) {
1686
+ if (raw === ESC) {
1687
+ searchMode = false;
1688
+ searchQuery = "";
1689
+ renderPrompt();
1690
+ continue;
1691
+ }
1692
+ if (raw === ENTER || raw === NEWLINE) {
1693
+ searchMode = false;
1694
+ cursor = buf.length;
1695
+ renderPrompt();
1696
+ continue;
1697
+ }
1698
+ if (raw === BACKSPACE) {
1699
+ searchQuery = searchQuery.slice(0, -1);
1700
+ } else if (raw >= " ") {
1701
+ searchQuery += raw;
1702
+ }
1703
+ if (searchQuery) {
1704
+ const found = [...history].reverse().find((h) => h.includes(searchQuery));
1705
+ if (found) {
1706
+ buf = found;
1707
+ cursor = buf.length;
1708
+ }
1709
+ }
1710
+ renderSearchPrompt();
1711
+ continue;
1712
+ }
1713
+ if (raw.includes(BPASTE_START)) {
1714
+ let accumulated = raw.slice(raw.indexOf(BPASTE_START) + BPASTE_START.length);
1715
+ while (!accumulated.includes(BPASTE_END)) {
1716
+ const next = await readKey(reader);
1717
+ if (!next)
1718
+ break;
1719
+ accumulated += next;
1720
+ }
1721
+ const endIdx = accumulated.indexOf(BPASTE_END);
1722
+ const pasted = endIdx !== -1 ? accumulated.slice(0, endIdx) : accumulated;
1723
+ const imageResult = await tryExtractImageFromPaste(pasted, cwd);
1724
+ if (imageResult) {
1725
+ imageAttachments.push(imageResult.attachment);
1726
+ buf = buf.slice(0, cursor) + imageResult.label + buf.slice(cursor);
1727
+ cursor += imageResult.label.length;
1728
+ renderPrompt();
1729
+ continue;
1730
+ }
1731
+ pasteBuffer = pasted;
1732
+ buf = buf.slice(0, cursor) + PASTE_SENTINEL + buf.slice(cursor);
1733
+ cursor += PASTE_SENTINEL_LEN;
1734
+ renderPrompt();
1735
+ continue;
1736
+ }
1737
+ if (raw.startsWith(ESC)) {
1738
+ if (raw === `${CSI}A` || raw === `${ESC}[A`) {
1739
+ if (histIdx === history.length)
1740
+ savedInput = buf;
1741
+ if (histIdx > 0) {
1742
+ histIdx--;
1743
+ applyHistory();
1744
+ }
1745
+ continue;
1746
+ }
1747
+ if (raw === `${CSI}B` || raw === `${ESC}[B`) {
1748
+ if (histIdx < history.length) {
1749
+ histIdx++;
1750
+ applyHistory();
1751
+ }
1752
+ continue;
1753
+ }
1754
+ if (raw === `${CSI}C` || raw === `${ESC}[C`) {
1755
+ if (cursor < buf.length) {
1756
+ cursor++;
1757
+ renderPrompt();
1758
+ }
1759
+ continue;
1760
+ }
1761
+ if (raw === `${CSI}D` || raw === `${ESC}[D`) {
1762
+ if (cursor > 0) {
1763
+ cursor--;
1764
+ renderPrompt();
1765
+ }
1766
+ continue;
1767
+ }
1768
+ if (raw === `${ESC}b` || raw === `${ESC}[1;3D`) {
1769
+ while (cursor > 0 && buf[cursor - 1] === " ")
1770
+ cursor--;
1771
+ while (cursor > 0 && buf[cursor - 1] !== " ")
1772
+ cursor--;
1773
+ renderPrompt();
1774
+ continue;
1775
+ }
1776
+ if (raw === `${ESC}f` || raw === `${ESC}[1;3C`) {
1777
+ while (cursor < buf.length && buf[cursor] === " ")
1778
+ cursor++;
1779
+ while (cursor < buf.length && buf[cursor] !== " ")
1780
+ cursor++;
1781
+ renderPrompt();
1782
+ continue;
1783
+ }
1784
+ if (raw === ESC) {
1785
+ process.stdout.write(`
1786
+ `);
1787
+ return { type: "interrupt" };
1788
+ }
1789
+ continue;
1790
+ }
1791
+ if (raw === CTRL_C) {
1792
+ process.stdout.write(`
1793
+ `);
1794
+ return { type: "interrupt" };
1795
+ }
1796
+ if (raw === CTRL_D) {
1797
+ process.stdout.write(`
1798
+ `);
1799
+ return { type: "eof" };
1800
+ }
1801
+ if (raw === CTRL_A) {
1802
+ cursor = 0;
1803
+ renderPrompt();
1804
+ continue;
1805
+ }
1806
+ if (raw === CTRL_E) {
1807
+ cursor = buf.length;
1808
+ renderPrompt();
1809
+ continue;
1810
+ }
1811
+ if (raw === CTRL_W) {
1812
+ const end = cursor;
1813
+ while (cursor > 0 && buf[cursor - 1] === " ")
1814
+ cursor--;
1815
+ while (cursor > 0 && buf[cursor - 1] !== " ")
1816
+ cursor--;
1817
+ buf = buf.slice(0, cursor) + buf.slice(end);
1818
+ if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
1819
+ pasteBuffer = null;
1820
+ renderPrompt();
1821
+ continue;
1822
+ }
1823
+ if (raw === CTRL_U) {
1824
+ buf = buf.slice(cursor);
1825
+ cursor = 0;
1826
+ if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
1827
+ pasteBuffer = null;
1828
+ renderPrompt();
1829
+ continue;
1830
+ }
1831
+ if (raw === CTRL_K) {
1832
+ buf = buf.slice(0, cursor);
1833
+ if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
1834
+ pasteBuffer = null;
1835
+ renderPrompt();
1836
+ continue;
1837
+ }
1838
+ if (raw === CTRL_L) {
1839
+ process.stdout.write("\x1B[2J\x1B[H");
1840
+ renderPrompt();
1841
+ continue;
1842
+ }
1843
+ if (raw === CTRL_R) {
1844
+ searchMode = true;
1845
+ searchQuery = "";
1846
+ renderSearchPrompt();
1847
+ continue;
1848
+ }
1849
+ if (raw === BACKSPACE) {
1850
+ if (cursor > 0) {
1851
+ buf = buf.slice(0, cursor - 1) + buf.slice(cursor);
1852
+ cursor--;
1853
+ if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
1854
+ pasteBuffer = null;
1855
+ renderPrompt();
1856
+ }
1857
+ continue;
1858
+ }
1859
+ if (raw === TAB) {
1860
+ const beforeCursor = buf.slice(0, cursor);
1861
+ const atMatch = beforeCursor.match(/@(\S*)$/);
1862
+ if (atMatch) {
1863
+ const completions = await getFileCompletions(atMatch[0], cwd);
1864
+ if (completions.length === 1 && completions[0]) {
1865
+ const replacement = completions[0];
1866
+ buf = buf.slice(0, cursor - (atMatch[0] ?? "").length) + replacement + buf.slice(cursor);
1867
+ cursor = cursor - (atMatch[0] ?? "").length + replacement.length;
1868
+ renderPrompt();
1869
+ } else if (completions.length > 1) {
1870
+ process.stdout.write(`
1871
+ `);
1872
+ for (const c5 of completions)
1873
+ process.stdout.write(` ${c5}
1874
+ `);
1875
+ renderPrompt();
1876
+ }
1877
+ }
1878
+ continue;
1879
+ }
1880
+ if (raw === ENTER || raw === NEWLINE) {
1881
+ const expanded = pasteBuffer ? buf.replace(PASTE_SENTINEL, pasteBuffer) : buf;
1882
+ pasteBuffer = null;
1883
+ const text = expanded.trim();
1884
+ process.stdout.write(`
1885
+ `);
1886
+ buf = "";
1887
+ cursor = 0;
1888
+ histIdx = history.length + (text ? 1 : 0);
1889
+ if (!text && !imageAttachments.length) {
1890
+ renderPrompt();
1891
+ continue;
1892
+ }
1893
+ const cleanText = imageAttachments.length ? text.replace(/\[image: [^\]]+\]/g, "").trim() : text;
1894
+ addPromptHistory(cleanText);
1895
+ if (cleanText.startsWith("/")) {
1896
+ const spaceIdx = cleanText.indexOf(" ");
1897
+ const command = spaceIdx === -1 ? cleanText.slice(1) : cleanText.slice(1, spaceIdx);
1898
+ const args = spaceIdx === -1 ? "" : cleanText.slice(spaceIdx + 1).trim();
1899
+ return { type: "command", command, args };
1900
+ }
1901
+ if (cleanText.startsWith("!")) {
1902
+ return { type: "shell", command: cleanText.slice(1).trim() };
1903
+ }
1904
+ return {
1905
+ type: "submit",
1906
+ text: cleanText,
1907
+ images: imageAttachments
1908
+ };
1909
+ }
1910
+ if (raw.length > 1) {
1911
+ const pasted = raw.replace(/\r?\n$/, "");
1912
+ const imageResult = await tryExtractImageFromPaste(pasted, cwd);
1913
+ if (imageResult) {
1914
+ imageAttachments.push(imageResult.attachment);
1915
+ buf = buf.slice(0, cursor) + imageResult.label + buf.slice(cursor);
1916
+ cursor += imageResult.label.length;
1917
+ renderPrompt();
1918
+ continue;
1919
+ }
1920
+ pasteBuffer = pasted;
1921
+ buf = buf.slice(0, cursor) + PASTE_SENTINEL + buf.slice(cursor);
1922
+ cursor += PASTE_SENTINEL_LEN;
1923
+ renderPrompt();
1924
+ continue;
1925
+ }
1926
+ if (raw >= " " || raw === "\t") {
1927
+ buf = buf.slice(0, cursor) + raw + buf.slice(cursor);
1928
+ cursor++;
1929
+ renderPrompt();
1930
+ }
1931
+ }
1932
+ } finally {
1933
+ process.stdout.write(BPASTE_DISABLE);
1934
+ process.stdin.setRawMode(false);
1935
+ process.stdin.pause();
1936
+ }
1937
+ }
1938
+
1939
+ // src/llm-api/turn.ts
1940
+ import { dynamicTool, jsonSchema, stepCountIs, streamText } from "ai";
1941
+ import { z } from "zod";
1942
+ var MAX_STEPS = 50;
1943
+ function isZodSchema(s) {
1944
+ return s !== null && typeof s === "object" && "_def" in s;
1945
+ }
1946
+ function toCoreTool(def) {
1947
+ const schema = isZodSchema(def.schema) ? def.schema : jsonSchema(def.schema);
1948
+ return dynamicTool({
1949
+ description: def.description,
1950
+ inputSchema: schema,
1951
+ execute: async (input) => {
1952
+ try {
1953
+ return await def.execute(input);
1954
+ } catch (err) {
1955
+ throw err instanceof Error ? err : new Error(String(err));
1956
+ }
1957
+ }
1958
+ });
1959
+ }
1960
+ async function* runTurn(options) {
1961
+ const { model, messages, tools, systemPrompt, signal } = options;
1962
+ const toolSet = {};
1963
+ for (const def of tools) {
1964
+ toolSet[def.name] = toCoreTool(def);
1965
+ }
1966
+ let inputTokens = 0;
1967
+ let outputTokens = 0;
1968
+ let contextTokens = 0;
1969
+ try {
1970
+ const streamOpts = {
1971
+ model,
1972
+ messages,
1973
+ tools: toolSet,
1974
+ stopWhen: stepCountIs(MAX_STEPS),
1975
+ onStepFinish: (step) => {
1976
+ inputTokens += step.usage?.inputTokens ?? 0;
1977
+ outputTokens += step.usage?.outputTokens ?? 0;
1978
+ contextTokens = step.usage?.inputTokens ?? contextTokens;
1979
+ },
1980
+ ...systemPrompt ? { system: systemPrompt } : {},
1981
+ ...signal ? { abortSignal: signal } : {}
1982
+ };
1983
+ const result = streamText(streamOpts);
1984
+ for await (const chunk of result.fullStream) {
1985
+ if (signal?.aborted)
1986
+ break;
1987
+ const c5 = chunk;
1988
+ switch (c5.type) {
1989
+ case "text-delta": {
1990
+ const delta = typeof c5.text === "string" ? c5.text : typeof c5.textDelta === "string" ? c5.textDelta : "";
1991
+ yield {
1992
+ type: "text-delta",
1993
+ delta
1994
+ };
1995
+ break;
1996
+ }
1997
+ case "tool-call": {
1998
+ yield {
1999
+ type: "tool-call-start",
2000
+ toolCallId: String(c5.toolCallId ?? ""),
2001
+ toolName: String(c5.toolName ?? ""),
2002
+ args: c5.input ?? c5.args
2003
+ };
2004
+ break;
2005
+ }
2006
+ case "tool-result": {
2007
+ yield {
2008
+ type: "tool-result",
2009
+ toolCallId: String(c5.toolCallId ?? ""),
2010
+ toolName: String(c5.toolName ?? ""),
2011
+ result: "output" in c5 ? c5.output : ("result" in c5) ? c5.result : undefined,
2012
+ isError: false
2013
+ };
2014
+ break;
2015
+ }
2016
+ case "tool-error": {
2017
+ yield {
2018
+ type: "tool-result",
2019
+ toolCallId: String(c5.toolCallId ?? ""),
2020
+ toolName: String(c5.toolName ?? ""),
2021
+ result: c5.error ?? "Tool execution failed",
2022
+ isError: true
2023
+ };
2024
+ break;
2025
+ }
2026
+ case "error": {
2027
+ const err = c5.error;
2028
+ throw err instanceof Error ? err : new Error(String(err));
2029
+ }
2030
+ }
2031
+ }
2032
+ const finalResponse = await result.response;
2033
+ const newMessages = finalResponse?.messages ?? [];
2034
+ yield {
2035
+ type: "turn-complete",
2036
+ inputTokens,
2037
+ outputTokens,
2038
+ contextTokens,
2039
+ messages: newMessages
2040
+ };
2041
+ } catch (err) {
2042
+ yield {
2043
+ type: "turn-error",
2044
+ error: err instanceof Error ? err : new Error(String(err))
2045
+ };
2046
+ }
2047
+ }
2048
+
2049
+ // src/mcp/client.ts
2050
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2051
+ import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
2052
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
2053
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
2054
+ async function connectMcpServer(config) {
2055
+ const client = new Client({ name: "mini-coder", version: "0.1.0" });
2056
+ if (config.transport === "http") {
2057
+ if (!config.url) {
2058
+ throw new Error(`MCP server "${config.name}" requires a url`);
2059
+ }
2060
+ const url = new URL(config.url);
2061
+ let transport;
2062
+ try {
2063
+ const streamable = new StreamableHTTPClientTransport(url);
2064
+ transport = streamable;
2065
+ await client.connect(transport);
2066
+ } catch {
2067
+ transport = new SSEClientTransport(url);
2068
+ await client.connect(transport);
2069
+ }
2070
+ } else if (config.transport === "stdio") {
2071
+ if (!config.command) {
2072
+ throw new Error(`MCP server "${config.name}" requires a command`);
2073
+ }
2074
+ const stdioParams = config.env ? { command: config.command, args: config.args ?? [], env: config.env } : { command: config.command, args: config.args ?? [] };
2075
+ const transport = new StdioClientTransport(stdioParams);
2076
+ await client.connect(transport);
2077
+ } else {
2078
+ throw new Error(`Unknown MCP transport: ${config.transport}`);
2079
+ }
2080
+ const { tools: mcpTools } = await client.listTools();
2081
+ const tools = mcpTools.map((t) => ({
2082
+ name: `mcp_${config.name}_${t.name}`,
2083
+ description: `[MCP:${config.name}] ${t.description ?? t.name}`,
2084
+ schema: t.inputSchema,
2085
+ execute: async (input) => {
2086
+ const result = await client.callTool({
2087
+ name: t.name,
2088
+ arguments: input
2089
+ });
2090
+ if (result.isError) {
2091
+ const content = result.content;
2092
+ const errText = content.filter((b) => b.type === "text").map((b) => b.text ?? "").join(`
2093
+ `);
2094
+ throw new Error(errText || "MCP tool returned an error");
2095
+ }
2096
+ return result.content;
2097
+ }
2098
+ }));
2099
+ return {
2100
+ name: config.name,
2101
+ tools,
2102
+ close: () => client.close()
2103
+ };
2104
+ }
2105
+
2106
+ // src/tools/snapshot.ts
2107
+ import { readFileSync, unlinkSync as unlinkSync2 } from "fs";
2108
+ import { join as join3 } from "path";
2109
+ async function gitBytes(args, cwd) {
2110
+ try {
2111
+ const proc = Bun.spawn(["git", ...args], {
2112
+ cwd,
2113
+ stdout: "pipe",
2114
+ stderr: "pipe"
2115
+ });
2116
+ const [bytes] = await Promise.all([
2117
+ new Response(proc.stdout).bytes(),
2118
+ new Response(proc.stderr).bytes()
2119
+ ]);
2120
+ const code = await proc.exited;
2121
+ return { bytes, code };
2122
+ } catch {
2123
+ return { bytes: new Uint8Array, code: -1 };
2124
+ }
2125
+ }
2126
+ async function git(args, cwd) {
2127
+ const { bytes, code } = await gitBytes(args, cwd);
2128
+ return { stdout: new TextDecoder().decode(bytes), code };
2129
+ }
2130
+ async function getStatusEntries(repoRoot) {
2131
+ const status = await git(["status", "--porcelain", "-z", "-u"], repoRoot);
2132
+ if (status.code !== 0)
2133
+ return null;
2134
+ const raw = status.stdout;
2135
+ if (raw === "")
2136
+ return [];
2137
+ const entries = [];
2138
+ const parts = raw.split("\x00");
2139
+ let i = 0;
2140
+ while (i < parts.length) {
2141
+ const entry = parts[i];
2142
+ i++;
2143
+ if (!entry || entry.length < 4)
2144
+ continue;
2145
+ const xy = entry.slice(0, 2);
2146
+ const filePath = entry.slice(3);
2147
+ const x = xy[0] ?? " ";
2148
+ const y = xy[1] ?? " ";
2149
+ if (x === "R" || x === "C") {
2150
+ const oldPath = parts[i];
2151
+ i++;
2152
+ if (oldPath) {
2153
+ entries.push({ path: oldPath, existsOnDisk: false, isNew: false });
2154
+ }
2155
+ if (filePath) {
2156
+ entries.push({ path: filePath, existsOnDisk: true, isNew: true });
2157
+ }
2158
+ continue;
2159
+ }
2160
+ if (x === "D" || y === "D") {
2161
+ if (filePath)
2162
+ entries.push({ path: filePath, existsOnDisk: false, isNew: false });
2163
+ continue;
2164
+ }
2165
+ if (x === "?" && y === "?") {
2166
+ if (filePath)
2167
+ entries.push({ path: filePath, existsOnDisk: true, isNew: true });
2168
+ continue;
2169
+ }
2170
+ if (filePath)
2171
+ entries.push({ path: filePath, existsOnDisk: true, isNew: false });
2172
+ }
2173
+ const seen = new Set;
2174
+ return entries.filter((e) => {
2175
+ if (seen.has(e.path))
2176
+ return false;
2177
+ seen.add(e.path);
2178
+ return true;
2179
+ });
2180
+ }
2181
+ async function getRepoRoot(cwd) {
2182
+ const result = await git(["rev-parse", "--show-toplevel"], cwd);
2183
+ if (result.code !== 0)
2184
+ return null;
2185
+ return result.stdout.trim();
2186
+ }
2187
+ async function takeSnapshot(cwd, sessionId, turnIndex) {
2188
+ try {
2189
+ const repoRoot = await getRepoRoot(cwd);
2190
+ if (repoRoot === null)
2191
+ return false;
2192
+ const entries = await getStatusEntries(repoRoot);
2193
+ if (entries === null)
2194
+ return false;
2195
+ if (entries.length === 0)
2196
+ return false;
2197
+ const files = [];
2198
+ for (const entry of entries) {
2199
+ const absPath = join3(repoRoot, entry.path);
2200
+ if (!entry.existsOnDisk) {
2201
+ const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
2202
+ if (code === 0) {
2203
+ files.push({
2204
+ path: entry.path,
2205
+ content: bytes,
2206
+ existed: true
2207
+ });
2208
+ }
2209
+ continue;
2210
+ }
2211
+ if (entry.isNew) {
2212
+ try {
2213
+ const content = readFileSync(absPath);
2214
+ files.push({
2215
+ path: entry.path,
2216
+ content: new Uint8Array(content),
2217
+ existed: false
2218
+ });
2219
+ } catch {}
2220
+ continue;
2221
+ }
2222
+ try {
2223
+ const content = readFileSync(absPath);
2224
+ files.push({
2225
+ path: entry.path,
2226
+ content: new Uint8Array(content),
2227
+ existed: true
2228
+ });
2229
+ } catch {}
2230
+ }
2231
+ if (files.length === 0)
2232
+ return false;
2233
+ saveSnapshot(sessionId, turnIndex, files);
2234
+ return true;
2235
+ } catch {
2236
+ return false;
2237
+ }
2238
+ }
2239
+ async function restoreSnapshot(cwd, sessionId, turnIndex) {
2240
+ try {
2241
+ const files = loadSnapshot(sessionId, turnIndex);
2242
+ const repoRoot = await getRepoRoot(cwd);
2243
+ if (files.length === 0)
2244
+ return { restored: false, reason: "not-found" };
2245
+ const root = repoRoot ?? cwd;
2246
+ let anyFailed = false;
2247
+ for (const file of files) {
2248
+ const absPath = join3(root, file.path);
2249
+ if (!file.existed) {
2250
+ try {
2251
+ if (await Bun.file(absPath).exists()) {
2252
+ unlinkSync2(absPath);
2253
+ }
2254
+ } catch {
2255
+ anyFailed = true;
2256
+ }
2257
+ continue;
2258
+ }
2259
+ if (file.content !== null) {
2260
+ try {
2261
+ await Bun.write(absPath, file.content);
2262
+ } catch {
2263
+ anyFailed = true;
2264
+ }
2265
+ }
2266
+ }
2267
+ if (!anyFailed) {
2268
+ deleteSnapshot(sessionId, turnIndex);
2269
+ }
2270
+ return anyFailed ? { restored: false, reason: "error" } : { restored: true };
2271
+ } catch {
2272
+ return { restored: false, reason: "error" };
2273
+ }
2274
+ }
2275
+
2276
+ // src/session/manager.ts
2277
+ import { homedir as homedir3 } from "os";
2278
+ import * as c5 from "yoctocolors";
2279
+ function newSession(model, cwd) {
2280
+ const id = generateSessionId();
2281
+ createSession({ id, cwd, model });
2282
+ return { id, model, messages: [] };
2283
+ }
2284
+ function resumeSession(id) {
2285
+ const row = getSession(id);
2286
+ if (!row)
2287
+ return null;
2288
+ const messages = loadMessages(id);
2289
+ return { id: row.id, model: row.model, messages };
2290
+ }
2291
+ function touchActiveSession(session) {
2292
+ touchSession(session.id, session.model);
2293
+ }
2294
+ function printSessionList() {
2295
+ const sessions = listSessions(20);
2296
+ if (sessions.length === 0) {
2297
+ writeln(c5.dim("No sessions found."));
2298
+ return;
2299
+ }
2300
+ writeln(`
2301
+ ${c5.bold("Recent sessions:")}`);
2302
+ for (const s of sessions) {
2303
+ const date = new Date(s.updated_at).toLocaleString();
2304
+ const cwd = s.cwd.startsWith(homedir3()) ? `~${s.cwd.slice(homedir3().length)}` : s.cwd;
2305
+ const title = s.title || c5.dim("(untitled)");
2306
+ writeln(` ${c5.dim(s.id.padEnd(14))} ${title.padEnd(30)} ${c5.cyan(s.model.split("/").pop() ?? s.model).padEnd(20)} ${c5.dim(cwd)} ${c5.dim(date)}`);
2307
+ }
2308
+ writeln(`
2309
+ ${c5.dim("Use")} mc --resume <id> ${c5.dim("to continue a session.")}`);
2310
+ }
2311
+ function getMostRecentSession() {
2312
+ const sessions = listSessions(1);
2313
+ return sessions[0] ?? null;
2314
+ }
2315
+
2316
+ // src/tools/create.ts
2317
+ import { existsSync as existsSync2, mkdirSync as mkdirSync2 } from "fs";
2318
+ import { dirname, join as join4, relative as relative2 } from "path";
2319
+ import { z as z2 } from "zod";
2320
+
2321
+ // src/tools/diff.ts
2322
+ function generateDiff(filePath, before, after) {
2323
+ const beforeLines = before === "" ? [] : before.split(`
2324
+ `);
2325
+ const afterLines = after === "" ? [] : after.split(`
2326
+ `);
2327
+ const diff = computeUnifiedDiff(beforeLines, afterLines);
2328
+ if (diff.length === 0)
2329
+ return "(no changes)";
2330
+ return `--- ${filePath}
2331
+ +++ ${filePath}
2332
+ ${diff}`;
2333
+ }
2334
+ function lcsTable(before, after) {
2335
+ const m = before.length;
2336
+ const n = after.length;
2337
+ const w = n + 1;
2338
+ const dp = new Array((m + 1) * w).fill(0);
2339
+ for (let i = 1;i <= m; i++) {
2340
+ for (let j = 1;j <= n; j++) {
2341
+ if (before[i - 1] === after[j - 1]) {
2342
+ dp[i * w + j] = (dp[(i - 1) * w + (j - 1)] ?? 0) + 1;
2343
+ } else {
2344
+ dp[i * w + j] = Math.max(dp[(i - 1) * w + j] ?? 0, dp[i * w + (j - 1)] ?? 0);
2345
+ }
2346
+ }
2347
+ }
2348
+ return dp;
2349
+ }
2350
+ function editScript(before, after) {
2351
+ const n = after.length;
2352
+ const w = n + 1;
2353
+ const dp = lcsTable(before, after);
2354
+ const ops = [];
2355
+ let i = before.length;
2356
+ let j = after.length;
2357
+ while (i > 0 || j > 0) {
2358
+ const bLine = before[i - 1] ?? "";
2359
+ const aLine = after[j - 1] ?? "";
2360
+ if (i > 0 && j > 0 && bLine === aLine) {
2361
+ ops.push({ kind: "eq", line: bLine });
2362
+ i--;
2363
+ j--;
2364
+ } else if (j > 0 && (i === 0 || (dp[i * w + (j - 1)] ?? 0) >= (dp[(i - 1) * w + j] ?? 0))) {
2365
+ ops.push({ kind: "ins", line: aLine });
2366
+ j--;
2367
+ } else {
2368
+ ops.push({ kind: "del", line: bLine });
2369
+ i--;
2370
+ }
2371
+ }
2372
+ ops.reverse();
2373
+ return ops;
2374
+ }
2375
+ function computeUnifiedDiff(before, after) {
2376
+ const CONTEXT = 3;
2377
+ const ops = editScript(before, after);
2378
+ const indexed = [];
2379
+ let bi = 0;
2380
+ let ai = 0;
2381
+ for (const op of ops) {
2382
+ indexed.push({ ...op, bi, ai });
2383
+ if (op.kind === "eq" || op.kind === "del")
2384
+ bi++;
2385
+ if (op.kind === "eq" || op.kind === "ins")
2386
+ ai++;
2387
+ }
2388
+ const changedIdx = [];
2389
+ for (let k = 0;k < indexed.length; k++) {
2390
+ if (indexed[k]?.kind !== "eq")
2391
+ changedIdx.push(k);
2392
+ }
2393
+ if (changedIdx.length === 0)
2394
+ return "";
2395
+ const groups = [];
2396
+ let groupStart = changedIdx[0] ?? 0;
2397
+ let groupEnd = changedIdx[0] ?? 0;
2398
+ for (let k = 1;k < changedIdx.length; k++) {
2399
+ const prev = changedIdx[k - 1] ?? 0;
2400
+ const cur = changedIdx[k] ?? 0;
2401
+ let equalCount = 0;
2402
+ for (let x = prev + 1;x < cur; x++) {
2403
+ if (indexed[x]?.kind === "eq")
2404
+ equalCount++;
2405
+ }
2406
+ if (equalCount > 2 * CONTEXT) {
2407
+ groups.push([groupStart, groupEnd]);
2408
+ groupStart = cur;
2409
+ }
2410
+ groupEnd = cur;
2411
+ }
2412
+ groups.push([groupStart, groupEnd]);
2413
+ const hunks = [];
2414
+ for (const [gStart, gEnd] of groups) {
2415
+ const opStart = Math.max(0, gStart - CONTEXT);
2416
+ const opEnd = Math.min(indexed.length - 1, gEnd + CONTEXT);
2417
+ const lines = [];
2418
+ let beforeCount = 0;
2419
+ let afterCount = 0;
2420
+ for (let x = opStart;x <= opEnd; x++) {
2421
+ const op = indexed[x];
2422
+ if (!op)
2423
+ continue;
2424
+ if (op.kind === "eq") {
2425
+ lines.push(` ${op.line}`);
2426
+ beforeCount++;
2427
+ afterCount++;
2428
+ } else if (op.kind === "del") {
2429
+ lines.push(`-${op.line}`);
2430
+ beforeCount++;
2431
+ } else {
2432
+ lines.push(`+${op.line}`);
2433
+ afterCount++;
2434
+ }
2435
+ }
2436
+ const firstOp = indexed[opStart];
2437
+ const beforeStart = beforeCount === 0 ? 0 : (firstOp?.bi ?? 0) + 1;
2438
+ const afterStart = afterCount === 0 ? 0 : (firstOp?.ai ?? 0) + 1;
2439
+ hunks.push(`@@ -${beforeStart},${beforeCount} +${afterStart},${afterCount} @@
2440
+ ${lines.join(`
2441
+ `)}`);
2442
+ }
2443
+ return hunks.join(`
2444
+ `);
2445
+ }
2446
+
2447
+ // src/tools/create.ts
2448
+ var CreateSchema = z2.object({
2449
+ path: z2.string().describe("File path to write (absolute or relative to cwd)"),
2450
+ content: z2.string().describe("Full content to write to the file")
2451
+ });
2452
+ var createTool = {
2453
+ name: "create",
2454
+ description: "Create a new file or fully overwrite an existing file with the given content. " + "Use this only for new files. " + "For targeted line edits on existing files, **use `replace` or `insert` instead**.",
2455
+ schema: CreateSchema,
2456
+ execute: async (input) => {
2457
+ const cwd = input.cwd ?? process.cwd();
2458
+ const filePath = input.path.startsWith("/") ? input.path : join4(cwd, input.path);
2459
+ const relPath = relative2(cwd, filePath);
2460
+ const dir = dirname(filePath);
2461
+ if (!existsSync2(dir))
2462
+ mkdirSync2(dir, { recursive: true });
2463
+ const file = Bun.file(filePath);
2464
+ const created = !await file.exists();
2465
+ const before = created ? "" : await file.text();
2466
+ await Bun.write(filePath, input.content);
2467
+ const diff = generateDiff(relPath, before, input.content);
2468
+ return { path: relPath, diff, created };
2469
+ }
2470
+ };
2471
+
2472
+ // src/tools/glob.ts
2473
+ import { join as join5, relative as relative3 } from "path";
2474
+ import { z as z3 } from "zod";
2475
+ var GlobSchema = z3.object({
2476
+ pattern: z3.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
2477
+ ignore: z3.array(z3.string()).optional().describe("Glob patterns to exclude")
2478
+ });
2479
+ var MAX_RESULTS = 500;
2480
+ var globTool = {
2481
+ name: "glob",
2482
+ description: "Find files matching a glob pattern. Returns relative paths sorted by modification time. " + "Use this to discover files before reading them.",
2483
+ schema: GlobSchema,
2484
+ execute: async (input) => {
2485
+ const cwd = input.cwd ?? process.cwd();
2486
+ const defaultIgnore = [
2487
+ "node_modules/**",
2488
+ ".git/**",
2489
+ "dist/**",
2490
+ "*.db",
2491
+ "*.db-shm",
2492
+ "*.db-wal"
2493
+ ];
2494
+ const ignorePatterns = [...defaultIgnore, ...input.ignore ?? []];
2495
+ const glob = new Bun.Glob(input.pattern);
2496
+ const matches = [];
2497
+ for await (const file of glob.scan({ cwd, onlyFiles: true })) {
2498
+ const ignored = ignorePatterns.some((pat) => {
2499
+ const ig = new Bun.Glob(pat);
2500
+ return ig.match(file);
2501
+ });
2502
+ if (ignored)
2503
+ continue;
2504
+ try {
2505
+ const fullPath = join5(cwd, file);
2506
+ const stat = await Bun.file(fullPath).stat?.() ?? null;
2507
+ matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
2508
+ } catch {
2509
+ matches.push({ path: file, mtime: 0 });
2510
+ }
2511
+ if (matches.length >= MAX_RESULTS + 1)
2512
+ break;
2513
+ }
2514
+ const truncated = matches.length > MAX_RESULTS;
2515
+ if (truncated)
2516
+ matches.pop();
2517
+ matches.sort((a, b) => b.mtime - a.mtime);
2518
+ const files = matches.map((m) => relative3(cwd, join5(cwd, m.path)));
2519
+ return { files, count: files.length, truncated };
2520
+ }
2521
+ };
2522
+
2523
+ // src/tools/grep.ts
2524
+ import { join as join6 } from "path";
2525
+ import { z as z4 } from "zod";
2526
+
2527
+ // src/tools/hashline.ts
2528
+ var FNV_OFFSET_BASIS = 2166136261;
2529
+ var FNV_PRIME = 16777619;
2530
+ var HASH_SCAN_RANGE = 10;
2531
+ function hashLine(content) {
2532
+ let hash = FNV_OFFSET_BASIS;
2533
+ for (let i = 0;i < content.length; i++) {
2534
+ hash ^= content.charCodeAt(i);
2535
+ hash = hash * FNV_PRIME >>> 0;
2536
+ }
2537
+ const twoHex = (hash & 255).toString(16).padStart(2, "0");
2538
+ return twoHex;
2539
+ }
2540
+ function formatHashLine(lineNum, content) {
2541
+ return `${lineNum}:${hashLine(content)}| ${content}`;
2542
+ }
2543
+ function findLineByHash(lines, hash, hintLine) {
2544
+ const normalized = hash.toLowerCase();
2545
+ const hintIdx = hintLine - 1;
2546
+ const foundMatches = [];
2547
+ const matches = (idx) => {
2548
+ const line = lines[idx] ?? "";
2549
+ return hashLine(line) === normalized;
2550
+ };
2551
+ if (hintIdx >= 0 && hintIdx < lines.length) {
2552
+ if (matches(hintIdx))
2553
+ return hintIdx + 1;
2554
+ }
2555
+ for (let offset = 1;offset <= HASH_SCAN_RANGE; offset++) {
2556
+ const lower = hintIdx - offset;
2557
+ if (lower >= 0 && lower < lines.length && matches(lower)) {
2558
+ foundMatches.push(lower + 1);
2559
+ }
2560
+ const higher = hintIdx + offset;
2561
+ if (higher >= 0 && higher < lines.length && matches(higher)) {
2562
+ foundMatches.push(higher + 1);
2563
+ }
2564
+ }
2565
+ if (foundMatches.length === 1)
2566
+ return foundMatches[0] ?? null;
2567
+ return null;
2568
+ }
2569
+
2570
+ // src/tools/grep.ts
2571
+ var GrepSchema = z4.object({
2572
+ pattern: z4.string().describe("Regular expression to search for"),
2573
+ include: z4.string().optional().describe("Glob pattern to filter files, e.g. '*.ts' or '*.{ts,tsx}'"),
2574
+ contextLines: z4.number().int().min(0).max(10).optional().default(2).describe("Lines of context to include around each match"),
2575
+ caseSensitive: z4.boolean().optional().default(true),
2576
+ maxResults: z4.number().int().min(1).max(200).optional().default(50)
2577
+ });
2578
+ var DEFAULT_IGNORE = [
2579
+ "node_modules",
2580
+ ".git",
2581
+ "dist",
2582
+ "*.db",
2583
+ "*.db-shm",
2584
+ "*.db-wal",
2585
+ "bun.lock"
2586
+ ];
2587
+ var grepTool = {
2588
+ name: "grep",
2589
+ description: "Search for a regex pattern across files. Returns file paths, line numbers, and context. " + "Use this to find code patterns, function definitions, or specific text.",
2590
+ schema: GrepSchema,
2591
+ execute: async (input) => {
2592
+ const cwd = input.cwd ?? process.cwd();
2593
+ const flags = input.caseSensitive ? "" : "i";
2594
+ const regex = new RegExp(input.pattern, flags);
2595
+ const maxResults = input.maxResults ?? 50;
2596
+ const contextLines = input.contextLines ?? 2;
2597
+ const include = input.include ?? "**/*";
2598
+ const fileGlob = new Bun.Glob(include);
2599
+ const ignoreGlob = DEFAULT_IGNORE.map((p) => new Bun.Glob(p));
2600
+ const allMatches = [];
2601
+ let truncated = false;
2602
+ outer:
2603
+ for await (const relPath of fileGlob.scan({
2604
+ cwd,
2605
+ onlyFiles: true
2606
+ })) {
2607
+ if (ignoreGlob.some((g) => g.match(relPath) || g.match(relPath.split("/")[0] ?? ""))) {
2608
+ continue;
2609
+ }
2610
+ const fullPath = join6(cwd, relPath);
2611
+ let text;
2612
+ try {
2613
+ text = await Bun.file(fullPath).text();
2614
+ } catch {
2615
+ continue;
2616
+ }
2617
+ if (text.includes("\x00"))
2618
+ continue;
2619
+ const lines = text.split(`
2620
+ `);
2621
+ for (let i = 0;i < lines.length; i++) {
2622
+ const line = lines[i] ?? "";
2623
+ const match = regex.exec(line);
2624
+ if (!match)
2625
+ continue;
2626
+ const ctxStart = Math.max(0, i - contextLines);
2627
+ const ctxEnd = Math.min(lines.length - 1, i + contextLines);
2628
+ const context = [];
2629
+ for (let c6 = ctxStart;c6 <= ctxEnd; c6++) {
2630
+ context.push({
2631
+ line: c6 + 1,
2632
+ text: formatHashLine(c6 + 1, lines[c6] ?? ""),
2633
+ isMatch: c6 === i
2634
+ });
2635
+ }
2636
+ allMatches.push({
2637
+ file: relPath,
2638
+ line: i + 1,
2639
+ column: match.index + 1,
2640
+ text: formatHashLine(i + 1, line),
2641
+ context
2642
+ });
2643
+ if (allMatches.length >= maxResults + 1) {
2644
+ truncated = true;
2645
+ break outer;
2646
+ }
2647
+ }
2648
+ }
2649
+ if (truncated)
2650
+ allMatches.pop();
2651
+ return {
2652
+ matches: allMatches,
2653
+ totalMatches: allMatches.length,
2654
+ truncated
2655
+ };
2656
+ }
2657
+ };
2658
+
2659
+ // src/tools/hooks.ts
2660
+ import { constants, accessSync } from "fs";
2661
+ import { homedir as homedir4 } from "os";
2662
+ import { join as join7 } from "path";
2663
+ function isExecutable(filePath) {
2664
+ try {
2665
+ accessSync(filePath, constants.X_OK);
2666
+ return true;
2667
+ } catch {
2668
+ return false;
2669
+ }
2670
+ }
2671
+ function findHook(toolName, cwd) {
2672
+ const scriptName = `post-${toolName}`;
2673
+ const candidates = [
2674
+ join7(cwd, ".agents", "hooks", scriptName),
2675
+ join7(homedir4(), ".agents", "hooks", scriptName)
2676
+ ];
2677
+ for (const p of candidates) {
2678
+ if (isExecutable(p))
2679
+ return p;
2680
+ }
2681
+ return null;
2682
+ }
2683
+ function createHookCache(toolNames, cwd) {
2684
+ const cache = new Map;
2685
+ for (const name of toolNames) {
2686
+ cache.set(name, findHook(name, cwd));
2687
+ }
2688
+ return (toolName) => cache.get(toolName) ?? null;
2689
+ }
2690
+ async function runHook(scriptPath, env, cwd) {
2691
+ try {
2692
+ const proc = Bun.spawn(["bash", scriptPath], {
2693
+ cwd,
2694
+ env: { ...process.env, ...env },
2695
+ stdout: "ignore",
2696
+ stderr: "ignore"
2697
+ });
2698
+ const code = await proc.exited;
2699
+ return code === 0;
2700
+ } catch {
2701
+ return false;
2702
+ }
2703
+ }
2704
+ function hookEnvForCreate(result, cwd) {
2705
+ return {
2706
+ TOOL: "create",
2707
+ FILEPATH: result.path,
2708
+ DIFF: result.diff,
2709
+ CREATED: String(result.created),
2710
+ CWD: cwd
2711
+ };
2712
+ }
2713
+ function hookEnvForReplace(result, cwd) {
2714
+ return {
2715
+ TOOL: "replace",
2716
+ FILEPATH: result.path,
2717
+ DIFF: result.diff,
2718
+ DELETED: String(result.deleted),
2719
+ CWD: cwd
2720
+ };
2721
+ }
2722
+ function hookEnvForInsert(result, cwd) {
2723
+ return {
2724
+ TOOL: "insert",
2725
+ FILEPATH: result.path,
2726
+ DIFF: result.diff,
2727
+ CWD: cwd
2728
+ };
2729
+ }
2730
+ function hookEnvForShell(result, input, cwd) {
2731
+ return {
2732
+ TOOL: "shell",
2733
+ COMMAND: input.command,
2734
+ EXIT_CODE: String(result.exitCode),
2735
+ TIMED_OUT: String(result.timedOut),
2736
+ STDOUT: result.stdout,
2737
+ STDERR: result.stderr,
2738
+ CWD: cwd
2739
+ };
2740
+ }
2741
+ function hookEnvForGlob(input, cwd) {
2742
+ return {
2743
+ TOOL: "glob",
2744
+ PATTERN: input.pattern,
2745
+ CWD: cwd
2746
+ };
2747
+ }
2748
+ function hookEnvForGrep(input, cwd) {
2749
+ return {
2750
+ TOOL: "grep",
2751
+ PATTERN: input.pattern,
2752
+ CWD: cwd
2753
+ };
2754
+ }
2755
+ function hookEnvForRead(input, cwd) {
2756
+ return {
2757
+ TOOL: "read",
2758
+ FILEPATH: input.path,
2759
+ CWD: cwd
2760
+ };
2761
+ }
2762
+
2763
+ // src/tools/insert.ts
2764
+ import { join as join8, relative as relative4 } from "path";
2765
+ import { z as z5 } from "zod";
2766
+ var InsertSchema = z5.object({
2767
+ path: z5.string().describe("File path to edit (absolute or relative to cwd)"),
2768
+ anchor: z5.string().describe('Anchor line from a prior read/grep, e.g. "11:a3"'),
2769
+ position: z5.enum(["before", "after"]).describe('Insert the content "before" or "after" the anchor line'),
2770
+ content: z5.string().describe("Text to insert")
2771
+ });
2772
+ var HASH_NOT_FOUND_ERROR = "Hash not found. Re-read the file to get current anchors.";
2773
+ var insertTool = {
2774
+ name: "insert",
2775
+ description: "Insert new lines before or after an anchor line in an existing file. " + "The anchor line itself is not modified. " + 'Anchors come from the `read` or `grep` tools (format: "line:hash", e.g. "11:a3"). ' + "To replace or delete lines use `replace`. To create a file use `create`.",
2776
+ schema: InsertSchema,
2777
+ execute: async (input) => {
2778
+ const cwd = input.cwd ?? process.cwd();
2779
+ const filePath = input.path.startsWith("/") ? input.path : join8(cwd, input.path);
2780
+ const relPath = relative4(cwd, filePath);
2781
+ const file = Bun.file(filePath);
2782
+ if (!await file.exists()) {
2783
+ throw new Error(`File not found: "${relPath}". To create a new file use the \`create\` tool.`);
2784
+ }
2785
+ const parsed = parseAnchor(input.anchor);
2786
+ const original = await file.text();
2787
+ const lines = original.split(`
2788
+ `);
2789
+ const anchorLine = findLineByHash(lines, parsed.hash, parsed.line);
2790
+ if (!anchorLine)
2791
+ throw new Error(HASH_NOT_FOUND_ERROR);
2792
+ const insertAt = input.position === "before" ? anchorLine - 1 : anchorLine;
2793
+ const insertLines = input.content.split(`
2794
+ `);
2795
+ const updatedLines = [
2796
+ ...lines.slice(0, insertAt),
2797
+ ...insertLines,
2798
+ ...lines.slice(insertAt)
2799
+ ];
2800
+ const updated = updatedLines.join(`
2801
+ `);
2802
+ await Bun.write(filePath, updated);
2803
+ const diff = generateDiff(relPath, original, updated);
2804
+ return { path: relPath, diff };
2805
+ }
2806
+ };
2807
+ function parseAnchor(value) {
2808
+ const match = /^\s*(\d+):([0-9a-fA-F]{2})\s*$/.exec(value);
2809
+ if (!match) {
2810
+ throw new Error(`Invalid anchor. Expected format: "line:hh" (e.g. "11:a3").`);
2811
+ }
2812
+ const line = Number(match[1]);
2813
+ if (!Number.isInteger(line) || line < 1) {
2814
+ throw new Error("Invalid anchor line number.");
2815
+ }
2816
+ const hash = match[2];
2817
+ if (!hash) {
2818
+ throw new Error(`Invalid anchor. Expected format: "line:hh" (e.g. "11:a3").`);
2819
+ }
2820
+ return { line, hash: hash.toLowerCase() };
2821
+ }
2822
+
2823
+ // src/tools/read.ts
2824
+ import { join as join9, relative as relative5 } from "path";
2825
+ import { z as z6 } from "zod";
2826
+ var ReadSchema = z6.object({
2827
+ path: z6.string().describe("File path to read (absolute or relative to cwd)"),
2828
+ line: z6.number().int().min(1).optional().describe("1-indexed starting line (default: 1)"),
2829
+ count: z6.number().int().min(1).max(500).optional().describe("Lines to read (default: 500, max: 500)")
2830
+ });
2831
+ var MAX_COUNT = 500;
2832
+ var MAX_BYTES = 1e6;
2833
+ var readTool = {
2834
+ name: "read",
2835
+ description: "Read a file's contents. `line` sets the starting line (1-indexed, default 1). " + "`count` sets how many lines to read (default 500, max 500). " + "Check `truncated` and `totalLines` in the result to detect when more content exists; " + "paginate by incrementing `line`.",
2836
+ schema: ReadSchema,
2837
+ execute: async (input) => {
2838
+ const cwd = input.cwd ?? process.cwd();
2839
+ const filePath = input.path.startsWith("/") ? input.path : join9(cwd, input.path);
2840
+ const file = Bun.file(filePath);
2841
+ const exists = await file.exists();
2842
+ if (!exists) {
2843
+ throw new Error(`File not found: ${input.path}`);
2844
+ }
2845
+ const size = file.size;
2846
+ if (size > MAX_BYTES * 5) {
2847
+ throw new Error(`File too large (${Math.round(size / 1024)}KB). Use grep to search within it.`);
2848
+ }
2849
+ const raw = await file.text();
2850
+ const allLines = raw.split(`
2851
+ `);
2852
+ const totalLines = allLines.length;
2853
+ const startLine = input.line ?? 1;
2854
+ const count = Math.min(input.count ?? MAX_COUNT, MAX_COUNT);
2855
+ const clampedStart = Math.max(1, Math.min(startLine, totalLines));
2856
+ const endLine = Math.min(totalLines, clampedStart + count - 1);
2857
+ const truncated = endLine < totalLines;
2858
+ const selectedLines = allLines.slice(clampedStart - 1, endLine);
2859
+ const content = selectedLines.map((line, i) => formatHashLine(clampedStart + i, line)).join(`
2860
+ `);
2861
+ return {
2862
+ path: relative5(cwd, filePath),
2863
+ content,
2864
+ totalLines,
2865
+ line: clampedStart,
2866
+ truncated
2867
+ };
2868
+ }
2869
+ };
2870
+
2871
+ // src/tools/replace.ts
2872
+ import { join as join10, relative as relative6 } from "path";
2873
+ import { z as z7 } from "zod";
2874
+ var ReplaceSchema = z7.object({
2875
+ path: z7.string().describe("File path to edit (absolute or relative to cwd)"),
2876
+ startAnchor: z7.string().describe('Start anchor from a prior read/grep, e.g. "11:a3"'),
2877
+ endAnchor: z7.string().optional().describe('End anchor (inclusive), e.g. "33:0e". Omit to target only the startAnchor line.'),
2878
+ newContent: z7.string().optional().describe("Replacement text. Omit or pass empty string to delete the range.")
2879
+ });
2880
+ var HASH_NOT_FOUND_ERROR2 = "Hash not found. Re-read the file to get current anchors.";
2881
+ var replaceTool = {
2882
+ name: "replace",
2883
+ description: "Replace or delete a range of lines in an existing file using hashline anchors. " + 'Anchors come from the `read` or `grep` tools (format: "line:hash", e.g. "11:a3"). ' + "Provide startAnchor alone to target a single line, or add endAnchor for a range. " + "Set newContent to the replacement text, or omit it to delete the range. " + "To create a file use `create`. To insert without replacing any lines use `insert`.",
2884
+ schema: ReplaceSchema,
2885
+ execute: async (input) => {
2886
+ const cwd = input.cwd ?? process.cwd();
2887
+ const filePath = input.path.startsWith("/") ? input.path : join10(cwd, input.path);
2888
+ const relPath = relative6(cwd, filePath);
2889
+ const file = Bun.file(filePath);
2890
+ if (!await file.exists()) {
2891
+ throw new Error(`File not found: "${relPath}". To create a new file use the \`create\` tool.`);
2892
+ }
2893
+ const start = parseAnchor2(input.startAnchor, "startAnchor");
2894
+ const end = input.endAnchor ? parseAnchor2(input.endAnchor, "endAnchor") : null;
2895
+ if (end && end.line < start.line) {
2896
+ throw new Error("endAnchor line number must be >= startAnchor line number.");
2897
+ }
2898
+ const original = await file.text();
2899
+ const lines = original.split(`
2900
+ `);
2901
+ const startLine = findLineByHash(lines, start.hash, start.line);
2902
+ if (!startLine)
2903
+ throw new Error(HASH_NOT_FOUND_ERROR2);
2904
+ let endLine = startLine;
2905
+ if (end) {
2906
+ const resolvedEnd = findLineByHash(lines, end.hash, end.line);
2907
+ if (!resolvedEnd)
2908
+ throw new Error(HASH_NOT_FOUND_ERROR2);
2909
+ endLine = resolvedEnd;
2910
+ }
2911
+ if (endLine < startLine) {
2912
+ throw new Error("endAnchor resolves before startAnchor.");
2913
+ }
2914
+ const replacement = input.newContent === undefined || input.newContent === "" ? [] : input.newContent.split(`
2915
+ `);
2916
+ const updatedLines = [
2917
+ ...lines.slice(0, startLine - 1),
2918
+ ...replacement,
2919
+ ...lines.slice(endLine)
2920
+ ];
2921
+ const updated = updatedLines.join(`
2922
+ `);
2923
+ await Bun.write(filePath, updated);
2924
+ const diff = generateDiff(relPath, original, updated);
2925
+ return { path: relPath, diff, deleted: replacement.length === 0 };
2926
+ }
2927
+ };
2928
+ function parseAnchor2(value, name) {
2929
+ const match = /^\s*(\d+):([0-9a-fA-F]{2})\s*$/.exec(value);
2930
+ if (!match) {
2931
+ throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
2932
+ }
2933
+ const line = Number(match[1]);
2934
+ if (!Number.isInteger(line) || line < 1) {
2935
+ throw new Error(`Invalid ${name} line number.`);
2936
+ }
2937
+ const hash = match[2];
2938
+ if (!hash) {
2939
+ throw new Error(`Invalid ${name}. Expected format: "line:hh" (e.g. "11:a3").`);
2940
+ }
2941
+ return { line, hash: hash.toLowerCase() };
2942
+ }
2943
+
2944
+ // src/tools/shell.ts
2945
+ import { z as z8 } from "zod";
2946
+ var ShellSchema = z8.object({
2947
+ command: z8.string().describe("Shell command to execute"),
2948
+ timeout: z8.number().int().min(1000).max(300000).optional().default(30000).describe("Timeout in milliseconds (default: 30s, max: 5min)"),
2949
+ env: z8.record(z8.string(), z8.string()).optional().describe("Additional environment variables to set")
2950
+ });
2951
+ var MAX_OUTPUT_BYTES = 1e4;
2952
+ var shellTool = {
2953
+ name: "shell",
2954
+ description: "Execute a shell command. Returns stdout, stderr, and exit code. " + "Use this for running tests, builds, git commands, and other CLI operations. " + "Prefer non-interactive commands. Avoid commands that run indefinitely.",
2955
+ schema: ShellSchema,
2956
+ execute: async (input) => {
2957
+ const cwd = input.cwd ?? process.cwd();
2958
+ const timeout = input.timeout ?? 30000;
2959
+ const env = Object.assign({}, process.env, input.env ?? {});
2960
+ let timedOut = false;
2961
+ const proc = Bun.spawn(["bash", "-c", input.command], {
2962
+ cwd,
2963
+ env,
2964
+ stdout: "pipe",
2965
+ stderr: "pipe"
2966
+ });
2967
+ const timer = setTimeout(() => {
2968
+ timedOut = true;
2969
+ try {
2970
+ proc.kill("SIGTERM");
2971
+ setTimeout(() => {
2972
+ try {
2973
+ proc.kill("SIGKILL");
2974
+ } catch {}
2975
+ }, 2000);
2976
+ } catch {}
2977
+ }, timeout);
2978
+ async function collectStream(stream) {
2979
+ const reader = stream.getReader();
2980
+ const chunks = [];
2981
+ let totalBytes = 0;
2982
+ let truncated = false;
2983
+ while (true) {
2984
+ const { done, value } = await reader.read();
2985
+ if (done)
2986
+ break;
2987
+ if (value) {
2988
+ if (totalBytes + value.length > MAX_OUTPUT_BYTES) {
2989
+ chunks.push(value.slice(0, MAX_OUTPUT_BYTES - totalBytes));
2990
+ truncated = true;
2991
+ reader.cancel().catch(() => {});
2992
+ break;
2993
+ }
2994
+ chunks.push(value);
2995
+ totalBytes += value.length;
2996
+ }
2997
+ }
2998
+ const text = Buffer.concat(chunks).toString("utf-8");
2999
+ return truncated ? `${text}
3000
+ [output truncated]` : text;
3001
+ }
3002
+ let stdout = "";
3003
+ let stderr = "";
3004
+ let exitCode = 1;
3005
+ try {
3006
+ [stdout, stderr] = await Promise.all([
3007
+ collectStream(proc.stdout),
3008
+ collectStream(proc.stderr)
3009
+ ]);
3010
+ exitCode = await proc.exited;
3011
+ } finally {
3012
+ clearTimeout(timer);
3013
+ restoreTerminal();
3014
+ }
3015
+ return {
3016
+ stdout: stdout.trimEnd(),
3017
+ stderr: stderr.trimEnd(),
3018
+ exitCode,
3019
+ success: exitCode === 0,
3020
+ timedOut
3021
+ };
3022
+ }
3023
+ };
3024
+
3025
+ // src/tools/subagent.ts
3026
+ import { z as z9 } from "zod";
3027
+ var SubagentInput = z9.object({
3028
+ prompt: z9.string().describe("The task or question to give the subagent")
3029
+ });
3030
+ function createSubagentTool(runSubagent) {
3031
+ return {
3032
+ name: "subagent",
3033
+ description: "Spawn a sub-agent to handle a focused subtask. " + "Use this for parallel exploration, specialised analysis, or tasks that benefit from " + "a fresh context window. The subagent has access to all the same tools.",
3034
+ schema: SubagentInput,
3035
+ execute: async (input) => {
3036
+ return runSubagent(input.prompt);
3037
+ }
3038
+ };
3039
+ }
3040
+
3041
+ // src/agent/tools.ts
3042
+ function withCwdDefault(tool, cwd) {
3043
+ const originalExecute = tool.execute;
3044
+ return {
3045
+ ...tool,
3046
+ execute: async (input) => {
3047
+ const withDefault = {
3048
+ cwd,
3049
+ ...typeof input === "object" && input ? input : {}
3050
+ };
3051
+ return originalExecute(withDefault);
3052
+ }
3053
+ };
3054
+ }
3055
+ function withHooks(tool, lookupHook, cwd, buildEnv, onHook) {
3056
+ const originalExecute = tool.execute;
3057
+ return {
3058
+ ...tool,
3059
+ execute: async (input) => {
3060
+ const result = await originalExecute(input);
3061
+ const hookScript = lookupHook(tool.name);
3062
+ if (hookScript) {
3063
+ const env = buildEnv(result, input);
3064
+ const success = await runHook(hookScript, env, cwd);
3065
+ onHook(tool.name, hookScript, success);
3066
+ }
3067
+ return result;
3068
+ }
3069
+ };
3070
+ }
3071
+ var MAX_SUBAGENT_DEPTH = 3;
3072
+ var HOOKABLE_TOOLS = [
3073
+ "glob",
3074
+ "grep",
3075
+ "read",
3076
+ "create",
3077
+ "replace",
3078
+ "insert",
3079
+ "shell"
3080
+ ];
3081
+ function buildToolSet(opts) {
3082
+ const { cwd, onHook } = opts;
3083
+ const depth = opts.depth ?? 0;
3084
+ const lookupHook = createHookCache(HOOKABLE_TOOLS, cwd);
3085
+ return [
3086
+ withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
3087
+ withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
3088
+ withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
3089
+ withHooks(withCwdDefault(createTool, cwd), lookupHook, cwd, (result) => hookEnvForCreate(result, cwd), onHook),
3090
+ withHooks(withCwdDefault(replaceTool, cwd), lookupHook, cwd, (result) => hookEnvForReplace(result, cwd), onHook),
3091
+ withHooks(withCwdDefault(insertTool, cwd), lookupHook, cwd, (result) => hookEnvForInsert(result, cwd), onHook),
3092
+ withHooks(withCwdDefault(shellTool, cwd), lookupHook, cwd, (result, input) => hookEnvForShell(result, input, cwd), onHook),
3093
+ createSubagentTool(async (prompt) => {
3094
+ if (depth >= MAX_SUBAGENT_DEPTH) {
3095
+ throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
3096
+ }
3097
+ return opts.runSubagent(prompt, depth + 1);
3098
+ })
3099
+ ];
3100
+ }
3101
+ function buildReadOnlyToolSet(opts) {
3102
+ const { cwd } = opts;
3103
+ return [
3104
+ withCwdDefault(globTool, cwd),
3105
+ withCwdDefault(grepTool, cwd),
3106
+ withCwdDefault(readTool, cwd)
3107
+ ];
3108
+ }
3109
+
3110
+ // src/agent/agent.ts
3111
+ async function getGitBranch(cwd) {
3112
+ try {
3113
+ const proc = Bun.spawn(["git", "rev-parse", "--abbrev-ref", "HEAD"], {
3114
+ cwd,
3115
+ stdout: "pipe",
3116
+ stderr: "pipe"
3117
+ });
3118
+ const out = await new Response(proc.stdout).text();
3119
+ const code = await proc.exited;
3120
+ if (code !== 0)
3121
+ return null;
3122
+ return out.trim() || null;
3123
+ } catch {
3124
+ return null;
3125
+ }
3126
+ }
3127
+ function loadContextFile(cwd) {
3128
+ const candidates = [
3129
+ join11(cwd, "AGENTS.md"),
3130
+ join11(cwd, "CLAUDE.md"),
3131
+ join11(getConfigDir(), "AGENTS.md")
3132
+ ];
3133
+ for (const p of candidates) {
3134
+ if (existsSync3(p)) {
3135
+ try {
3136
+ return readFileSync2(p, "utf-8");
3137
+ } catch {}
3138
+ }
3139
+ }
3140
+ return null;
3141
+ }
3142
+ function buildSystemPrompt(cwd) {
3143
+ const contextFile = loadContextFile(cwd);
3144
+ const cwdDisplay = cwd.startsWith(homedir5()) ? `~${cwd.slice(homedir5().length)}` : cwd;
3145
+ const now = new Date().toLocaleString(undefined, { hour12: false });
3146
+ let prompt = `You are mini-coder, a small and fast CLI coding agent.
3147
+ You have access to tools to read files, search code, make edits, run shell commands, and spawn subagents.
3148
+
3149
+ Current working directory: ${cwdDisplay}
3150
+ Current date/time: ${now}
3151
+
3152
+ Guidelines:
3153
+ - Be concise and precise. Avoid unnecessary preamble.
3154
+ - Prefer small, targeted edits over large rewrites.
3155
+ - Always read a file before editing it.
3156
+ - Use glob to discover files, grep to find patterns, read to inspect contents.
3157
+ - Use shell for tests, builds, and git operations.
3158
+ - When in doubt, ask the user before making destructive changes.`;
3159
+ if (contextFile) {
3160
+ prompt += `
3161
+
3162
+ # Project context
3163
+
3164
+ ${contextFile}`;
3165
+ }
3166
+ return prompt;
3167
+ }
3168
+ async function runShellPassthrough(command, cwd) {
3169
+ const proc = Bun.spawn(["bash", "-c", command], {
3170
+ cwd,
3171
+ stdout: "pipe",
3172
+ stderr: "pipe"
3173
+ });
3174
+ try {
3175
+ const [stdout, stderr] = await Promise.all([
3176
+ new Response(proc.stdout).text(),
3177
+ new Response(proc.stderr).text()
3178
+ ]);
3179
+ await proc.exited;
3180
+ const out = [stdout, stderr].filter(Boolean).join(`
3181
+ `).trim();
3182
+ if (out)
3183
+ writeln(c6.dim(out));
3184
+ return out;
3185
+ } finally {
3186
+ restoreTerminal();
3187
+ }
3188
+ }
3189
+ async function runAgent(opts) {
3190
+ const cwd = opts.cwd;
3191
+ let currentModel = opts.model;
3192
+ let session;
3193
+ if (opts.sessionId) {
3194
+ const resumed = resumeSession(opts.sessionId);
3195
+ if (!resumed) {
3196
+ renderError(`Session "${opts.sessionId}" not found.`);
3197
+ process.exit(1);
3198
+ }
3199
+ session = resumed;
3200
+ currentModel = session.model;
3201
+ deleteAllSnapshots(session.id);
3202
+ renderInfo(`Resumed session ${session.id} (${c6.cyan(currentModel)})`);
3203
+ } else {
3204
+ session = newSession(currentModel, cwd);
3205
+ }
3206
+ let turnIndex = getMaxTurnIndex(session.id) + 1;
3207
+ const coreHistory = [...session.messages];
3208
+ const runSubagent = async (prompt, depth = 0) => {
3209
+ const subMessages = [{ role: "user", content: prompt }];
3210
+ const subTools = buildToolSet({
3211
+ cwd,
3212
+ depth,
3213
+ runSubagent,
3214
+ onHook: renderHook
3215
+ });
3216
+ const subLlm = resolveModel(currentModel);
3217
+ const systemPrompt = buildSystemPrompt(cwd);
3218
+ let result = "";
3219
+ let inputTokens = 0;
3220
+ let outputTokens = 0;
3221
+ const activity = [];
3222
+ const pendingCalls = new Map;
3223
+ const events = runTurn({
3224
+ model: subLlm,
3225
+ messages: subMessages,
3226
+ tools: subTools,
3227
+ systemPrompt
3228
+ });
3229
+ for await (const event of events) {
3230
+ if (event.type === "text-delta")
3231
+ result += event.delta;
3232
+ if (event.type === "tool-call-start") {
3233
+ pendingCalls.set(event.toolCallId, {
3234
+ toolName: event.toolName,
3235
+ args: event.args
3236
+ });
3237
+ }
3238
+ if (event.type === "tool-result") {
3239
+ const pending = pendingCalls.get(event.toolCallId);
3240
+ if (pending) {
3241
+ pendingCalls.delete(event.toolCallId);
3242
+ activity.push({
3243
+ toolName: pending.toolName,
3244
+ args: pending.args,
3245
+ result: event.result,
3246
+ isError: event.isError
3247
+ });
3248
+ }
3249
+ }
3250
+ if (event.type === "turn-complete") {
3251
+ inputTokens = event.inputTokens;
3252
+ outputTokens = event.outputTokens;
3253
+ }
3254
+ }
3255
+ return { result, inputTokens, outputTokens, activity };
3256
+ };
3257
+ const tools = buildToolSet({
3258
+ cwd,
3259
+ depth: 0,
3260
+ runSubagent,
3261
+ onHook: renderHook
3262
+ });
3263
+ const mcpTools = [];
3264
+ async function connectAndAddMcp(name) {
3265
+ const rows = listMcpServers();
3266
+ const row = rows.find((r) => r.name === name);
3267
+ if (!row)
3268
+ throw new Error(`MCP server "${name}" not found in DB`);
3269
+ const cfg = {
3270
+ name: row.name,
3271
+ transport: row.transport,
3272
+ ...row.url ? { url: row.url } : {},
3273
+ ...row.command ? { command: row.command } : {},
3274
+ ...row.args ? { args: JSON.parse(row.args) } : {},
3275
+ ...row.env ? { env: JSON.parse(row.env) } : {}
3276
+ };
3277
+ const client = await connectMcpServer(cfg);
3278
+ tools.push(...client.tools);
3279
+ mcpTools.push(...client.tools);
3280
+ }
3281
+ for (const row of listMcpServers()) {
3282
+ try {
3283
+ await connectAndAddMcp(row.name);
3284
+ renderInfo(`MCP: connected ${c6.cyan(row.name)}`);
3285
+ } catch (e) {
3286
+ renderError(`MCP: failed to connect ${row.name}: ${String(e)}`);
3287
+ }
3288
+ }
3289
+ let planMode = false;
3290
+ let ralphMode = false;
3291
+ const cmdCtx = {
3292
+ get currentModel() {
3293
+ return currentModel;
3294
+ },
3295
+ setModel: (m) => {
3296
+ currentModel = m;
3297
+ session.model = m;
3298
+ setPreferredModel(m);
3299
+ },
3300
+ get planMode() {
3301
+ return planMode;
3302
+ },
3303
+ get ralphMode() {
3304
+ return ralphMode;
3305
+ },
3306
+ setRalphMode: (v) => {
3307
+ ralphMode = v;
3308
+ },
3309
+ setPlanMode: (v) => {
3310
+ planMode = v;
3311
+ },
3312
+ cwd,
3313
+ runSubagent: (prompt) => runSubagent(prompt),
3314
+ undoLastTurn: async () => {
3315
+ if (session.messages.length === 0)
3316
+ return false;
3317
+ let lastUserIdx = -1;
3318
+ for (let i = session.messages.length - 1;i >= 0; i--) {
3319
+ if (session.messages[i]?.role === "user") {
3320
+ lastUserIdx = i;
3321
+ break;
3322
+ }
3323
+ }
3324
+ if (lastUserIdx === -1)
3325
+ return false;
3326
+ session.messages.splice(lastUserIdx);
3327
+ let coreLastUserIdx = -1;
3328
+ for (let i = coreHistory.length - 1;i >= 0; i--) {
3329
+ if (coreHistory[i]?.role === "user") {
3330
+ coreLastUserIdx = i;
3331
+ break;
3332
+ }
3333
+ }
3334
+ if (coreLastUserIdx !== -1)
3335
+ coreHistory.splice(coreLastUserIdx);
3336
+ const deleted = deleteLastTurn(session.id);
3337
+ const poppedTurn = snapshotStack.pop() ?? null;
3338
+ if (turnIndex > 0)
3339
+ turnIndex--;
3340
+ if (poppedTurn !== null) {
3341
+ const restoreResult = await restoreSnapshot(cwd, session.id, poppedTurn);
3342
+ if (restoreResult.restored === false && restoreResult.reason === "error") {
3343
+ renderError("snapshot restore failed \u2014 some files may not have been reverted");
3344
+ }
3345
+ }
3346
+ return deleted;
3347
+ },
3348
+ connectMcpServer: connectAndAddMcp,
3349
+ startNewSession: () => {
3350
+ deleteAllSnapshots(session.id);
3351
+ session = newSession(currentModel, cwd);
3352
+ coreHistory.length = 0;
3353
+ turnIndex = 1;
3354
+ totalIn = 0;
3355
+ totalOut = 0;
3356
+ lastContextTokens = 0;
3357
+ snapshotStack.length = 0;
3358
+ }
3359
+ };
3360
+ const spinner = new Spinner;
3361
+ let totalIn = 0;
3362
+ let totalOut = 0;
3363
+ let lastContextTokens = 0;
3364
+ const snapshotStack = [];
3365
+ if (opts.initialPrompt) {
3366
+ await processUserInput(opts.initialPrompt);
3367
+ }
3368
+ while (true) {
3369
+ await renderStatusBarForSession();
3370
+ let input;
3371
+ try {
3372
+ input = await readline({ cwd, planMode, ralphMode });
3373
+ } catch {
3374
+ break;
3375
+ }
3376
+ switch (input.type) {
3377
+ case "eof":
3378
+ writeln(c6.dim("Goodbye."));
3379
+ return;
3380
+ case "interrupt":
3381
+ continue;
3382
+ case "command": {
3383
+ const result = await handleCommand(input.command, input.args, cmdCtx);
3384
+ if (result.type === "exit") {
3385
+ writeln(c6.dim("Goodbye."));
3386
+ return;
3387
+ }
3388
+ if (result.type === "inject-user-message") {
3389
+ await processUserInput(result.text);
3390
+ }
3391
+ continue;
3392
+ }
3393
+ case "shell": {
3394
+ const out = await runShellPassthrough(input.command, cwd);
3395
+ if (out) {
3396
+ const thisTurn = turnIndex++;
3397
+ const msg = {
3398
+ role: "user",
3399
+ content: `Shell output of \`${input.command}\`:
3400
+ \`\`\`
3401
+ ${out}
3402
+ \`\`\``
3403
+ };
3404
+ session.messages.push(msg);
3405
+ saveMessages(session.id, [msg], thisTurn);
3406
+ coreHistory.push(msg);
3407
+ }
3408
+ continue;
3409
+ }
3410
+ case "submit": {
3411
+ const RALPH_MAX_ITERATIONS = 20;
3412
+ let ralphIteration = 1;
3413
+ let lastText = await processUserInput(input.text, input.images);
3414
+ if (ralphMode) {
3415
+ const goal = input.text;
3416
+ const goalImages = input.images;
3417
+ while (ralphMode) {
3418
+ if (hasRalphSignal(lastText)) {
3419
+ ralphMode = false;
3420
+ writeln(`${PREFIX.info} ${c6.dim("ralph mode off")}`);
3421
+ break;
3422
+ }
3423
+ if (ralphIteration >= RALPH_MAX_ITERATIONS) {
3424
+ writeln(`${PREFIX.info} ${c6.yellow("ralph")} ${c6.dim("\u2014 max iterations reached, stopping")}`);
3425
+ ralphMode = false;
3426
+ break;
3427
+ }
3428
+ ralphIteration++;
3429
+ cmdCtx.startNewSession();
3430
+ lastText = await processUserInput(goal, goalImages);
3431
+ }
3432
+ }
3433
+ continue;
3434
+ }
3435
+ }
3436
+ }
3437
+ async function processUserInput(text, pastedImages = []) {
3438
+ const abortController = new AbortController;
3439
+ let wasAborted = false;
3440
+ const onSigInt = () => {
3441
+ wasAborted = true;
3442
+ abortController.abort();
3443
+ process.removeListener("SIGINT", onSigInt);
3444
+ };
3445
+ process.on("SIGINT", onSigInt);
3446
+ const { text: resolvedText, images: refImages } = await resolveFileRefs(text, cwd);
3447
+ const allImages = [...pastedImages, ...refImages];
3448
+ const thisTurn = turnIndex++;
3449
+ const snapped = await takeSnapshot(cwd, session.id, thisTurn);
3450
+ if (wasAborted) {
3451
+ process.removeListener("SIGINT", onSigInt);
3452
+ if (snapped)
3453
+ deleteSnapshot(session.id, thisTurn);
3454
+ turnIndex--;
3455
+ return "";
3456
+ }
3457
+ const coreContent = planMode ? `${resolvedText}
3458
+
3459
+ <system-message>PLAN MODE ACTIVE: Help the user gather context for the plan -- READ ONLY</system-message>` : ralphMode ? `${resolvedText}
3460
+
3461
+ <system-message>RALPH MODE: You are in an autonomous loop. When the task is fully complete (all tests pass, no outstanding issues), output exactly \`/ralph\` as your final message to exit the loop. Otherwise, keep working.</system-message>` : resolvedText;
3462
+ const userMsg = allImages.length > 0 ? {
3463
+ role: "user",
3464
+ content: [
3465
+ { type: "text", text: coreContent },
3466
+ ...allImages.map((img) => ({
3467
+ type: "image",
3468
+ image: img.data,
3469
+ mediaType: img.mediaType
3470
+ }))
3471
+ ]
3472
+ } : { role: "user", content: coreContent };
3473
+ session.messages.push(userMsg);
3474
+ saveMessages(session.id, [userMsg], thisTurn);
3475
+ coreHistory.push(userMsg);
3476
+ const llm = resolveModel(currentModel);
3477
+ const systemPrompt = buildSystemPrompt(cwd);
3478
+ let lastAssistantText = "";
3479
+ let turnRolledBack = false;
3480
+ const rollbackTurn = () => {
3481
+ if (turnRolledBack)
3482
+ return;
3483
+ turnRolledBack = true;
3484
+ coreHistory.pop();
3485
+ session.messages.pop();
3486
+ deleteLastTurn(session.id, thisTurn);
3487
+ if (snapped)
3488
+ deleteSnapshot(session.id, thisTurn);
3489
+ snapshotStack.pop();
3490
+ turnIndex--;
3491
+ };
3492
+ try {
3493
+ snapshotStack.push(snapped ? thisTurn : null);
3494
+ spinner.start("thinking");
3495
+ const events = runTurn({
3496
+ model: llm,
3497
+ messages: coreHistory,
3498
+ tools: planMode ? [...buildReadOnlyToolSet({ cwd }), ...mcpTools] : tools,
3499
+ systemPrompt,
3500
+ signal: abortController.signal
3501
+ });
3502
+ const { inputTokens, outputTokens, contextTokens, newMessages } = await renderTurn(events, spinner);
3503
+ if (newMessages.length > 0) {
3504
+ coreHistory.push(...newMessages);
3505
+ session.messages.push(...newMessages);
3506
+ saveMessages(session.id, newMessages, thisTurn);
3507
+ } else {
3508
+ rollbackTurn();
3509
+ }
3510
+ lastAssistantText = extractAssistantText(newMessages);
3511
+ totalIn += inputTokens;
3512
+ totalOut += outputTokens;
3513
+ lastContextTokens = contextTokens;
3514
+ touchActiveSession(session);
3515
+ } catch (err) {
3516
+ rollbackTurn();
3517
+ throw err;
3518
+ } finally {
3519
+ process.removeListener("SIGINT", onSigInt);
3520
+ if (wasAborted)
3521
+ ralphMode = false;
3522
+ }
3523
+ return lastAssistantText;
3524
+ }
3525
+ async function renderStatusBarForSession() {
3526
+ const branch = await getGitBranch(cwd);
3527
+ const provider = currentModel.split("/")[0] ?? "";
3528
+ const modelShort = currentModel.split("/").slice(1).join("/");
3529
+ const cwdDisplay = cwd.startsWith(homedir5()) ? `~${cwd.slice(homedir5().length)}` : cwd;
3530
+ renderStatusBar({
3531
+ model: modelShort,
3532
+ provider,
3533
+ cwd: cwdDisplay,
3534
+ gitBranch: branch,
3535
+ sessionId: session.id,
3536
+ inputTokens: totalIn,
3537
+ outputTokens: totalOut,
3538
+ contextTokens: lastContextTokens,
3539
+ contextWindow: getContextWindow(currentModel),
3540
+ ralphMode
3541
+ });
3542
+ }
3543
+ }
3544
+ function extractAssistantText(newMessages) {
3545
+ const parts = [];
3546
+ for (const msg of newMessages) {
3547
+ if (msg.role !== "assistant")
3548
+ continue;
3549
+ const content = msg.content;
3550
+ if (typeof content === "string") {
3551
+ parts.push(content);
3552
+ } else if (Array.isArray(content)) {
3553
+ for (const part of content) {
3554
+ if (part?.type === "text" && part.text)
3555
+ parts.push(part.text);
3556
+ }
3557
+ }
3558
+ }
3559
+ return parts.join(`
3560
+ `);
3561
+ }
3562
+ function hasRalphSignal(text) {
3563
+ return /\/ralph\b/.test(text);
3564
+ }
3565
+ async function resolveFileRefs(text, cwd) {
3566
+ const atPattern = /@([\w./\-_]+)/g;
3567
+ let result = text;
3568
+ const matches = [...text.matchAll(atPattern)];
3569
+ const images = [];
3570
+ for (const match of matches.reverse()) {
3571
+ const ref = match[1];
3572
+ if (!ref)
3573
+ continue;
3574
+ const filePath = ref.startsWith("/") ? ref : join11(cwd, ref);
3575
+ if (isImageFilename(ref)) {
3576
+ const attachment = await loadImageFile(filePath);
3577
+ if (attachment) {
3578
+ images.unshift(attachment);
3579
+ result = result.slice(0, match.index) + result.slice((match.index ?? 0) + match[0].length);
3580
+ continue;
3581
+ }
3582
+ }
3583
+ try {
3584
+ const content = await Bun.file(filePath).text();
3585
+ const lines = content.split(`
3586
+ `);
3587
+ const preview = lines.length > 200 ? `${lines.slice(0, 200).join(`
3588
+ `)}
3589
+ [truncated]` : content;
3590
+ const replacement = `\`${ref}\`:
3591
+ \`\`\`
3592
+ ${preview}
3593
+ \`\`\``;
3594
+ result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
3595
+ } catch {}
3596
+ }
3597
+ return { text: result, images };
3598
+ }
3599
+
3600
+ // src/index.ts
3601
+ registerTerminalCleanup();
3602
+ function parseArgs(argv) {
3603
+ const args = {
3604
+ model: null,
3605
+ sessionId: null,
3606
+ listSessions: false,
3607
+ resumeLast: false,
3608
+ prompt: null,
3609
+ cwd: process.cwd(),
3610
+ help: false
3611
+ };
3612
+ const positional = [];
3613
+ for (let i = 0;i < argv.length; i++) {
3614
+ const arg = argv[i] ?? "";
3615
+ switch (arg) {
3616
+ case "--model":
3617
+ case "-m":
3618
+ args.model = argv[++i] ?? null;
3619
+ break;
3620
+ case "--resume":
3621
+ case "-r":
3622
+ args.sessionId = argv[++i] ?? null;
3623
+ break;
3624
+ case "--continue":
3625
+ case "-c":
3626
+ args.resumeLast = true;
3627
+ break;
3628
+ case "--list":
3629
+ case "-l":
3630
+ args.listSessions = true;
3631
+ break;
3632
+ case "--cwd":
3633
+ args.cwd = argv[++i] ?? process.cwd();
3634
+ break;
3635
+ case "--help":
3636
+ case "-h":
3637
+ args.help = true;
3638
+ break;
3639
+ default:
3640
+ if (!arg.startsWith("-"))
3641
+ positional.push(arg);
3642
+ }
3643
+ }
3644
+ if (positional.length > 0) {
3645
+ args.prompt = positional.join(" ");
3646
+ }
3647
+ return args;
3648
+ }
3649
+ function printHelp() {
3650
+ writeln(`${c7.bold("mini-coder")} \u2014 a small, fast CLI coding agent
3651
+ `);
3652
+ writeln(`${c7.bold("Usage:")} mc [options] [prompt]
3653
+ `);
3654
+ writeln(`${c7.bold("Options:")}`);
3655
+ const opts = [
3656
+ ["-m, --model <id>", "Model to use (e.g. zen/claude-sonnet-4-6)"],
3657
+ ["-c, --continue", "Continue the most recent session"],
3658
+ ["-r, --resume <id>", "Resume a specific session by ID"],
3659
+ ["-l, --list", "List recent sessions"],
3660
+ ["--cwd <path>", "Set working directory (default: current dir)"],
3661
+ ["-h, --help", "Show this help"]
3662
+ ];
3663
+ for (const [flag, desc] of opts) {
3664
+ writeln(` ${c7.cyan((flag ?? "").padEnd(22))} ${c7.dim(desc ?? "")}`);
3665
+ }
3666
+ writeln(`
3667
+ ${c7.bold("Provider env vars:")}`);
3668
+ const envs = [
3669
+ ["OPENCODE_API_KEY", "OpenCode Zen (recommended)"],
3670
+ ["ANTHROPIC_API_KEY", "Anthropic direct"],
3671
+ ["OPENAI_API_KEY", "OpenAI direct"],
3672
+ ["GOOGLE_API_KEY", "Google Gemini direct"],
3673
+ ["OLLAMA_BASE_URL", "Ollama base URL (default: http://localhost:11434)"]
3674
+ ];
3675
+ for (const [env, desc] of envs) {
3676
+ writeln(` ${c7.yellow((env ?? "").padEnd(22))} ${c7.dim(desc ?? "")}`);
3677
+ }
3678
+ writeln(`
3679
+ ${c7.bold("Examples:")}`);
3680
+ writeln(` mc ${c7.dim("# interactive session")}`);
3681
+ writeln(` mc "explain this codebase" ${c7.dim("# one-shot prompt then interactive")}`);
3682
+ writeln(` mc -c ${c7.dim("# continue last session")}`);
3683
+ writeln(` mc -m ollama/llama3.2 ${c7.dim("# use local Ollama model")}`);
3684
+ writeln(` mc -l ${c7.dim("# list sessions")}`);
3685
+ }
3686
+ async function main() {
3687
+ const argv = process.argv.slice(2);
3688
+ const args = parseArgs(argv);
3689
+ if (args.help) {
3690
+ printHelp();
3691
+ process.exit(0);
3692
+ }
3693
+ if (args.listSessions) {
3694
+ printSessionList();
3695
+ process.exit(0);
3696
+ }
3697
+ let sessionId;
3698
+ if (args.resumeLast) {
3699
+ const last = getMostRecentSession();
3700
+ if (last) {
3701
+ sessionId = last.id;
3702
+ } else {
3703
+ writeln(c7.dim("No previous session found, starting fresh."));
3704
+ }
3705
+ } else if (args.sessionId) {
3706
+ sessionId = args.sessionId;
3707
+ }
3708
+ const model = args.model ?? getPreferredModel() ?? autoDiscoverModel();
3709
+ if (!args.prompt) {
3710
+ renderBanner(model, args.cwd);
3711
+ }
3712
+ try {
3713
+ const agentOpts = { model, cwd: args.cwd };
3714
+ if (sessionId)
3715
+ agentOpts.sessionId = sessionId;
3716
+ if (args.prompt)
3717
+ agentOpts.initialPrompt = args.prompt;
3718
+ await runAgent(agentOpts);
3719
+ } catch (err) {
3720
+ renderError(err);
3721
+ process.exit(1);
3722
+ }
3723
+ }
3724
+ main().catch((err) => {
3725
+ console.error(err);
3726
+ process.exit(1);
3727
+ });