kimiflare 0.16.0 → 0.17.0

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/index.js CHANGED
@@ -872,22 +872,18 @@ function isReadOnlyBash(command) {
872
872
  return false;
873
873
  }
874
874
  }
875
- const argCheck = COMMANDS_NEEDING_ARG_CHECK[cmd];
876
- if (argCheck) {
877
- return argCheck(args);
878
- }
879
875
  return READONLY_COMMANDS.has(cmd);
880
876
  }
881
877
  function systemPromptForMode(m) {
882
878
  if (m === "plan") {
883
- return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat) along with read/glob/grep/web-fetch. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
879
+ return "\n\nPLAN MODE is active. The user wants you to investigate and produce a plan WITHOUT making any changes. Do not call write, edit, or mutating bash commands. You may use read-only bash commands (e.g., git log, git diff, ls, cat, grep) along with read/glob/grep/web-fetch. Scripting interpreters (node, python3, ruby, perl, awk) and build/package tools (npm, cargo, go, tsc, jest, etc.) are blocked in plan mode. At the end, present a concise plan (bullets, files to change, approach). The user will review and then exit plan mode to execute.";
884
880
  }
885
881
  if (m === "auto") {
886
882
  return "\n\nAUTO MODE is active. The user has opted into autonomous execution \u2014 every tool call will be auto-approved. Work efficiently, but do not take irreversible destructive actions (rm -rf, git push --force, dropping tables, etc.) without pausing to describe them in chat first. Prefer smaller reversible steps.";
887
883
  }
888
884
  return "";
889
885
  }
