reasonix 0.5.0 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -617,6 +617,170 @@ async function runHooks(opts) {
617
617
  return { event, outcomes, blocked };
618
618
  }
619
619
 
620
+ // src/tokenizer.ts
621
+ import { readFileSync as readFileSync2 } from "fs";
622
+ import { createRequire } from "module";
623
+ import { dirname, join as join2 } from "path";
624
+ import { fileURLToPath } from "url";
625
+ import { gunzipSync } from "zlib";
626
+ function buildByteToChar() {
627
+ const result = new Array(256);
628
+ const bs = [];
629
+ for (let b = 33; b <= 126; b++) bs.push(b);
630
+ for (let b = 161; b <= 172; b++) bs.push(b);
631
+ for (let b = 174; b <= 255; b++) bs.push(b);
632
+ const cs = bs.slice();
633
+ let n = 0;
634
+ for (let b = 0; b < 256; b++) {
635
+ if (!bs.includes(b)) {
636
+ bs.push(b);
637
+ cs.push(256 + n);
638
+ n++;
639
+ }
640
+ }
641
+ for (let i = 0; i < bs.length; i++) {
642
+ result[bs[i]] = String.fromCodePoint(cs[i]);
643
+ }
644
+ return result;
645
+ }
646
+ var cached = null;
647
+ function resolveDataPath() {
648
+ if (process.env.REASONIX_TOKENIZER_PATH) return process.env.REASONIX_TOKENIZER_PATH;
649
+ try {
650
+ const here = dirname(fileURLToPath(import.meta.url));
651
+ return join2(here, "..", "data", "deepseek-tokenizer.json.gz");
652
+ } catch {
653
+ const req = createRequire(import.meta.url);
654
+ return join2(
655
+ dirname(req.resolve("reasonix/package.json")),
656
+ "data",
657
+ "deepseek-tokenizer.json.gz"
658
+ );
659
+ }
660
+ }
661
+ function loadTokenizer() {
662
+ if (cached) return cached;
663
+ const buf = readFileSync2(resolveDataPath());
664
+ const json = gunzipSync(buf).toString("utf8");
665
+ const data = JSON.parse(json);
666
+ const mergeRank = /* @__PURE__ */ new Map();
667
+ for (let i = 0; i < data.model.merges.length; i++) {
668
+ mergeRank.set(data.model.merges[i], i);
669
+ }
670
+ const splitRegexes = [];
671
+ for (const p of data.pre_tokenizer.pretokenizers) {
672
+ if (p.type === "Split") {
673
+ splitRegexes.push(new RegExp(p.pattern.Regex, "gu"));
674
+ }
675
+ }
676
+ const addedMap = /* @__PURE__ */ new Map();
677
+ const addedContents = [];
678
+ for (const t of data.added_tokens) {
679
+ if (!t.special) {
680
+ addedMap.set(t.content, t.id);
681
+ addedContents.push(t.content);
682
+ }
683
+ }
684
+ addedContents.sort((a, b) => b.length - a.length);
685
+ const addedPattern = addedContents.length ? new RegExp(addedContents.map(escapeRegex).join("|"), "g") : null;
686
+ cached = {
687
+ vocab: data.model.vocab,
688
+ mergeRank,
689
+ splitRegexes,
690
+ byteToChar: buildByteToChar(),
691
+ addedPattern,
692
+ addedMap
693
+ };
694
+ return cached;
695
+ }
696
+ function escapeRegex(s) {
697
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
698
+ }
699
+ function applySplit(chunks, re) {
700
+ const out = [];
701
+ for (const chunk of chunks) {
702
+ if (!chunk) continue;
703
+ re.lastIndex = 0;
704
+ let last = 0;
705
+ for (const m of chunk.matchAll(re)) {
706
+ const idx = m.index ?? 0;
707
+ if (idx > last) out.push(chunk.slice(last, idx));
708
+ if (m[0].length > 0) out.push(m[0]);
709
+ last = idx + m[0].length;
710
+ }
711
+ if (last < chunk.length) out.push(chunk.slice(last));
712
+ }
713
+ return out;
714
+ }
715
+ function byteLevelEncode(s, byteToChar) {
716
+ const bytes = new TextEncoder().encode(s);
717
+ let out = "";
718
+ for (let i = 0; i < bytes.length; i++) out += byteToChar[bytes[i]];
719
+ return out;
720
+ }
721
+ function bpeEncode(piece, mergeRank) {
722
+ if (piece.length <= 1) return piece ? [piece] : [];
723
+ let word = Array.from(piece);
724
+ while (true) {
725
+ let bestIdx = -1;
726
+ let bestRank = Number.POSITIVE_INFINITY;
727
+ for (let i = 0; i < word.length - 1; i++) {
728
+ const pair = `${word[i]} ${word[i + 1]}`;
729
+ const rank = mergeRank.get(pair);
730
+ if (rank !== void 0 && rank < bestRank) {
731
+ bestRank = rank;
732
+ bestIdx = i;
733
+ if (rank === 0) break;
734
+ }
735
+ }
736
+ if (bestIdx < 0) break;
737
+ word = [
738
+ ...word.slice(0, bestIdx),
739
+ word[bestIdx] + word[bestIdx + 1],
740
+ ...word.slice(bestIdx + 2)
741
+ ];
742
+ if (word.length === 1) break;
743
+ }
744
+ return word;
745
+ }
746
+ function encode(text) {
747
+ if (!text) return [];
748
+ const t = loadTokenizer();
749
+ const ids = [];
750
+ const process2 = (segment) => {
751
+ if (!segment) return;
752
+ let chunks = [segment];
753
+ for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
754
+ for (const chunk of chunks) {
755
+ if (!chunk) continue;
756
+ const byteLevel = byteLevelEncode(chunk, t.byteToChar);
757
+ const pieces = bpeEncode(byteLevel, t.mergeRank);
758
+ for (const p of pieces) {
759
+ const id = t.vocab[p];
760
+ if (id !== void 0) ids.push(id);
761
+ }
762
+ }
763
+ };
764
+ if (t.addedPattern) {
765
+ t.addedPattern.lastIndex = 0;
766
+ let last = 0;
767
+ for (const m of text.matchAll(t.addedPattern)) {
768
+ const idx = m.index ?? 0;
769
+ if (idx > last) process2(text.slice(last, idx));
770
+ const id = t.addedMap.get(m[0]);
771
+ if (id !== void 0) ids.push(id);
772
+ last = idx + m[0].length;
773
+ }
774
+ if (last < text.length) process2(text.slice(last));
775
+ } else {
776
+ process2(text);
777
+ }
778
+ return ids;
779
+ }
780
+ function countTokens(text) {
781
+ return encode(text).length;
782
+ }
783
+
620
784
  // src/repair/flatten.ts
