mini-coder 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mc.js CHANGED
@@ -2,16 +2,86 @@
2
2
  // @bun
3
3
 
4
4
  // src/index.ts
5
- import * as c7 from "yoctocolors";
5
+ import * as c8 from "yoctocolors";
6
6
 
7
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";
8
+ import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
9
+ import { join as join14 } from "path";
10
+ import * as c7 from "yoctocolors";
11
+
12
+ // src/cli/agents.ts
13
+ import { existsSync, readFileSync, readdirSync } from "fs";
14
+ import { homedir } from "os";
15
+ import { basename, join } from "path";
16
+
17
+ // src/cli/frontmatter.ts
18
+ var FM_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
19
+ function parseFrontmatter(raw) {
20
+ const m = raw.match(FM_RE);
21
+ if (!m)
22
+ return { meta: {}, body: raw };
23
+ const meta = {};
24
+ const yamlBlock = m[1] ?? "";
25
+ for (const line of yamlBlock.split(`
26
+ `)) {
27
+ const colon = line.indexOf(":");
28
+ if (colon === -1)
29
+ continue;
30
+ const key = line.slice(0, colon).trim();
31
+ const val = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
32
+ if (key === "name")
33
+ meta.name = val;
34
+ if (key === "description")
35
+ meta.description = val;
36
+ if (key === "model")
37
+ meta.model = val;
38
+ }
39
+ return { meta, body: (m[2] ?? "").trim() };
40
+ }
41
+
42
+ // src/cli/agents.ts
43
+ function loadFromDir(dir, source) {
44
+ const agents = new Map;
45
+ if (!existsSync(dir))
46
+ return agents;
47
+ let entries;
48
+ try {
49
+ entries = readdirSync(dir);
50
+ } catch {
51
+ return agents;
52
+ }
53
+ for (const entry of entries) {
54
+ if (!entry.endsWith(".md"))
55
+ continue;
56
+ const name = basename(entry, ".md");
57
+ const filePath = join(dir, entry);
58
+ let raw;
59
+ try {
60
+ raw = readFileSync(filePath, "utf-8");
61
+ } catch {
62
+ continue;
63
+ }
64
+ const { meta, body } = parseFrontmatter(raw);
65
+ agents.set(name, {
66
+ name,
67
+ description: meta.description ?? name,
68
+ ...meta.model ? { model: meta.model } : {},
69
+ systemPrompt: body,
70
+ source
71
+ });
72
+ }
73
+ return agents;
74
+ }
75
+ function loadAgents(cwd) {
76
+ const globalDir = join(homedir(), ".agents", "agents");
77
+ const localDir = join(cwd, ".agents", "agents");
78
+ const global = loadFromDir(globalDir, "global");
79
+ const local = loadFromDir(localDir, "local");
80
+ return new Map([...global, ...local]);
81
+ }
12
82
 
13
83
  // src/cli/commands.ts
14
- import * as c3 from "yoctocolors";
84
+ import * as c4 from "yoctocolors";
15
85
 
16
86
  // src/llm-api/providers.ts
17
87
  import { createAnthropic } from "@ai-sdk/anthropic";
@@ -20,34 +90,18 @@ import { createOpenAI } from "@ai-sdk/openai";
20
90
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
21
91
  import { createOllama } from "ollama-ai-provider";
22
92
  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
- ]);
93
+ function zenEndpointFor(modelId) {
94
+ if (modelId.startsWith("claude-"))
95
+ return zenAnthropic()(modelId);
96
+ if (modelId.startsWith("gpt-"))
97
+ return zenOpenAI()(modelId);
98
+ if (modelId.startsWith("gemini-"))
99
+ return zenGoogle()(modelId);
100
+ return zenCompat()(modelId);
101
+ }
49
102
  var _zenAnthropic = null;
50
103
  var _zenOpenAI = null;
104
+ var _zenGoogle = null;
51
105
  var _zenCompat = null;
52
106
  function getZenApiKey() {
53
107
  const key = process.env.OPENCODE_API_KEY;
@@ -73,11 +127,14 @@ function zenOpenAI() {
73
127
  }
74
128
  return _zenOpenAI;
75
129
  }
