mini-coder 0.0.19 → 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.
Files changed (3) hide show
  1. package/dist/mc.js +521 -130
  2. package/docs/skills.md +47 -42
  3. package/package.json +6 -6
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";
@@ -385,39 +385,53 @@ import * as c4 from "yoctocolors";
385
385
  function renderInline(text) {
386
386
  let out = "";
387
387
  let i = 0;
388
+ let segStart = 0;
388
389
  let prevWasBoldClose = false;
389
390
  while (i < text.length) {
390
- if (text[i] === "`") {
391
+ const ch = text[i];
392
+ if (ch === "`") {
391
393
  const end = text.indexOf("`", i + 1);
392
394
  if (end !== -1) {
395
+ if (i > segStart)
396
+ out += text.slice(segStart, i);
393
397
  out += c4.yellow(text.slice(i, end + 1));
394
398
  i = end + 1;
399
+ segStart = i;
395
400
  prevWasBoldClose = false;
396
401
  continue;
397
402
  }
398
403
  }
399
- if (text.slice(i, i + 2) === "**" && text[i + 2] !== "*") {
400
- const end = text.indexOf("**", i + 2);
401
- if (end !== -1) {
402
- out += c4.bold(text.slice(i + 2, end));
403
- i = end + 2;
404
- prevWasBoldClose = true;
405
- 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
+ }
406
416
  }
407
- }
408
- if (text[i] === "*" && text[i + 1] !== "*" && (prevWasBoldClose || text[i - 1] !== "*")) {
409
- const end = text.indexOf("*", i + 1);
410
- if (end !== -1 && text[end - 1] !== "*") {
411
- out += c4.dim(text.slice(i + 1, end));
412
- i = end + 1;
413
- prevWasBoldClose = false;
414
- 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
+ }
415
428
  }
416
429
  }
417
- out += text[i];
418
- i++;
419
430
  prevWasBoldClose = false;
431
+ i++;
420
432
  }
433
+ if (segStart < text.length)
434
+ out += text.slice(segStart);
421
435
  return out;
422
436
  }
423
437
  function renderLine(raw, inFence) {
@@ -481,6 +495,39 @@ function renderMarkdown(text) {
481
495
  `);
482
496
  }
483
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
+
484
531
  // src/cli/tool-render.ts
485
532
  import { homedir as homedir2 } from "os";
486
533
  import * as c5 from "yoctocolors";
@@ -705,7 +752,19 @@ function renderToolResult(toolName, result, isError, _toolCallId) {
705
752
  if (toolName === "subagent") {
706
753
  const r = result;
707
754
  const label = r.agentName ? ` ${c5.dim(c5.cyan(`[@${r.agentName}]`))}` : "";
708
- 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)}` : ""}`)}`);
709
768
  return;
710
769
  }
711
770
  if (toolName === "webSearch") {
@@ -777,16 +836,30 @@ async function renderTurn(events, spinner, opts) {
777
836
  let accumulatedText = "";
778
837
  let accumulatedReasoning = "";
779
838
  let inFence = false;
839
+ let reasoningBlankLineRun = 0;
780
840
  let inputTokens = 0;
781
841
  let outputTokens = 0;
782
842
  let contextTokens = 0;
783
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
+ }
784
857
  function renderAndWrite(raw, endWithNewline) {
785
858
  spinner.stop();
786
- const rendered = renderLine(raw, inFence);
787
- inFence = rendered.inFence;
788
- const finalOutput = inReasoning ? c6.dim(rendered.output) : rendered.output;
789
- write(finalOutput);
859
+ const out = renderSingleLine(raw);
860
+ if (out === null)
861
+ return;
862
+ write(out);
790
863
  if (endWithNewline) {
791
864
  write(`
792
865
  `);
@@ -798,17 +871,30 @@ async function renderTurn(events, spinner, opts) {
798
871
  writeln();
799
872
  inText = false;
800
873
  inReasoning = false;
874
+ reasoningBlankLineRun = 0;
801
875
  }
802
876
  }
803
877
  function flushCompleteLines() {
878
+ let start = 0;
804
879
  let boundary = rawBuffer.indexOf(`
805
- `);
880
+ `, start);
881
+ if (boundary === -1)
882
+ return;
883
+ spinner.stop();
884
+ let batchOutput = "";
806
885
  while (boundary !== -1) {
807
- renderAndWrite(rawBuffer.slice(0, boundary), true);
808
- rawBuffer = rawBuffer.slice(boundary + 1);
886
+ const raw = rawBuffer.slice(start, boundary);
887
+ start = boundary + 1;
809
888
  boundary = rawBuffer.indexOf(`
810
- `);
889
+ `, start);
890
+ const out = renderSingleLine(raw);
891
+ if (out !== null)
892
+ batchOutput += `${out}
893
+ `;
811
894
  }
895
+ rawBuffer = start > 0 ? rawBuffer.slice(start) : rawBuffer;
896
+ if (batchOutput)
897
+ write(batchOutput);
812
898
  }