890
- var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS, COMMANDS_NEEDING_ARG_CHECK;
886
+ var MODES, MUTATING_TOOLS, DANGEROUS_PATTERNS, GIT_READONLY_SUBCOMMANDS, READONLY_COMMANDS;
891
887
  var init_mode = __esm({
892
888
  "src/mode.ts"() {
893
889
  "use strict";
@@ -958,16 +954,8 @@ var init_mode = __esm({
958
954
  "id",
959
955
  "whoami",
960
956
  "groups",
961
- // Dev tools (version/info only)
962
- "node",
963
- "npx",
964
- "python3",
965
- "ruby",
966
- "perl",
967
957
  // Utilities
968
958
  "jq",
969
- "yq",
970
- "awk",
971
959
  "cut",
972
960
  "tr",
973
961
  "base64",
@@ -990,32 +978,6 @@ var init_mode = __esm({
990
978
  "ss",
991
979
  "lsof"
992
980
  ]);
993
- COMMANDS_NEEDING_ARG_CHECK = {
994
- find: (args) => !args.some((a) => a === "-delete" || a === "-exec"),
995
- sed: (args) => !args.some((a) => a === "-i" || a.startsWith("-i")),
996
- tar: (args) => args[0] === "-tf" || args[0] === "--list",
997
- unzip: (args) => args[0] === "-l",
998
- curl: (args) => !args.some((a) => a === "-o" || a === "-O" || a === "-d" || a === "--data" || a.startsWith("-X")),
999
- wget: (args) => !args.some((a) => a === "-O" || a === "--output-document" || a.startsWith("--post")),
1000
- npm: (args) => ["list", "view", "config"].includes(args[0] ?? "") && !(args[0] === "config" && args[1] && !args[1].startsWith("get") && args[1] !== "list"),
1001
- tsc: (args) => args.every(
1002
- (a) => ["--noEmit", "--version", "--showConfig", "--help", "-h", "--init"].includes(a)
1003
- ),
1004
- eslint: (args) => args.every(
1005
- (a) => ["--version", "--print-config", "--help", "-h"].includes(a) || !a.startsWith("-")
1006
- ),
1007
- prettier: (args) => args.every(
1008
- (a) => ["--version", "--check", "--help", "-h"].includes(a) || !a.startsWith("-")
1009
- ),
1010
- jest: (args) => args.every(
1011
- (a) => ["--version", "--listTests", "--showConfig", "--help", "-h"].includes(a) || !a.startsWith("-")
1012
- ),
1013
- vitest: (args) => args.every(
1014
- (a) => ["--version", "--help", "-h"].includes(a) || !a.startsWith("-")
1015
- ),
1016
- go: (args) => ["version", "env", "list", "mod"].includes(args[0] ?? "") && !(args[0] === "mod" && args[1] && !["graph", "download", "why", "verify"].includes(args[1])),
1017
- cargo: (args) => ["--version", "-V", "check", "test", "metadata"].includes(args[0] ?? "") && !(args[0] === "test" && args.includes("--no-run") === false)
1018
- };
1019
981
  }
1020
982
  });
1021
983
 
@@ -1049,7 +1011,12 @@ How to work:
1049
1011
  - You have a 262k-token context window. Read as much of a file as needed rather than guessing.
1050
1012
  - If a request is ambiguous, ask one focused question instead of making large assumptions.
1051
1013
  - When you finish a task, stop. Do not add a closing summary.
1052
- - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.`;
1014
+ - When creating git commits, you must include \`Co-authored-by: kimiflare <kimiflare@proton.me>\` in the commit message so kimiflare is credited as a contributor. The bash tool will also auto-append this trailer when it detects git commit-creating commands.
1015
+
1016
+ Tool output reduction:
1017
+ - Large tool outputs (grep, read, bash, web_fetch) are reduced to compact summaries by default to preserve context window.
1018
+ - When you see "[output reduced]" with an artifact ID, you can call \`expand_artifact\` with that ID to retrieve the full raw output if you need more detail.
1019
+ - You can also re-run the original tool with more targeted parameters (e.g. read with offset/limit, grep with output_mode="files") instead of expanding.`;
1053
1020
  }
1054
1021
  function buildSessionPrefix(opts2) {
1055
1022
  const now2 = opts2.now ?? /* @__PURE__ */ new Date();
@@ -1103,11 +1070,6 @@ function resolvePath(cwd, input) {
1103
1070
  }
1104
1071
  return isAbsolute(input) ? input : resolve(cwd, input);
1105
1072
  }
1106
- function truncate(s, n) {
1107
- if (s.length <= n) return s;
1108
- return s.slice(0, n) + `
1109
- ... [truncated, ${s.length - n} chars omitted]`;
1110
- }
1111
1073
  function collapsePath(input, cwd, maxLen = 40) {
1112
1074
  if (!input) return input;
1113
1075
  let abs;
@@ -1144,7 +1106,7 @@ var init_read = __esm({
1144
1106
  MAX_BYTES = 2 * 1024 * 1024;
1145
1107
  readTool = {
1146
1108
  name: "read",
1147
- description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style.",
1109
+ description: "Read a text file from the local filesystem. Supports optional line offset/limit. Refuses files larger than 2MB. Returns contents with 1-indexed line numbers prefixed, cat -n style. When reading a full file without offset/limit, the output is reduced to a compact outline (imports, exports, signatures, preview) by default; use expand_artifact to retrieve the full content or specify offset/limit for a targeted slice.",
1148
1110
  parameters: {
1149
1111
  type: "object",
1150
1112
  properties: {
@@ -1340,26 +1302,23 @@ ${stdout.trimEnd()}`);
1340
1302
  ${stderr.trimEnd()}`);
1341
1303
  if (!stdout && !stderr) parts.push("(no output)");
1342
1304
  const raw = parts.join("\n");
1343
- const reduced = truncate(raw, OUTPUT_CAP);
1344
1305
  resolve2({
1345
- content: reduced,
1306
+ content: raw,
1346
1307
  rawBytes: Buffer.byteLength(raw, "utf8"),
1347
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1308
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1348
1309
  });
1349
1310
  });
1350
1311
  });
1351
1312
  }
1352
- var DEFAULT_TIMEOUT, MAX_TIMEOUT, OUTPUT_CAP, bashTool;
1313
+ var DEFAULT_TIMEOUT, MAX_TIMEOUT, bashTool;
1353
1314
  var init_bash = __esm({
1354
1315
  "src/tools/bash.ts"() {
1355
1316
  "use strict";
1356
- init_paths();
1357
1317
  DEFAULT_TIMEOUT = 12e4;
1358
1318
  MAX_TIMEOUT = 6e5;
1359
- OUTPUT_CAP = 3e4;
1360
1319
  bashTool = {
1361
1320
  name: "bash",
1362
- description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured, combined, and capped at 30KB.",
1321
+ description: "Run a shell command via `bash -lc`. Prompts the user for permission before executing. stdout and stderr are captured and combined. Large outputs are reduced to a compact summary by default; use expand_artifact to retrieve the full log.",
1363
1322
  parameters: {
1364
1323
  type: "object",
1365
1324
  properties: {
@@ -1444,11 +1403,10 @@ async function runRipgrep(args, root, mode) {
1444
1403
  const { stdout } = await pExecFile("rg", rgArgs, { maxBuffer: 10 * 1024 * 1024 });
1445
1404
  const trimmed = stdout.trim();
1446
1405
  if (!trimmed) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1447
- const reduced = truncate(trimmed, 3e4);
1448
1406
  return {
1449
- content: reduced,
1407
+ content: trimmed,
1450
1408
  rawBytes: Buffer.byteLength(trimmed, "utf8"),
1451
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1409
+ reducedBytes: Buffer.byteLength(trimmed, "utf8")
1452
1410
  };
1453
1411
  } catch (e) {
1454
1412
  const err = e;
@@ -1487,11 +1445,10 @@ async function runJsFallback(args, root, mode) {
1487
1445
  }
1488
1446
  if (!out.length) return { content: "(no matches)", rawBytes: 0, reducedBytes: 0 };
1489
1447
  const raw = out.join("\n");
1490
- const reduced = truncate(raw, 3e4);
1491
1448
  return {
1492
- content: reduced,
1449
+ content: raw,
1493
1450
  rawBytes: Buffer.byteLength(raw, "utf8"),
1494
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1451
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1495
1452
  };
1496
1453
  }
1497
1454
  var pExecFile, cachedHasRg, grepTool;
@@ -1534,17 +1491,15 @@ var init_grep = __esm({
1534
1491
 
1535
1492
  // src/tools/web-fetch.ts
1536
1493
  import TurndownService from "turndown";
1537
- var MAX_BYTES2, MAX_OUTPUT, TIMEOUT_MS, webFetchTool;
1494
+ var MAX_BYTES2, TIMEOUT_MS, webFetchTool;
1538
1495
  var init_web_fetch = __esm({
1539
1496
  "src/tools/web-fetch.ts"() {
1540
1497
  "use strict";
1541
- init_paths();
1542
1498
  MAX_BYTES2 = 1 * 1024 * 1024;
1543
- MAX_OUTPUT = 1e5;
1544
1499
  TIMEOUT_MS = 2e4;
1545
1500
  webFetchTool = {
1546
1501
  name: "web_fetch",
1547
- description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Output is capped at ~100KB.",
1502
+ description: "Fetch a URL over HTTPS and return its content. HTML pages are converted to markdown. Large pages are reduced to a summary by default; use expand_artifact to retrieve the full content.",
1548
1503
  parameters: {
1549
1504
  type: "object",
1550
1505
  properties: {
@@ -1578,11 +1533,10 @@ ${td.turndown(bounded)}`;
1578
1533
 
1579
1534
  ${bounded}`;
1580
1535
  }
1581
- const reduced = truncate(raw, MAX_OUTPUT);
1582
1536
  return {
1583
- content: reduced,
1537
+ content: raw,
1584
1538
  rawBytes: Buffer.byteLength(raw, "utf8"),
1585
- reducedBytes: Buffer.byteLength(reduced, "utf8")
1539
+ reducedBytes: Buffer.byteLength(raw, "utf8")
1586
1540
  };
1587
1541
  } finally {
1588
1542
  clearTimeout(timer);
@@ -1674,6 +1628,423 @@ var init_tasks = __esm({
1674
1628
  }
1675
1629
  });
1676
1630
 
1631
+ // src/tools/artifact-store.ts
1632
+ var ToolArtifactStore;
1633
+ var init_artifact_store = __esm({
1634
+ "src/tools/artifact-store.ts"() {
1635
+ "use strict";
1636
+ ToolArtifactStore = class {
1637
+ artifacts = /* @__PURE__ */ new Map();
1638
+ nextId = 0;
1639
+ maxArtifacts;
1640
+ maxTotalChars;
1641
+ constructor(opts2) {
1642
+ this.maxArtifacts = opts2?.maxArtifacts ?? 500;
1643
+ this.maxTotalChars = opts2?.maxTotalChars ?? 2e6;
1644
+ }
1645
+ /** Store raw content and return a stable artifact ID. */
1646
+ store(raw) {
1647
+ const id = `art_${++this.nextId}`;
1648
+ while (this.totalChars() + raw.length > this.maxTotalChars && this.artifacts.size > 0) {
1649
+ this.evictOldest();
1650
+ }
1651
+ while (this.artifacts.size >= this.maxArtifacts && this.artifacts.size > 0) {
1652
+ this.evictOldest();
1653
+ }
1654
+ this.artifacts.set(id, raw);
1655
+ return id;
1656
+ }
1657
+ retrieve(id) {
1658
+ return this.artifacts.get(id);
1659
+ }
1660
+ has(id) {
1661
+ return this.artifacts.has(id);
1662
+ }
1663
+ clear() {
1664
+ this.artifacts.clear();
1665
+ this.nextId = 0;
1666
+ }
1667
+ size() {
1668
+ return this.artifacts.size;
1669
+ }
1670
+ totalChars() {
1671
+ let sum = 0;
1672
+ for (const raw of this.artifacts.values()) {
1673
+ sum += raw.length;
1674
+ }
1675
+ return sum;
1676
+ }
1677
+ evictOldest() {
1678
+ const first = this.artifacts.keys().next().value;
1679
+ if (first !== void 0) {
1680
+ this.artifacts.delete(first);
1681
+ }
1682
+ }
1683
+ };
1684
+ }
1685
+ });
1686
+
1687
+ // src/tools/reducer.ts
1688
+ function reduceToolOutput(toolName, raw, args, store, config = DEFAULT_REDUCER_CONFIG) {
1689
+ const rawBytes = Buffer.byteLength(raw, "utf8");
1690
+ const artifactId = store.store(raw);
1691
+ if (!config.enabled) {
1692
+ return { content: raw, rawBytes, reducedBytes: rawBytes, artifactId };
1693
+ }
1694
+ let reduced;
1695
+ let wasReduced = false;
1696
+ let hint;
1697
+ switch (toolName) {
1698
+ case "grep": {
1699
+ const r = reduceGrep(raw, args, config.grep);
1700
+ reduced = r.body;
1701
+ wasReduced = r.wasReduced;
1702
+ hint = r.hint;
1703
+ break;
1704
+ }
1705
+ case "read": {
1706
+ const r = reduceRead(raw, args, config.read);
1707
+ reduced = r.body;
1708
+ wasReduced = r.wasReduced;
1709
+ hint = r.hint;
1710
+ break;
1711
+ }
1712
+ case "bash": {
1713
+ const r = reduceBash(raw, args, config.bash);
1714
+ reduced = r.body;
1715
+ wasReduced = r.wasReduced;
1716
+ hint = r.hint;
1717
+ break;
1718
+ }
1719
+ case "web_fetch": {
1720
+ const r = reduceWebFetch(raw, args, config.webFetch);
1721
+ reduced = r.body;
1722
+ wasReduced = r.wasReduced;
1723
+ hint = r.hint;
1724
+ break;
1725
+ }
1726
+ default:
1727
+ reduced = raw;
1728
+ break;
1729
+ }
1730
+ if (!wasReduced) {
1731
+ return { content: reduced, rawBytes, reducedBytes: rawBytes, artifactId };
1732
+ }
1733
+ const footer = `[output reduced \u2014 full raw stored as artifact ${artifactId}]`;
1734
+ const content = hint ? `${reduced}
1735
+ ${footer}
1736
+ ${hint}` : `${reduced}
1737
+ ${footer}`;
1738
+ const reducedBytes = Buffer.byteLength(content, "utf8");
1739
+ return { content, rawBytes, reducedBytes, artifactId };
1740
+ }
1741
+ function parseGrepLines(raw) {
1742
+ const matches = [];
1743
+ for (const line of raw.split("\n")) {
1744
+ const trimmed = line.trim();
1745
+ if (!trimmed) continue;
1746
+ const m = trimmed.match(/^(.+?):(\d+)?:(.*)$/);
1747
+ if (m) {
1748
+ matches.push({ file: m[1], line: m[2] ? parseInt(m[2], 10) : 0, text: m[3] });
1749
+ } else {
1750
+ matches.push({ file: trimmed, line: 0, text: "" });
1751
+ }
1752
+ }
1753
+ return matches;
1754
+ }
1755
+ function reduceGrep(raw, args, cfg) {
1756
+ const isFilesMode = args.output_mode === "files";
1757
+ const matches = parseGrepLines(raw);
1758
+ if (matches.length === 0) {
1759
+ return { body: raw, wasReduced: false };
1760
+ }
1761
+ if (isFilesMode) {
1762
+ const files = [...new Set(matches.map((m) => m.file))];
1763
+ const lines2 = [`${files.length} file(s) matched:`, ...files];
1764
+ return {
1765
+ body: lines2.join("\n"),
1766
+ wasReduced: true,
1767
+ hint: 'Re-run with output_mode="content" for match details.'
1768
+ };
1769
+ }
1770
+ const byFile = /* @__PURE__ */ new Map();
1771
+ for (const m of matches) {
1772
+ const list = byFile.get(m.file) ?? [];
1773
+ list.push(m);
1774
+ byFile.set(m.file, list);
1775
+ }
1776
+ const lines = [];
1777
+ let totalShown = 0;
1778
+ const totalHits = matches.length;
1779
+ const fileCount = byFile.size;
1780
+ lines.push(`Matched ${fileCount} file(s) (${totalHits} total hits):`);
1781
+ for (const [file, hits] of byFile) {
1782
+ if (totalShown >= cfg.maxTotalLines) break;
1783
+ lines.push(` ${file}: ${hits.length} hit(s)`);
1784
+ const toShow = Math.min(hits.length, cfg.maxMatchesPerFile);
1785
+ for (let i = 0; i < toShow; i++) {
1786
+ const h = hits[i];
1787
+ const text = h.text.length > cfg.maxLineLength ? h.text.slice(0, cfg.maxLineLength) + "\u2026" : h.text;
1788
+ const prefix = h.line > 0 ? ` ${h.line}:` : " ";
1789
+ lines.push(`${prefix}${text}`);
1790
+ totalShown++;
1791
+ if (totalShown >= cfg.maxTotalLines) break;
1792
+ }
1793
+ }
1794
+ if (totalShown < totalHits) {
1795
+ lines.push(` \u2026 (${totalHits - totalShown} more hits omitted)`);
1796
+ }
1797
+ return {
1798
+ body: lines.join("\n"),
1799
+ wasReduced: totalHits > totalShown || fileCount > 1,
1800
+ hint: 'Use expand_artifact for full matches, or re-run with output_mode="files" for paths only.'
1801
+ };
1802
+ }
1803
+ function reduceRead(raw, args, cfg) {
1804
+ const hasSlice = typeof args.offset === "number" || typeof args.limit === "number";
1805
+ if (hasSlice) {
1806
+ const lines = raw.split("\n");
1807
+ if (lines.length > cfg.maxSliceLines) {
1808
+ const kept = lines.slice(0, cfg.maxSliceLines).join("\n");
1809
+ return {
1810
+ body: kept,
1811
+ wasReduced: true,
1812
+ hint: `\u2026 (${lines.length - cfg.maxSliceLines} more lines omitted)`
1813
+ };
1814
+ }
1815
+ return { body: raw, wasReduced: false };
1816
+ }
1817
+ const allLines = raw.split("\n");
1818
+ const totalLines = allLines.length;
1819
+ const cleanLines = allLines.map((l) => l.replace(/^\s*\d+\t/, ""));
1820
+ const imports = [];
1821
+ const exports = [];
1822
+ const functions = [];
1823
+ const classes = [];
1824
+ for (let i = 0; i < cleanLines.length; i++) {
1825
+ const line = cleanLines[i];
1826
+ const lineNum = i + 1;
1827
+ if (/^import\s+/.test(line)) {
1828
+ imports.push(`${lineNum}: ${line.trim()}`);
1829
+ } else if (/^(?:export\s+)?class\s+\w+/.test(line)) {
1830
+ classes.push(`${lineNum}: ${line.trim()}`);
1831
+ } else if (/^export\s+/.test(line)) {
1832
+ exports.push(`${lineNum}: ${line.trim()}`);
1833
+ } else if (/^(?:async\s+)?function\s+\w+/.test(line)) {
1834
+ functions.push(`${lineNum}: ${line.trim()}`);
1835
+ }
1836
+ }
1837
+ const parts = [];
1838
+ parts.push(`File: ${totalLines} lines total`);
1839
+ if (imports.length > 0) {
1840
+ parts.push(`
1841
+ Imports (${imports.length}):`);
1842
+ parts.push(...imports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1843
+ }
1844
+ if (exports.length > 0) {
1845
+ parts.push(`
1846
+ Exports (${exports.length}):`);
1847
+ parts.push(...exports.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1848
+ }
1849
+ if (functions.length > 0) {
1850
+ parts.push(`
1851
+ Functions (${functions.length}):`);
1852
+ parts.push(...functions.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1853
+ }
1854
+ if (classes.length > 0) {
1855
+ parts.push(`
1856
+ Classes (${classes.length}):`);
1857
+ parts.push(...classes.slice(0, Math.floor(cfg.maxOutlineLines / 4)));
1858
+ }
1859
+ const previewCount = Math.min(cfg.maxPreviewLines, totalLines);
1860
+ parts.push(`
1861
+ Preview (lines 1\u2013${previewCount}):`);
1862
+ parts.push(...allLines.slice(0, previewCount));
1863
+ return {
1864
+ body: parts.join("\n"),
1865
+ wasReduced: true,
1866
+ hint: "Use expand_artifact for full file, or read with offset/limit for a specific slice."
1867
+ };
1868
+ }
1869
+ function reduceBash(raw, _args, cfg) {
1870
+ const lines = raw.split("\n");
1871
+ if (lines.length <= cfg.maxTotalLines) {
1872
+ return { body: raw, wasReduced: false };
1873
+ }
1874
+ let header = "";
1875
+ let bodyStart = 0;
1876
+ if (lines[0]?.startsWith("exit=") || lines[0]?.startsWith("(timed out")) {
1877
+ header = lines[0];
1878
+ bodyStart = 1;
1879
+ }
1880
+ const body = lines.slice(bodyStart);
1881
+ const isFailure = header.includes("exit=1") || raw.includes("Error:") || raw.includes("error:") || raw.includes("FAIL") || raw.includes("failed");
1882
+ const out = [header];
1883
+ if (isFailure) {
1884
+ const errorIndices = [];
1885
+ for (let i = 0; i < body.length; i++) {
1886
+ const line = body[i];
1887
+ if (/\bError\b/i.test(line) || /\berror\b/i.test(line) || /\bFAIL\b/i.test(line) || /\bfailed\b/i.test(line) || /^\s+at\s+/.test(line) || /\s+Error:\s+/.test(line)) {
1888
+ for (let j = Math.max(0, i - 2); j <= Math.min(body.length - 1, i + 2); j++) {
1889
+ if (!errorIndices.includes(j)) errorIndices.push(j);
1890
+ }
1891
+ }
1892
+ }
1893
+ errorIndices.sort((a, b) => a - b);
1894
+ const cappedError = errorIndices.slice(0, cfg.maxErrorBlockLines);
1895
+ if (cappedError.length > 0) {
1896
+ out.push("--- error block ---");
1897
+ for (const idx of cappedError) {
1898
+ out.push(body[idx]);
1899
+ }
1900
+ }
1901
+ const testNames = [];
1902
+ for (const line of body) {
1903
+ const m = line.match(/(?:✗|✕|×|FAIL)\s+(.+)/) || line.match(/failing\s*\d*\s*:?\s*(.+)/i) || line.match(/Test\s+\w+\s+failed/i);
1904
+ if (m && m[1]) {
1905
+ const name = m[1].trim().slice(0, 120);
1906
+ if (!testNames.includes(name)) testNames.push(name);
1907
+ }
1908
+ }
1909
+ if (testNames.length > 0) {
1910
+ out.push("--- failing tests ---");
1911
+ out.push(...testNames.slice(0, 10));
1912
+ }
1913
+ }
1914
+ const trailing = body.slice(-cfg.maxTrailingLines);
1915
+ out.push("--- last lines ---");
1916
+ out.push(...trailing);
1917
+ let result = out.join("\n");
1918
+ if (cfg.dedupeConsecutiveLines) {
1919
+ result = dedupeConsecutive(result);
1920
+ }
1921
+ return {
1922
+ body: result,
1923
+ wasReduced: true,
1924
+ hint: "Use expand_artifact for full output."
1925
+ };
1926
+ }
1927
+ function dedupeConsecutive(text) {
1928
+ const lines = text.split("\n");
1929
+ const out = [];
1930
+ let repeatCount = 1;
1931
+ for (let i = 0; i < lines.length; i++) {
1932
+ const line = lines[i];
1933
+ const next = lines[i + 1];
1934
+ if (next !== void 0 && next === line) {
1935
+ repeatCount++;
1936
+ continue;
1937
+ }
1938
+ if (repeatCount > 2) {
1939
+ out.push(line);
1940
+ out.push(`\u2026 (${repeatCount - 1} identical lines omitted)`);
1941
+ } else {
1942
+ for (let j = 0; j < repeatCount; j++) {
1943
+ out.push(line);
1944
+ }
1945
+ }
1946
+ repeatCount = 1;
1947
+ }
1948
+ return out.join("\n");
1949
+ }
1950
+ function reduceWebFetch(raw, args, cfg) {
1951
+ const url = typeof args.url === "string" ? args.url : "(unknown URL)";
1952
+ const titleMatch = raw.match(/^#\s+(.+)$/m);
1953
+ const title = titleMatch ? titleMatch[1].trim() : "(no title)";
1954
+ const parts = [];
1955
+ parts.push(`Title: ${title}`);
1956
+ parts.push(`URL: ${url}`);
1957
+ const headings = raw.match(/^#{1,3}\s+.+$/gm) ?? [];
1958
+ if (headings.length > 0) {
1959
+ parts.push("\nSections:");
1960
+ for (const h of headings.slice(0, 10)) {
1961
+ parts.push(` ${h}`);
1962
+ }
1963
+ }
1964
+ const bodyStart = raw.indexOf("\n\n");
1965
+ const body = bodyStart > 0 ? raw.slice(bodyStart + 2) : raw;
1966
+ const excerpt = body.slice(0, cfg.maxChars).trim();
1967
+ if (excerpt) {
1968
+ parts.push(`
1969
+ Excerpt (${excerpt.length} chars):`);
1970
+ parts.push(excerpt);
1971
+ }
1972
+ if (body.length > cfg.maxChars) {
1973
+ parts.push(`
1974
+ \u2026 (${body.length - cfg.maxChars} more chars omitted)`);
1975
+ }
1976
+ return {
1977
+ body: parts.join("\n"),
1978
+ wasReduced: true,
1979
+ hint: "Use expand_artifact for full page content."
1980
+ };
1981
+ }
1982
+ var DEFAULT_REDUCER_CONFIG;
1983
+ var init_reducer = __esm({
1984
+ "src/tools/reducer.ts"() {
1985
+ "use strict";
1986
+ DEFAULT_REDUCER_CONFIG = {
1987
+ enabled: true,
1988
+ grep: {
1989
+ maxTotalLines: 50,
1990
+ maxMatchesPerFile: 3,
1991
+ maxLineLength: 200,
1992
+ maxOutputChars: 3e3
1993
+ },
1994
+ read: {
1995
+ maxOutlineLines: 60,
1996
+ maxSliceLines: 200,
1997
+ maxPreviewLines: 30,
1998
+ maxOutputChars: 4e3
1999
+ },
2000
+ bash: {
2001
+ maxTotalLines: 40,
2002
+ maxErrorBlockLines: 20,
2003
+ maxTrailingLines: 20,
2004
+ maxOutputChars: 4e3,
2005
+ dedupeConsecutiveLines: true
2006
+ },
2007
+ webFetch: {
2008
+ maxChars: 2e3,
2009
+ maxHeadingChars: 500
2010
+ }
2011
+ };
2012
+ }
2013
+ });
2014
+
2015
+ // src/tools/expand-artifact.ts
2016
+ function makeExpandArtifactTool(store) {
2017
+ return {
2018
+ name: "expand_artifact",
2019
+ description: "Retrieve the full raw content of a previously reduced tool output by its artifact ID. Use this when the compact summary is insufficient and you need the complete original output.",
2020
+ parameters: {
2021
+ type: "object",
2022
+ properties: {
2023
+ artifact_id: {
2024
+ type: "string",
2025
+ description: "The artifact ID from a reduced tool output footer, e.g. art_42."
2026
+ }
2027
+ },
2028
+ required: ["artifact_id"],
2029
+ additionalProperties: false
2030
+ },
2031
+ needsPermission: false,
2032
+ render: (args) => ({ title: `expand ${args.artifact_id}` }),
2033
+ run: async (args) => {
2034
+ const raw = store.retrieve(args.artifact_id);
2035
+ if (!raw) {
2036
+ return `Artifact "${args.artifact_id}" not found. It may have been evicted from memory. Re-run the original tool to regenerate the output.`;
2037
+ }
2038
+ return raw;
2039
+ }
2040
+ };
2041
+ }
2042
+ var init_expand_artifact = __esm({
2043
+ "src/tools/expand-artifact.ts"() {
2044
+ "use strict";
2045
+ }
2046
+ });
2047
+
1677
2048
  // src/tools/executor.ts
1678
2049
  function normalizeToolOutput(result) {
1679
2050
  if (typeof result === "string") {
@@ -1697,6 +2068,9 @@ var init_executor = __esm({
1697
2068
  init_grep();
1698
2069
  init_web_fetch();
1699
2070
  init_tasks();
2071
+ init_artifact_store();
2072
+ init_reducer();
2073
+ init_expand_artifact();
1700
2074
  ALL_TOOLS = [
1701
2075
  readTool,
1702
2076
  writeTool,
@@ -1710,8 +2084,11 @@ var init_executor = __esm({
1710
2084
  ToolExecutor = class {
1711
2085
  sessionAllowed = /* @__PURE__ */ new Set();
1712
2086
  tools;
2087
+ artifactStore;
1713
2088
  constructor(tools = ALL_TOOLS) {
1714
2089
  this.tools = new Map(tools.map((t) => [t.name, t]));
2090
+ this.artifactStore = new ToolArtifactStore();
2091
+ this.tools.set("expand_artifact", makeExpandArtifactTool(this.artifactStore));
1715
2092
  }
1716
2093
  list() {
1717
2094
  return [...this.tools.values()];
@@ -1725,6 +2102,9 @@ var init_executor = __esm({
1725
2102
  clearSessionPermissions() {
1726
2103
  this.sessionAllowed.clear();
1727
2104
  }
2105
+ clearArtifacts() {
2106
+ this.artifactStore.clear();
2107
+ }
1728
2108
  async run(call, askPermission, ctx) {
1729
2109
  const tool = this.tools.get(call.name);
1730
2110
  if (!tool) {
@@ -1764,13 +2144,21 @@ var init_executor = __esm({
1764
2144
  try {
1765
2145
  const result = await tool.run(args, ctx);
1766
2146
  const normalized = normalizeToolOutput(result);
2147
+ const reduced = reduceToolOutput(
2148
+ call.name,
2149
+ normalized.content,
2150
+ args,
2151
+ this.artifactStore,
2152
+ DEFAULT_REDUCER_CONFIG
2153
+ );
1767
2154
  return {
1768
2155
  tool_call_id: call.id,
1769
2156
  name: call.name,
1770
- content: normalized.content,
2157
+ content: reduced.content,
1771
2158
  ok: true,
1772
- rawBytes: normalized.rawBytes,
1773
- reducedBytes: normalized.reducedBytes
2159
+ rawBytes: reduced.rawBytes,
2160
+ reducedBytes: reduced.reducedBytes,
2161
+ artifactId: reduced.artifactId
1774
2162
  };
1775
2163
  } catch (e) {
1776
2164
  const msg = `Error running ${call.name}: ${e.message ?? String(e)}`;
@@ -2921,7 +3309,7 @@ function buildRightParts(usage, contextLimit) {
2921
3309
  `in ${usage.prompt_tokens}${cached ? ` (${cached} cached)` : ""}`,
2922
3310
  `out ${usage.completion_tokens}`,
2923
3311
  `ctx ${pct}%`,
2924
- `${cost.total.toFixed(5)}`
3312
+ `$${cost.total.toFixed(5)}`
2925
3313
  ];
2926
3314
  }
2927
3315
  function shortModel(m) {
@@ -5282,6 +5670,7 @@ function App({ initialCfg, initialUpdateResult }) {
5282
5670
  sessionIdRef.current = null;
5283
5671
  sessionStateRef.current = emptySessionState();
5284
5672
  artifactStoreRef.current = new ArtifactStore();
5673
+ executorRef.current.clearArtifacts();
5285
5674
  setEvents([]);
5286
5675
  setUsage(null);
5287
5676
  setTasks([]);