76
- function zenGoogle(modelId) {
77
- return createGoogleGenerativeAI({
78
- apiKey: getZenApiKey(),
79
- baseURL: ZEN_BASE
80
- });
130
+ function zenGoogle() {
131
+ if (!_zenGoogle) {
132
+ _zenGoogle = createGoogleGenerativeAI({
133
+ apiKey: getZenApiKey(),
134
+ baseURL: ZEN_BASE
135
+ });
136
+ }
137
+ return _zenGoogle;
81
138
  }
82
139
  function zenCompat() {
83
140
  if (!_zenCompat) {
@@ -146,16 +203,7 @@ function resolveModel(modelString) {
146
203
  const modelId = modelString.slice(slashIdx + 1);
147
204
  switch (provider) {
148
205
  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);
206
+ return zenEndpointFor(modelId);
159
207
  }
160
208
  case "anthropic":
161
209
  return directAnthropic()(modelId);
@@ -235,17 +283,17 @@ async function fetchAvailableModels() {
235
283
 
236
284
  // src/session/db.ts
237
285
  import { Database } from "bun:sqlite";
238
- import { existsSync, mkdirSync, unlinkSync } from "fs";
239
- import { homedir } from "os";
240
- import { join } from "path";
286
+ import { existsSync as existsSync2, mkdirSync, unlinkSync } from "fs";
287
+ import { homedir as homedir2 } from "os";
288
+ import { join as join2 } from "path";
241
289
  function getConfigDir() {
242
- return join(homedir(), ".config", "mini-coder");
290
+ return join2(homedir2(), ".config", "mini-coder");
243
291
  }
244
292
  function getDbPath() {
245
293
  const dir = getConfigDir();
246
- if (!existsSync(dir))
294
+ if (!existsSync2(dir))
247
295
  mkdirSync(dir, { recursive: true });
248
- return join(dir, "sessions.db");
296
+ return join2(dir, "sessions.db");
249
297
  }
250
298
  var DB_VERSION = 3;
251
299
  var SCHEMA = `
@@ -321,7 +369,7 @@ function getDb() {
321
369
  db.close();
322
370
  } catch {}
323
371
  for (const path of [dbPath, `${dbPath}-wal`, `${dbPath}-shm`]) {
324
- if (existsSync(path))
372
+ if (existsSync2(path))
325
373
  unlinkSync(path);
326
374
  }
327
375
  db = new Database(dbPath, { create: true });
@@ -471,6 +519,18 @@ function generateSessionId() {
471
519
  return `${ts}-${rand}`;
472
520
  }
473
521
 
522
+ // src/cli/custom-commands.ts
523
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
524
+ import { homedir as homedir4 } from "os";
525
+ import { basename as basename2, join as join3 } from "path";
526
+
527
+ // src/cli/config-conflicts.ts
528
+ import * as c3 from "yoctocolors";
529
+
530
+ // src/cli/output.ts
531
+ import { homedir as homedir3 } from "os";
532
+ import * as c2 from "yoctocolors";
533
+
474
534
  // src/cli/markdown.ts
475
535
  import * as c from "yoctocolors";
476
536
  function renderInline(text) {
@@ -584,10 +644,11 @@ function renderChunk(text, inFence) {
584
644
  }
585
645
 
586
646
  // 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";
647
+ var HOME = homedir3();
648
+ var PACKAGE_VERSION = "0.1.0";
649
+ function tildePath(p) {
650
+ return p.startsWith(HOME) ? `~${p.slice(HOME.length)}` : p;
651
+ }
591
652
  function restoreTerminal() {
592
653
  try {
593
654
  process.stderr.write("\x1B[?25h");
@@ -1209,6 +1270,165 @@ var PREFIX = {
1209
1270
  success: G.ok
1210
1271
  };
1211
1272
 
1273
+ // src/cli/config-conflicts.ts
1274
+ function warnConventionConflicts(kind, scope, agentsNames, claudeNames) {
1275
+ const agents = new Set(agentsNames);
1276
+ const claude = new Set(claudeNames);
1277
+ const conflicts = [];
1278
+ for (const name of agents) {
1279
+ if (claude.has(name))
1280
+ conflicts.push(name);
1281
+ }
1282
+ if (conflicts.length === 0)
1283
+ return;
1284
+ conflicts.sort((a, b) => a.localeCompare(b));
1285
+ const list = conflicts.map((n) => c3.cyan(n)).join(c3.dim(", "));
1286
+ writeln(`${G.warn} conflicting ${kind} in ${scope} .agents and .claude: ${list} ${c3.dim("\u2014 using .agents version")}`);
1287
+ }
1288
+
1289
+ // src/cli/custom-commands.ts
1290
+ function loadFromDir2(dir, source) {
1291
+ const commands = new Map;
1292
+ if (!existsSync3(dir))
1293
+ return commands;
1294
+ let entries;
1295
+ try {
1296
+ entries = readdirSync2(dir);
1297
+ } catch {
1298
+ return commands;
1299
+ }
1300
+ for (const entry of entries) {
1301
+ if (!entry.endsWith(".md"))
1302
+ continue;
1303
+ const name = basename2(entry, ".md");
1304
+ const filePath = join3(dir, entry);
1305
+ let raw;
1306
+ try {
1307
+ raw = readFileSync2(filePath, "utf-8");
1308
+ } catch {
1309
+ continue;
1310
+ }
1311
+ const { meta, body } = parseFrontmatter(raw);
1312
+ commands.set(name, {
1313
+ name,
1314
+ description: meta.description ?? name,
1315
+ ...meta.model ? { model: meta.model } : {},
1316
+ template: body,
1317
+ source
1318
+ });
1319
+ }
1320
+ return commands;
1321
+ }
1322
+ function loadCustomCommands(cwd) {
1323
+ const globalAgentsDir = join3(homedir4(), ".agents", "commands");
1324
+ const globalClaudeDir = join3(homedir4(), ".claude", "commands");
1325
+ const localAgentsDir = join3(cwd, ".agents", "commands");
1326
+ const localClaudeDir = join3(cwd, ".claude", "commands");
1327
+ const globalAgents = loadFromDir2(globalAgentsDir, "global");
1328
+ const globalClaude = loadFromDir2(globalClaudeDir, "global");
1329
+ const localAgents = loadFromDir2(localAgentsDir, "local");
1330
+ const localClaude = loadFromDir2(localClaudeDir, "local");
1331
+ warnConventionConflicts("commands", "global", globalAgents.keys(), globalClaude.keys());
1332
+ warnConventionConflicts("commands", "local", localAgents.keys(), localClaude.keys());
1333
+ return new Map([
1334
+ ...globalClaude,
1335
+ ...globalAgents,
1336
+ ...localClaude,
1337
+ ...localAgents
1338
+ ]);
1339
+ }
1340
+ async function expandTemplate(template, args, cwd) {
1341
+ const tokens = args.match(/("([^"]*)")|('([^']*)')|(\S+)/g)?.map((t) => t.replace(/^["']|["']$/g, "")) ?? [];
1342
+ let result = template;
1343
+ for (let i = 9;i >= 1; i--) {
1344
+ result = result.replaceAll(`$${i}`, tokens[i - 1] ?? "");
1345
+ }
1346
+ result = result.replaceAll("$ARGUMENTS", args);
1347
+ const SHELL_RE = /!`([^`]+)`/g;
1348
+ const shellMatches = [...result.matchAll(SHELL_RE)];
1349
+ for (const match of shellMatches) {
1350
+ const cmd = match[1] ?? "";
1351
+ let output = "";
1352
+ try {
1353
+ const signal = AbortSignal.timeout(1e4);
1354
+ const proc = Bun.spawn(["bash", "-c", cmd], {
1355
+ cwd,
1356
+ stdout: "pipe",
1357
+ stderr: "pipe"
1358
+ });
1359
+ await Promise.race([
1360
+ proc.exited,
1361
+ new Promise((_, reject) => signal.addEventListener("abort", () => {
1362
+ proc.kill();
1363
+ reject(new Error("timeout"));
1364
+ }))
1365
+ ]);
1366
+ const [stdout, stderr] = await Promise.all([
1367
+ new Response(proc.stdout).text(),
1368
+ new Response(proc.stderr).text()
1369
+ ]);
1370
+ const exitCode = proc.exitCode ?? 0;
1371
+ output = exitCode === 0 ? [stdout, stderr].filter(Boolean).join(`
1372
+ `).trim() : stdout.trim();
1373
+ } catch {}
1374
+ result = result.replaceAll(match[0], output);
1375
+ }
1376
+ return result;
1377
+ }
1378
+
1379
+ // src/cli/skills.ts
1380
+ import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3, statSync } from "fs";
1381
+ import { homedir as homedir5 } from "os";
1382
+ import { join as join4 } from "path";
1383
+ function loadFromDir3(dir, source) {
1384
+ const skills = new Map;
1385
+ if (!existsSync4(dir))
1386
+ return skills;
1387
+ let entries;
1388
+ try {
1389
+ entries = readdirSync3(dir);
1390
+ } catch {
1391
+ return skills;
1392
+ }
1393
+ for (const entry of entries) {
1394
+ const skillFile = join4(dir, entry, "SKILL.md");
1395
+ try {
1396
+ if (!statSync(join4(dir, entry)).isDirectory())
1397
+ continue;
1398
+ if (!existsSync4(skillFile))
1399
+ continue;
1400
+ const content = readFileSync3(skillFile, "utf-8");
1401
+ const { meta } = parseFrontmatter(content);
1402
+ const name = meta.name ?? entry;
1403
+ skills.set(name, {
1404
+ name,
1405
+ description: meta.description ?? name,
1406
+ content,
1407
+ source
1408
+ });
1409
+ } catch {}
1410
+ }
1411
+ return skills;
1412
+ }
1413
+ function loadSkills(cwd) {
1414
+ const globalAgentsDir = join4(homedir5(), ".agents", "skills");
1415
+ const globalClaudeDir = join4(homedir5(), ".claude", "skills");
1416
+ const localAgentsDir = join4(cwd, ".agents", "skills");
1417
+ const localClaudeDir = join4(cwd, ".claude", "skills");
1418
+ const globalAgents = loadFromDir3(globalAgentsDir, "global");
1419
+ const globalClaude = loadFromDir3(globalClaudeDir, "global");
1420
+ const localAgents = loadFromDir3(localAgentsDir, "local");
1421
+ const localClaude = loadFromDir3(localClaudeDir, "local");
1422
+ warnConventionConflicts("skills", "global", globalAgents.keys(), globalClaude.keys());
1423
+ warnConventionConflicts("skills", "local", localAgents.keys(), localClaude.keys());
1424
+ return new Map([
1425
+ ...globalClaude,
1426
+ ...globalAgents,
1427
+ ...localClaude,
1428
+ ...localAgents
1429
+ ]);
1430
+ }
1431
+
1212
1432
  // src/cli/commands.ts
1213
1433
  async function handleModel(ctx, args) {
1214
1434
  if (args) {
@@ -1219,20 +1439,20 @@ async function handleModel(ctx, args) {
1219
1439
  if (match) {
1220
1440
  modelId = match.id;
1221
1441
  } else {
1222
- writeln(`${PREFIX.error} unknown model ${c3.cyan(args)} ${c3.dim("\u2014 run /models for the full list")}`);
1442
+ writeln(`${PREFIX.error} unknown model ${c4.cyan(args)} ${c4.dim("\u2014 run /models for the full list")}`);
1223
1443
  return;
1224
1444
  }
1225
1445
  }
1226
1446
  ctx.setModel(modelId);
1227
- writeln(`${PREFIX.success} model \u2192 ${c3.cyan(modelId)}`);
1447
+ writeln(`${PREFIX.success} model \u2192 ${c4.cyan(modelId)}`);
1228
1448
  return;
1229
1449
  }
1230
- writeln(`${c3.dim(" fetching models\u2026")}`);
1450
+ writeln(`${c4.dim(" fetching models\u2026")}`);
1231
1451
  const models = await fetchAvailableModels();
1232
1452
  process.stdout.write("\x1B[1A\r\x1B[2K");
1233
1453
  if (models.length === 0) {
1234
1454
  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."));
1455
+ writeln(c4.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
1236
1456
  return;
1237
1457
  }
1238
1458
  const byProvider = new Map;
@@ -1246,27 +1466,27 @@ async function handleModel(ctx, args) {
1246
1466
  }
1247
1467
  writeln();
1248
1468
  for (const [provider, list] of byProvider) {
1249
- writeln(c3.bold(` ${provider}`));
1469
+ writeln(c4.bold(` ${provider}`));
1250
1470
  for (const m of list) {
1251
1471
  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)}`);
1472
+ const freeTag = m.free ? c4.green(" free") : "";
1473
+ const ctxTag = m.context ? c4.dim(` ${Math.round(m.context / 1000)}k`) : "";
1474
+ const cur = isCurrent ? c4.cyan(" \u25C0") : "";
1475
+ writeln(` ${c4.dim("\xB7")} ${m.displayName}${freeTag}${ctxTag}${cur}`);
1476
+ writeln(` ${c4.dim(m.id)}`);
1257
1477
  }
1258
1478
  }
1259
1479
  writeln();
1260
- writeln(c3.dim(" /model <id> to switch \xB7 e.g. /model zen/claude-sonnet-4-6"));
1480
+ writeln(c4.dim(" /model <id> to switch \xB7 e.g. /model zen/claude-sonnet-4-6"));
1261
1481
  }
1262
1482
  function handlePlan(ctx) {
1263
1483
  ctx.setPlanMode(!ctx.planMode);
1264
1484
  if (ctx.planMode) {
1265
1485
  if (ctx.ralphMode)
1266
1486
  ctx.setRalphMode(false);
1267
- writeln(`${PREFIX.info} ${c3.yellow("plan mode")} ${c3.dim("\u2014 read-only tools + MCP, no writes or shell")}`);
1487
+ writeln(`${PREFIX.info} ${c4.yellow("plan mode")} ${c4.dim("\u2014 read-only tools + MCP, no writes or shell")}`);
1268
1488
  } else {
1269
- writeln(`${PREFIX.info} ${c3.dim("plan mode off")}`);
1489
+ writeln(`${PREFIX.info} ${c4.dim("plan mode off")}`);
1270
1490
  }
1271
1491
  }
1272
1492
  function handleRalph(ctx) {
@@ -1274,17 +1494,17 @@ function handleRalph(ctx) {
1274
1494
  if (ctx.ralphMode) {
1275
1495
  if (ctx.planMode)
1276
1496
  ctx.setPlanMode(false);
1277
- writeln(`${PREFIX.info} ${c3.magenta("ralph mode")} ${c3.dim("\u2014 loops until done, fresh context each iteration")}`);
1497
+ writeln(`${PREFIX.info} ${c4.magenta("ralph mode")} ${c4.dim("\u2014 loops until done, fresh context each iteration")}`);
1278
1498
  } else {
1279
- writeln(`${PREFIX.info} ${c3.dim("ralph mode off")}`);
1499
+ writeln(`${PREFIX.info} ${c4.dim("ralph mode off")}`);
1280
1500
  }
1281
1501
  }
1282
1502
  async function handleUndo(ctx) {
1283
1503
  const ok = await ctx.undoLastTurn();
1284
1504
  if (ok) {
1285
- writeln(`${PREFIX.success} ${c3.dim("last turn undone \u2014 history and files restored")}`);
1505
+ writeln(`${PREFIX.success} ${c4.dim("last turn undone \u2014 history and files restored")}`);
1286
1506
  } else {
1287
- writeln(`${PREFIX.info} ${c3.dim("nothing to undo")}`);
1507
+ writeln(`${PREFIX.info} ${c4.dim("nothing to undo")}`);
1288
1508
  }
1289
1509
  }
1290
1510
  async function handleMcp(ctx, args) {
@@ -1294,28 +1514,28 @@ async function handleMcp(ctx, args) {
1294
1514
  case "list": {
1295
1515
  const servers = listMcpServers();
1296
1516
  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...]"));
1517
+ writeln(c4.dim(" no MCP servers configured"));
1518
+ writeln(c4.dim(" /mcp add <name> http <url> \xB7 /mcp add <name> stdio <cmd> [args...]"));
1299
1519
  return;
1300
1520
  }
1301
1521
  writeln();
1302
1522
  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}`);
1523
+ const detail = s.url ? c4.dim(` ${s.url}`) : s.command ? c4.dim(` ${s.command}`) : "";
1524
+ writeln(` ${c4.yellow("\u2699")} ${c4.bold(s.name)} ${c4.dim(s.transport)}${detail}`);
1305
1525
  }
1306
1526
  return;
1307
1527
  }
1308
1528
  case "add": {
1309
1529
  const [, name, transport, ...rest] = parts;
1310
1530
  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...]"));
1531
+ writeln(c4.red(" usage: /mcp add <name> http <url>"));
1532
+ writeln(c4.red(" /mcp add <name> stdio <cmd> [args...]"));
1313
1533
  return;
1314
1534
  }
1315
1535
  if (transport === "http") {
1316
1536
  const url = rest[0];
1317
1537
  if (!url) {
1318
- writeln(c3.red(" usage: /mcp add <name> http <url>"));
1538
+ writeln(c4.red(" usage: /mcp add <name> http <url>"));
1319
1539
  return;
1320
1540
  }
1321
1541
  upsertMcpServer({
@@ -1329,7 +1549,7 @@ async function handleMcp(ctx, args) {
1329
1549
  } else if (transport === "stdio") {
1330
1550
  const [command, ...cmdArgs] = rest;
1331
1551
  if (!command) {
1332
- writeln(c3.red(" usage: /mcp add <name> stdio <cmd> [args...]"));
1552
+ writeln(c4.red(" usage: /mcp add <name> stdio <cmd> [args...]"));
1333
1553
  return;
1334
1554
  }
1335
1555
  upsertMcpServer({
@@ -1341,14 +1561,14 @@ async function handleMcp(ctx, args) {
1341
1561
  env: null
1342
1562
  });
1343
1563
  } else {
1344
- writeln(c3.red(` unknown transport: ${transport} (use http or stdio)`));
1564
+ writeln(c4.red(` unknown transport: ${transport} (use http or stdio)`));
1345
1565
  return;
1346
1566
  }
1347
1567
  try {
1348
1568
  await ctx.connectMcpServer(name);
1349
- writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} added and connected`);
1569
+ writeln(`${PREFIX.success} mcp server ${c4.cyan(name)} added and connected`);
1350
1570
  } catch (e) {
1351
- writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} saved ${c3.dim(`(connection failed: ${String(e)})`)}`);
1571
+ writeln(`${PREFIX.success} mcp server ${c4.cyan(name)} saved ${c4.dim(`(connection failed: ${String(e)})`)}`);
1352
1572
  }
1353
1573
  return;
1354
1574
  }
@@ -1356,50 +1576,36 @@ async function handleMcp(ctx, args) {
1356
1576
  case "rm": {
1357
1577
  const [, name] = parts;
1358
1578
  if (!name) {
1359
- writeln(c3.red(" usage: /mcp remove <name>"));
1579
+ writeln(c4.red(" usage: /mcp remove <name>"));
1360
1580
  return;
1361
1581
  }
1362
1582
  deleteMcpServer(name);
1363
- writeln(`${PREFIX.success} mcp server ${c3.cyan(name)} removed`);
1583
+ writeln(`${PREFIX.success} mcp server ${c4.cyan(name)} removed`);
1364
1584
  return;
1365
1585
  }
1366
1586
  default:
1367
- writeln(c3.red(` unknown: /mcp ${sub}`));
1368
- writeln(c3.dim(" subcommands: list \xB7 add \xB7 remove"));
1587
+ writeln(c4.red(` unknown: /mcp ${sub}`));
1588
+ writeln(c4.dim(" subcommands: list \xB7 add \xB7 remove"));
1369
1589
  }
1370
1590
  }
1371
1591
  var REVIEW_PROMPT = (cwd, focus) => `You are a code reviewer. Review recent changes and provide actionable feedback.
1372
1592
 
1373
1593
  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.
1594
+ ${focus ? `Review: ${focus}` : "Review the current changes"}
1595
+
1596
+ Perform a sensible code review:
1597
+
1598
+ - Correctness: Are the changes in alignment with the goal?
1599
+ - Code quality: Is there duplicate, dead or bad code patterns introduced or as a result of the changes?
1600
+ - Is the code performant?
1601
+ - Never flag style choices as bugs, don't be a zeolot.
1602
+ - Never flag false positives, if you think something is wrong, check before saying it's an issue.
1603
+
1604
+ Output a small summary with only the issues found. If nothing of note was found reply saying that.
1399
1605
  `;
1400
1606
  async function handleReview(ctx, args) {
1401
1607
  const focus = args.trim();
1402
- writeln(`${PREFIX.info} ${c3.cyan("review")} ${c3.dim("\u2014 spawning review subagent\u2026")}`);
1608
+ writeln(`${PREFIX.info} ${c4.cyan("review")} ${c4.dim("\u2014 spawning review subagent\u2026")}`);
1403
1609
  writeln();
1404
1610
  try {
1405
1611
  const output = await ctx.runSubagent(REVIEW_PROMPT(ctx.cwd, focus));
@@ -1424,9 +1630,37 @@ ${output.result}
1424
1630
  }
1425
1631
  function handleNew(ctx) {
1426
1632
  ctx.startNewSession();
1427
- writeln(`${PREFIX.success} ${c3.dim("new session started \u2014 context cleared")}`);
1633
+ writeln(`${PREFIX.success} ${c4.dim("new session started \u2014 context cleared")}`);
1634
+ }
1635
+ async function handleCustomCommand(cmd, args, ctx) {
1636
+ const prompt = await expandTemplate(cmd.template, args, ctx.cwd);
1637
+ const label = c4.cyan(cmd.name);
1638
+ const srcPath = cmd.source === "local" ? `.agents/commands/${cmd.name}.md` : `~/.agents/commands/${cmd.name}.md`;
1639
+ const src = c4.dim(`[${srcPath}]`);
1640
+ writeln(`${PREFIX.info} ${label} ${src}`);
1641
+ writeln();
1642
+ try {
1643
+ const output = await ctx.runSubagent(prompt, cmd.model);
1644
+ if (output.activity.length) {
1645
+ renderSubagentActivity(output.activity, " ", 1);
1646
+ writeln();
1647
+ }
1648
+ write(renderMarkdown(output.result));
1649
+ writeln();
1650
+ return {
1651
+ type: "inject-user-message",
1652
+ text: `/${cmd.name} output:
1653
+
1654
+ ${output.result}
1655
+
1656
+ <system-message>Summarize the findings above to the user.</system-message>`
1657
+ };
1658
+ } catch (e) {
1659
+ writeln(`${PREFIX.error} /${cmd.name} failed: ${String(e)}`);
1660
+ return { type: "handled" };
1661
+ }
1428
1662
  }
1429
- function handleHelp() {
1663
+ function handleHelp(ctx, custom) {
1430
1664
  writeln();
1431
1665
  const cmds = [
1432
1666
  ["/model [id]", "list or switch models (fetches live list)"],
@@ -1445,16 +1679,49 @@ function handleHelp() {
1445
1679
  ["/exit", "quit"]
1446
1680
  ];
1447
1681
  for (const [cmd, desc] of cmds) {
1448
- writeln(` ${c3.cyan(cmd.padEnd(26))} ${c3.dim(desc)}`);
1682
+ writeln(` ${c4.cyan(cmd.padEnd(26))} ${c4.dim(desc)}`);
1683
+ }
1684
+ if (custom.size > 0) {
1685
+ writeln();
1686
+ writeln(c4.dim(" custom commands:"));
1687
+ for (const cmd of custom.values()) {
1688
+ const tag = cmd.source === "local" ? c4.dim(" (local)") : c4.dim(" (global)");
1689
+ writeln(` ${c4.green(`/${cmd.name}`.padEnd(26))} ${c4.dim(cmd.description)}${tag}`);
1690
+ }
1691
+ }
1692
+ const agents = loadAgents(ctx.cwd);
1693
+ if (agents.size > 0) {
1694
+ writeln();
1695
+ writeln(c4.dim(" agents (~/.agents/agents/ or .agents/agents/):"));
1696
+ for (const agent of agents.values()) {
1697
+ const tag = agent.source === "local" ? c4.dim(" (local)") : c4.dim(" (global)");
1698
+ writeln(` ${c4.magenta(`@${agent.name}`.padEnd(26))} ${c4.dim(agent.description)}${tag}`);
1699
+ }
1700
+ }
1701
+ const skills = loadSkills(ctx.cwd);
1702
+ if (skills.size > 0) {
1703
+ writeln();
1704
+ writeln(c4.dim(" skills (~/.agents/skills/ or .agents/skills/):"));
1705
+ for (const skill of skills.values()) {
1706
+ const tag = skill.source === "local" ? c4.dim(" (local)") : c4.dim(" (global)");
1707
+ writeln(` ${c4.yellow(`@${skill.name}`.padEnd(26))} ${c4.dim(skill.description)}${tag}`);
1708
+ }
1449
1709
  }
1450
1710
  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")}`);
1711
+ writeln(` ${c4.green("@agent".padEnd(26))} ${c4.dim("run prompt through a custom agent (Tab to complete)")}`);
1712
+ writeln(` ${c4.green("@skill".padEnd(26))} ${c4.dim("inject skill instructions into prompt (Tab to complete)")}`);
1713
+ writeln(` ${c4.green("@file".padEnd(26))} ${c4.dim("inject file contents into prompt (Tab to complete)")}`);
1714
+ writeln(` ${c4.green("!cmd".padEnd(26))} ${c4.dim("run shell command, output added as context")}`);
1453
1715
  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`);
1716
+ writeln(` ${c4.dim("ctrl+c")} cancel ${c4.dim("\xB7")} ${c4.dim("ctrl+d")} exit ${c4.dim("\xB7")} ${c4.dim("ctrl+r")} history search ${c4.dim("\xB7")} ${c4.dim("\u2191\u2193")} history`);
1455
1717
  writeln();
1456
1718
  }
1457
1719
  async function handleCommand(command, args, ctx) {
1720
+ const custom = loadCustomCommands(ctx.cwd);
1721
+ const customCmd = custom.get(command.toLowerCase());
1722
+ if (customCmd) {
1723
+ return await handleCustomCommand(customCmd, args, ctx);
1724
+ }
1458
1725
  switch (command.toLowerCase()) {
1459
1726
  case "model":
1460
1727
  case "models":
@@ -1479,15 +1746,16 @@ async function handleCommand(command, args, ctx) {
1479
1746
  return await handleReview(ctx, args);
1480
1747
  case "help":
1481
1748
  case "?":
1482
- handleHelp();
1749
+ handleHelp(ctx, custom);
1483
1750
  return { type: "handled" };
1484
1751
  case "exit":
1485
1752
  case "quit":
1486
1753
  case "q":
1487
1754
  return { type: "exit" };
1488
- default:
1489
- writeln(`${PREFIX.error} unknown: /${command} ${c3.dim("\u2014 /help for commands")}`);
1755
+ default: {
1756
+ writeln(`${PREFIX.error} unknown: /${command} ${c4.dim("\u2014 /help for commands")}`);
1490
1757
  return { type: "unknown", command };
1758
+ }
1491
1759
  }
1492
1760
  }
1493
1761
 
@@ -1527,8 +1795,8 @@ async function loadImageFile(filePath) {
1527
1795
  }
1528
1796
 
1529
1797
  // src/cli/input.ts
1530
- import { join as join2, relative } from "path";
1531
- import * as c4 from "yoctocolors";
1798
+ import { join as join5, relative } from "path";
1799
+ import * as c5 from "yoctocolors";
1532
1800
  var ESC = "\x1B";
1533
1801
  var CSI = `${ESC}[`;
1534
1802
  var CLEAR_LINE = `\r${CSI}2K`;
@@ -1556,16 +1824,33 @@ var CTRL_K = "\v";
1556
1824
  var CTRL_L = "\f";
1557
1825
  var CTRL_R = "\x12";
1558
1826
  var TAB = "\t";
1559
- async function getFileCompletions(prefix, cwd) {
1827
+ async function getAtCompletions(prefix, cwd) {
1560
1828
  const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
1561
- const glob = new Bun.Glob(`**/*${query}*`);
1562
1829
  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)
1830
+ const MAX = 10;
1831
+ const skills = loadSkills(cwd);
1832
+ for (const [name] of skills) {
1833
+ if (results.length >= MAX)
1568
1834
  break;
1835
+ if (name.includes(query))
1836
+ results.push(`@${name}`);
1837
+ }
1838
+ const agents = loadAgents(cwd);
1839
+ for (const [name] of agents) {
1840
+ if (results.length >= MAX)
1841
+ break;
1842
+ if (name.includes(query))
1843
+ results.push(`@${name}`);
1844
+ }
1845
+ if (results.length < MAX) {
1846
+ const glob = new Bun.Glob(`**/*${query}*`);
1847
+ for await (const file of glob.scan({ cwd, onlyFiles: true })) {
1848
+ if (file.includes("node_modules") || file.includes(".git"))
1849
+ continue;
1850
+ results.push(`@${relative(cwd, join5(cwd, file))}`);
1851
+ if (results.length >= MAX)
1852
+ break;
1853
+ }
1569
1854
  }
1570
1855
  return results;
1571
1856
  }
@@ -1584,7 +1869,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
1584
1869
  }
1585
1870
  }
1586
1871
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
1587
- const filePath = trimmed.startsWith("/") ? trimmed : join2(cwd, trimmed);
1872
+ const filePath = trimmed.startsWith("/") ? trimmed : join5(cwd, trimmed);
1588
1873
  const attachment = await loadImageFile(filePath);
1589
1874
  if (attachment) {
1590
1875
  const name = filePath.split("/").pop() ?? trimmed;
@@ -1632,9 +1917,9 @@ function pasteLabel(text) {
1632
1917
  const more = extra > 0 ? ` +${extra} more line${extra === 1 ? "" : "s"}` : "";
1633
1918
  return `[pasted: "${preview}"${more}]`;
1634
1919
  }
1635
- var PROMPT = c4.green("\u25B6 ");
1636
- var PROMPT_PLAN = c4.yellow("\u2B22 ");
1637
- var PROMPT_RALPH = c4.magenta("\u21BB ");
1920
+ var PROMPT = c5.green("\u25B6 ");
1921
+ var PROMPT_PLAN = c5.yellow("\u2B22 ");
1922
+ var PROMPT_RALPH = c5.magenta("\u21BB ");
1638
1923
  var PROMPT_RAW_LEN = 2;
1639
1924
  async function readline(opts) {
1640
1925
  const cwd = opts.cwd ?? process.cwd();
@@ -1653,7 +1938,7 @@ async function readline(opts) {
1653
1938
  const reader = getStdinReader();
1654
1939
  function renderPrompt() {
1655
1940
  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)));
1941
+ const visualBuf = (pasteBuffer ? buf.replace(PASTE_SENTINEL, c5.dim(pasteLabel(pasteBuffer))) : buf).replace(/\[image: [^\]]+\]/g, (m) => c5.dim(c5.cyan(m)));
1657
1942
  const visualCursor = pasteBuffer ? (() => {
1658
1943
  const sentinelPos = buf.indexOf(PASTE_SENTINEL);
1659
1944
  if (sentinelPos === -1 || cursor <= sentinelPos)
@@ -1665,7 +1950,7 @@ async function readline(opts) {
1665
1950
  process.stdout.write(`${CLEAR_LINE}${prompt}${display}${CSI}${PROMPT_RAW_LEN + visualCursor + 1}G`);
1666
1951
  }
1667
1952
  function renderSearchPrompt() {
1668
- process.stdout.write(`${CLEAR_LINE}${c4.cyan("search:")} ${searchQuery}\u2588`);
1953
+ process.stdout.write(`${CLEAR_LINE}${c5.cyan("search:")} ${searchQuery}\u2588`);
1669
1954
  }
1670
1955
  function applyHistory() {
1671
1956
  if (histIdx < history.length) {
@@ -1781,6 +2066,18 @@ async function readline(opts) {
1781
2066
  renderPrompt();
1782
2067
  continue;
1783
2068
  }
2069
+ if (raw === `${ESC}${BACKSPACE}`) {
2070
+ const end = cursor;
2071
+ while (cursor > 0 && buf[cursor - 1] === " ")
2072
+ cursor--;
2073
+ while (cursor > 0 && buf[cursor - 1] !== " ")
2074
+ cursor--;
2075
+ buf = buf.slice(0, cursor) + buf.slice(end);
2076
+ if (pasteBuffer && !buf.includes(PASTE_SENTINEL))
2077
+ pasteBuffer = null;
2078
+ renderPrompt();
2079
+ continue;
2080
+ }
1784
2081
  if (raw === ESC) {
1785
2082
  process.stdout.write(`
1786
2083
  `);
@@ -1860,7 +2157,7 @@ async function readline(opts) {
1860
2157
  const beforeCursor = buf.slice(0, cursor);
1861
2158
  const atMatch = beforeCursor.match(/@(\S*)$/);
1862
2159
  if (atMatch) {
1863
- const completions = await getFileCompletions(atMatch[0], cwd);
2160
+ const completions = await getAtCompletions(atMatch[0], cwd);
1864
2161
  if (completions.length === 1 && completions[0]) {
1865
2162
  const replacement = completions[0];
1866
2163
  buf = buf.slice(0, cursor - (atMatch[0] ?? "").length) + replacement + buf.slice(cursor);
@@ -1869,8 +2166,8 @@ async function readline(opts) {
1869
2166
  } else if (completions.length > 1) {
1870
2167
  process.stdout.write(`
1871
2168
  `);
1872
- for (const c5 of completions)
1873
- process.stdout.write(` ${c5}
2169
+ for (const c6 of completions)
2170
+ process.stdout.write(` ${c6}
1874
2171
  `);
1875
2172
  renderPrompt();
1876
2173
  }
@@ -1984,10 +2281,10 @@ async function* runTurn(options) {
1984
2281
  for await (const chunk of result.fullStream) {
1985
2282
  if (signal?.aborted)
1986
2283
  break;
1987
- const c5 = chunk;
1988
- switch (c5.type) {
2284
+ const c6 = chunk;
2285
+ switch (c6.type) {
1989
2286
  case "text-delta": {
1990
- const delta = typeof c5.text === "string" ? c5.text : typeof c5.textDelta === "string" ? c5.textDelta : "";
2287
+ const delta = typeof c6.text === "string" ? c6.text : typeof c6.textDelta === "string" ? c6.textDelta : "";
1991
2288
  yield {
1992
2289
  type: "text-delta",
1993
2290
  delta
@@ -1997,18 +2294,18 @@ async function* runTurn(options) {
1997
2294
  case "tool-call": {
1998
2295
  yield {
1999
2296
  type: "tool-call-start",
2000
- toolCallId: String(c5.toolCallId ?? ""),
2001
- toolName: String(c5.toolName ?? ""),
2002
- args: c5.input ?? c5.args
2297
+ toolCallId: String(c6.toolCallId ?? ""),
2298
+ toolName: String(c6.toolName ?? ""),
2299
+ args: c6.input ?? c6.args
2003
2300
  };
2004
2301
  break;
2005
2302
  }
2006
2303
  case "tool-result": {
2007
2304
  yield {
2008
2305
  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,
2306
+ toolCallId: String(c6.toolCallId ?? ""),
2307
+ toolName: String(c6.toolName ?? ""),
2308
+ result: "output" in c6 ? c6.output : ("result" in c6) ? c6.result : undefined,
2012
2309
  isError: false
2013
2310
  };
2014
2311
  break;
@@ -2016,15 +2313,15 @@ async function* runTurn(options) {
2016
2313
  case "tool-error": {
2017
2314
  yield {
2018
2315
  type: "tool-result",
2019
- toolCallId: String(c5.toolCallId ?? ""),
2020
- toolName: String(c5.toolName ?? ""),
2021
- result: c5.error ?? "Tool execution failed",
2316
+ toolCallId: String(c6.toolCallId ?? ""),
2317
+ toolName: String(c6.toolName ?? ""),
2318
+ result: c6.error ?? "Tool execution failed",
2022
2319
  isError: true
2023
2320
  };
2024
2321
  break;
2025
2322
  }
2026
2323
  case "error": {
2027
- const err = c5.error;
2324
+ const err = c6.error;
2028
2325
  throw err instanceof Error ? err : new Error(String(err));
2029
2326
  }
2030
2327
  }
@@ -2104,8 +2401,8 @@ async function connectMcpServer(config) {
2104
2401
  }
2105
2402
 
2106
2403
  // src/tools/snapshot.ts
2107
- import { readFileSync, unlinkSync as unlinkSync2 } from "fs";
2108
- import { join as join3 } from "path";
2404
+ import { readFileSync as readFileSync4, unlinkSync as unlinkSync2 } from "fs";
2405
+ import { join as join6 } from "path";
2109
2406
  async function gitBytes(args, cwd) {
2110
2407
  try {
2111
2408
  const proc = Bun.spawn(["git", ...args], {
@@ -2196,7 +2493,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
2196
2493
  return false;
2197
2494
  const files = [];
2198
2495
  for (const entry of entries) {
2199
- const absPath = join3(repoRoot, entry.path);
2496
+ const absPath = join6(repoRoot, entry.path);
2200
2497
  if (!entry.existsOnDisk) {
2201
2498
  const { bytes, code } = await gitBytes(["show", `HEAD:${entry.path}`], repoRoot);
2202
2499
  if (code === 0) {
@@ -2210,7 +2507,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
2210
2507
  }
2211
2508
  if (entry.isNew) {
2212
2509
  try {
2213
- const content = readFileSync(absPath);
2510
+ const content = readFileSync4(absPath);
2214
2511
  files.push({
2215
2512
  path: entry.path,
2216
2513
  content: new Uint8Array(content),
@@ -2220,7 +2517,7 @@ async function takeSnapshot(cwd, sessionId, turnIndex) {
2220
2517
  continue;
2221
2518
  }
2222
2519
  try {
2223
- const content = readFileSync(absPath);
2520
+ const content = readFileSync4(absPath);
2224
2521
  files.push({
2225
2522
  path: entry.path,
2226
2523
  content: new Uint8Array(content),
@@ -2245,7 +2542,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
2245
2542
  const root = repoRoot ?? cwd;
2246
2543
  let anyFailed = false;
2247
2544
  for (const file of files) {
2248
- const absPath = join3(root, file.path);
2545
+ const absPath = join6(root, file.path);
2249
2546
  if (!file.existed) {
2250
2547
  try {
2251
2548
  if (await Bun.file(absPath).exists()) {
@@ -2274,8 +2571,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
2274
2571
  }
2275
2572
 
2276
2573
  // src/session/manager.ts
2277
- import { homedir as homedir3 } from "os";
2278
- import * as c5 from "yoctocolors";
2574
+ import * as c6 from "yoctocolors";
2279
2575
  function newSession(model, cwd) {
2280
2576
  const id = generateSessionId();
2281
2577
  createSession({ id, cwd, model });
@@ -2294,19 +2590,19 @@ function touchActiveSession(session) {
2294
2590
  function printSessionList() {
2295
2591
  const sessions = listSessions(20);
2296
2592
  if (sessions.length === 0) {
2297
- writeln(c5.dim("No sessions found."));
2593
+ writeln(c6.dim("No sessions found."));
2298
2594
  return;
2299
2595
  }
2300
2596
  writeln(`
2301
- ${c5.bold("Recent sessions:")}`);
2597
+ ${c6.bold("Recent sessions:")}`);
2302
2598
  for (const s of sessions) {
2303
2599
  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)}`);
2600
+ const cwd = tildePath(s.cwd);
2601
+ const title = s.title || c6.dim("(untitled)");
2602
+ writeln(` ${c6.dim(s.id.padEnd(14))} ${title.padEnd(30)} ${c6.cyan(s.model.split("/").pop() ?? s.model).padEnd(20)} ${c6.dim(cwd)} ${c6.dim(date)}`);
2307
2603
  }
2308
2604
  writeln(`
2309
- ${c5.dim("Use")} mc --resume <id> ${c5.dim("to continue a session.")}`);
2605
+ ${c6.dim("Use")} mc --resume <id> ${c6.dim("to continue a session.")}`);
2310
2606
  }
2311
2607
  function getMostRecentSession() {
2312
2608
  const sessions = listSessions(1);
@@ -2314,8 +2610,8 @@ function getMostRecentSession() {
2314
2610
  }
2315
2611
 
2316
2612
  // 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";
2613
+ import { existsSync as existsSync5, mkdirSync as mkdirSync2 } from "fs";
2614
+ import { dirname, join as join7, relative as relative2 } from "path";
2319
2615
  import { z as z2 } from "zod";
2320
2616
 
2321
2617
  // src/tools/diff.ts
@@ -2455,10 +2751,10 @@ var createTool = {
2455
2751
  schema: CreateSchema,
2456
2752
  execute: async (input) => {
2457
2753
  const cwd = input.cwd ?? process.cwd();
2458
- const filePath = input.path.startsWith("/") ? input.path : join4(cwd, input.path);
2754
+ const filePath = input.path.startsWith("/") ? input.path : join7(cwd, input.path);
2459
2755
  const relPath = relative2(cwd, filePath);
2460
2756
  const dir = dirname(filePath);
2461
- if (!existsSync2(dir))
2757
+ if (!existsSync5(dir))
2462
2758
  mkdirSync2(dir, { recursive: true });
2463
2759
  const file = Bun.file(filePath);
2464
2760
  const created = !await file.exists();
@@ -2470,7 +2766,7 @@ var createTool = {
2470
2766
  };
2471
2767
 
2472
2768
  // src/tools/glob.ts
2473
- import { join as join5, relative as relative3 } from "path";
2769
+ import { join as join8, relative as relative3 } from "path";
2474
2770
  import { z as z3 } from "zod";
2475
2771
  var GlobSchema = z3.object({
2476
2772
  pattern: z3.string().describe("Glob pattern to match files against, e.g. '**/*.ts'"),
@@ -2502,7 +2798,7 @@ var globTool = {
2502
2798
  if (ignored)
2503
2799
  continue;
2504
2800
  try {
2505
- const fullPath = join5(cwd, file);
2801
+ const fullPath = join8(cwd, file);
2506
2802
  const stat = await Bun.file(fullPath).stat?.() ?? null;
2507
2803
  matches.push({ path: file, mtime: stat?.mtime?.getTime() ?? 0 });
2508
2804
  } catch {
@@ -2515,13 +2811,13 @@ var globTool = {
2515
2811
  if (truncated)
2516
2812
  matches.pop();
2517
2813
  matches.sort((a, b) => b.mtime - a.mtime);
2518
- const files = matches.map((m) => relative3(cwd, join5(cwd, m.path)));
2814
+ const files = matches.map((m) => relative3(cwd, join8(cwd, m.path)));
2519
2815
  return { files, count: files.length, truncated };
2520
2816
  }
2521
2817
  };
2522
2818
 
2523
2819
  // src/tools/grep.ts
2524
- import { join as join6 } from "path";
2820
+ import { join as join9 } from "path";
2525
2821
  import { z as z4 } from "zod";
2526
2822
 
2527
2823
  // src/tools/hashline.ts
@@ -2607,7 +2903,7 @@ var grepTool = {
2607
2903
  if (ignoreGlob.some((g) => g.match(relPath) || g.match(relPath.split("/")[0] ?? ""))) {
2608
2904
  continue;
2609
2905
  }
2610
- const fullPath = join6(cwd, relPath);
2906
+ const fullPath = join9(cwd, relPath);
2611
2907
  let text;
2612
2908
  try {
2613
2909
  text = await Bun.file(fullPath).text();
@@ -2626,11 +2922,11 @@ var grepTool = {
2626
2922
  const ctxStart = Math.max(0, i - contextLines);
2627
2923
  const ctxEnd = Math.min(lines.length - 1, i + contextLines);
2628
2924
  const context = [];
2629
- for (let c6 = ctxStart;c6 <= ctxEnd; c6++) {
2925
+ for (let c7 = ctxStart;c7 <= ctxEnd; c7++) {
2630
2926
  context.push({
2631
- line: c6 + 1,
2632
- text: formatHashLine(c6 + 1, lines[c6] ?? ""),
2633
- isMatch: c6 === i
2927
+ line: c7 + 1,
2928
+ text: formatHashLine(c7 + 1, lines[c7] ?? ""),
2929
+ isMatch: c7 === i
2634
2930
  });
2635
2931
  }
2636
2932
  allMatches.push({
@@ -2658,8 +2954,8 @@ var grepTool = {
2658
2954
 
2659
2955
  // src/tools/hooks.ts
2660
2956
  import { constants, accessSync } from "fs";
2661
- import { homedir as homedir4 } from "os";
2662
- import { join as join7 } from "path";
2957
+ import { homedir as homedir6 } from "os";
2958
+ import { join as join10 } from "path";
2663
2959
  function isExecutable(filePath) {
2664
2960
  try {
2665
2961
  accessSync(filePath, constants.X_OK);
@@ -2671,8 +2967,8 @@ function isExecutable(filePath) {
2671
2967
  function findHook(toolName, cwd) {
2672
2968
  const scriptName = `post-${toolName}`;
2673
2969
  const candidates = [
2674
- join7(cwd, ".agents", "hooks", scriptName),
2675
- join7(homedir4(), ".agents", "hooks", scriptName)
2970
+ join10(cwd, ".agents", "hooks", scriptName),
2971
+ join10(homedir6(), ".agents", "hooks", scriptName)
2676
2972
  ];
2677
2973
  for (const p of candidates) {
2678
2974
  if (isExecutable(p))
@@ -2761,7 +3057,7 @@ function hookEnvForRead(input, cwd) {
2761
3057
  }
2762
3058
 
2763
3059
  // src/tools/insert.ts
2764
- import { join as join8, relative as relative4 } from "path";
3060
+ import { join as join11, relative as relative4 } from "path";
2765
3061
  import { z as z5 } from "zod";
2766
3062
  var InsertSchema = z5.object({
2767
3063
  path: z5.string().describe("File path to edit (absolute or relative to cwd)"),
@@ -2776,7 +3072,7 @@ var insertTool = {
2776
3072
  schema: InsertSchema,
2777
3073
  execute: async (input) => {
2778
3074
  const cwd = input.cwd ?? process.cwd();
2779
- const filePath = input.path.startsWith("/") ? input.path : join8(cwd, input.path);
3075
+ const filePath = input.path.startsWith("/") ? input.path : join11(cwd, input.path);
2780
3076
  const relPath = relative4(cwd, filePath);
2781
3077
  const file = Bun.file(filePath);
2782
3078
  if (!await file.exists()) {
@@ -2821,7 +3117,7 @@ function parseAnchor(value) {
2821
3117
  }
2822
3118
 
2823
3119
  // src/tools/read.ts
2824
- import { join as join9, relative as relative5 } from "path";
3120
+ import { join as join12, relative as relative5 } from "path";
2825
3121
  import { z as z6 } from "zod";
2826
3122
  var ReadSchema = z6.object({
2827
3123
  path: z6.string().describe("File path to read (absolute or relative to cwd)"),
@@ -2836,7 +3132,7 @@ var readTool = {
2836
3132
  schema: ReadSchema,
2837
3133
  execute: async (input) => {
2838
3134
  const cwd = input.cwd ?? process.cwd();
2839
- const filePath = input.path.startsWith("/") ? input.path : join9(cwd, input.path);
3135
+ const filePath = input.path.startsWith("/") ? input.path : join12(cwd, input.path);
2840
3136
  const file = Bun.file(filePath);
2841
3137
  const exists = await file.exists();
2842
3138
  if (!exists) {
@@ -2869,7 +3165,7 @@ var readTool = {
2869
3165
  };
2870
3166
 
2871
3167
  // src/tools/replace.ts
2872
- import { join as join10, relative as relative6 } from "path";
3168
+ import { join as join13, relative as relative6 } from "path";
2873
3169
  import { z as z7 } from "zod";
2874
3170
  var ReplaceSchema = z7.object({
2875
3171
  path: z7.string().describe("File path to edit (absolute or relative to cwd)"),
@@ -2884,7 +3180,7 @@ var replaceTool = {
2884
3180
  schema: ReplaceSchema,
2885
3181
  execute: async (input) => {
2886
3182
  const cwd = input.cwd ?? process.cwd();
2887
- const filePath = input.path.startsWith("/") ? input.path : join10(cwd, input.path);
3183
+ const filePath = input.path.startsWith("/") ? input.path : join13(cwd, input.path);
2888
3184
  const relPath = relative6(cwd, filePath);
2889
3185
  const file = Bun.file(filePath);
2890
3186
  if (!await file.exists()) {
@@ -3025,15 +3321,19 @@ var shellTool = {
3025
3321
  // src/tools/subagent.ts
3026
3322
  import { z as z9 } from "zod";
3027
3323
  var SubagentInput = z9.object({
3028
- prompt: z9.string().describe("The task or question to give the subagent")
3324
+ prompt: z9.string().describe("The task or question to give the subagent"),
3325
+ agentName: z9.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
3029
3326
  });
3030
- function createSubagentTool(runSubagent) {
3327
+ function createSubagentTool(runSubagent, availableAgents) {
3328
+ const agentSection = availableAgents.size > 0 ? `
3329
+
3330
+ When the user's message contains @<agent-name>, delegate to that agent by setting agentName to the exact agent name. Available custom agents: ${[...availableAgents.entries()].map(([name, cfg]) => `"${name}" (${cfg.description})`).join(", ")}.` : "";
3031
3331
  return {
3032
3332
  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.",
3333
+ 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.${agentSection}`,
3034
3334
  schema: SubagentInput,
3035
3335
  execute: async (input) => {
3036
- return runSubagent(input.prompt);
3336
+ return runSubagent(input.prompt, input.agentName);
3037
3337
  }
3038
3338
  };
3039
3339
  }
@@ -3090,12 +3390,12 @@ function buildToolSet(opts) {
3090
3390
  withHooks(withCwdDefault(replaceTool, cwd), lookupHook, cwd, (result) => hookEnvForReplace(result, cwd), onHook),
3091
3391
  withHooks(withCwdDefault(insertTool, cwd), lookupHook, cwd, (result) => hookEnvForInsert(result, cwd), onHook),
3092
3392
  withHooks(withCwdDefault(shellTool, cwd), lookupHook, cwd, (result, input) => hookEnvForShell(result, input, cwd), onHook),
3093
- createSubagentTool(async (prompt) => {
3393
+ createSubagentTool(async (prompt, agentName) => {
3094
3394
  if (depth >= MAX_SUBAGENT_DEPTH) {
3095
3395
  throw new Error(`Subagent depth limit reached (max ${MAX_SUBAGENT_DEPTH}). ` + `Cannot spawn another subagent from depth ${depth}.`);
3096
3396
  }
3097
- return opts.runSubagent(prompt, depth + 1);
3098
- })
3397
+ return opts.runSubagent(prompt, depth + 1, agentName);
3398
+ }, opts.availableAgents)
3099
3399
  ];
3100
3400
  }
3101
3401
  function buildReadOnlyToolSet(opts) {
@@ -3126,14 +3426,14 @@ async function getGitBranch(cwd) {
3126
3426
  }
3127
3427
  function loadContextFile(cwd) {
3128
3428
  const candidates = [
3129
- join11(cwd, "AGENTS.md"),
3130
- join11(cwd, "CLAUDE.md"),
3131
- join11(getConfigDir(), "AGENTS.md")
3429
+ join14(cwd, "AGENTS.md"),
3430
+ join14(cwd, "CLAUDE.md"),
3431
+ join14(getConfigDir(), "AGENTS.md")
3132
3432
  ];
3133
3433
  for (const p of candidates) {
3134
- if (existsSync3(p)) {
3434
+ if (existsSync6(p)) {
3135
3435
  try {
3136
- return readFileSync2(p, "utf-8");
3436
+ return readFileSync5(p, "utf-8");
3137
3437
  } catch {}
3138
3438
  }
3139
3439
  }
@@ -3141,7 +3441,7 @@ function loadContextFile(cwd) {
3141
3441
  }
3142
3442
  function buildSystemPrompt(cwd) {
3143
3443
  const contextFile = loadContextFile(cwd);
3144
- const cwdDisplay = cwd.startsWith(homedir5()) ? `~${cwd.slice(homedir5().length)}` : cwd;
3444
+ const cwdDisplay = tildePath(cwd);
3145
3445
  const now = new Date().toLocaleString(undefined, { hour12: false });
3146
3446
  let prompt = `You are mini-coder, a small and fast CLI coding agent.
3147
3447
  You have access to tools to read files, search code, make edits, run shell commands, and spawn subagents.
@@ -3180,7 +3480,7 @@ async function runShellPassthrough(command, cwd) {
3180
3480
  const out = [stdout, stderr].filter(Boolean).join(`
3181
3481
  `).trim();
3182
3482
  if (out)
3183
- writeln(c6.dim(out));
3483
+ writeln(c7.dim(out));
3184
3484
  return out;
3185
3485
  } finally {
3186
3486
  restoreTerminal();
@@ -3199,22 +3499,29 @@ async function runAgent(opts) {
3199
3499
  session = resumed;
3200
3500
  currentModel = session.model;
3201
3501
  deleteAllSnapshots(session.id);
3202
- renderInfo(`Resumed session ${session.id} (${c6.cyan(currentModel)})`);
3502
+ renderInfo(`Resumed session ${session.id} (${c7.cyan(currentModel)})`);
3203
3503
  } else {
3204
3504
  session = newSession(currentModel, cwd);
3205
3505
  }
3206
3506
  let turnIndex = getMaxTurnIndex(session.id) + 1;
3207
3507
  const coreHistory = [...session.messages];
3208
- const runSubagent = async (prompt, depth = 0) => {
3508
+ const runSubagent = async (prompt, depth = 0, agentName, modelOverride) => {
3509
+ const allAgents = loadAgents(cwd);
3510
+ const agentConfig = agentName ? allAgents.get(agentName) : undefined;
3511
+ if (agentName && !agentConfig) {
3512
+ throw new Error(`Unknown agent "${agentName}". Available agents: ${[...allAgents.keys()].join(", ") || "(none)"}`);
3513
+ }
3514
+ const model = modelOverride ?? agentConfig?.model ?? currentModel;
3515
+ const systemPrompt = agentConfig?.systemPrompt ?? buildSystemPrompt(cwd);
3209
3516
  const subMessages = [{ role: "user", content: prompt }];
3210
3517
  const subTools = buildToolSet({
3211
3518
  cwd,
3212
3519
  depth,
3213
3520
  runSubagent,
3214
- onHook: renderHook
3521
+ onHook: renderHook,
3522
+ availableAgents: allAgents
3215
3523
  });
3216
- const subLlm = resolveModel(currentModel);
3217
- const systemPrompt = buildSystemPrompt(cwd);
3524
+ const subLlm = resolveModel(model);
3218
3525
  let result = "";
3219
3526
  let inputTokens = 0;
3220
3527
  let outputTokens = 0;
@@ -3254,11 +3561,13 @@ async function runAgent(opts) {
3254
3561
  }
3255
3562
  return { result, inputTokens, outputTokens, activity };
3256
3563
  };
3564
+ const agents = loadAgents(cwd);
3257
3565
  const tools = buildToolSet({
3258
3566
  cwd,
3259
3567
  depth: 0,
3260
3568
  runSubagent,
3261
- onHook: renderHook
3569
+ onHook: renderHook,
3570
+ availableAgents: agents
3262
3571
  });
3263
3572
  const mcpTools = [];
3264
3573
  async function connectAndAddMcp(name) {
@@ -3281,7 +3590,7 @@ async function runAgent(opts) {
3281
3590
  for (const row of listMcpServers()) {
3282
3591
  try {
3283
3592
  await connectAndAddMcp(row.name);
3284
- renderInfo(`MCP: connected ${c6.cyan(row.name)}`);
3593
+ renderInfo(`MCP: connected ${c7.cyan(row.name)}`);
3285
3594
  } catch (e) {
3286
3595
  renderError(`MCP: failed to connect ${row.name}: ${String(e)}`);
3287
3596
  }
@@ -3310,7 +3619,7 @@ async function runAgent(opts) {
3310
3619
  planMode = v;
3311
3620
  },
3312
3621
  cwd,
3313
- runSubagent: (prompt) => runSubagent(prompt),
3622
+ runSubagent: (prompt, model) => runSubagent(prompt, 0, undefined, model),
3314
3623
  undoLastTurn: async () => {
3315
3624
  if (session.messages.length === 0)
3316
3625
  return false;
@@ -3375,14 +3684,14 @@ async function runAgent(opts) {
3375
3684
  }
3376
3685
  switch (input.type) {
3377
3686
  case "eof":
3378
- writeln(c6.dim("Goodbye."));
3687
+ writeln(c7.dim("Goodbye."));
3379
3688
  return;
3380
3689
  case "interrupt":
3381
3690
  continue;
3382
3691
  case "command": {
3383
3692
  const result = await handleCommand(input.command, input.args, cmdCtx);
3384
3693
  if (result.type === "exit") {
3385
- writeln(c6.dim("Goodbye."));
3694
+ writeln(c7.dim("Goodbye."));
3386
3695
  return;
3387
3696
  }
3388
3697
  if (result.type === "inject-user-message") {
@@ -3417,11 +3726,11 @@ ${out}
3417
3726
  while (ralphMode) {
3418
3727
  if (hasRalphSignal(lastText)) {
3419
3728
  ralphMode = false;
3420
- writeln(`${PREFIX.info} ${c6.dim("ralph mode off")}`);
3729
+ writeln(`${PREFIX.info} ${c7.dim("ralph mode off")}`);
3421
3730
  break;
3422
3731
  }
3423
3732
  if (ralphIteration >= RALPH_MAX_ITERATIONS) {
3424
- writeln(`${PREFIX.info} ${c6.yellow("ralph")} ${c6.dim("\u2014 max iterations reached, stopping")}`);
3733
+ writeln(`${PREFIX.info} ${c7.yellow("ralph")} ${c7.dim("\u2014 max iterations reached, stopping")}`);
3425
3734
  ralphMode = false;
3426
3735
  break;
3427
3736
  }
@@ -3447,13 +3756,6 @@ ${out}
3447
3756
  const allImages = [...pastedImages, ...refImages];
3448
3757
  const thisTurn = turnIndex++;
3449
3758
  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
3759
  const coreContent = planMode ? `${resolvedText}
3458
3760
 
3459
3761
  <system-message>PLAN MODE ACTIVE: Help the user gather context for the plan -- READ ONLY</system-message>` : ralphMode ? `${resolvedText}
@@ -3470,6 +3772,19 @@ ${out}
3470
3772
  }))
3471
3773
  ]
3472
3774
  } : { role: "user", content: coreContent };
3775
+ if (wasAborted) {
3776
+ process.removeListener("SIGINT", onSigInt);
3777
+ const stubMsg = {
3778
+ role: "assistant",
3779
+ content: "[interrupted]"
3780
+ };
3781
+ session.messages.push(userMsg, stubMsg);
3782
+ saveMessages(session.id, [userMsg, stubMsg], thisTurn);
3783
+ coreHistory.push(userMsg, stubMsg);
3784
+ snapshotStack.push(snapped ? thisTurn : null);
3785
+ touchActiveSession(session);
3786
+ return "";
3787
+ }
3473
3788
  session.messages.push(userMsg);
3474
3789
  saveMessages(session.id, [userMsg], thisTurn);
3475
3790
  coreHistory.push(userMsg);
@@ -3504,6 +3819,14 @@ ${out}
3504
3819
  coreHistory.push(...newMessages);
3505
3820
  session.messages.push(...newMessages);
3506
3821
  saveMessages(session.id, newMessages, thisTurn);
3822
+ } else if (wasAborted) {
3823
+ const stubMsg = {
3824
+ role: "assistant",
3825
+ content: "[interrupted]"
3826
+ };
3827
+ coreHistory.push(stubMsg);
3828
+ session.messages.push(stubMsg);
3829
+ saveMessages(session.id, [stubMsg], thisTurn);
3507
3830
  } else {
3508
3831
  rollbackTurn();
3509
3832
  }
@@ -3526,7 +3849,7 @@ ${out}
3526
3849
  const branch = await getGitBranch(cwd);
3527
3850
  const provider = currentModel.split("/")[0] ?? "";
3528
3851
  const modelShort = currentModel.split("/").slice(1).join("/");
3529
- const cwdDisplay = cwd.startsWith(homedir5()) ? `~${cwd.slice(homedir5().length)}` : cwd;
3852
+ const cwdDisplay = tildePath(cwd);
3530
3853
  renderStatusBar({
3531
3854
  model: modelShort,
3532
3855
  provider,
@@ -3567,11 +3890,20 @@ async function resolveFileRefs(text, cwd) {
3567
3890
  let result = text;
3568
3891
  const matches = [...text.matchAll(atPattern)];
3569
3892
  const images = [];
3570
- for (const match of matches.reverse()) {
3893
+ const skills = loadSkills(cwd);
3894
+ for (const match of [...matches].reverse()) {
3571
3895
  const ref = match[1];
3572
3896
  if (!ref)
3573
3897
  continue;
3574
- const filePath = ref.startsWith("/") ? ref : join11(cwd, ref);
3898
+ const skill = skills.get(ref);
3899
+ if (skill) {
3900
+ const replacement = `<skill name="${skill.name}">
3901
+ ${skill.content}
3902
+ </skill>`;
3903
+ result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
3904
+ continue;
3905
+ }
3906
+ const filePath = ref.startsWith("/") ? ref : join14(cwd, ref);
3575
3907
  if (isImageFilename(ref)) {
3576
3908
  const attachment = await loadImageFile(filePath);
3577
3909
  if (attachment) {
@@ -3647,11 +3979,11 @@ function parseArgs(argv) {
3647
3979
  return args;
3648
3980
  }
3649
3981
  function printHelp() {
3650
- writeln(`${c7.bold("mini-coder")} \u2014 a small, fast CLI coding agent
3982
+ writeln(`${c8.bold("mini-coder")} \u2014 a small, fast CLI coding agent
3651
3983
  `);
3652
- writeln(`${c7.bold("Usage:")} mc [options] [prompt]
3984
+ writeln(`${c8.bold("Usage:")} mc [options] [prompt]
3653
3985
  `);
3654
- writeln(`${c7.bold("Options:")}`);
3986
+ writeln(`${c8.bold("Options:")}`);
3655
3987
  const opts = [
3656
3988
  ["-m, --model <id>", "Model to use (e.g. zen/claude-sonnet-4-6)"],
3657
3989
  ["-c, --continue", "Continue the most recent session"],
@@ -3661,10 +3993,10 @@ function printHelp() {
3661
3993
  ["-h, --help", "Show this help"]
3662
3994
  ];
3663
3995
  for (const [flag, desc] of opts) {
3664
- writeln(` ${c7.cyan((flag ?? "").padEnd(22))} ${c7.dim(desc ?? "")}`);
3996
+ writeln(` ${c8.cyan((flag ?? "").padEnd(22))} ${c8.dim(desc ?? "")}`);
3665
3997
  }
3666
3998
  writeln(`
3667
- ${c7.bold("Provider env vars:")}`);
3999
+ ${c8.bold("Provider env vars:")}`);
3668
4000
  const envs = [
3669
4001
  ["OPENCODE_API_KEY", "OpenCode Zen (recommended)"],
3670
4002
  ["ANTHROPIC_API_KEY", "Anthropic direct"],
@@ -3673,15 +4005,15 @@ ${c7.bold("Provider env vars:")}`);
3673
4005
  ["OLLAMA_BASE_URL", "Ollama base URL (default: http://localhost:11434)"]
3674
4006
  ];
3675
4007
  for (const [env, desc] of envs) {
3676
- writeln(` ${c7.yellow((env ?? "").padEnd(22))} ${c7.dim(desc ?? "")}`);
4008
+ writeln(` ${c8.yellow((env ?? "").padEnd(22))} ${c8.dim(desc ?? "")}`);
3677
4009
  }
3678
4010
  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")}`);
4011
+ ${c8.bold("Examples:")}`);
4012
+ writeln(` mc ${c8.dim("# interactive session")}`);
4013
+ writeln(` mc "explain this codebase" ${c8.dim("# one-shot prompt then interactive")}`);
4014
+ writeln(` mc -c ${c8.dim("# continue last session")}`);
4015
+ writeln(` mc -m ollama/llama3.2 ${c8.dim("# use local Ollama model")}`);
4016
+ writeln(` mc -l ${c8.dim("# list sessions")}`);
3685
4017
  }
3686
4018
  async function main() {
3687
4019
  const argv = process.argv.slice(2);
@@ -3700,7 +4032,7 @@ async function main() {
3700
4032
  if (last) {
3701
4033
  sessionId = last.id;
3702
4034
  } else {
3703
- writeln(c7.dim("No previous session found, starting fresh."));
4035
+ writeln(c8.dim("No previous session found, starting fresh."));
3704
4036
  }
3705
4037
  } else if (args.sessionId) {
3706
4038
  sessionId = args.sessionId;