813
899
  function flushAll() {
814
900
  if (!rawBuffer) {
@@ -834,7 +920,8 @@ async function renderTurn(events, spinner, opts) {
834
920
  break;
835
921
  }
836
922
  case "reasoning-delta": {
837
- accumulatedReasoning += event.delta;
923
+ const delta = normalizeReasoningDelta(event.delta);
924
+ accumulatedReasoning += delta;
838
925
  if (!showReasoning) {
839
926
  break;
840
927
  }
@@ -843,9 +930,10 @@ async function renderTurn(events, spinner, opts) {
843
930
  flushAnyText();
844
931
  }
845
932
  spinner.stop();
933
+ writeln(`${G.info} ${c6.dim("reasoning")}`);
846
934
  inReasoning = true;
847
935
  }
848
- rawBuffer += event.delta;
936
+ rawBuffer += delta;
849
937
  flushCompleteLines();
850
938
  break;
851
939
  }
@@ -862,9 +950,18 @@ async function renderTurn(events, spinner, opts) {
862
950
  spinner.start("thinking");
863
951
  break;
864
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
+ }
865
959
  case "turn-complete": {
960
+ const hadContent = inText || inReasoning;
866
961
  flushAnyText();
867
962
  spinner.stop();
963
+ if (!hadContent)
964
+ writeln();
868
965
  inputTokens = event.inputTokens;
869
966
  outputTokens = event.outputTokens;
870
967
  contextTokens = event.contextTokens;
@@ -895,13 +992,13 @@ async function renderTurn(events, spinner, opts) {
895
992
  outputTokens,
896
993
  contextTokens,
897
994
  newMessages,
898
- reasoningText: accumulatedReasoning
995
+ reasoningText: normalizeReasoningText(accumulatedReasoning)
899
996
  };
900
997
  }
901
998
 
902
999
  // src/cli/output.ts
903
1000
  var HOME2 = homedir3();
904
- var PACKAGE_VERSION = "0.0.19";
1001
+ var PACKAGE_VERSION = "0.0.20";
905
1002
  function tildePath(p) {
906
1003
  return p.startsWith(HOME2) ? `~${p.slice(HOME2.length)}` : p;
907
1004
  }
@@ -1606,7 +1703,7 @@ function deleteAllSnapshots(sessionId) {
1606
1703
  import * as c11 from "yoctocolors";
1607
1704
 
1608
1705
  // src/cli/input.ts
1609
- import { join as join4, relative } from "path";
1706
+ import { join as join5, relative } from "path";
1610
1707
  import * as c9 from "yoctocolors";
1611
1708
 
1612
1709
  // src/cli/image-types.ts
@@ -1645,21 +1742,198 @@ async function loadImageFile(filePath) {
1645
1742
  }
1646
1743
 
1647
1744
  // src/cli/skills.ts
1648
- function loadSkills(cwd, homeDir) {
1649
- return loadMarkdownConfigs({
1650
- type: "skills",
1651
- strategy: "nested",
1652
- nestedFileName: "SKILL.md",
1653
- cwd,
1654
- homeDir,
1655
- includeClaudeDirs: true,
1656
- mapConfig: ({ name, meta, raw, source }) => ({
1657
- name,
1658
- description: meta.description ?? name,
1659
- 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,
1660
1858
  source
1661
- })
1662
- });
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);
1663
1937
  }
1664
1938
 
1665
1939
  // src/cli/input.ts
@@ -1696,7 +1970,7 @@ async function getAtCompletions(prefix, cwd) {
1696
1970
  const query = prefix.startsWith("@") ? prefix.slice(1) : prefix;
1697
1971
  const results = [];
1698
1972
  const MAX = 10;
1699
- const skills = loadSkills(cwd);
1973
+ const skills = loadSkillsIndex(cwd);
1700
1974
  for (const [name] of skills) {
1701
1975
  if (results.length >= MAX)
1702
1976
  break;
@@ -1715,7 +1989,7 @@ async function getAtCompletions(prefix, cwd) {
1715
1989
  for await (const file of glob.scan({ cwd, onlyFiles: true })) {
1716
1990
  if (file.includes("node_modules") || file.includes(".git"))
1717
1991
  continue;
1718
- results.push(`@${relative(cwd, join4(cwd, file))}`);
1992
+ results.push(`@${relative(cwd, join5(cwd, file))}`);
1719
1993
  if (results.length >= MAX)
1720
1994
  break;
1721
1995
  }
@@ -1737,7 +2011,7 @@ async function tryExtractImageFromPaste(pasted, cwd) {
1737
2011
  }
1738
2012
  }
1739
2013
  if (!trimmed.includes(" ") && isImageFilename(trimmed)) {
1740
- const filePath = trimmed.startsWith("/") ? trimmed : join4(cwd, trimmed);
2014
+ const filePath = trimmed.startsWith("/") ? trimmed : join5(cwd, trimmed);
1741
2015
  const attachment = await loadImageFile(filePath);
1742
2016
  if (attachment) {
1743
2017
  const name = filePath.split("/").pop() ?? trimmed;
@@ -1783,6 +2057,17 @@ function getTurnControlAction(chunk) {
1783
2057
  }
1784
2058
  return null;
1785
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
+ }
1786
2071
  function exitOnCtrlC(opts) {
1787
2072
  if (opts?.printNewline)
1788
2073
  process.stdout.write(`
@@ -1795,15 +2080,16 @@ function exitOnCtrlC(opts) {
1795
2080
  terminal.restoreTerminal();
1796
2081
  process.exit(130);
1797
2082
  }
1798
- function watchForCancel(abortController) {
2083
+ function watchForCancel(abortController, options) {
1799
2084
  if (!terminal.isTTY)
1800
2085
  return () => {};
1801
2086
  const onCancel = () => {
1802
2087
  cleanup();
1803
2088
  abortController.abort();
1804
2089
  };
2090
+ const getAction = options?.allowSubagentEsc ? getSubagentControlAction : getTurnControlAction;
1805
2091
  const onData = (chunk) => {
1806
- const action = getTurnControlAction(chunk);
2092
+ const action = getAction(chunk);
1807
2093
  if (action === "cancel") {
1808
2094
  onCancel();
1809
2095
  return;
@@ -2216,15 +2502,15 @@ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
2216
2502
 
2217
2503
  // src/llm-api/api-log.ts
2218
2504
  import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2 } from "fs";
2219
- import { homedir as homedir6 } from "os";
2220
- import { join as join5 } from "path";
2505
+ import { homedir as homedir7 } from "os";
2506
+ import { join as join6 } from "path";
2221
2507
  var writer2 = null;
2222
2508
  var MAX_ENTRY_BYTES = 8 * 1024;
2223
2509
  function initApiLog() {
2224
2510
  if (writer2)
2225
2511
  return;
2226
- const dirPath = join5(homedir6(), ".config", "mini-coder");
2227
- const logPath = join5(dirPath, "api.log");
2512
+ const dirPath = join6(homedir7(), ".config", "mini-coder");
2513
+ const logPath = join6(dirPath, "api.log");
2228
2514
  mkdirSync3(dirPath, { recursive: true });
2229
2515
  writeFileSync2(logPath, "");
2230
2516
  writer2 = Bun.file(logPath).writer();
@@ -3660,9 +3946,23 @@ async function* runTurn(options) {
3660
3946
  if (openAIItemIdsStrippedMessages !== gptSanitizedMessages) {
3661
3947
  logApiEvent("openai item ids stripped from history", { modelString });
3662
3948
  }
3663
- logApiEvent("turn context pre-prune", getMessageDiagnostics(openAIItemIdsStrippedMessages));
3949
+ const prePruneDiagnostics = getMessageDiagnostics(openAIItemIdsStrippedMessages);
3950
+ logApiEvent("turn context pre-prune", prePruneDiagnostics);
3664
3951
  const prunedMessages = applyContextPruning(openAIItemIdsStrippedMessages, pruningMode);
3665
- 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
+ }
3666
3966
  const turnMessages = compactToolResultPayloads(prunedMessages, toolResultPayloadCapBytes);
3667
3967
  if (turnMessages !== prunedMessages) {
3668
3968
  logApiEvent("turn context post-compaction", {
@@ -3874,10 +4174,10 @@ function getMostRecentSession() {
3874
4174
 
3875
4175
  // src/tools/snapshot.ts
3876
4176
  import { unlinkSync as unlinkSync2 } from "fs";
3877
- import { isAbsolute, join as join6, relative as relative2 } from "path";
4177
+ import { isAbsolute, join as join7, relative as relative2 } from "path";
3878
4178
  async function snapshotBeforeEdit(cwd, sessionId, turnIndex, filePath, snappedPaths) {
3879
4179
  try {
3880
- const absPath = isAbsolute(filePath) ? filePath : join6(cwd, filePath);
4180
+ const absPath = isAbsolute(filePath) ? filePath : join7(cwd, filePath);
3881
4181
  const relPath = isAbsolute(filePath) ? relative2(cwd, filePath) : filePath;
3882
4182
  if (snappedPaths.has(relPath))
3883
4183
  return;
@@ -3899,7 +4199,7 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
3899
4199
  return { restored: false, reason: "not-found" };
3900
4200
  let anyFailed = false;
3901
4201
  for (const file of files) {
3902
- const absPath = join6(cwd, file.path);
4202
+ const absPath = join7(cwd, file.path);
3903
4203
  if (!file.existed) {
3904
4204
  try {
3905
4205
  if (await Bun.file(absPath).exists()) {
@@ -3928,24 +4228,24 @@ async function restoreSnapshot(cwd, sessionId, turnIndex) {
3928
4228
  }
3929
4229
 
3930
4230
  // src/agent/system-prompt.ts
3931
- import { existsSync as existsSync3, readFileSync as readFileSync2 } from "fs";
3932
- import { homedir as homedir7 } from "os";
3933
- 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";
3934
4234
  function tryReadFile(p) {
3935
- if (!existsSync3(p))
4235
+ if (!existsSync4(p))
3936
4236
  return null;
3937
4237
  try {
3938
- return readFileSync2(p, "utf-8");
4238
+ return readFileSync3(p, "utf-8");
3939
4239
  } catch {
3940
4240
  return null;
3941
4241
  }
3942
4242
  }
3943
4243
  function loadGlobalContextFile(homeDir) {
3944
- const globalDir = join7(homeDir, ".agents");
3945
- 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"));
3946
4246
  }
3947
4247
  function loadLocalContextFile(cwd) {
3948
- 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"));
3949
4249
  }
3950
4250
  var AUTONOMY = `
3951
4251
 
@@ -3986,7 +4286,7 @@ var FINAL_MESSAGE = `
3986
4286
  - If verification could not be run, say so clearly.`;
3987
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.`;
3988
4288
  function buildSystemPrompt(sessionTimeAnchor, cwd, extraSystemPrompt, isSubagent, homeDir) {
3989
- const globalContext = loadGlobalContextFile(homeDir ?? homedir7());
4289
+ const globalContext = loadGlobalContextFile(homeDir ?? homedir8());
3990
4290
  const localContext = loadLocalContextFile(cwd);
3991
4291
  const cwdDisplay = tildePath(cwd);
3992
4292
  let prompt = `You are mini-coder, a small and fast CLI coding agent.
@@ -4024,6 +4324,17 @@ ${globalContext}`;
4024
4324
  ${localContext}`;
4025
4325
  }
4026
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
+ }
4027
4338
  if (isSubagent) {
4028
4339
  prompt += `
4029
4340
 
@@ -4038,8 +4349,8 @@ ${extraSystemPrompt}`;
4038
4349
  }
4039
4350
 
4040
4351
  // src/tools/create.ts
4041
- import { existsSync as existsSync4, mkdirSync as mkdirSync4 } from "fs";
4042
- import { dirname } from "path";
4352
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4 } from "fs";
4353
+ import { dirname as dirname2 } from "path";
4043
4354
  import { z } from "zod";
4044
4355
 
4045
4356
  // src/tools/diff.ts
@@ -4169,10 +4480,10 @@ ${lines.join(`
4169
4480
  }
4170
4481
 
4171
4482
  // src/tools/shared.ts
4172
- import { join as join8, relative as relative3 } from "path";
4483
+ import { join as join9, relative as relative3 } from "path";
4173
4484
  function resolvePath(cwdInput, pathInput) {
4174
4485
  const cwd = cwdInput ?? process.cwd();
4175
- const filePath = pathInput.startsWith("/") ? pathInput : join8(cwd, pathInput);
4486
+ const filePath = pathInput.startsWith("/") ? pathInput : join9(cwd, pathInput);
4176
4487
  const relPath = relative3(cwd, filePath);
4177
4488
  return { cwd, filePath, relPath };
4178
4489
  }
@@ -4222,8 +4533,8 @@ var createTool = {
4222
4533
  schema: CreateSchema,
4223
4534
  execute: async (input) => {
4224
4535
  const { filePath, relPath } = resolvePath(input.cwd, input.path);
4225
- const dir = dirname(filePath);
4226
- if (!existsSync4(dir))
4536
+ const dir = dirname2(filePath);
4537
+ if (!existsSync5(dir))
4227
4538
  mkdirSync4(dir, { recursive: true });
4228
4539
  const file = Bun.file(filePath);
4229
4540
  const created = !await file.exists();
@@ -4294,15 +4605,15 @@ var webContentTool = {
4294
4605
  };
4295
4606
 
4296
4607
  // src/tools/glob.ts
4297
- import { resolve as resolve2 } from "path";
4608
+ import { resolve as resolve3 } from "path";
4298
4609
  import { z as z3 } from "zod";
4299
4610
 
4300
4611
  // src/tools/ignore.ts
4301
- import { join as join9 } from "path";
4612
+ import { join as join10 } from "path";
4302
4613
  import ignore from "ignore";
4303
4614
  async function loadGitignore(cwd) {
4304
4615
  try {
4305
- const gitignore = await Bun.file(join9(cwd, ".gitignore")).text();
4616
+ const gitignore = await Bun.file(join10(cwd, ".gitignore")).text();
4306
4617
  return ignore().add(gitignore);
4307
4618
  } catch {
4308
4619
  return null;
@@ -4310,11 +4621,11 @@ async function loadGitignore(cwd) {
4310
4621
  }
4311
4622
 
4312
4623
  // src/tools/scan-path.ts
4313
- import { relative as relative4, resolve, sep } from "path";
4624
+ import { relative as relative4, resolve as resolve2, sep } from "path";
4314
4625
  var LEADING_PARENT_SEGMENTS = /^(?:\.\.\/)+/;
4315
4626
  function getScannedPathInfo(cwd, scanPath) {
4316
- const cwdAbsolute = resolve(cwd);
4317
- const absolute = resolve(cwdAbsolute, scanPath);
4627
+ const cwdAbsolute = resolve2(cwd);
4628
+ const absolute = resolve2(cwdAbsolute, scanPath);
4318
4629
  const relativePath = relative4(cwdAbsolute, absolute).replaceAll("\\", "/");
4319
4630
  const inCwd = absolute === cwdAbsolute || absolute.startsWith(cwdAbsolute === sep ? sep : `${cwdAbsolute}${sep}`);
4320
4631
  const ignoreTargets = getIgnoreTargets(relativePath, inCwd);
@@ -4366,7 +4677,7 @@ var globTool = {
4366
4677
  if (ignored)
4367
4678
  continue;
4368
4679
  try {
4369
- const fullPath = resolve2(cwd, relativePath);
4680
+ const fullPath = resolve3(cwd, relativePath);
4370
4681
  const stat = await Bun.file(fullPath).stat?.() ?? null;
4371
4682
  matches.push({
4372
4683
  path: relativePath,
@@ -4530,8 +4841,8 @@ var grepTool = {
4530
4841
 
4531
4842
  // src/tools/hooks.ts
4532
4843
  import { accessSync, constants } from "fs";
4533
- import { homedir as homedir8 } from "os";
4534
- import { join as join10 } from "path";
4844
+ import { homedir as homedir9 } from "os";
4845
+ import { join as join11 } from "path";
4535
4846
  function isExecutable(filePath) {
4536
4847
  try {
4537
4848
  accessSync(filePath, constants.X_OK);
@@ -4543,8 +4854,8 @@ function isExecutable(filePath) {
4543
4854
  function findHook(toolName, cwd) {
4544
4855
  const scriptName = `post-${toolName}`;
4545
4856
  const candidates = [
4546
- join10(cwd, ".agents", "hooks", scriptName),
4547
- join10(homedir8(), ".agents", "hooks", scriptName)
4857
+ join11(cwd, ".agents", "hooks", scriptName),
4858
+ join11(homedir9(), ".agents", "hooks", scriptName)
4548
4859
  ];
4549
4860
  for (const p of candidates) {
4550
4861
  if (isExecutable(p))
@@ -4863,11 +5174,41 @@ var shellTool = {
4863
5174
  }
4864
5175
  };
4865
5176
 
4866
- // src/tools/subagent.ts
5177
+ // src/tools/skills.ts
4867
5178
  import { z as z9 } from "zod";
4868
- var SubagentInput = z9.object({
4869
- prompt: z9.string().describe("The task or question to give the subagent"),
4870
- 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.")
4871
5212
  });
4872
5213
  function createSubagentTool(runSubagent, availableAgents) {
4873
5214
  const agentSection = availableAgents.size > 0 ? `
@@ -4950,6 +5291,8 @@ function buildToolSet(opts) {
4950
5291
  withHooks(withCwdDefault(globTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGlob(input, cwd), onHook),
4951
5292
  withHooks(withCwdDefault(grepTool, cwd), lookupHook, cwd, (_, input) => hookEnvForGrep(input, cwd), onHook),
4952
5293
  withHooks(withCwdDefault(readTool, cwd), lookupHook, cwd, (_, input) => hookEnvForRead(input, cwd), onHook),
5294
+ withCwdDefault(listSkillsTool, cwd),
5295
+ withCwdDefault(readSkillTool, cwd),
4953
5296
  withHooks(withCwdDefault(createTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForCreate(result, cwd), onHook, finalizeWriteResult),
4954
5297
  withHooks(withCwdDefault(replaceTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForReplace(result, cwd), onHook, finalizeWriteResult),
4955
5298
  withHooks(withCwdDefault(insertTool, cwd, opts.snapshotCallback), lookupHook, cwd, (result) => hookEnvForInsert(result, cwd), onHook, finalizeWriteResult),
@@ -4969,7 +5312,9 @@ function buildReadOnlyToolSet(opts) {
4969
5312
  const tools = [
4970
5313
  withCwdDefault(globTool, cwd),
4971
5314
  withCwdDefault(grepTool, cwd),
4972
- withCwdDefault(readTool, cwd)
5315
+ withCwdDefault(readTool, cwd),
5316
+ withCwdDefault(listSkillsTool, cwd),
5317
+ withCwdDefault(readSkillTool, cwd)
4973
5318
  ];
4974
5319
  if (process.env.EXA_API_KEY) {
4975
5320
  tools.push(webSearchTool, webContentTool);
@@ -5182,10 +5527,13 @@ function formatSubagentDiagnostics(stdout, stderr) {
5182
5527
  function createSubagentRunner(cwd, getCurrentModel) {
5183
5528
  const activeProcs = new Set;
5184
5529
  const subagentDepth = Number.parseInt(process.env.MC_SUBAGENT_DEPTH ?? "0", 10);
5185
- const runSubagent = async (prompt, agentName, modelOverride) => {
5530
+ const runSubagent = async (prompt, agentName, modelOverride, abortSignal) => {
5186
5531
  if (subagentDepth >= MAX_SUBAGENT_DEPTH) {
5187
5532
  throw new Error(`Subagent recursion limit reached (depth ${subagentDepth}). ` + `Cannot spawn another subagent.`);
5188
5533
  }
5534
+ if (abortSignal?.aborted) {
5535
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5536
+ }
5189
5537
  const model = modelOverride ?? getCurrentModel();
5190
5538
  const cmd = [
5191
5539
  ...getMcCommand(),
@@ -5208,6 +5556,16 @@ function createSubagentRunner(cwd, getCurrentModel) {
5208
5556
  stdio: ["ignore", "pipe", "pipe", "pipe"]
5209
5557
  });
5210
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
+ }
5211
5569
  try {
5212
5570
  const [text, stdout, stderr] = await Promise.all([
5213
5571
  Bun.file(proc.stdio[3]).text(),
@@ -5215,6 +5573,9 @@ function createSubagentRunner(cwd, getCurrentModel) {
5215
5573
  consumeTail(proc.stderr),
5216
5574
  proc.exited
5217
5575
  ]);
5576
+ if (aborted || abortSignal?.aborted) {
5577
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5578
+ }
5218
5579
  const diagnostics = formatSubagentDiagnostics(stdout, stderr);
5219
5580
  const trimmed = text.trim();
5220
5581
  if (!trimmed) {
@@ -5240,7 +5601,15 @@ function createSubagentRunner(cwd, getCurrentModel) {
5240
5601
  if (agentName)
5241
5602
  output.agentName = agentName;
5242
5603
  return output;
5604
+ } catch (error) {
5605
+ if (aborted || abortSignal?.aborted) {
5606
+ throw new DOMException("Subagent execution was interrupted", "AbortError");
5607
+ }
5608
+ throw error;
5243
5609
  } finally {
5610
+ if (abortSignal) {
5611
+ abortSignal.removeEventListener("abort", onAbort);
5612
+ }
5244
5613
  activeProcs.delete(proc);
5245
5614
  }
5246
5615
  };
@@ -5437,7 +5806,7 @@ async function initAgent(opts) {
5437
5806
  runner.planMode = v;
5438
5807
  },
5439
5808
  cwd,
5440
- runSubagent: (prompt, agentName, model) => runSubagent(prompt, agentName, model),
5809
+ runSubagent: (prompt, agentName, model, abortSignal) => runSubagent(prompt, agentName, model, abortSignal),
5441
5810
  get activeAgent() {
5442
5811
  return activeAgentName;
5443
5812
  },
@@ -5570,9 +5939,9 @@ ${c13.bold("Examples:")}`);
5570
5939
  }
5571
5940
 
5572
5941
  // src/cli/bootstrap.ts
5573
- import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3 } from "fs";
5574
- import { homedir as homedir9 } from "os";
5575
- 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";
5576
5945
  import * as c14 from "yoctocolors";
5577
5946
  var REVIEW_COMMAND_CONTENT = `---
5578
5947
  description: Review recent changes for correctness, code quality, and performance
@@ -5592,9 +5961,9 @@ Perform a sensible code review:
5592
5961
  Output a small summary with only the issues found. If nothing is wrong, say so.
5593
5962
  `;
5594
5963
  function bootstrapGlobalDefaults() {
5595
- const commandsDir = join11(homedir9(), ".agents", "commands");
5596
- const reviewPath = join11(commandsDir, "review.md");
5597
- if (!existsSync5(reviewPath)) {
5964
+ const commandsDir = join12(homedir10(), ".agents", "commands");
5965
+ const reviewPath = join12(commandsDir, "review.md");
5966
+ if (!existsSync6(reviewPath)) {
5598
5967
  mkdirSync5(commandsDir, { recursive: true });
5599
5968
  writeFileSync3(reviewPath, REVIEW_COMMAND_CONTENT, "utf-8");
5600
5969
  writeln(`${c14.green("\u2713")} created ${c14.dim("~/.agents/commands/review.md")} ${c14.dim("(edit it to customise your reviews)")}`);
@@ -5602,26 +5971,35 @@ function bootstrapGlobalDefaults() {
5602
5971
  }
5603
5972
 
5604
5973
  // src/cli/file-refs.ts
5605
- import { join as join12 } from "path";
5974
+ import { join as join13 } from "path";
5606
5975
  async function resolveFileRefs(text, cwd) {
5607
5976
  const atPattern = /@([\w./\-_]+)/g;
5608
5977
  let result = text;
5609
5978
  const matches = [...text.matchAll(atPattern)];
5610
5979
  const images = [];
5611
- const skills = loadSkills(cwd);
5980
+ const skills = loadSkillsIndex(cwd);
5981
+ const loadedSkills = new Map;
5612
5982
  for (const match of [...matches].reverse()) {
5613
5983
  const ref = match[1];
5614
5984
  if (!ref)
5615
5985
  continue;
5616
- const skill = skills.get(ref);
5617
- if (skill) {
5618
- const replacement = `<skill name="${skill.name}">
5619
- ${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}
5620
5998
  </skill>`;
5621
5999
  result = result.slice(0, match.index) + replacement + result.slice((match.index ?? 0) + match[0].length);
5622
6000
  continue;
5623
6001
  }
5624
- const filePath = ref.startsWith("/") ? ref : join12(cwd, ref);
6002
+ const filePath = ref.startsWith("/") ? ref : join13(cwd, ref);
5625
6003
  if (isImageFilename(ref)) {
5626
6004
  const attachment = await loadImageFile(filePath);
5627
6005
  if (attachment) {
@@ -5668,7 +6046,9 @@ class HeadlessReporter {
5668
6046
  accumulatedText += event.delta;
5669
6047
  break;
5670
6048
  case "reasoning-delta":
5671
- reasoningText += event.delta;
6049
+ reasoningText += normalizeReasoningDelta(event.delta);
6050
+ break;
6051
+ case "context-pruned":
5672
6052
  break;
5673
6053
  case "turn-complete":
5674
6054
  inputTokens = event.inputTokens;
@@ -5692,7 +6072,7 @@ class HeadlessReporter {
5692
6072
  outputTokens,
5693
6073
  contextTokens,
5694
6074
  newMessages,
5695
- reasoningText
6075
+ reasoningText: normalizeReasoningText(reasoningText)
5696
6076
  };
5697
6077
  }
5698
6078
  renderStatusBar(_data) {}
@@ -5747,8 +6127,8 @@ async function runShellPassthrough(command, cwd, reporter) {
5747
6127
  import * as c16 from "yoctocolors";
5748
6128
 
5749
6129
  // src/cli/custom-commands.ts
5750
- import { existsSync as existsSync6, readFileSync as readFileSync3 } from "fs";
5751
- import { join as join13 } from "path";
6130
+ import { existsSync as existsSync7, readFileSync as readFileSync4 } from "fs";
6131
+ import { join as join14 } from "path";
5752
6132
  function loadCustomCommands(cwd, homeDir) {
5753
6133
  return loadMarkdownConfigs({
5754
6134
  type: "commands",
@@ -5805,12 +6185,12 @@ async function expandTemplate(template, args, cwd) {
5805
6185
  const fileMatches = [...result.matchAll(FILE_REF_RE)];
5806
6186
  for (const match of fileMatches) {
5807
6187
  const filePath = match[1] ?? "";
5808
- const fullPath = join13(cwd, filePath);
5809
- if (!existsSync6(fullPath))
6188
+ const fullPath = join14(cwd, filePath);
6189
+ if (!existsSync7(fullPath))
5810
6190
  continue;
5811
6191
  let content = "";
5812
6192
  try {
5813
- content = readFileSync3(fullPath, "utf-8");
6193
+ content = readFileSync4(fullPath, "utf-8");
5814
6194
  } catch {
5815
6195
  continue;
5816
6196
  }
@@ -5870,10 +6250,10 @@ async function handleModel(ctx, args) {
5870
6250
  }
5871
6251
  return;
5872
6252
  }
5873
- writeln(`${c16.dim(" fetching models\u2026")}`);
6253
+ ctx.startSpinner("fetching models");
5874
6254
  const snapshot = await fetchAvailableModels();
5875
6255
  const models = snapshot.models;
5876
- process.stdout.write("\x1B[1A\r\x1B[2K");
6256
+ ctx.stopSpinner();
5877
6257
  if (models.length === 0) {
5878
6258
  writeln(`${PREFIX.error} No models found. Check your API keys or Ollama connection.`);
5879
6259
  writeln(c16.dim(" Set OPENCODE_API_KEY for Zen, or start Ollama for local models."));
@@ -6063,14 +6443,14 @@ async function handleMcp(ctx, args) {
6063
6443
  case "add": {
6064
6444
  const [, name, transport, ...rest] = parts;
6065
6445
  if (!name || !transport || rest.length === 0) {
6066
- writeln(c16.red(" usage: /mcp add <name> http <url>"));
6067
- 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...]`);
6068
6448
  return;
6069
6449
  }
6070
6450
  if (transport === "http") {
6071
6451
  const url = rest[0];
6072
6452
  if (!url) {
6073
- writeln(c16.red(" usage: /mcp add <name> http <url>"));
6453
+ writeln(`${PREFIX.error} usage: /mcp add <name> http <url>`);
6074
6454
  return;
6075
6455
  }
6076
6456
  upsertMcpServer({
@@ -6084,7 +6464,7 @@ async function handleMcp(ctx, args) {
6084
6464
  } else if (transport === "stdio") {
6085
6465
  const [command, ...cmdArgs] = rest;
6086
6466
  if (!command) {
6087
- writeln(c16.red(" usage: /mcp add <name> stdio <cmd> [args...]"));
6467
+ writeln(`${PREFIX.error} usage: /mcp add <name> stdio <cmd> [args...]`);
6088
6468
  return;
6089
6469
  }
6090
6470
  upsertMcpServer({
@@ -6096,7 +6476,7 @@ async function handleMcp(ctx, args) {
6096
6476
  env: null
6097
6477
  });
6098
6478
  } else {
6099
- writeln(c16.red(` unknown transport: ${transport} (use http or stdio)`));
6479
+ writeln(`${PREFIX.error} unknown transport: ${transport} (use http or stdio)`);
6100
6480
  return;
6101
6481
  }
6102
6482
  try {
@@ -6111,7 +6491,7 @@ async function handleMcp(ctx, args) {
6111
6491
  case "rm": {
6112
6492
  const [, name] = parts;
6113
6493
  if (!name) {
6114
- writeln(c16.red(" usage: /mcp remove <name>"));
6494
+ writeln(`${PREFIX.error} usage: /mcp remove <name>`);
6115
6495
  return;
6116
6496
  }
6117
6497
  deleteMcpServer(name);
@@ -6119,7 +6499,7 @@ async function handleMcp(ctx, args) {
6119
6499
  return;
6120
6500
  }
6121
6501
  default:
6122
- writeln(c16.red(` unknown: /mcp ${sub}`));
6502
+ writeln(`${PREFIX.error} unknown: /mcp ${sub}`);
6123
6503
  writeln(c16.dim(" subcommands: list \xB7 add \xB7 remove"));
6124
6504
  }
6125
6505
  }
@@ -6138,9 +6518,13 @@ async function handleCustomCommand(cmd, args, ctx) {
6138
6518
  if (!fork) {
6139
6519
  return { type: "inject-user-message", text: prompt };
6140
6520
  }
6521
+ const abortController = new AbortController;
6522
+ const stopWatcher = watchForCancel(abortController, {
6523
+ allowSubagentEsc: true
6524
+ });
6141
6525
  try {
6142
6526
  ctx.startSpinner("subagent");
6143
- const output = await ctx.runSubagent(prompt, cmd.agent, cmd.model);
6527
+ const output = await ctx.runSubagent(prompt, cmd.agent, cmd.model, abortController.signal);
6144
6528
  write(renderMarkdown(output.result));
6145
6529
  writeln();
6146
6530
  return {
@@ -6152,9 +6536,13 @@ ${output.result}
6152
6536
  <system-message>Summarize the findings above to the user.</system-message>`
6153
6537
  };
6154
6538
  } catch (e) {
6539
+ if (isAbortError(e)) {
6540
+ return { type: "handled" };
6541
+ }
6155
6542
  writeln(`${PREFIX.error} /${cmd.name} failed: ${String(e)}`);
6156
6543
  return { type: "handled" };
6157
6544
  } finally {
6545
+ stopWatcher();
6158
6546
  ctx.stopSpinner();
6159
6547
  }
6160
6548
  }
@@ -6234,10 +6622,10 @@ function handleHelp(ctx, custom) {
6234
6622
  writeln(` ${c16.magenta(`@${agent.name}`.padEnd(26))} ${c16.dim(agent.description)}${modeTag}${tag}`);
6235
6623
  }
6236
6624
  }
6237
- const skills = loadSkills(ctx.cwd);
6625
+ const skills = loadSkillsIndex(ctx.cwd);
6238
6626
  if (skills.size > 0) {
6239
6627
  writeln();
6240
- 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):"));
6241
6629
  for (const skill of skills.values()) {
6242
6630
  const tag = skill.source === "local" ? c16.dim(" (local)") : c16.dim(" (global)");
6243
6631
  writeln(` ${c16.yellow(`@${skill.name}`.padEnd(26))} ${c16.dim(skill.description)}${tag}`);
@@ -6245,7 +6633,7 @@ function handleHelp(ctx, custom) {
6245
6633
  }
6246
6634
  writeln();
6247
6635
  writeln(` ${c16.green("@agent".padEnd(26))} ${c16.dim("run prompt through a custom agent (Tab to complete)")}`);
6248
- 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)")}`);
6249
6637
  writeln(` ${c16.green("@file".padEnd(26))} ${c16.dim("inject file contents into prompt (Tab to complete)")}`);
6250
6638
  writeln(` ${c16.green("!cmd".padEnd(26))} ${c16.dim("run shell command, output added as context")}`);
6251
6639
  writeln();
@@ -6421,13 +6809,14 @@ ${out}
6421
6809
  }
6422
6810
 
6423
6811
  // src/cli/output-reporter.ts
6424
- import * as c18 from "yoctocolors";
6425
6812
  class CliReporter {
6426
6813
  spinner = new Spinner;
6427
6814
  info(msg) {
6815
+ this.spinner.stop();
6428
6816
  renderInfo(msg);
6429
6817
  }
6430
6818
  error(msg, hint) {
6819
+ this.spinner.stop();
6431
6820
  if (typeof msg === "string") {
6432
6821
  renderError(msg, hint);
6433
6822
  } else {
@@ -6435,9 +6824,11 @@ class CliReporter {
6435
6824
  }
6436
6825
  }
6437
6826
  warn(msg) {
6438
- writeln(`! ${c18.yellow(msg)}`);
6827
+ this.spinner.stop();
6828
+ writeln(`${G.warn} ${msg}`);
6439
6829
  }
6440
6830
  writeText(text) {
6831
+ this.spinner.stop();
6441
6832
  writeln(text);
6442
6833
  }
6443
6834
  startSpinner(label) {
@@ -6484,7 +6875,7 @@ async function main() {
6484
6875
  if (last) {
6485
6876
  sessionId = last.id;
6486
6877
  } else {
6487
- writeln(c19.dim("No previous session found, starting fresh."));
6878
+ writeln(c18.dim("No previous session found, starting fresh."));
6488
6879
  }
6489
6880
  } else if (args.sessionId) {
6490
6881
  sessionId = args.sessionId;
package/docs/skills.md CHANGED
@@ -1,28 +1,48 @@
1
1
  # Skills
2
2
 
3
- A skill is a reusable instruction file injected inline into your prompt.
4
- Use `@skill-name` to load it — the content is inserted into the message
5
- before it's sent to the LLM.
3
+ Skills are reusable instruction files discovered automatically from local and global directories.
6
4
 
7
- > **Skills are never auto-loaded.** They must be explicitly referenced
8
- > with `@skill-name` in your prompt. Nothing is injected automatically.
5
+ - The model sees **skill metadata only** by default (name, description, source).
6
+ - Full `SKILL.md` content is loaded **on demand**:
7
+ - when explicitly requested with the runtime skill tools (`listSkills` / `readSkill`), or
8
+ - when you reference `@skill-name` in your prompt.
9
9
 
10
- ## Where to put them
10
+ ## Discovery locations
11
11
 
12
- Each skill is a folder containing a `SKILL.md`:
12
+ Skills live in folders containing `SKILL.md`:
13
13
 
14
14
  | Location | Scope |
15
15
  |---|---|
16
- | `.agents/skills/<name>/SKILL.md` | Current repo only |
17
- | `~/.agents/skills/<name>/SKILL.md` | All projects (global) |
18
- | `.claude/skills/<name>/SKILL.md` | Current repo only (Claude-compatible) |
19
- | `~/.claude/skills/<name>/SKILL.md` | All projects (global, Claude-compatible) |
16
+ | `.agents/skills/<name>/SKILL.md` | Local |
17
+ | `.claude/skills/<name>/SKILL.md` | Local (Claude-compatible) |
18
+ | `~/.agents/skills/<name>/SKILL.md` | Global |
19
+ | `~/.claude/skills/<name>/SKILL.md` | Global (Claude-compatible) |
20
20
 
21
- Local skills override global ones with the same name. At the same scope, `.agents` wins over `.claude`.
21
+ Local discovery walks up from the current working directory to the git worktree root.
22
22
 
23
- ## Create a skill
23
+ ## Precedence rules
24
+
25
+ If multiple skills share the same `name`, precedence is deterministic:
26
+
27
+ 1. Nearest local directory wins over farther ancestor directories.
28
+ 2. Any local skill wins over global.
29
+ 3. At the same scope/path level, `.agents` wins over `.claude`.
30
+
31
+ ## Frontmatter validation
32
+
33
+ `SKILL.md` frontmatter must include:
24
34
 
25
- The folder name becomes the skill name (unless overridden by `name:` in frontmatter).
35
+ - `name` (required)
36
+ - `description` (required)
37
+
38
+ `name` constraints:
39
+
40
+ - lowercase alphanumeric and hyphen format (`^[a-z0-9]+(?:-[a-z0-9]+)*$`)
41
+ - 1–64 characters
42
+
43
+ Invalid skills are skipped with warnings. Unknown frontmatter fields are allowed.
44
+
45
+ ## Create a skill
26
46
 
27
47
  `.agents/skills/conventional-commits/SKILL.md`:
28
48
 
@@ -34,40 +54,25 @@ description: Conventional commit message format rules
34
54
 
35
55
  # Conventional Commits
36
56
 
37
- All commit messages must follow this format:
38
-
39
- <type>(<scope>): <short summary>
40
-
41
- Types: feat, fix, docs, refactor, test, chore
42
- - Summary is lowercase, no period at the end
43
- - Breaking changes: add `!` after type, e.g. `feat!:`
44
- - Body is optional, wrapped at 72 chars
57
+ Use:
58
+ <type>(<scope>): <short summary>
45
59
  ```
46
60
 
47
- Then in the REPL:
61
+ ## Use a skill explicitly
48
62
 
49
- ```
63
+ ```text
50
64
  @conventional-commits write a commit message for my staged changes
51
65
  ```
52
66
 
53
- The skill content is wrapped in `<skill name="…">…</skill>` tags and
54
- included in the message sent to the LLM.
55
-
56
- ## Frontmatter fields
57
-
58
- | Field | Required | Description |
59
- |---|---|---|
60
- | `name` | No | Skill name for `@` reference. Defaults to folder name. |
61
- | `description` | No | Shown in `/help`. Defaults to name. |
62
-
63
- ## Tab completion
64
-
65
- Type `@` and press `Tab` to autocomplete skill names alongside agents and files.
66
-
67
- ## Listing skills
67
+ `@skill-name` injects the raw skill body wrapped as:
68
68
 
69
+ ```xml
70
+ <skill name="conventional-commits">
71
+ ...
72
+ </skill>
69
73
  ```
70
- /help
71
- ```
72
74
 
73
- Skills are listed in yellow, tagged `(local)` or `(global)`.
75
+ ## Tab completion and help
76
+
77
+ - Type `@` then `Tab` to complete skill names.
78
+ - Run `/help` to list discovered skills with `(local)` / `(global)` tags.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mini-coder",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "A small, fast CLI coding agent",
5
5
  "module": "src/index.ts",
6
6
  "type": "module",
@@ -19,12 +19,12 @@
19
19
  "jscpd": "jscpd src"
20
20
  },
21
21
  "dependencies": {
22
- "@ai-sdk/anthropic": "^3.0.58",
23
- "@ai-sdk/google": "^3.0.43",
24
- "@ai-sdk/openai": "^3.0.41",
25
- "@ai-sdk/openai-compatible": "^2.0.35",
22
+ "@ai-sdk/anthropic": "^3.0.60",
23
+ "@ai-sdk/google": "^3.0.51",
24
+ "@ai-sdk/openai": "^3.0.45",
25
+ "@ai-sdk/openai-compatible": "^2.0.36",
26
26
  "@modelcontextprotocol/sdk": "^1.27.1",
27
- "ai": "^6.0.116",
27
+ "ai": "^6.0.127",
28
28
  "ignore": "^7.0.5",
29
29
  "yoctocolors": "^2.1.2",
30
30
  "zod": "^4.3.6"