mini-coder 0.0.18 → 0.0.20

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
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/index.ts
5
5
  import { writeSync } from "fs";
6
- import * as c19 from "yoctocolors";
6
+ import * as c18 from "yoctocolors";
7
7
 
8
8
  // src/agent/agent.ts
9
9
  import * as c12 from "yoctocolors";
@@ -88,7 +88,7 @@ var terminal = new TerminalIO;
88
88
  import * as c from "yoctocolors";
89
89
 
90
90
  // src/cli/error-log.ts
91
- import { mkdirSync } from "fs";
91
+ import { mkdirSync, writeFileSync } from "fs";
92
92
  import { homedir } from "os";
93
93
  import { join } from "path";
94
94
  var writer = null;
@@ -98,6 +98,7 @@ function initErrorLog() {
98
98
  const dirPath = join(homedir(), ".config", "mini-coder");
99
99
  const logPath = join(dirPath, "errors.log");
100
100
  mkdirSync(dirPath, { recursive: true });
101
+ writeFileSync(logPath, "");
101
102
  writer = Bun.file(logPath).writer();
102
103
  process.on("uncaughtException", (err) => {
103
104
  logError(err, "uncaught");
@@ -212,6 +213,12 @@ function parseAppError(err) {
212
213
  hint: "Check network or local server"
213
214
  };
214
215
  }
216
+ if (code === "ECONNRESET" || message.includes("ECONNRESET") || message.includes("socket connection was closed unexpectedly")) {
217
+ return {
218
+ headline: "Connection lost",
219
+ hint: "The server closed the connection \u2014 retry or switch model with /model"
220
+ };
221
+ }
215
222
  const firstLine = message.split(`
216
223
  `)[0]?.trim() || "Unknown error";
217
224
  return { headline: firstLine };
@@ -378,39 +385,53 @@ import * as c4 from "yoctocolors";
378
385
  function renderInline(text) {
379
386
  let out = "";
380
387
  let i = 0;
388
+ let segStart = 0;
381
389
  let prevWasBoldClose = false;
382
390
  while (i < text.length) {
383
- if (text[i] === "`") {
391
+ const ch = text[i];
392
+ if (ch === "`") {
384
393
  const end = text.indexOf("`", i + 1);
385
394
  if (end !== -1) {
395
+ if (i > segStart)
396
+ out += text.slice(segStart, i);
386
397
  out += c4.yellow(text.slice(i, end + 1));
387
398
  i = end + 1;
399
+ segStart = i;
388
400
  prevWasBoldClose = false;
389
401
  continue;
390
402
  }
391
403
  }
392
- if (text.slice(i, i + 2) === "**" && text[i + 2] !== "*") {
393
- const end = text.indexOf("**", i + 2);
394
- if (end !== -1) {
395
- out += c4.bold(text.slice(i + 2, end));
396
- i = end + 2;
397
- prevWasBoldClose = true;
398
- continue;
404
+ if (ch === "*") {
405
+ if (text[i + 1] === "*" && text[i + 2] !== "*") {
406
+ const end = text.indexOf("**", i + 2);
407
+ if (end !== -1) {
408
+ if (i > segStart)
409
+ out += text.slice(segStart, i);
410
+ out += c4.bold(text.slice(i + 2, end));
411
+ i = end + 2;
412
+ segStart = i;
413
+ prevWasBoldClose = true;
414
+ continue;
415
+ }
399
416
  }
400
- }
401
- if (text[i] === "*" && text[i + 1] !== "*" && (prevWasBoldClose || text[i - 1] !== "*")) {
402
- const end = text.indexOf("*", i + 1);
403
- if (end !== -1 && text[end - 1] !== "*") {
404
- out += c4.dim(text.slice(i + 1, end));
405
- i = end + 1;
406
- prevWasBoldClose = false;
407
- continue;
417
+ if (text[i + 1] !== "*" && (prevWasBoldClose || text[i - 1] !== "*")) {
418
+ const end = text.indexOf("*", i + 1);
419
+ if (end !== -1 && text[end - 1] !== "*") {
420
+ if (i > segStart)
421
+ out += text.slice(segStart, i);
422
+ out += c4.dim(text.slice(i + 1, end));
423
+ i = end + 1;
424
+ segStart = i;
425
+ prevWasBoldClose = false;
426
+ continue;
427
+ }
408
428
  }
409
429
  }
410
- out += text[i];
411
- i++;
412
430
  prevWasBoldClose = false;
431
+ i++;
413
432
  }
433
+ if (segStart < text.length)
434
+ out += text.slice(segStart);
414
435
  return out;
415
436
  }
416
437
  function renderLine(raw, inFence) {
@@ -474,6 +495,39 @@ function renderMarkdown(text) {
474
495
  `);
475
496
  }
476
497
 
498
+ // src/cli/reasoning.ts
499
+ function normalizeReasoningDelta(delta) {
500
+ return delta.replace(/\r\n?/g, `
501
+ `);
502
+ }
503
+ function normalizeReasoningText(text) {
504
+ const normalized = normalizeReasoningDelta(text);
505
+ const lines = normalized.split(`
506
+ `).map((line) => line.replace(/[ \t]+$/g, ""));
507
+ let start = 0;
508
+ while (start < lines.length && lines[start]?.trim() === "")
509
+ start++;
510
+ let end = lines.length - 1;
511
+ while (end >= start && lines[end]?.trim() === "")
512
+ end--;
513
+ if (start > end)
514
+ return "";
515
+ const compact = [];
516
+ let blankRun = 0;
517
+ for (const line of lines.slice(start, end + 1)) {
518
+ if (line.trim() === "") {
519
+ blankRun += 1;
520
+ if (blankRun <= 1)
521
+ compact.push("");
522
+ continue;
523
+ }
524
+ blankRun = 0;
525
+ compact.push(line);
526
+ }
527
+ return compact.join(`
528
+ `);
529
+ }
530
+
477
531
  // src/cli/tool-render.ts
478
532
  import { homedir as homedir2 } from "os";
479
533
  import * as c5 from "yoctocolors";
@@ -698,7 +752,19 @@ function renderToolResult(toolName, result, isError, _toolCallId) {
698
752
  if (toolName === "subagent") {
699
753
  const r = result;
700
754
  const label = r.agentName ? ` ${c5.dim(c5.cyan(`[@${r.agentName}]`))}` : "";
701
- writeln(` ${c5.cyan("\u2190")}${label} ${c5.dim(`subagent done (${r.inputTokens ?? 0}in / ${r.outputTokens ?? 0}out tokens)`)}`);
755
+ writeln(` ${G.agent}${label} ${c5.dim(`subagent done (${r.inputTokens ?? 0}in / ${r.outputTokens ?? 0}out tokens)`)}`);
756
+ return;
757
+ }
758
+ if (toolName === "readSkill") {
759
+ const r = result;
760
+ if (!r.skill) {
761
+ writeln(` ${G.info} ${c5.dim("skill-auto-load miss")}`);
762
+ return;
763
+ }
764
+ const name = r.skill.name ?? "(unknown)";
765
+ const source = r.skill.source ?? "unknown";
766
+ const description = r.skill.description?.trim();
767
+ writeln(` ${G.info} ${c5.dim(`skill-auto-loaded name=${name} source=${source}${description ? ` description=${JSON.stringify(description)}` : ""}`)}`);
702
768
  return;
703
769
  }
704
770
  if (toolName === "webSearch") {
@@ -770,16 +836,30 @@ async function renderTurn(events, spinner, opts) {
770
836
  let accumulatedText = "";
771
837
  let accumulatedReasoning = "";
772
838
  let inFence = false;
839
+ let reasoningBlankLineRun = 0;
773
840
  let inputTokens = 0;
774
841
  let outputTokens = 0;
775
842
  let contextTokens = 0;
776
843
  let newMessages = [];
844
+ function renderSingleLine(raw) {
845
+ const source = inReasoning ? raw.replace(/[ \t]+$/g, "") : raw;
846
+ if (inReasoning && source.trim() === "") {
847
+ reasoningBlankLineRun += 1;
848
+ if (reasoningBlankLineRun > 1)
849
+ return null;
850
+ } else if (inReasoning) {
851
+ reasoningBlankLineRun = 0;
852
+ }
853
+ const rendered = renderLine(source, inFence);
854
+ inFence = rendered.inFence;
855
+ return inReasoning ? `${c6.dim("\u2502 ")}${rendered.output}` : rendered.output;
856
+ }
777
857
  function renderAndWrite(raw, endWithNewline) {
778
858
  spinner.stop();
779
- const rendered = renderLine(raw, inFence);
780
- inFence = rendered.inFence;
781
- const finalOutput = inReasoning ? c6.dim(rendered.output) : rendered.output;
782
- write(finalOutput);
859
+ const out = renderSingleLine(raw);
860
+ if (out === null)
861
+ return;
862
+ write(out);
783
863
  if (endWithNewline) {
784
864
  write(`
785
865
  `);
@@ -791,17 +871,30 @@ async function renderTurn(events, spinner, opts) {
791
871
  writeln();
792
872
  inText = false;
793
873
  inReasoning = false;
874
+ reasoningBlankLineRun = 0;
794
875
  }
795
876
  }
796
877
  function flushCompleteLines() {
878
+ let start = 0;
797
879
  let boundary = rawBuffer.indexOf(`
798
- `);
880
+ `, start);
881
+ if (boundary === -1)
882
+ return;
883
+ spinner.stop();
884
+ let batchOutput = "";
799
885
  while (boundary !== -1) {
800
- renderAndWrite(rawBuffer.slice(0, boundary), true);
801
- rawBuffer = rawBuffer.slice(boundary + 1);
886
+ const raw = rawBuffer.slice(start, boundary);
887
+ start = boundary + 1;
802
888
  boundary = rawBuffer.indexOf(`
803
- `);
889
+ `, start);
890
+ const out = renderSingleLine(raw);
891
+ if (out !== null)
892
+ batchOutput += `${out}
893
+ `;
804
894
  }
895
+ rawBuffer = start > 0 ? rawBuffer.slice(start) : rawBuffer;
896
+ if (batchOutput)
897
+ write(batchOutput);
805
898
  }
806
899
  function flushAll() {
807
900
  if (!rawBuffer) {
@@ -827,7 +920,8 @@ async function renderTurn(events, spinner, opts) {
827
920
  break;
828
921
  }
829
922
  case "reasoning-delta": {
830
- accumulatedReasoning += event.delta;
923
+ const delta = normalizeReasoningDelta(event.delta);
924
+ accumulatedReasoning += delta;
831
925
  if (!showReasoning) {
832
926
  break;
833
927
  }
@@ -836,9 +930,10 @@ async function renderTurn(events, spinner, opts) {
836
930
  flushAnyText();
837
931
  }
838
932
  spinner.stop();
933
+ writeln(`${G.info} ${c6.dim("reasoning")}`);
839
934
  inReasoning = true;
840
935
  }
841
- rawBuffer += event.delta;
936
+ rawBuffer += delta;
842
937
  flushCompleteLines();
843
938
  break;
844
939
  }
@@ -855,9 +950,18 @@ async function renderTurn(events, spinner, opts) {
855
950
  spinner.start("thinking");
856
951
  break;
857
952
  }
953
+ case "context-pruned": {
954
+ flushAnyText();
955
+ spinner.stop();
956
+ writeln(`${G.info} ${c6.dim(`context-pruned mode=${event.mode} removed_messages=${event.removedMessageCount} removed_bytes=${event.removedBytes} messages_before=${event.beforeMessageCount} messages_after=${event.afterMessageCount}`)}`);
957
+ break;
958
+ }
858
959
  case "turn-complete": {
960
+ const hadContent = inText || inReasoning;
859
961
  flushAnyText();
860
962
  spinner.stop();
963
+ if (!hadContent)
964
+ writeln();
861
965
  inputTokens = event.inputTokens;
862
966
  outputTokens = event.outputTokens;
863
967
  contextTokens = event.contextTokens;
@@ -888,13 +992,13 @@ async function renderTurn(events, spinner, opts) {
888
992
  outputTokens,
889
993
  contextTokens,
890
994
  newMessages,
891
- reasoningText: accumulatedReasoning
995
+ reasoningText: normalizeReasoningText(accumulatedReasoning)
892
996
  };
893
997
  }
894
998
 
895
999
  // src/cli/output.ts
896
1000
  var HOME2 = homedir3();
897
- var PACKAGE_VERSION = "0.0.18";
1001
+ var PACKAGE_VERSION = "0.0.20";
898
1002
  function tildePath(p) {
899
1003
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
900
1004
  }
@@ -1295,6 +1399,21 @@ function getDb() {
1295
1399
  }
1296
1400
  return _db;
1297
1401
  }
1402
+ var MAX_SESSIONS = 100;
1403
+ var MAX_PROMPT_HISTORY = 500;
1404
+ function pruneOldData() {
1405
+ const db = getDb();
1406
+ const deletedSessions = db.run(`DELETE FROM sessions WHERE id NOT IN (
1407
+ SELECT id FROM sessions ORDER BY updated_at DESC LIMIT ?
1408
+ )`, [MAX_SESSIONS]).changes;
1409
+ const deletedHistory = db.run(`DELETE FROM prompt_history WHERE id NOT IN (
1410
+ SELECT id FROM prompt_history ORDER BY id DESC LIMIT ?
1411
+ )`, [MAX_PROMPT_HISTORY]).changes;
1412
+ if (deletedSessions > 0 || deletedHistory > 0) {
1413
+ db.exec("VACUUM;");
1414
+ }
1415
+ db.exec("PRAGMA wal_checkpoint(TRUNCATE);");
1416
+ }
1298
1417
  // src/session/db/mcp-repo.ts
1299
1418
  function listMcpServers() {
1300
1419
  return getDb().query("SELECT name, transport, url, command, args, env FROM mcp_servers ORDER BY name").all();
@@ -1584,7 +1703,7 @@ function deleteAllSnapshots(sessionId) {
1584
1703
  import * as c11 from "yoctocolors";
1585
1704
 
1586
1705
  // src/cli/input.ts
1587
- import { join as join4, relative } from "path";
1706
+ import { join as join5, relative } from "path";
1588
1707
  import * as c9 from "yoctocolors";
1589
1708
 
1590
1709
  // src/cli/image-types.ts
@@ -1623,21 +1742,198 @@ async function loadImageFile(filePath) {
1623
1742
  }
1624
1743
 
1625
1744
  // src/cli/skills.ts
1626
- function loadSkills(cwd, homeDir) {
1627
- return loadMarkdownConfigs({
1628
- type: "skills",
1629
- strategy: "nested",
1630
- nestedFileName: "SKILL.md",
1631
- cwd,
1632
- homeDir,
1633
- includeClaudeDirs: true,
1634
- mapConfig: ({ name, meta, raw, source }) => ({
1635
- name,
1636
- description: meta.description ?? name,
1637
- content: raw,
1745
+ import {
1746
+ closeSync,
1747
+ existsSync as existsSync3,
1748
+ openSync,
1749
+ readdirSync as readdirSync2,
1750
+ readFileSync as readFileSync2,
1751
+ readSync,
1752
+ statSync as statSync2
1753
+ } from "fs";
1754
+ import { homedir as homedir6 } from "os";
1755
+ import { dirname, join as join4, resolve } from "path";
1756
+ var MAX_FRONTMATTER_BYTES = 64 * 1024;
1757
+ var SKILL_NAME_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
1758
+ var MIN_SKILL_NAME_LENGTH = 1;
1759
+ var MAX_SKILL_NAME_LENGTH = 64;
1760
+ var warnedInvalidSkills = new Set;
1761
+ function parseSkillFrontmatter(filePath) {
1762
+ let fd = null;
1763
+ try {
1764
+ fd = openSync(filePath, "r");
1765
+ const chunk = Buffer.allocUnsafe(MAX_FRONTMATTER_BYTES);
1766
+ const bytesRead = readSync(fd, chunk, 0, MAX_FRONTMATTER_BYTES, 0);
1767
+ const text = chunk.toString("utf8", 0, bytesRead);
1768
+ const lines = text.split(`
1769
+ `);
1770
+ if ((lines[0] ?? "").trim() !== "---")
1771
+ return {};
1772
+ const meta = {};
1773
+ for (let i = 1;i < lines.length; i++) {
1774
+ const line = (lines[i] ?? "").replace(/\r$/, "");
1775
+ if (line.trim() === "---")
1776
+ break;
1777
+ const colon = line.indexOf(":");
1778
+ if (colon === -1)
1779
+ continue;
1780
+ const key = line.slice(0, colon).trim();
1781
+ const value = line.slice(colon + 1).trim().replace(/^["']|["']$/g, "");
1782
+ if (key === "name")
1783
+ meta.name = value;
1784
+ if (key === "description")
1785
+ meta.description = value;
1786
+ }
1787
+ return meta;
1788
+ } catch {
1789
+ return {};
1790
+ } finally {
1791
+ if (fd !== null)
1792
+ closeSync(fd);
1793
+ }
1794
+ }
1795
+ function getCandidateFrontmatter(candidate) {
1796
+ if (!candidate.frontmatter) {
1797
+ candidate.frontmatter = parseSkillFrontmatter(candidate.filePath);
1798
+ }
1799
+ return candidate.frontmatter;
1800
+ }
1801
+ function candidateConflictName(candidate) {
1802
+ return getCandidateFrontmatter(candidate).name?.trim() || candidate.folderName;
1803
+ }
1804
+ function findGitBoundary(cwd) {
1805
+ let current = resolve(cwd);
1806
+ while (true) {
1807
+ if (existsSync3(join4(current, ".git")))
1808
+ return current;
1809
+ const parent = dirname(current);
1810
+ if (parent === current)
1811
+ return null;
1812
+ current = parent;
1813
+ }
1814
+ }
1815
+ function localSearchRoots(cwd) {
1816
+ const start = resolve(cwd);
1817
+ const stop = findGitBoundary(start);
1818
+ if (!stop)
1819
+ return [start];
1820
+ const roots = [];
1821
+ let current = start;
1822
+ while (true) {
1823
+ roots.push(current);
1824
+ if (current === stop)
1825
+ break;
1826
+ const parent = dirname(current);
1827
+ if (parent === current)
1828
+ break;
1829
+ current = parent;
1830
+ }
1831
+ return roots;
1832
+ }
1833
+ function listSkillCandidates(skillsDir, source, rootPath) {
1834
+ if (!existsSync3(skillsDir))
1835
+ return [];
1836
+ let entries;
1837
+ try {
1838
+ entries = readdirSync2(skillsDir).sort((a, b) => a.localeCompare(b));
1839
+ } catch {
1840
+ return [];
1841
+ }
1842
+ const candidates = [];
1843
+ for (const entry of entries) {
1844
+ const skillDir = join4(skillsDir, entry);
1845
+ try {
1846
+ if (!statSync2(skillDir).isDirectory())
1847
+ continue;
1848
+ } catch {
1849
+ continue;
1850
+ }
1851
+ const filePath = join4(skillDir, "SKILL.md");
1852
+ if (!existsSync3(filePath))
1853
+ continue;
1854
+ candidates.push({
1855
+ folderName: entry,
1856
+ filePath,
1857
+ rootPath,
1638
1858
  source
1639
- })
1640
- });
1859
+ });
1860
+ }
1861
+ return candidates;
1862
+ }
1863
+ function warnInvalidSkill(filePath, reason) {
1864
+ const key = `${filePath}:${reason}`;
1865
+ if (warnedInvalidSkills.has(key))
1866
+ return;
1867
+ warnedInvalidSkills.add(key);
1868
+ writeln(`${G.warn} skipping invalid skill ${filePath}: ${reason}`);
1869
+ }
1870
+ function validateSkill(candidate) {
1871
+ const meta = getCandidateFrontmatter(candidate);
1872
+ const name = meta.name?.trim();
1873
+ const description = meta.description?.trim();
1874
+ if (!name) {
1875
+ warnInvalidSkill(candidate.filePath, "frontmatter field `name` is required");
1876
+ return null;
1877
+ }
1878
+ if (!description) {
1879
+ warnInvalidSkill(candidate.filePath, "frontmatter field `description` is required");
1880
+ return null;
1881
+ }
1882
+ if (name.length < MIN_SKILL_NAME_LENGTH || name.length > MAX_SKILL_NAME_LENGTH) {
1883
+ warnInvalidSkill(candidate.filePath, `name must be ${MIN_SKILL_NAME_LENGTH}-${MAX_SKILL_NAME_LENGTH} characters`);
1884
+ return null;
1885
+ }
1886
+ if (!SKILL_NAME_RE.test(name)) {
1887
+ warnInvalidSkill(candidate.filePath, "name must match lowercase alnum + hyphen format");
1888
+ return null;
1889
+ }
1890
+ return {
1891
+ name,
1892
+ description,
1893
+ source: candidate.source,
1894
+ rootPath: candidate.rootPath,
1895
+ filePath: candidate.filePath
1896
+ };
1897
+ }
1898
+ function allSkillCandidates(cwd, homeDir) {
1899
+ const home = homeDir ?? homedir6();
1900
+ const localRootsNearToFar = localSearchRoots(cwd);
1901
+ const ordered = [];
1902
+ const globalClaude = listSkillCandidates(join4(home, ".claude", "skills"), "global", home);
1903
+ const globalAgents = listSkillCandidates(join4(home, ".agents", "skills"), "global", home);
1904
+ warnConventionConflicts("skills", "global", globalAgents.map((skill) => candidateConflictName(skill)), globalClaude.map((skill) => candidateConflictName(skill)));
1905
+ ordered.push(...globalClaude, ...globalAgents);
1906
+ for (const root of [...localRootsNearToFar].reverse()) {
1907
+ const localClaude = listSkillCandidates(join4(root, ".claude", "skills"), "local", root);
1908
+ const localAgents = listSkillCandidates(join4(root, ".agents", "skills"), "local", root);
1909
+ warnConventionConflicts("skills", "local", localAgents.map((skill) => candidateConflictName(skill)), localClaude.map((skill) => candidateConflictName(skill)));
1910
+ ordered.push(...localClaude, ...localAgents);
1911
+ }
1912
+ return ordered;
1913
+ }
1914
+ function loadSkillsIndex(cwd, homeDir) {
1915
+ const index = new Map;
1916
+ for (const candidate of allSkillCandidates(cwd, homeDir)) {
1917
+ const skill = validateSkill(candidate);
1918
+ if (!skill)
1919
+ continue;
1920
+ index.set(skill.name, skill);
1921
+ }
1922
+ return index;
1923
+ }
1924
+ function loadSkillContentFromMeta(skill) {
1925
+ try {
1926
+ const content = readFileSync2(skill.filePath, "utf-8");
1927
+ return { name: skill.name, content, source: skill.source };
1928
+ } catch {
1929
+ return null;
1930
+ }
1931
+ }
1932
+ function loadSkillContent(name, cwd, homeDir) {
1933
+ const skill = loadSkillsIndex(cwd, homeDir).get(name);
1934
+ if (!skill)
1935
+ return null;
1936
+ return loadSkillContentFromMeta(skill);
1641
1937
  }
1642
1938
 
1643
1939
  // src/cli/input.ts
@@ -1674,7 +1970,7 @@ async function getAtCompletions(prefix, cwd) {
1674
1970
  const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
1675
1971
  const results = [];
1676
1972
  const MAX = 10;
1677
- const skills = loadSkills(cwd);
1973
+ const skills = loadSkillsIndex(cwd);
1678
1974
  for (const [name] of skills) {
1679
1975
  if (results.length >= MAX)
1680
1976
  break;
@@ -1693,7 +1989,7 @@ async function getAtCompletions(prefix, cwd) {
1693
1989
  for await (const file of glob.scan({ cwd, onlyFiles: true })) {
1694
1990
  if (file.includes("node_modules") || file.includes(".git"))
1695
1991
  continue;
1696
- results.push(`@${relative(cwd, join4(cwd, file))}`);
1992
+ results.push(`@${relative(cwd, join5(cwd, file))}`);
1697
1993
  if (results.length >= MAX)
1698
1994
  break;
1699
1995
  }
@@ -1715,7 +2011,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
1715
2011
  }
1716
2012
  }
1717
2013
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
1718
- const filePath = trimmed.startsWith("/") ? trimmed : join4(cwd, trimmed);
2014
+ const filePath = trimmed.startsWith("/") ? trimmed : join5(cwd, trimmed);
1719
2015
  const attachment = await loadImageFile(filePath);
1720
2016
  if (attachment) {
1721
2017
  const name = filePath.split("/").pop() ?? trimmed;
@@ -1761,6 +2057,17 @@ function getTurnControlAction(chunk) {
1761
2057
  }
1762
2058
  return null;
1763
2059
  }
2060
+ function getSubagentControlAction(chunk) {
2061
+ for (const byte of chunk) {
2062
+ if (byte === CTRL_C_BYTE)
2063
+ return "quit";
2064
+ }
2065
+ for (const byte of chunk) {
2066
+ if (byte === ESC_BYTE)
2067
+ return "cancel";
2068
+ }
2069
+ return null;
2070
+ }
1764
2071
  function exitOnCtrlC(opts) {
1765
2072
  if (opts?.printNewline)
1766
2073
  process.stdout.write(`
@@ -1773,15 +2080,16 @@ function exitOnCtrlC(opts) {
1773
2080
  terminal.restoreTerminal();
1774
2081
  process.exit(130);
1775
2082
  }
1776
- function watchForCancel(abortController) {
2083
+ function watchForCancel(abortController, options) {
1777
2084
  if (!terminal.isTTY)
1778
2085
  return () => {};
1779
2086
  const onCancel = () => {
1780
2087
  cleanup();
1781
2088
  abortController.abort();
1782
2089
  };
2090
+ const getAction = options?.allowSubagentEsc ? getSubagentControlAction : getTurnControlAction;
1783
2091
  const onData = (chunk) => {
1784
- const action = getTurnControlAction(chunk);
2092
+ const action = getAction(chunk);
1785
2093
  if (action === "cancel") {
1786
2094
  onCancel();
1787
2095
  return;
@@ -2193,18 +2501,47 @@ import { createOpenAI } from "@ai-sdk/openai";
2193
2501
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2194
2502
 
2195
2503
  // src/llm-api/api-log.ts
2196
- import { mkdirSync as mkdirSync3 } from "fs";
2197
- import { homedir as homedir6 } from "os";
2198
- import { join as join5 } from "path";
2504
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2505
+ import { homedir as homedir7 } from "os";
2506
+ import { join as join6 } from "path";
2199
2507
  var writer2 = null;
2508
+ var MAX_ENTRY_BYTES = 8 * 1024;
2200
2509
  function initApiLog() {
2201
2510
  if (writer2)
2202
2511
  return;
2203
- const dirPath = join5(homedir6(), ".config", "mini-coder");
2204
- const logPath = join5(dirPath, "api.log");
2512
+ const dirPath = join6(homedir7(), ".config", "mini-coder");
2513
+ const logPath = join6(dirPath, "api.log");
2205
2514
  mkdirSync3(dirPath, { recursive: true });
2515
+ writeFileSync2(logPath, "");
2206
2516
  writer2 = Bun.file(logPath).writer();
2207
2517
  }
2518
+ function isObject2(v) {
2519
+ return typeof v === "object" && v !== null;
2520
+ }
2521
+ var LOG_DROP_KEYS = new Set([
2522
+ "requestBodyValues",
2523
+ "responseBody",
2524
+ "responseHeaders",
2525
+ "stack"
2526
+ ]);
2527
+ function sanitizeForLog(data) {
2528
+ if (!isObject2(data))
2529
+ return data;
2530
+ const result = {};
2531
+ for (const key in data) {
2532
+ if (LOG_DROP_KEYS.has(key))
2533
+ continue;
2534
+ const value = data[key];
2535
+ if (key === "errors" && Array.isArray(value)) {
2536
+ result[key] = value.map((e) => sanitizeForLog(e));
2537
+ } else if (key === "lastError" && isObject2(value)) {
2538
+ result[key] = sanitizeForLog(value);
2539
+ } else {
2540
+ result[key] = value;
2541
+ }
2542
+ }
2543
+ return result;
2544
+ }
2208
2545
  function logApiEvent(event, data) {
2209
2546
  if (!writer2)
2210
2547
  return;
@@ -2213,7 +2550,13 @@ function logApiEvent(event, data) {
2213
2550
  `;
2214
2551
  if (data !== undefined) {
2215
2552
  try {
2216
- entry += JSON.stringify(data, null, 2).split(`
2553
+ const safe = sanitizeForLog(data);
2554
+ let serialized = JSON.stringify(safe, null, 2);
2555
+ if (serialized.length > MAX_ENTRY_BYTES) {
2556
+ serialized = `${serialized.slice(0, MAX_ENTRY_BYTES)}
2557
+ \u2026truncated`;
2558
+ }
2559
+ entry += serialized.split(`
2217
2560
  `).map((line) => ` ${line}`).join(`
2218
2561
  `);
2219
2562
  entry += `
@@ -3162,14 +3505,14 @@ function applyContextPruning(messages, mode) {
3162
3505
  return pruneMessages({
3163
3506
  messages,
3164
3507
  reasoning: "before-last-message",
3165
- toolCalls: "before-last-2-messages",
3508
+ toolCalls: "before-last-20-messages",
3166
3509
  emptyMessages: "remove"
3167
3510
  });
3168
3511
  }
3169
3512
  return pruneMessages({
3170
3513
  messages,
3171
3514
  reasoning: "before-last-message",
3172
- toolCalls: "before-last-3-messages",
3515
+ toolCalls: "before-last-40-messages",
3173
3516
  emptyMessages: "remove"
3174
3517
  });
3175
3518
  }
@@ -3603,9 +3946,23 @@ async function* runTurn(options) {
3603
3946
  if (openAIItemIdsStrippedMessages !== gptSanitizedMessages) {
3604
3947
  logApiEvent("openai item ids stripped from history", { modelString });
3605
3948
  }
3606
- logApiEvent("turn context pre-prune", getMessageDiagnostics(openAIItemIdsStrippedMessages));
3949
+ const prePruneDiagnostics = getMessageDiagnostics(openAIItemIdsStrippedMessages);
3950
+ logApiEvent("turn context pre-prune", prePruneDiagnostics);
3607
3951
  const prunedMessages = applyContextPruning(openAIItemIdsStrippedMessages, pruningMode);
3608
- logApiEvent("turn context post-prune", getMessageDiagnostics(prunedMessages));
3952
+ const postPruneDiagnostics = getMessageDiagnostics(prunedMessages);
3953
+ logApiEvent("turn context post-prune", postPruneDiagnostics);
3954
+ if ((pruningMode === "balanced" || pruningMode === "aggressive") && (postPruneDiagnostics.messageCount < prePruneDiagnostics.messageCount || postPruneDiagnostics.totalBytes < prePruneDiagnostics.totalBytes)) {
3955
+ yield {
3956
+ type: "context-pruned",
3957
+ mode: pruningMode,
3958
+ beforeMessageCount: prePruneDiagnostics.messageCount,
3959
+ afterMessageCount: postPruneDiagnostics.messageCount,
3960
+ removedMessageCount: prePruneDiagnostics.messageCount - postPruneDiagnostics.messageCount,
3961
+ beforeTotalBytes: prePruneDiagnostics.totalBytes,
3962
+ afterTotalBytes: postPruneDiagnostics.totalBytes,
3963
+ removedBytes: prePruneDiagnostics.totalBytes - postPruneDiagnostics.totalBytes
3964
+ };
3965
+ }
3609
3966
  const turnMessages = compactToolResultPayloads(prunedMessages, toolResultPayloadCapBytes);
3610
3967
  if (turnMessages !== prunedMessages) {
3611
3968
  logApiEvent("turn context post-compaction", {
@@ -3817,10 +4174,10 @@ function getMostRecentSession() {
3817
4174
 
3818
4175
  // src/tools/snapshot.ts
3819
4176
  import { unlinkSync as unlinkSync2 } from "fs";
3820
- import { isAbsolute, join as join6, relative as relative2 } from "path";
4177
+ import { isAbsolute, join as join7, relative as relative2 } from "path";
3821
4178
  async function snapshotBeforeEdit(cwd, sessionId, turnIndex, filePath, snappedPaths) {
3822
4179
  try {
3823
- const absPath = isAbsolute(filePath) ? filePath : join6(cwd, filePath);
4180
+ const absPath = isAbsolute(filePath) ? filePath : join7(cwd, filePath);
3824
4181
  const relPath = isAbsolute(filePath) ? relative2(cwd, filePath) : filePath;
3825
4182
  if (snappedPaths.has(relPath))
3826
4183
  return;
@@ -3842,7 +4199,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
3842
4199
  return { restored: false, reason: "not-found" };
3843
4200
  let anyFailed = false;
3844
4201
  for (const file of files) {
3845
- const absPath = join6(cwd, file.path);
4202
+ const absPath = join7(cwd, file.path);
3846
4203
  if (!file.existed) {
3847
4204
  try {
3848
4205
  if (await Bun.file(absPath).exists()) {
@@ -3871,24 +4228,24 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
3871
4228
  }
3872
4229
 
3873
4230
  // src/agent/system-prompt.ts
3874
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3875
- import { homedir as homedir7 } from "os";
3876
- import { join as join7 } from "path";
4231
+ import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
4232
+ import { homedir as homedir8 } from "os";
4233
+ import { join as join8 } from "path";
3877
4234
  function tryReadFile(p) {
3878
- if (!existsSync3(p))
4235
+ if (!existsSync4(p))
3879
4236
  return null;
3880
4237
  try {
3881
- return readFileSync2(p, "utf-8");
4238
+ return readFileSync3(p, "utf-8");
3882
4239
  } catch {
3883
4240
  return null;
3884
4241
  }
3885
4242
  }
3886
4243
  function loadGlobalContextFile(homeDir) {
3887
- const globalDir = join7(homeDir, ".agents");
3888
- return tryReadFile(join7(globalDir, "AGENTS.md")) ?? tryReadFile(join7(globalDir, "CLAUDE.md"));
4244
+ const globalDir = join8(homeDir, ".agents");
4245
+ return tryReadFile(join8(globalDir, "AGENTS.md")) ?? tryReadFile(join8(globalDir, "CLAUDE.md"));
3889
4246
  }
3890
4247
  function loadLocalContextFile(cwd) {
3891
- return tryReadFile(join7(cwd, ".agents", "AGENTS.md")) ?? tryReadFile(join7(cwd, "CLAUDE.md")) ?? tryReadFile(join7(cwd, "AGENTS.md"));
4248
+ return tryReadFile(join8(cwd, ".agents", "AGENTS.md")) ?? tryReadFile(join8(cwd, "CLAUDE.md")) ?? tryReadFile(join8(cwd, "AGENTS.md"));
3892
4249
  }
3893
4250
  var AUTONOMY = `
3894
4251
 
@@ -3929,7 +4286,7 @@ var FINAL_MESSAGE = `
3929
4286
  - If verification could not be run, say so clearly.`;
3930
4287
  var SUBAGENT_DELEGATION = `You are running as a subagent. Complete the task you have been given directly using your tools. Do not spawn further subagents unless the subtask is unambiguously separable and self-contained.`;
3931
4288
  function buildSystemPrompt(sessionTimeAnchor, cwd, extraSystemPrompt, isSubagent, homeDir) {
3932
- const globalContext = loadGlobalContextFile(homeDir ?? homedir7());
4289
+ const globalContext = loadGlobalContextFile(homeDir ?? homedir8());
3933
4290
  const localContext = loadLocalContextFile(cwd);
3934
4291
  const cwdDisplay = tildePath(cwd);
3935
4292
  let prompt = `You are mini-coder, a small and fast CLI coding agent.
@@ -3967,6 +4324,17 @@ ${globalContext}`;
3967
4324
  ${localContext}`;
3968
4325
  }
3969
4326
  }
4327
+ const skills = Array.from(loadSkillsIndex(cwd, homeDir).values());
4328
+ if (skills.length > 0) {
4329
+ prompt += `
4330
+
4331
+ # Available skills (metadata only)`;
4332
+ prompt += "\nUse `listSkills` to browse and `readSkill` to load one SKILL.md on demand.\n";
4333
+ for (const skill of skills) {
4334
+ prompt += `
4335
+ - ${skill.name}: ${skill.description} (${skill.source})`;
4336
+ }
4337
+ }
3970
4338
  if (isSubagent) {
3971
4339
  prompt += `
3972
4340
 
@@ -3981,8 +4349,8 @@ ${extraSystemPrompt}`;
3981
4349
  }
3982
4350
 
3983
4351
  // src/tools/create.ts
3984
- import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "fs";
3985
- import { dirname } from "path";
4352
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
4353
+ import { dirname as dirname2 } from "path";
3986
4354
  import { z } from "zod";
3987
4355
 
3988
4356
  // src/tools/diff.ts
@@ -4112,10 +4480,10 @@ ${lines.join(`
4112
4480
  }
4113
4481
 
4114
4482
  // src/tools/shared.ts
4115
- import { join as join8, relative as relative3 } from "path";
4483
+ import { join as join9, relative as relative3 } from "path";
4116
4484
  function resolvePath(cwdInput, pathInput) {
4117
4485
  const cwd = cwdInput ?? process.cwd();
4118
- const filePath = pathInput.startsWith("/") ? pathInput : join8(cwd, pathInput);
4486
+ const filePath = pathInput.startsWith("/") ? pathInput : join9(cwd, pathInput);
4119
4487
  const relPath = relative3(cwd, filePath);
4120
4488
  return { cwd, filePath, relPath };
4121
4489
  }
@@ -4165,8 +4533,8 @@ var createTool = {
4165
4533
  schema: CreateSchema,
4166
4534
  execute: async (input) => {
4167
4535
  const { filePath, relPath } = resolvePath(input.cwd, input.path);
4168
- const dir = dirname(filePath);
4169
- if (!existsSync4(dir))
4536
+ const dir = dirname2(filePath);
4537
+ if (!existsSync5(dir))
4170
4538
  mkdirSync4(dir, { recursive: true });
4171
4539
  const file = Bun.file(filePath);
4172
4540
  const created = !await file.exists();
@@ -4237,15 +4605,15 @@ var webContentTool = {
4237
4605
  };
4238
4606
 
4239
4607
  // src/tools/glob.ts
4240
- import { resolve as resolve2 } from "path";
4608
+ import { resolve as resolve3 } from "path";
4241
4609
  import { z as z3 } from "zod";
4242
4610
 
4243
4611
  // src/tools/ignore.ts
4244
- import { join as join9 } from "path";
4612
+ import { join as join10 } from "path";
4245
4613
  import ignore from "ignore";
4246
4614
  async function loadGitignore(cwd) {
4247
4615
  try {
4248
- const gitignore = await Bun.file(join9(cwd, ".gitignore")).text();
4616
+ const gitignore = await Bun.file(join10(cwd, ".gitignore")).text();
4249
4617
  return ignore().add(gitignore);
4250
4618
  } catch {
4251
4619
  return null;
@@ -4253,11 +4621,11 @@ async function loadGitignore(cwd) {
4253
4621
  }
4254
4622
 
4255
4623
  // src/tools/scan-path.ts
4256
- import { relative as relative4, resolve, sep } from "path";
4624
+ import { relative as relative4, resolve as resolve2, sep } from "path";
4257
4625
  var LEADING_PARENT_SEGMENTS = /^(?:\.\.\/)+/;
4258
4626
  function getScannedPathInfo(cwd, scanPath) {
4259
- const cwdAbsolute = resolve(cwd);
4260
- const absolute = resolve(cwdAbsolute, scanPath);
4627
+ const cwdAbsolute = resolve2(cwd);
4628
+ const absolute = resolve2(cwdAbsolute, scanPath);
4261
4629
  const relativePath = relative4(cwdAbsolute, absolute).replaceAll("\\", "/");
4262
4630
  const inCwd = absolute === cwdAbsolute || absolute.startsWith(cwdAbsolute === sep ? sep : `${cwdAbsolute}${sep}`);
4263
4631
  const ignoreTargets = getIgnoreTargets(relativePath, inCwd);
@@ -4309,7 +4677,7 @@ var globTool = {
4309
4677
  if (ignored)
4310
4678
  continue;
4311
4679
  try {
4312
- const fullPath = resolve2(cwd, relativePath);
4680
+ const fullPath = resolve3(cwd, relativePath);
4313
4681
  const stat = await Bun.file(fullPath).stat?.() ?? null;
4314
4682
  matches.push({
4315
4683
  path: relativePath,
@@ -4473,8 +4841,8 @@ var grepTool = {
4473
4841
 
4474
4842
  // src/tools/hooks.ts
4475
4843
  import { accessSync, constants } from "fs";
4476
- import { homedir as homedir8 } from "os";
4477
- import { join as join10 } from "path";
4844
+ import { homedir as homedir9 } from "os";
4845
+ import { join as join11 } from "path";
4478
4846
  function isExecutable(filePath) {
4479
4847
  try {
4480
4848
  accessSync(filePath, constants.X_OK);
@@ -4486,8 +4854,8 @@ function isExecutable(filePath) {
4486
4854
  function findHook(toolName, cwd) {
4487
4855
  const scriptName = `post-${toolName}`;
4488
4856
  const candidates = [
4489
- join10(cwd, ".agents", "hooks", scriptName),
4490
- join10(homedir8(), ".agents", "hooks", scriptName)
4857
+ join11(cwd, ".agents", "hooks", scriptName),
4858
+ join11(homedir9(), ".agents", "hooks", scriptName)
4491
4859
  ];
4492
4860
  for (const p of candidates) {
4493
4861
  if (isExecutable(p))
@@ -4806,11 +5174,41 @@ var shellTool = {
4806
5174
  }
4807
5175
  };
4808
5176
 
4809
- // src/tools/subagent.ts
5177
+ // src/tools/skills.ts
4810
5178
  import { z as z9 } from "zod";
4811
- var SubagentInput = z9.object({
4812
- prompt: z9.string().describe("The task or question to give the subagent"),
4813
- agentName: z9.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
5179
+ var ListSkillsSchema = z9.object({});
5180
+ var listSkillsTool = {
5181
+ name: "listSkills",
5182
+ description: "List available skills metadata (name, description, source) without loading SKILL.md bodies.",
5183
+ schema: ListSkillsSchema,
5184
+ execute: async (input) => {
5185
+ const cwd = input.cwd ?? process.cwd();
5186
+ const skills = Array.from(loadSkillsIndex(cwd).values()).map((skill) => ({
5187
+ name: skill.name,
5188
+ description: skill.description,
5189
+ source: skill.source
5190
+ }));
5191
+ return { skills };
5192
+ }
5193
+ };
5194
+ var ReadSkillSchema = z9.object({
5195
+ name: z9.string().describe("Skill name to load")
5196
+ });
5197
+ var readSkillTool = {
5198
+ name: "readSkill",
5199
+ description: "Load full SKILL.md content for one skill by name.",
5200
+ schema: ReadSkillSchema,
5201
+ execute: async (input) => {
5202
+ const cwd = input.cwd ?? process.cwd();
5203
+ return { skill: loadSkillContent(input.name, cwd) };
5204
+ }
5205
+ };
5206
+
5207
+ // src/tools/subagent.ts
5208
+ import { z as z10 } from "zod";
5209
+ var SubagentInput = z10.object({
5210
+ prompt: z10.string().describe("The task or question to give the subagent"),
5211
+ agentName: z10.string().optional().describe("Name of a custom agent to use (from .agents/agents/). Omit to use a generic subagent.")
4814
5212
  });
4815
5213
  function createSubagentTool(runSubagent, availableAgents) {
4816
5214
  const agentSection = availableAgents.size > 0 ? `
@@ -4893,6 +5291,8 @@ function buildToolSet(opts) {
4893
5291
  withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
4894
5292
  withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
4895
5293
  withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
5294
+ withCwdDefault(listSkillsTool, cwd),
5295
+ withCwdDefault(readSkillTool, cwd),
4896
5296
  withHooks(withCwdDefault(createTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForCreate(result, cwd), onHook, finalizeWriteResult),
4897
5297
  withHooks(withCwdDefault(replaceTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForReplace(result, cwd), onHook, finalizeWriteResult),
4898
5298
  withHooks(withCwdDefault(insertTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForInsert(result, cwd), onHook, finalizeWriteResult),
@@ -4912,7 +5312,9 @@ function buildReadOnlyToolSet(opts) {
4912
5312
  const tools = [
4913
5313
  withCwdDefault(globTool, cwd),
4914
5314
  withCwdDefault(grepTool, cwd),
4915
- withCwdDefault(readTool, cwd)
5315
+ withCwdDefault(readTool, cwd),
5316
+ withCwdDefault(listSkillsTool, cwd),
5317
+ withCwdDefault(readSkillTool, cwd)
4916
5318
  ];
4917
5319
  if (process.env.EXA_API_KEY) {
4918
5320
  tools.push(webSearchTool, webContentTool);
@@ -5125,10 +5527,13 @@ function formatSubagentDiagnostics(stdout, stderr) {
5125
5527
  function createSubagentRunner(cwd, getCurrentModel) {
5126
5528
  const activeProcs = new Set;
5127
5529
  const subagentDepth = Number.parseInt(process.env.MC_SUBAGENT_DEPTH ?? "0", 10);
5128
- const runSubagent = async (prompt, agentName, modelOverride) => {
5530
+ const runSubagent = async (prompt, agentName, modelOverride, abortSignal) => {
5129
5531
  if (subagentDepth >= MAX_SUBAGENT_DEPTH) {
5130
5532
  throw new Error(`Subagent recursion limit reached (depth ${subagentDepth}). ` + `Cannot spawn another subagent.`);
5131
5533
  }
5534
+ if (abortSignal?.aborted) {
5535
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5536
+ }
5132
5537
  const model = modelOverride ?? getCurrentModel();
5133
5538
  const cmd = [
5134
5539
  ...getMcCommand(),
@@ -5151,6 +5556,16 @@ function createSubagentRunner(cwd, getCurrentModel) {
5151
5556
  stdio: ["ignore", "pipe", "pipe", "pipe"]
5152
5557
  });
5153
5558
  activeProcs.add(proc);
5559
+ let aborted = false;
5560
+ const onAbort = () => {
5561
+ aborted = true;
5562
+ try {
5563
+ proc.kill("SIGTERM");
5564
+ } catch {}
5565
+ };
5566
+ if (abortSignal) {
5567
+ abortSignal.addEventListener("abort", onAbort);
5568
+ }
5154
5569
  try {
5155
5570
  const [text, stdout, stderr] = await Promise.all([
5156
5571
  Bun.file(proc.stdio[3]).text(),
@@ -5158,6 +5573,9 @@ function createSubagentRunner(cwd, getCurrentModel) {
5158
5573
  consumeTail(proc.stderr),
5159
5574
  proc.exited
5160
5575
  ]);
5576
+ if (aborted || abortSignal?.aborted) {
5577
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5578
+ }
5161
5579
  const diagnostics = formatSubagentDiagnostics(stdout, stderr);
5162
5580
  const trimmed = text.trim();
5163
5581
  if (!trimmed) {
@@ -5183,7 +5601,15 @@ function createSubagentRunner(cwd, getCurrentModel) {
5183
5601
  if (agentName)
5184
5602
  output.agentName = agentName;
5185
5603
  return output;
5604
+ } catch (error) {
5605
+ if (aborted || abortSignal?.aborted) {
5606
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5607
+ }
5608
+ throw error;
5186
5609
  } finally {
5610
+ if (abortSignal) {
5611
+ abortSignal.removeEventListener("abort", onAbort);
5612
+ }
5187
5613
  activeProcs.delete(proc);
5188
5614
  }
5189
5615
  };
@@ -5380,7 +5806,7 @@ async function initAgent(opts) {
5380
5806
  runner.planMode = v;
5381
5807
  },
5382
5808
  cwd,
5383
- runSubagent: (prompt, agentName, model) => runSubagent(prompt, agentName, model),
5809
+ runSubagent: (prompt, agentName, model, abortSignal) => runSubagent(prompt, agentName, model, abortSignal),
5384
5810
  get activeAgent() {
5385
5811
  return activeAgentName;
5386
5812
  },
@@ -5513,9 +5939,9 @@ ${c13.bold("Examples:")}`);
5513
5939
  }
5514
5940
 
5515
5941
  // src/cli/bootstrap.ts
5516
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync } from "fs";
5517
- import { homedir as homedir9 } from "os";
5518
- import { join as join11 } from "path";
5942
+ import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5943
+ import { homedir as homedir10 } from "os";
5944
+ import { join as join12 } from "path";
5519
5945
  import * as c14 from "yoctocolors";
5520
5946
  var REVIEW_COMMAND_CONTENT = `---
5521
5947
  description: Review recent changes for correctness, code quality, and performance
@@ -5535,36 +5961,45 @@ Perform a sensible code review:
5535
5961
  Output a small summary with only the issues found. If nothing is wrong, say so.
5536
5962
  `;
5537
5963
  function bootstrapGlobalDefaults() {
5538
- const commandsDir = join11(homedir9(), ".agents", "commands");
5539
- const reviewPath = join11(commandsDir, "review.md");
5540
- if (!existsSync5(reviewPath)) {
5964
+ const commandsDir = join12(homedir10(), ".agents", "commands");
5965
+ const reviewPath = join12(commandsDir, "review.md");
5966
+ if (!existsSync6(reviewPath)) {
5541
5967
  mkdirSync5(commandsDir, { recursive: true });
5542
- writeFileSync(reviewPath, REVIEW_COMMAND_CONTENT, "utf-8");
5968
+ writeFileSync3(reviewPath, REVIEW_COMMAND_CONTENT, "utf-8");
5543
5969
  writeln(`${c14.green("\u2713")} created ${c14.dim("~/.agents/commands/review.md")} ${c14.dim("(edit it to customise your reviews)")}`);
5544
5970
  }
5545
5971
  }
5546
5972
 
5547
5973
  // src/cli/file-refs.ts
5548
- import { join as join12 } from "path";
5974
+ import { join as join13 } from "path";
5549
5975
  async function resolveFileRefs(text, cwd) {
5550
5976
  const atPattern = /@([\w./\-_]+)/g;
5551
5977
  let result = text;
5552
5978
  const matches = [...text.matchAll(atPattern)];
5553
5979
  const images = [];
5554
- const skills = loadSkills(cwd);
5980
+ const skills = loadSkillsIndex(cwd);
5981
+ const loadedSkills = new Map;
5555
5982
  for (const match of [...matches].reverse()) {
5556
5983
  const ref = match[1];
5557
5984
  if (!ref)
5558
5985
  continue;
5559
- const skill = skills.get(ref);
5560
- if (skill) {
5561
- const replacement = `<skill name="${skill.name}">
5562
- ${skill.content}
5986
+ const skillMeta = skills.get(ref);
5987
+ if (skillMeta) {
5988
+ let content = loadedSkills.get(skillMeta.name);
5989
+ if (!content) {
5990
+ const loaded2 = loadSkillContentFromMeta(skillMeta);
5991
+ if (!loaded2)
5992
+ continue;
5993
+ content = loaded2.content;
5994
+ loadedSkills.set(skillMeta.name, content);
5995
+ }
5996
+ const replacement = `<skill name="${skillMeta.name}">
5997
+ ${content}
5563
5998
  </skill>`;
5564
5999
  result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
5565
6000
  continue;
5566
6001
  }
5567
- const filePath = ref.startsWith("/") ? ref : join12(cwd, ref);
6002
+ const filePath = ref.startsWith("/") ? ref : join13(cwd, ref);
5568
6003
  if (isImageFilename(ref)) {
5569
6004
  const attachment = await loadImageFile(filePath);
5570
6005
  if (attachment) {
@@ -5611,7 +6046,9 @@ class HeadlessReporter {
5611
6046
  accumulatedText += event.delta;
5612
6047
  break;
5613
6048
  case "reasoning-delta":
5614
- reasoningText += event.delta;
6049
+ reasoningText += normalizeReasoningDelta(event.delta);
6050
+ break;
6051
+ case "context-pruned":
5615
6052
  break;
5616
6053
  case "turn-complete":
5617
6054
  inputTokens = event.inputTokens;
@@ -5635,7 +6072,7 @@ class HeadlessReporter {
5635
6072
  outputTokens,
5636
6073
  contextTokens,
5637
6074
  newMessages,
5638
- reasoningText
6075
+ reasoningText: normalizeReasoningText(reasoningText)
5639
6076
  };
5640
6077
  }
5641
6078
  renderStatusBar(_data) {}
@@ -5690,8 +6127,8 @@ async function runShellPassthrough(command, cwd, reporter) {
5690
6127
  import * as c16 from "yoctocolors";
5691
6128
 
5692
6129
  // src/cli/custom-commands.ts
5693
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
5694
- import { join as join13 } from "path";
6130
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
6131
+ import { join as join14 } from "path";
5695
6132
  function loadCustomCommands(cwd, homeDir) {
5696
6133
  return loadMarkdownConfigs({
5697
6134
  type: "commands",
@@ -5748,12 +6185,12 @@ async function expandTemplate(template, args, cwd) {
5748
6185
  const fileMatches = [...result.matchAll(FILE_REF_RE)];
5749
6186
  for (const match of fileMatches) {
5750
6187
  const filePath = match[1] ?? "";
5751
- const fullPath = join13(cwd, filePath);
5752
- if (!existsSync6(fullPath))
6188
+ const fullPath = join14(cwd, filePath);
6189
+ if (!existsSync7(fullPath))
5753
6190
  continue;
5754
6191
  let content = "";
5755
6192
  try {
5756
- content = readFileSync3(fullPath, "utf-8");
6193
+ content = readFileSync4(fullPath, "utf-8");
5757
6194
  } catch {
5758
6195
  continue;
5759
6196
  }
@@ -5813,10 +6250,10 @@ async function handleModel(ctx, args) {
5813
6250
  }
5814
6251
  return;
5815
6252
  }
5816
- writeln(`${c16.dim(" fetching models\u2026")}`);
6253
+ ctx.startSpinner("fetching models");
5817
6254
  const snapshot = await fetchAvailableModels();
5818
6255
  const models = snapshot.models;
5819
- process.stdout.write("\x1B[1A\r\x1B[2K");
6256
+ ctx.stopSpinner();
5820
6257
  if (models.length === 0) {
5821
6258
  writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
5822
6259
  writeln(c16.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
@@ -6006,14 +6443,14 @@ async function handleMcp(ctx, args) {
6006
6443
  case "add": {
6007
6444
  const [, name, transport, ...rest] = parts;
6008
6445
  if (!name || !transport || rest.length === 0) {
6009
- writeln(c16.red(" usage: /mcp add <name> http <url>"));
6010
- writeln(c16.red(" /mcp add <name> stdio <cmd> [args...]"));
6446
+ writeln(`${PREFIX.error} usage: /mcp add <name> http <url>`);
6447
+ writeln(`${PREFIX.error} /mcp add <name> stdio <cmd> [args...]`);
6011
6448
  return;
6012
6449
  }
6013
6450
  if (transport === "http") {
6014
6451
  const url = rest[0];
6015
6452
  if (!url) {
6016
- writeln(c16.red(" usage: /mcp add <name> http <url>"));
6453
+ writeln(`${PREFIX.error} usage: /mcp add <name> http <url>`);
6017
6454
  return;
6018
6455
  }
6019
6456
  upsertMcpServer({
@@ -6027,7 +6464,7 @@ async function handleMcp(ctx, args) {
6027
6464
  } else if (transport === "stdio") {
6028
6465
  const [command, ...cmdArgs] = rest;
6029
6466
  if (!command) {
6030
- writeln(c16.red(" usage: /mcp add <name> stdio <cmd> [args...]"));
6467
+ writeln(`${PREFIX.error} usage: /mcp add <name> stdio <cmd> [args...]`);
6031
6468
  return;
6032
6469
  }
6033
6470
  upsertMcpServer({
@@ -6039,7 +6476,7 @@ async function handleMcp(ctx, args) {
6039
6476
  env: null
6040
6477
  });
6041
6478
  } else {
6042
- writeln(c16.red(` unknown transport: ${transport} (use http or stdio)`));
6479
+ writeln(`${PREFIX.error} unknown transport: ${transport} (use http or stdio)`);
6043
6480
  return;
6044
6481
  }
6045
6482
  try {
@@ -6054,7 +6491,7 @@ async function handleMcp(ctx, args) {
6054
6491
  case "rm": {
6055
6492
  const [, name] = parts;
6056
6493
  if (!name) {
6057
- writeln(c16.red(" usage: /mcp remove <name>"));
6494
+ writeln(`${PREFIX.error} usage: /mcp remove <name>`);
6058
6495
  return;
6059
6496
  }
6060
6497
  deleteMcpServer(name);
@@ -6062,7 +6499,7 @@ async function handleMcp(ctx, args) {
6062
6499
  return;
6063
6500
  }
6064
6501
  default:
6065
- writeln(c16.red(` unknown: /mcp ${sub}`));
6502
+ writeln(`${PREFIX.error} unknown: /mcp ${sub}`);
6066
6503
  writeln(c16.dim(" subcommands: list \xB7 add \xB7 remove"));
6067
6504
  }
6068
6505
  }
@@ -6081,9 +6518,13 @@ async function handleCustomCommand(cmd, args, ctx) {
6081
6518
  if (!fork) {
6082
6519
  return { type: "inject-user-message", text: prompt };
6083
6520
  }
6521
+ const abortController = new AbortController;
6522
+ const stopWatcher = watchForCancel(abortController, {
6523
+ allowSubagentEsc: true
6524
+ });
6084
6525
  try {
6085
6526
  ctx.startSpinner("subagent");
6086
- const output = await ctx.runSubagent(prompt, cmd.agent, cmd.model);
6527
+ const output = await ctx.runSubagent(prompt, cmd.agent, cmd.model, abortController.signal);
6087
6528
  write(renderMarkdown(output.result));
6088
6529
  writeln();
6089
6530
  return {
@@ -6095,9 +6536,13 @@ ${output.result}
6095
6536
  <system-message>Summarize the findings above to the user.</system-message>`
6096
6537
  };
6097
6538
  } catch (e) {
6539
+ if (isAbortError(e)) {
6540
+ return { type: "handled" };
6541
+ }
6098
6542
  writeln(`${PREFIX.error} /${cmd.name} failed: ${String(e)}`);
6099
6543
  return { type: "handled" };
6100
6544
  } finally {
6545
+ stopWatcher();
6101
6546
  ctx.stopSpinner();
6102
6547
  }
6103
6548
  }
@@ -6177,10 +6622,10 @@ function handleHelp(ctx, custom) {
6177
6622
  writeln(` ${c16.magenta(`@${agent.name}`.padEnd(26))} ${c16.dim(agent.description)}${modeTag}${tag}`);
6178
6623
  }
6179
6624
  }
6180
- const skills = loadSkills(ctx.cwd);
6625
+ const skills = loadSkillsIndex(ctx.cwd);
6181
6626
  if (skills.size > 0) {
6182
6627
  writeln();
6183
- writeln(c16.dim(" skills (~/.agents/skills/ or .agents/skills/):"));
6628
+ writeln(c16.dim(" skills (walk-up local .agents/.claude/skills + global ~/.agents/skills ~/.claude/skills):"));
6184
6629
  for (const skill of skills.values()) {
6185
6630
  const tag = skill.source === "local" ? c16.dim(" (local)") : c16.dim(" (global)");
6186
6631
  writeln(` ${c16.yellow(`@${skill.name}`.padEnd(26))} ${c16.dim(skill.description)}${tag}`);
@@ -6188,7 +6633,7 @@ function handleHelp(ctx, custom) {
6188
6633
  }
6189
6634
  writeln();
6190
6635
  writeln(` ${c16.green("@agent".padEnd(26))} ${c16.dim("run prompt through a custom agent (Tab to complete)")}`);
6191
- writeln(` ${c16.green("@skill".padEnd(26))} ${c16.dim("inject skill instructions into prompt (Tab to complete)")}`);
6636
+ writeln(` ${c16.green("@skill".padEnd(26))} ${c16.dim("load a skill body on demand into the prompt (Tab to complete)")}`);
6192
6637
  writeln(` ${c16.green("@file".padEnd(26))} ${c16.dim("inject file contents into prompt (Tab to complete)")}`);
6193
6638
  writeln(` ${c16.green("!cmd".padEnd(26))} ${c16.dim("run shell command, output added as context")}`);
6194
6639
  writeln();
@@ -6296,7 +6741,9 @@ async function runInputLoop(opts) {
6296
6741
  }
6297
6742
  if (result.type === "inject-user-message") {
6298
6743
  const { text: resolvedText, images: refImages } = await resolveFileRefs(result.text, cwd);
6299
- await runner.processUserInput(resolvedText, refImages);
6744
+ try {
6745
+ await runner.processUserInput(resolvedText, refImages);
6746
+ } catch {}
6300
6747
  }
6301
6748
  continue;
6302
6749
  }
@@ -6322,7 +6769,9 @@ ${out}
6322
6769
  const { text: resolvedText, images: refImages } = await resolveFileRefs(input.text, cwd);
6323
6770
  const allImages = [...input.images || [], ...refImages];
6324
6771
  if (!runner.ralphMode) {
6325
- await runner.processUserInput(resolvedText, allImages);
6772
+ try {
6773
+ await runner.processUserInput(resolvedText, allImages);
6774
+ } catch {}
6326
6775
  continue;
6327
6776
  }
6328
6777
  if (allImages.length > 0) {
@@ -6360,13 +6809,14 @@ ${out}
6360
6809
  }
6361
6810
 
6362
6811
  // src/cli/output-reporter.ts
6363
- import * as c18 from "yoctocolors";
6364
6812
  class CliReporter {
6365
6813
  spinner = new Spinner;
6366
6814
  info(msg) {
6815
+ this.spinner.stop();
6367
6816
  renderInfo(msg);
6368
6817
  }
6369
6818
  error(msg, hint) {
6819
+ this.spinner.stop();
6370
6820
  if (typeof msg === "string") {
6371
6821
  renderError(msg, hint);
6372
6822
  } else {
@@ -6374,9 +6824,11 @@ class CliReporter {
6374
6824
  }
6375
6825
  }
6376
6826
  warn(msg) {
6377
- writeln(`! ${c18.yellow(msg)}`);
6827
+ this.spinner.stop();
6828
+ writeln(`${G.warn} ${msg}`);
6378
6829
  }
6379
6830
  writeText(text) {
6831
+ this.spinner.stop();
6380
6832
  writeln(text);
6381
6833
  }
6382
6834
  startSpinner(label) {
@@ -6404,6 +6856,7 @@ registerTerminalCleanup();
6404
6856
  initErrorLog();
6405
6857
  initApiLog();
6406
6858
  initModelInfoCache();
6859
+ pruneOldData();
6407
6860
  refreshModelInfoInBackground().catch(() => {});
6408
6861
  async function main() {
6409
6862
  const argv = process.argv.slice(2);
@@ -6422,7 +6875,7 @@ async function main() {
6422
6875
  if (last) {
6423
6876
  sessionId = last.id;
6424
6877
  } else {
6425
- writeln(c19.dim("No previous session found, starting fresh."));
6878
+ writeln(c18.dim("No previous session found, starting fresh."));
6426
6879
  }
6427
6880
  } else if (args.sessionId) {
6428
6881
  sessionId = args.sessionId;