621
785
  function analyzeSchema(schema) {
622
786
  if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
@@ -767,7 +931,14 @@ var ToolRegistry = class {
767
931
  try {
768
932
  const result = await tool.fn(args, { signal: opts.signal });
769
933
  const str = typeof result === "string" ? result : JSON.stringify(result);
770
- return opts.maxResultChars ? truncateForModel(str, opts.maxResultChars) : str;
934
+ let clipped = str;
935
+ if (opts.maxResultTokens !== void 0) {
936
+ clipped = truncateForModelByTokens(clipped, opts.maxResultTokens);
937
+ }
938
+ if (opts.maxResultChars !== void 0) {
939
+ clipped = truncateForModel(clipped, opts.maxResultChars);
940
+ }
941
+ return clipped;
771
942
  } catch (err) {
772
943
  const e = err;
773
944
  if (typeof e.toToolResult === "function") {
@@ -801,6 +972,7 @@ function hasDotKey(obj) {
801
972
 
802
973
  // src/mcp/registry.ts
803
974
  var DEFAULT_MAX_RESULT_CHARS = 32e3;
975
+ var DEFAULT_MAX_RESULT_TOKENS = 8e3;
804
976
  async function bridgeMcpTools(client, opts = {}) {
805
977
  const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
806
978
  const prefix = opts.namePrefix ?? "";
@@ -857,6 +1029,61 @@ function truncateForModel(s, maxChars) {
857
1029
 
858
1030
  ${tail}`;
859
1031
  }
1032
+ function truncateForModelByTokens(s, maxTokens) {
1033
+ if (maxTokens <= 0) return "";
1034
+ if (s.length <= maxTokens) return s;
1035
+ if (s.length <= maxTokens * 4) {
1036
+ const tokens = countTokens(s);
1037
+ if (tokens <= maxTokens) return s;
1038
+ }
1039
+ const markerOverhead = 48;
1040
+ const contentBudget = Math.max(0, maxTokens - markerOverhead);
1041
+ const tailBudget = Math.min(256, Math.floor(contentBudget * 0.1));
1042
+ const headBudget = Math.max(0, contentBudget - tailBudget);
1043
+ const head = sizePrefixToTokens(s, headBudget);
1044
+ const tail = sizeSuffixToTokens(s, tailBudget);
1045
+ const droppedChars = s.length - head.length - tail.length;
1046
+ const headTokens = head ? countTokens(head) : 0;
1047
+ const tailTokens = tail ? countTokens(tail) : 0;
1048
+ const sampleChars = head.length + tail.length;
1049
+ const sampleTokens = headTokens + tailTokens;
1050
+ const ratio = sampleChars > 0 ? sampleTokens / sampleChars : 0.3;
1051
+ const estTotalTokens = Math.ceil(s.length * ratio);
1052
+ const droppedTokens = Math.max(0, estTotalTokens - sampleTokens);
1053
+ return `${head}
1054
+
1055
+ [\u2026truncated ~${droppedTokens} tokens (${droppedChars} chars) \u2014 raise BridgeOptions.maxResultTokens, or call the tool with a narrower scope (filter, head, pagination)\u2026]
1056
+
1057
+ ${tail}`;
1058
+ }
1059
+ function sizePrefixToTokens(s, budget) {
1060
+ if (budget <= 0 || s.length === 0) return "";
1061
+ let size = Math.min(s.length, budget * 4);
1062
+ for (let iter = 0; iter < 6; iter++) {
1063
+ if (size <= 0) return "";
1064
+ const slice = s.slice(0, size);
1065
+ const count = countTokens(slice);
1066
+ if (count <= budget) return slice;
1067
+ const next = Math.floor(size * (budget / count) * 0.95);
1068
+ if (next >= size) return s.slice(0, Math.max(0, size - 1));
1069
+ size = next;
1070
+ }
1071
+ return s.slice(0, Math.max(0, size));
1072
+ }
1073
+ function sizeSuffixToTokens(s, budget) {
1074
+ if (budget <= 0 || s.length === 0) return "";
1075
+ let size = Math.min(s.length, budget * 4);
1076
+ for (let iter = 0; iter < 6; iter++) {
1077
+ if (size <= 0) return "";
1078
+ const slice = s.slice(-size);
1079
+ const count = countTokens(slice);
1080
+ if (count <= budget) return slice;
1081
+ const next = Math.floor(size * (budget / count) * 0.95);
1082
+ if (next >= size) return s.slice(-Math.max(0, size - 1));
1083
+ size = next;
1084
+ }
1085
+ return s.slice(-Math.max(0, size));
1086
+ }
860
1087
  function blockToString(block) {
861
1088
  if (block.type === "text") return block.text;
862
1089
  if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
@@ -1242,19 +1469,19 @@ import {
1242
1469
  chmodSync,
1243
1470
  existsSync as existsSync2,
1244
1471
  mkdirSync,
1245
- readFileSync as readFileSync2,
1472
+ readFileSync as readFileSync3,
1246
1473
  readdirSync,
1247
1474
  statSync,
1248
1475
  unlinkSync,
1249
1476
  writeFileSync
1250
1477
  } from "fs";
1251
1478
  import { homedir as homedir2 } from "os";
1252
- import { dirname, join as join2 } from "path";
1479
+ import { dirname as dirname2, join as join3 } from "path";
1253
1480
  function sessionsDir() {
1254
- return join2(homedir2(), ".reasonix", "sessions");
1481
+ return join3(homedir2(), ".reasonix", "sessions");
1255
1482
  }
1256
1483
  function sessionPath(name) {
1257
- return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1484
+ return join3(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1258
1485
  }
1259
1486
  function sanitizeName(name) {
1260
1487
  const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
@@ -1264,7 +1491,7 @@ function loadSessionMessages(name) {
1264
1491
  const path = sessionPath(name);
1265
1492
  if (!existsSync2(path)) return [];
1266
1493
  try {
1267
- const raw = readFileSync2(path, "utf8");
1494
+ const raw = readFileSync3(path, "utf8");
1268
1495
  const out = [];
1269
1496
  for (const line of raw.split(/\r?\n/)) {
1270
1497
  const trimmed = line.trim();
@@ -1282,7 +1509,7 @@ function loadSessionMessages(name) {
1282
1509
  }
1283
1510
  function appendSessionMessage(name, message) {
1284
1511
  const path = sessionPath(name);
1285
- mkdirSync(dirname(path), { recursive: true });
1512
+ mkdirSync(dirname2(path), { recursive: true });
1286
1513
  appendFileSync(path, `${JSON.stringify(message)}
1287
1514
  `, "utf8");
1288
1515
  try {
@@ -1296,7 +1523,7 @@ function listSessions() {
1296
1523
  try {
1297
1524
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1298
1525
  return files.map((file) => {
1299
- const path = join2(dir, file);
1526
+ const path = join3(dir, file);
1300
1527
  const stat = statSync(path);
1301
1528
  const name = file.replace(/\.jsonl$/, "");
1302
1529
  const messageCount = countLines(path);
@@ -1317,7 +1544,7 @@ function deleteSession(name) {
1317
1544
  }
1318
1545
  function rewriteSession(name, messages) {
1319
1546
  const path = sessionPath(name);
1320
- mkdirSync(dirname(path), { recursive: true });
1547
+ mkdirSync(dirname2(path), { recursive: true });
1321
1548
  const body = messages.map((m) => JSON.stringify(m)).join("\n");
1322
1549
  writeFileSync(path, body ? `${body}
1323
1550
  ` : "", "utf8");
@@ -1328,7 +1555,7 @@ function rewriteSession(name, messages) {
1328
1555
  }
1329
1556
  function countLines(path) {
1330
1557
  try {
1331
- const raw = readFileSync2(path, "utf8");
1558
+ const raw = readFileSync3(path, "utf8");
1332
1559
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1333
1560
  } catch {
1334
1561
  return 0;
@@ -1510,20 +1737,26 @@ var CacheFirstLoop = class {
1510
1737
  }
1511
1738
  /**
1512
1739
  * Shrink the log by re-truncating oversized tool results to a tighter
1513
- * cap, and persist the result back to disk so the next launch doesn't
1514
- * re-inherit a fat session file. Returns a summary the TUI can
1515
- * display.
1740
+ * token cap, and persist the result back to disk so the next launch
1741
+ * doesn't re-inherit a fat session file. Returns a summary the TUI
1742
+ * can display.
1743
+ *
1744
+ * The cap is in DeepSeek V3 tokens (not chars) — so CJK text gets
1745
+ * capped at the same effective context footprint as English instead
1746
+ * of slipping past a char cap at 2× the token cost. Default 4000
1747
+ * tokens, matching the token-aware dispatch cap from 0.5.2.
1516
1748
  *
1517
1749
  * Only tool-role messages are touched (same rationale as
1518
1750
  * {@link healLoadedMessages}). User and assistant messages carry
1519
1751
  * authored intent we can't mechanically shrink without losing
1520
1752
  * meaning.
1521
1753
  */
1522
- compact(tightCapChars = 4e3) {
1754
+ compact(maxTokens = 4e3) {
1523
1755
  const before = this.log.toMessages();
1524
- const { messages, healedCount, healedFrom } = shrinkOversizedToolResults(before, tightCapChars);
1525
- const afterBytes = messages.filter((m) => m.role === "tool").reduce((s, m) => s + (typeof m.content === "string" ? m.content.length : 0), 0);
1526
- const charsSaved = healedFrom - afterBytes;
1756
+ const { messages, healedCount, tokensSaved, charsSaved } = shrinkOversizedToolResultsByTokens(
1757
+ before,
1758
+ maxTokens
1759
+ );
1527
1760
  if (healedCount > 0) {
1528
1761
  this.log.compactInPlace(messages);
1529
1762
  if (this.sessionName) {
@@ -1533,7 +1766,7 @@ var CacheFirstLoop = class {
1533
1766
  }
1534
1767
  }
1535
1768
  }
1536
- return { healedCount, charsSaved };
1769
+ return { healedCount, tokensSaved, charsSaved };
1537
1770
  }
1538
1771
  appendAndPersist(message) {
1539
1772
  this.log.append(message);
@@ -1897,30 +2130,28 @@ var CacheFirstLoop = class {
1897
2130
  const ratio = usage.promptTokens / ctxMax;
1898
2131
  if (ratio > 0.6 && ratio <= 0.8) {
1899
2132
  const before = usage.promptTokens;
1900
- const soft = this.compact(16e3);
2133
+ const soft = this.compact(4e3);
1901
2134
  if (soft.healedCount > 0) {
1902
- const approxSaved = Math.round(soft.charsSaved / 4);
1903
- const after = Math.max(0, before - approxSaved);
2135
+ const after = Math.max(0, before - soft.tokensSaved);
1904
2136
  yield {
1905
2137
  turn: this._turn,
1906
2138
  role: "warning",
1907
2139
  content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
1908
2140
  ratio * 100
1909
- )}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 16k, saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
2141
+ )}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 4k tokens, saved ${soft.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
1910
2142
  };
1911
2143
  }
1912
2144
  }
1913
2145
  }
1914
2146
  if (usage && usage.promptTokens / ctxMax > 0.8) {
1915
2147
  const before = usage.promptTokens;
1916
- const compactResult = this.compact(4e3);
2148
+ const compactResult = this.compact(1e3);
1917
2149
  if (compactResult.healedCount > 0) {
1918
- const approxSaved = Math.round(compactResult.charsSaved / 4);
1919
- const after = before - approxSaved;
2150
+ const after = Math.max(0, before - compactResult.tokensSaved);
1920
2151
  yield {
1921
2152
  turn: this._turn,
1922
2153
  role: "warning",
1923
- content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
2154
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} \u2014 auto-compacted ${compactResult.healedCount} oversized tool result(s), saved ${compactResult.tokensSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Continuing.`
1924
2155
  };
1925
2156
  } else {
1926
2157
  yield {
@@ -1975,7 +2206,7 @@ ${reason}`;
1975
2206
  } else {
1976
2207
  result = await this.tools.dispatch(name, args, {
1977
2208
  signal,
1978
- maxResultChars: DEFAULT_MAX_RESULT_CHARS
2209
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
1979
2210
  });
1980
2211
  const postReport = await runHooks({
1981
2212
  hooks: this.hooks,
@@ -2121,6 +2352,25 @@ function shrinkOversizedToolResults(messages, maxChars) {
2121
2352
  });
2122
2353
  return { messages: out, healedCount, healedFrom };
2123
2354
  }
2355
+ function shrinkOversizedToolResultsByTokens(messages, maxTokens) {
2356
+ let healedCount = 0;
2357
+ let tokensSaved = 0;
2358
+ let charsSaved = 0;
2359
+ const out = messages.map((msg) => {
2360
+ if (msg.role !== "tool") return msg;
2361
+ const content = typeof msg.content === "string" ? msg.content : "";
2362
+ if (content.length <= maxTokens) return msg;
2363
+ const beforeTokens = countTokens(content);
2364
+ if (beforeTokens <= maxTokens) return msg;
2365
+ const truncated = truncateForModelByTokens(content, maxTokens);
2366
+ const afterTokens = countTokens(truncated);
2367
+ healedCount += 1;
2368
+ tokensSaved += Math.max(0, beforeTokens - afterTokens);
2369
+ charsSaved += Math.max(0, content.length - truncated.length);
2370
+ return { ...msg, content: truncated };
2371
+ });
2372
+ return { messages: out, healedCount, tokensSaved, charsSaved };
2373
+ }
2124
2374
  function healLoadedMessages(messages, maxChars) {
2125
2375
  const shrunk = shrinkOversizedToolResults(messages, maxChars);
2126
2376
  let healedCount = shrunk.healedCount;
@@ -2177,16 +2427,16 @@ function formatLoopError(err) {
2177
2427
  }
2178
2428
 
2179
2429
  // src/project-memory.ts
2180
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2181
- import { join as join3 } from "path";
2430
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2431
+ import { join as join4 } from "path";
2182
2432
  var PROJECT_MEMORY_FILE = "REASONIX.md";
2183
2433
  var PROJECT_MEMORY_MAX_CHARS = 8e3;
2184
2434
  function readProjectMemory(rootDir) {
2185
- const path = join3(rootDir, PROJECT_MEMORY_FILE);
2435
+ const path = join4(rootDir, PROJECT_MEMORY_FILE);
2186
2436
  if (!existsSync3(path)) return null;
2187
2437
  let raw;
2188
2438
  try {
2189
- raw = readFileSync3(path, "utf8");
2439
+ raw = readFileSync4(path, "utf8");
2190
2440
  } catch {
2191
2441
  return null;
2192
2442
  }
@@ -2224,18 +2474,18 @@ import { createHash as createHash2 } from "crypto";
2224
2474
  import {
2225
2475
  existsSync as existsSync5,
2226
2476
  mkdirSync as mkdirSync2,
2227
- readFileSync as readFileSync5,
2477
+ readFileSync as readFileSync6,
2228
2478
  readdirSync as readdirSync3,
2229
2479
  unlinkSync as unlinkSync2,
2230
2480
  writeFileSync as writeFileSync2
2231
2481
  } from "fs";
2232
2482
  import { homedir as homedir4 } from "os";
2233
- import { join as join5, resolve as resolve2 } from "path";
2483
+ import { join as join6, resolve as resolve2 } from "path";
2234
2484
 
2235
2485
  // src/skills.ts
2236
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2486
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2237
2487
  import { homedir as homedir3 } from "os";
2238
- import { join as join4, resolve } from "path";
2488
+ import { join as join5, resolve } from "path";
2239
2489
  var SKILLS_DIRNAME = "skills";
2240
2490
  var SKILL_FILE = "SKILL.md";
2241
2491
  var SKILLS_INDEX_MAX_CHARS = 4e3;
@@ -2282,11 +2532,11 @@ var SkillStore = class {
2282
2532
  const out = [];
2283
2533
  if (this.projectRoot) {
2284
2534
  out.push({
2285
- dir: join4(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2535
+ dir: join5(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2286
2536
  scope: "project"
2287
2537
  });
2288
2538
  }
2289
- out.push({ dir: join4(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2539
+ out.push({ dir: join5(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2290
2540
  return out;
2291
2541
  }
2292
2542
  /**
@@ -2322,11 +2572,11 @@ var SkillStore = class {
2322
2572
  if (!isValidSkillName(name)) return null;
2323
2573
  for (const { dir, scope } of this.roots()) {
2324
2574
  if (!existsSync4(dir)) continue;
2325
- const dirCandidate = join4(dir, name, SKILL_FILE);
2575
+ const dirCandidate = join5(dir, name, SKILL_FILE);
2326
2576
  if (existsSync4(dirCandidate) && statSync2(dirCandidate).isFile()) {
2327
2577
  return this.parse(dirCandidate, name, scope);
2328
2578
  }
2329
- const flatCandidate = join4(dir, `${name}.md`);
2579
+ const flatCandidate = join5(dir, `${name}.md`);
2330
2580
  if (existsSync4(flatCandidate) && statSync2(flatCandidate).isFile()) {
2331
2581
  return this.parse(flatCandidate, name, scope);
2332
2582
  }
@@ -2341,21 +2591,21 @@ var SkillStore = class {
2341
2591
  readEntry(dir, scope, entry) {
2342
2592
  if (entry.isDirectory()) {
2343
2593
  if (!isValidSkillName(entry.name)) return null;
2344
- const file = join4(dir, entry.name, SKILL_FILE);
2594
+ const file = join5(dir, entry.name, SKILL_FILE);
2345
2595
  if (!existsSync4(file)) return null;
2346
2596
  return this.parse(file, entry.name, scope);
2347
2597
  }
2348
2598
  if (entry.isFile() && entry.name.endsWith(".md")) {
2349
2599
  const stem = entry.name.slice(0, -3);
2350
2600
  if (!isValidSkillName(stem)) return null;
2351
- return this.parse(join4(dir, entry.name), stem, scope);
2601
+ return this.parse(join5(dir, entry.name), stem, scope);
2352
2602
  }
2353
2603
  return null;
2354
2604
  }
2355
2605
  parse(path, stem, scope) {
2356
2606
  let raw;
2357
2607
  try {
2358
- raw = readFileSync4(path, "utf8");
2608
+ raw = readFileSync5(path, "utf8");
2359
2609
  } catch {
2360
2610
  return null;
2361
2611
  }
@@ -2487,12 +2737,12 @@ function projectHash(rootDir) {
2487
2737
  }
2488
2738
  function scopeDir(opts) {
2489
2739
  if (opts.scope === "global") {
2490
- return join5(opts.homeDir, USER_MEMORY_DIR, "global");
2740
+ return join6(opts.homeDir, USER_MEMORY_DIR, "global");
2491
2741
  }
2492
2742
  if (!opts.projectRoot) {
2493
2743
  throw new Error("scope=project requires a projectRoot on MemoryStore");
2494
2744
  }
2495
- return join5(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2745
+ return join6(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2496
2746
  }
2497
2747
  function ensureDir(p) {
2498
2748
  if (!existsSync5(p)) mkdirSync2(p, { recursive: true });
@@ -2540,7 +2790,7 @@ var MemoryStore = class {
2540
2790
  homeDir;
2541
2791
  projectRoot;
2542
2792
  constructor(opts = {}) {
2543
- this.homeDir = opts.homeDir ?? join5(homedir4(), ".reasonix");
2793
+ this.homeDir = opts.homeDir ?? join6(homedir4(), ".reasonix");
2544
2794
  this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
2545
2795
  }
2546
2796
  /** Directory this store writes `scope` files into, creating it if needed. */
@@ -2551,7 +2801,7 @@ var MemoryStore = class {
2551
2801
  }
2552
2802
  /** Absolute path to a memory file (no existence check). */
2553
2803
  pathFor(scope, name) {
2554
- return join5(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2804
+ return join6(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2555
2805
  }
2556
2806
  /** True iff this store is configured with a project scope available. */
2557
2807
  hasProjectScope() {
@@ -2563,14 +2813,14 @@ var MemoryStore = class {
2563
2813
  */
2564
2814
  loadIndex(scope) {
2565
2815
  if (scope === "project" && !this.projectRoot) return null;
2566
- const file = join5(
2816
+ const file = join6(
2567
2817
  scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
2568
2818
  MEMORY_INDEX_FILE
2569
2819
  );
2570
2820
  if (!existsSync5(file)) return null;
2571
2821
  let raw;
2572
2822
  try {
2573
- raw = readFileSync5(file, "utf8");
2823
+ raw = readFileSync6(file, "utf8");
2574
2824
  } catch {
2575
2825
  return null;
2576
2826
  }
@@ -2588,7 +2838,7 @@ var MemoryStore = class {
2588
2838
  if (!existsSync5(file)) {
2589
2839
  throw new Error(`memory not found: scope=${scope} name=${name}`);
2590
2840
  }
2591
- const raw = readFileSync5(file, "utf8");
2841
+ const raw = readFileSync6(file, "utf8");
2592
2842
  const { data, body } = parseFrontmatter2(raw);
2593
2843
  return {
2594
2844
  name: data.name ?? name,
@@ -2650,7 +2900,7 @@ var MemoryStore = class {
2650
2900
  createdAt: todayIso()
2651
2901
  };
2652
2902
  const dir = this.dir(input.scope);
2653
- const file = join5(dir, `${name}.md`);
2903
+ const file = join6(dir, `${name}.md`);
2654
2904
  const content = `${formatFrontmatter(entry)}${body}
2655
2905
  `;
2656
2906
  writeFileSync2(file, content, "utf8");
@@ -2684,7 +2934,7 @@ var MemoryStore = class {
2684
2934
  return;
2685
2935
  }
2686
2936
  const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
2687
- const indexPath = join5(dir, MEMORY_INDEX_FILE);
2937
+ const indexPath = join6(dir, MEMORY_INDEX_FILE);
2688
2938
  if (mdFiles.length === 0) {
2689
2939
  if (existsSync5(indexPath)) unlinkSync2(indexPath);
2690
2940
  return;
@@ -3717,6 +3967,50 @@ function tokenizeCommand(cmd) {
3717
3967
  if (cur.length > 0) out.push(cur);
3718
3968
  return out;
3719
3969
  }
3970
+ function detectShellOperator(cmd) {
3971
+ const opPrefix = /^(?:2>&1|&>|\|{1,2}|&{1,2}|2>{1,2}|>{1,2}|<{1,2})/;
3972
+ let cur = "";
3973
+ let curQuoted = false;
3974
+ let quote = null;
3975
+ const check = () => {
3976
+ if (cur.length === 0 && !curQuoted) return null;
3977
+ if (!curQuoted) {
3978
+ const m = opPrefix.exec(cur);
3979
+ if (m) return m[0] ?? null;
3980
+ }
3981
+ return null;
3982
+ };
3983
+ for (let i = 0; i < cmd.length; i++) {
3984
+ const ch = cmd[i];
3985
+ if (quote) {
3986
+ if (ch === quote) {
3987
+ quote = null;
3988
+ } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
3989
+ cur += cmd[++i];
3990
+ curQuoted = true;
3991
+ } else {
3992
+ cur += ch;
3993
+ curQuoted = true;
3994
+ }
3995
+ continue;
3996
+ }
3997
+ if (ch === '"' || ch === "'") {
3998
+ quote = ch;
3999
+ curQuoted = true;
4000
+ continue;
4001
+ }
4002
+ if (ch === " " || ch === " ") {
4003
+ const op = check();
4004
+ if (op) return op;
4005
+ cur = "";
4006
+ curQuoted = false;
4007
+ continue;
4008
+ }
4009
+ cur += ch;
4010
+ }
4011
+ if (quote) return null;
4012
+ return check();
4013
+ }
3720
4014
  function isAllowed(cmd, extra = []) {
3721
4015
  const normalized = cmd.trim().replace(/\s+/g, " ");
3722
4016
  const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
@@ -3729,6 +4023,12 @@ function isAllowed(cmd, extra = []) {
3729
4023
  async function runCommand(cmd, opts) {
3730
4024
  const argv = tokenizeCommand(cmd);
3731
4025
  if (argv.length === 0) throw new Error("run_command: empty command");
4026
+ const operator = detectShellOperator(cmd);
4027
+ if (operator !== null) {
4028
+ throw new Error(
4029
+ `run_command: shell operator "${operator}" is not supported \u2014 this tool spawns one process, no shell expansion. Split into separate run_command calls and combine the output in your reasoning (e.g. instead of \`grep foo *.ts | wc -l\`, call \`grep -c foo *.ts\` or two separate commands). To pass "${operator}" as a literal argument, wrap it in quotes.`
4030
+ );
4031
+ }
3732
4032
  const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
3733
4033
  const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
3734
4034
  const spawnOpts = {
@@ -3906,7 +4206,7 @@ function registerShellTools(registry, opts) {
3906
4206
  properties: {
3907
4207
  command: {
3908
4208
  type: "string",
3909
- description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
4209
+ description: 'Full command line. Tokenized with POSIX-ish quoting; no shell expansion. Pipes (`|`), redirects (`>`, `<`, `2>`), and `&&`/`||` chaining are rejected with an error \u2014 split into separate calls instead. To pass an operator character as a literal argument (e.g. a regex), wrap it in quotes: `grep "a|b" file.txt`.'
3910
4210
  },
3911
4211
  timeoutSec: {
3912
4212
  type: "integer",
@@ -4125,12 +4425,12 @@ ${i + 1}. ${r.title}`);
4125
4425
  }
4126
4426
 
4127
4427
  // src/env.ts
4128
- import { readFileSync as readFileSync6 } from "fs";
4428
+ import { readFileSync as readFileSync7 } from "fs";
4129
4429
  import { resolve as resolve5 } from "path";
4130
4430
  function loadDotenv(path = ".env") {
4131
4431
  let raw;
4132
4432
  try {
4133
- raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
4433
+ raw = readFileSync7(resolve5(process.cwd(), path), "utf8");
4134
4434
  } catch {
4135
4435
  return;
4136
4436
  }
@@ -4149,7 +4449,7 @@ function loadDotenv(path = ".env") {
4149
4449
  }
4150
4450
 
4151
4451
  // src/transcript.ts
4152
- import { createWriteStream, readFileSync as readFileSync7 } from "fs";
4452
+ import { createWriteStream, readFileSync as readFileSync8 } from "fs";
4153
4453
  function recordFromLoopEvent(ev, extra) {
4154
4454
  const rec = {
4155
4455
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4200,7 +4500,7 @@ function openTranscriptFile(path, meta) {
4200
4500
  return stream;
4201
4501
  }
4202
4502
  function readTranscript(path) {
4203
- const raw = readFileSync7(path, "utf8");
4503
+ const raw = readFileSync8(path, "utf8");
4204
4504
  return parseTranscript(raw);
4205
4505
  }
4206
4506
  function isPlanStateEmptyShape(s) {
@@ -5255,8 +5555,8 @@ async function trySection(load) {
5255
5555
  }
5256
5556
 
5257
5557
  // src/code/edit-blocks.ts
5258
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
5259
- import { dirname as dirname3, resolve as resolve6 } from "path";
5558
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
5559
+ import { dirname as dirname4, resolve as resolve6 } from "path";
5260
5560
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
5261
5561
  function parseEditBlocks(text) {
5262
5562
  const out = [];
@@ -5294,11 +5594,11 @@ function applyEditBlock(block, rootDir) {
5294
5594
  message: "file does not exist; to create it, use an empty SEARCH block"
5295
5595
  };
5296
5596
  }
5297
- mkdirSync3(dirname3(absTarget), { recursive: true });
5597
+ mkdirSync3(dirname4(absTarget), { recursive: true });
5298
5598
  writeFileSync3(absTarget, block.replace, "utf8");
5299
5599
  return { path: block.path, status: "created" };
5300
5600
  }
5301
- const content = readFileSync8(absTarget, "utf8");
5601
+ const content = readFileSync9(absTarget, "utf8");
5302
5602
  if (searchEmpty) {
5303
5603
  return {
5304
5604
  path: block.path,
@@ -5337,7 +5637,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
5337
5637
  continue;
5338
5638
  }
5339
5639
  try {
5340
- snapshots.push({ path: b.path, prevContent: readFileSync8(abs, "utf8") });
5640
+ snapshots.push({ path: b.path, prevContent: readFileSync9(abs, "utf8") });
5341
5641
  } catch {
5342
5642
  snapshots.push({ path: b.path, prevContent: null });
5343
5643
  }
@@ -5380,8 +5680,8 @@ function sep() {
5380
5680
  }
5381
5681
 
5382
5682
  // src/code/prompt.ts
5383
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5384
- import { join as join7 } from "path";
5683
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
5684
+ import { join as join8 } from "path";
5385
5685
  var CODE_SYSTEM_PROMPT = `You are Reasonix Code, a coding assistant. You have filesystem tools (read_file, write_file, list_directory, search_files, etc.) rooted at the user's working directory.
5386
5686
 
5387
5687
  # Cite or shut up \u2014 non-negotiable
@@ -5502,11 +5802,11 @@ Two different rules depending on which tool:
5502
5802
  `;
5503
5803
  function codeSystemPrompt(rootDir) {
5504
5804
  const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
5505
- const gitignorePath = join7(rootDir, ".gitignore");
5805
+ const gitignorePath = join8(rootDir, ".gitignore");
5506
5806
  if (!existsSync8(gitignorePath)) return withMemory;
5507
5807
  let content;
5508
5808
  try {
5509
- content = readFileSync9(gitignorePath, "utf8");
5809
+ content = readFileSync10(gitignorePath, "utf8");
5510
5810
  } catch {
5511
5811
  return withMemory;
5512
5812
  }
@@ -5526,15 +5826,15 @@ ${truncated}
5526
5826
  }
5527
5827
 
5528
5828
  // src/config.ts
5529
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
5829
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
5530
5830
  import { homedir as homedir5 } from "os";
5531
- import { dirname as dirname4, join as join8 } from "path";
5831
+ import { dirname as dirname5, join as join9 } from "path";
5532
5832
  function defaultConfigPath() {
5533
- return join8(homedir5(), ".reasonix", "config.json");
5833
+ return join9(homedir5(), ".reasonix", "config.json");
5534
5834
  }
5535
5835
  function readConfig(path = defaultConfigPath()) {
5536
5836
  try {
5537
- const raw = readFileSync10(path, "utf8");
5837
+ const raw = readFileSync11(path, "utf8");
5538
5838
  const parsed = JSON.parse(raw);
5539
5839
  if (parsed && typeof parsed === "object") return parsed;
5540
5840
  } catch {
@@ -5542,7 +5842,7 @@ function readConfig(path = defaultConfigPath()) {
5542
5842
  return {};
5543
5843
  }
5544
5844
  function writeConfig(cfg, path = defaultConfigPath()) {
5545
- mkdirSync4(dirname4(path), { recursive: true });
5845
+ mkdirSync4(dirname5(path), { recursive: true });
5546
5846
  writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
5547
5847
  try {
5548
5848
  chmodSync2(path, 384);
@@ -5569,25 +5869,25 @@ function redactKey(key) {
5569
5869
  }
5570
5870
 
5571
5871
  // src/version.ts
5572
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
5872
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
5573
5873
  import { homedir as homedir6 } from "os";
5574
- import { dirname as dirname5, join as join9 } from "path";
5575
- import { fileURLToPath } from "url";
5874
+ import { dirname as dirname6, join as join10 } from "path";
5875
+ import { fileURLToPath as fileURLToPath2 } from "url";
5576
5876
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
5577
5877
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5578
5878
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
5579
5879
  function readPackageVersion() {
5580
5880
  try {
5581
- let dir = dirname5(fileURLToPath(import.meta.url));
5881
+ let dir = dirname6(fileURLToPath2(import.meta.url));
5582
5882
  for (let i = 0; i < 6; i++) {
5583
- const p = join9(dir, "package.json");
5883
+ const p = join10(dir, "package.json");
5584
5884
  if (existsSync9(p)) {
5585
- const pkg = JSON.parse(readFileSync11(p, "utf8"));
5885
+ const pkg = JSON.parse(readFileSync12(p, "utf8"));
5586
5886
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
5587
5887
  return pkg.version;
5588
5888
  }
5589
5889
  }
5590
- const parent = dirname5(dir);
5890
+ const parent = dirname6(dir);
5591
5891
  if (parent === dir) break;
5592
5892
  dir = parent;
5593
5893
  }
@@ -5597,11 +5897,11 @@ function readPackageVersion() {
5597
5897
  }
5598
5898
  var VERSION = readPackageVersion();
5599
5899
  function cachePath(homeDirOverride) {
5600
- return join9(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
5900
+ return join10(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
5601
5901
  }
5602
5902
  function readCache(homeDirOverride) {
5603
5903
  try {
5604
- const raw = readFileSync11(cachePath(homeDirOverride), "utf8");
5904
+ const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
5605
5905
  const parsed = JSON.parse(raw);
5606
5906
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
5607
5907
  return parsed;
@@ -5613,7 +5913,7 @@ function readCache(homeDirOverride) {
5613
5913
  function writeCache(entry, homeDirOverride) {
5614
5914
  try {
5615
5915
  const p = cachePath(homeDirOverride);
5616
- mkdirSync5(dirname5(p), { recursive: true });
5916
+ mkdirSync5(dirname6(p), { recursive: true });
5617
5917
  writeFileSync5(p, JSON.stringify(entry), "utf8");
5618
5918
  } catch {
5619
5919
  }
@@ -5621,8 +5921,8 @@ function writeCache(entry, homeDirOverride) {
5621
5921
  async function getLatestVersion(opts = {}) {
5622
5922
  const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
5623
5923
  if (!opts.force) {
5624
- const cached = readCache(opts.homeDir);
5625
- if (cached && Date.now() - cached.checkedAt < ttl) return cached.version;
5924
+ const cached2 = readCache(opts.homeDir);
5925
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
5626
5926
  }
5627
5927
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
5628
5928
  if (!fetchImpl) return null;
@@ -5670,11 +5970,11 @@ function isNpxInstall() {
5670
5970
  }
5671
5971
 
5672
5972
  // src/usage.ts
5673
- import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
5973
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
5674
5974
  import { homedir as homedir7 } from "os";
5675
- import { dirname as dirname6, join as join10 } from "path";
5975
+ import { dirname as dirname7, join as join11 } from "path";
5676
5976
  function defaultUsageLogPath(homeDirOverride) {
5677
- return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5977
+ return join11(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5678
5978
  }
5679
5979
  function appendUsage(input) {
5680
5980
  const record = {
@@ -5690,7 +5990,7 @@ function appendUsage(input) {
5690
5990
  };
5691
5991
  const path = input.path ?? defaultUsageLogPath();
5692
5992
  try {
5693
- mkdirSync6(dirname6(path), { recursive: true });
5993
+ mkdirSync6(dirname7(path), { recursive: true });
5694
5994
  appendFileSync2(path, `${JSON.stringify(record)}
5695
5995
  `, "utf8");
5696
5996
  } catch {
@@ -5701,7 +6001,7 @@ function readUsageLog(path = defaultUsageLogPath()) {
5701
6001
  if (!existsSync10(path)) return [];
5702
6002
  let raw;
5703
6003
  try {
5704
- raw = readFileSync12(path, "utf8");
6004
+ raw = readFileSync13(path, "utf8");
5705
6005
  } catch {
5706
6006
  return [];
5707
6007
  }
@@ -5799,6 +6099,7 @@ export {
5799
6099
  CODE_SYSTEM_PROMPT,
5800
6100
  CacheFirstLoop,
5801
6101
  DEFAULT_MAX_RESULT_CHARS,
6102
+ DEFAULT_MAX_RESULT_TOKENS,
5802
6103
  DeepSeekClient,
5803
6104
  HOOK_EVENTS,
5804
6105
  HOOK_SETTINGS_DIRNAME,
@@ -5848,6 +6149,7 @@ export {
5848
6149
  defaultSelector,
5849
6150
  defaultUsageLogPath,
5850
6151
  deleteSession,
6152
+ detectShellOperator,
5851
6153
  diffTranscripts,
5852
6154
  emptyPlanState,
5853
6155
  fetchWithRetry,
@@ -5922,6 +6224,7 @@ export {
5922
6224
  stripHallucinatedToolMarkup,
5923
6225
  tokenizeCommand,
5924
6226
  truncateForModel,
6227
+ truncateForModelByTokens,
5925
6228
  webFetch,
5926
6229
  webSearch,
5927
6230
  withUtf8Codepage,