reasonix 0.4.28 → 0.5.2

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
@@ -154,6 +154,28 @@ var DeepSeekClient = class {
154
154
  return null;
155
155
  }
156
156
  }
157
+ /**
158
+ * Fetch the model catalog DeepSeek currently exposes. Today this is
159
+ * `deepseek-chat` (V3) and `deepseek-reasoner` (R1), but querying is
160
+ * the only way to learn about new ones without a Reasonix release.
161
+ * Returns null on any network/auth failure so callers can degrade
162
+ * gracefully — e.g. `/models` falls back to the hardcoded hint.
163
+ */
164
+ async listModels(opts = {}) {
165
+ try {
166
+ const resp = await this._fetch(`${this.baseUrl}/models`, {
167
+ method: "GET",
168
+ headers: { Authorization: `Bearer ${this.apiKey}` },
169
+ signal: opts.signal
170
+ });
171
+ if (!resp.ok) return null;
172
+ const data = await resp.json();
173
+ if (!data || !Array.isArray(data.data)) return null;
174
+ return data;
175
+ } catch {
176
+ return null;
177
+ }
178
+ }
157
179
  async chat(opts) {
158
180
  const ctrl = new AbortController();
159
181
  const timer = setTimeout(() => ctrl.abort(), this.timeoutMs);
@@ -595,6 +617,170 @@ async function runHooks(opts) {
595
617
  return { event, outcomes, blocked };
596
618
  }
597
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
+
598
784
  // src/repair/flatten.ts
599
785
  function analyzeSchema(schema) {
600
786
  if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
@@ -744,7 +930,15 @@ var ToolRegistry = class {
744
930
  }
745
931
  try {
746
932
  const result = await tool.fn(args, { signal: opts.signal });
747
- return typeof result === "string" ? result : JSON.stringify(result);
933
+ const str = typeof result === "string" ? result : JSON.stringify(result);
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;
748
942
  } catch (err) {
749
943
  const e = err;
750
944
  if (typeof e.toToolResult === "function") {
@@ -778,6 +972,7 @@ function hasDotKey(obj) {
778
972
 
779
973
  // src/mcp/registry.ts
780
974
  var DEFAULT_MAX_RESULT_CHARS = 32e3;
975
+ var DEFAULT_MAX_RESULT_TOKENS = 8e3;
781
976
  async function bridgeMcpTools(client, opts = {}) {
782
977
  const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
783
978
  const prefix = opts.namePrefix ?? "";
@@ -834,6 +1029,61 @@ function truncateForModel(s, maxChars) {
834
1029
 
835
1030
  ${tail}`;
836
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
+ }
837
1087
  function blockToString(block) {
838
1088
  if (block.type === "text") return block.text;
839
1089
  if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
@@ -1219,19 +1469,19 @@ import {
1219
1469
  chmodSync,
1220
1470
  existsSync as existsSync2,
1221
1471
  mkdirSync,
1222
- readFileSync as readFileSync2,
1472
+ readFileSync as readFileSync3,
1223
1473
  readdirSync,
1224
1474
  statSync,
1225
1475
  unlinkSync,
1226
1476
  writeFileSync
1227
1477
  } from "fs";
1228
1478
  import { homedir as homedir2 } from "os";
1229
- import { dirname, join as join2 } from "path";
1479
+ import { dirname as dirname2, join as join3 } from "path";
1230
1480
  function sessionsDir() {
1231
- return join2(homedir2(), ".reasonix", "sessions");
1481
+ return join3(homedir2(), ".reasonix", "sessions");
1232
1482
  }
1233
1483
  function sessionPath(name) {
1234
- return join2(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1484
+ return join3(sessionsDir(), `${sanitizeName(name)}.jsonl`);
1235
1485
  }
1236
1486
  function sanitizeName(name) {
1237
1487
  const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
@@ -1241,7 +1491,7 @@ function loadSessionMessages(name) {
1241
1491
  const path = sessionPath(name);
1242
1492
  if (!existsSync2(path)) return [];
1243
1493
  try {
1244
- const raw = readFileSync2(path, "utf8");
1494
+ const raw = readFileSync3(path, "utf8");
1245
1495
  const out = [];
1246
1496
  for (const line of raw.split(/\r?\n/)) {
1247
1497
  const trimmed = line.trim();
@@ -1259,7 +1509,7 @@ function loadSessionMessages(name) {
1259
1509
  }
1260
1510
  function appendSessionMessage(name, message) {
1261
1511
  const path = sessionPath(name);
1262
- mkdirSync(dirname(path), { recursive: true });
1512
+ mkdirSync(dirname2(path), { recursive: true });
1263
1513
  appendFileSync(path, `${JSON.stringify(message)}
1264
1514
  `, "utf8");
1265
1515
  try {
@@ -1273,7 +1523,7 @@ function listSessions() {
1273
1523
  try {
1274
1524
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1275
1525
  return files.map((file) => {
1276
- const path = join2(dir, file);
1526
+ const path = join3(dir, file);
1277
1527
  const stat = statSync(path);
1278
1528
  const name = file.replace(/\.jsonl$/, "");
1279
1529
  const messageCount = countLines(path);
@@ -1294,7 +1544,7 @@ function deleteSession(name) {
1294
1544
  }
1295
1545
  function rewriteSession(name, messages) {
1296
1546
  const path = sessionPath(name);
1297
- mkdirSync(dirname(path), { recursive: true });
1547
+ mkdirSync(dirname2(path), { recursive: true });
1298
1548
  const body = messages.map((m) => JSON.stringify(m)).join("\n");
1299
1549
  writeFileSync(path, body ? `${body}
1300
1550
  ` : "", "utf8");
@@ -1305,7 +1555,7 @@ function rewriteSession(name, messages) {
1305
1555
  }
1306
1556
  function countLines(path) {
1307
1557
  try {
1308
- const raw = readFileSync2(path, "utf8");
1558
+ const raw = readFileSync3(path, "utf8");
1309
1559
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1310
1560
  } catch {
1311
1561
  return 0;
@@ -1314,8 +1564,8 @@ function countLines(path) {
1314
1564
 
1315
1565
  // src/telemetry.ts
1316
1566
  var DEEPSEEK_PRICING = {
1317
- "deepseek-chat": { inputCacheHit: 0.07, inputCacheMiss: 0.27, output: 1.1 },
1318
- "deepseek-reasoner": { inputCacheHit: 0.14, inputCacheMiss: 0.55, output: 2.19 }
1567
+ "deepseek-chat": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 },
1568
+ "deepseek-reasoner": { inputCacheHit: 0.028, inputCacheMiss: 0.28, output: 0.42 }
1319
1569
  };
1320
1570
  var CLAUDE_SONNET_PRICING = { input: 3, output: 15 };
1321
1571
  var DEEPSEEK_CONTEXT_TOKENS = {
@@ -1870,6 +2120,24 @@ var CacheFirstLoop = class {
1870
2120
  return;
1871
2121
  }
1872
2122
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
2123
+ if (usage) {
2124
+ const ratio = usage.promptTokens / ctxMax;
2125
+ if (ratio > 0.6 && ratio <= 0.8) {
2126
+ const before = usage.promptTokens;
2127
+ const soft = this.compact(16e3);
2128
+ if (soft.healedCount > 0) {
2129
+ const approxSaved = Math.round(soft.charsSaved / 4);
2130
+ const after = Math.max(0, before - approxSaved);
2131
+ yield {
2132
+ turn: this._turn,
2133
+ role: "warning",
2134
+ content: `context ${before.toLocaleString()}/${ctxMax.toLocaleString()} (${Math.round(
2135
+ ratio * 100
2136
+ )}%) \u2014 proactively compacted ${soft.healedCount} tool result(s) to 16k, saved ~${approxSaved.toLocaleString()} tokens (now ~${after.toLocaleString()}). Staying ahead of the 80% guard.`
2137
+ };
2138
+ }
2139
+ }
2140
+ }
1873
2141
  if (usage && usage.promptTokens / ctxMax > 0.8) {
1874
2142
  const before = usage.promptTokens;
1875
2143
  const compactResult = this.compact(4e3);
@@ -1932,7 +2200,10 @@ var CacheFirstLoop = class {
1932
2200
  result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
1933
2201
  ${reason}`;
1934
2202
  } else {
1935
- result = await this.tools.dispatch(name, args, { signal });
2203
+ result = await this.tools.dispatch(name, args, {
2204
+ signal,
2205
+ maxResultTokens: DEFAULT_MAX_RESULT_TOKENS
2206
+ });
1936
2207
  const postReport = await runHooks({
1937
2208
  hooks: this.hooks,
1938
2209
  payload: {
@@ -2133,16 +2404,16 @@ function formatLoopError(err) {
2133
2404
  }
2134
2405
 
2135
2406
  // src/project-memory.ts
2136
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
2137
- import { join as join3 } from "path";
2407
+ import { existsSync as existsSync3, readFileSync as readFileSync4 } from "fs";
2408
+ import { join as join4 } from "path";
2138
2409
  var PROJECT_MEMORY_FILE = "REASONIX.md";
2139
2410
  var PROJECT_MEMORY_MAX_CHARS = 8e3;
2140
2411
  function readProjectMemory(rootDir) {
2141
- const path = join3(rootDir, PROJECT_MEMORY_FILE);
2412
+ const path = join4(rootDir, PROJECT_MEMORY_FILE);
2142
2413
  if (!existsSync3(path)) return null;
2143
2414
  let raw;
2144
2415
  try {
2145
- raw = readFileSync3(path, "utf8");
2416
+ raw = readFileSync4(path, "utf8");
2146
2417
  } catch {
2147
2418
  return null;
2148
2419
  }
@@ -2180,18 +2451,18 @@ import { createHash as createHash2 } from "crypto";
2180
2451
  import {
2181
2452
  existsSync as existsSync5,
2182
2453
  mkdirSync as mkdirSync2,
2183
- readFileSync as readFileSync5,
2454
+ readFileSync as readFileSync6,
2184
2455
  readdirSync as readdirSync3,
2185
2456
  unlinkSync as unlinkSync2,
2186
2457
  writeFileSync as writeFileSync2
2187
2458
  } from "fs";
2188
2459
  import { homedir as homedir4 } from "os";
2189
- import { join as join5, resolve as resolve2 } from "path";
2460
+ import { join as join6, resolve as resolve2 } from "path";
2190
2461
 
2191
2462
  // src/skills.ts
2192
- import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2463
+ import { existsSync as existsSync4, readFileSync as readFileSync5, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
2193
2464
  import { homedir as homedir3 } from "os";
2194
- import { join as join4, resolve } from "path";
2465
+ import { join as join5, resolve } from "path";
2195
2466
  var SKILLS_DIRNAME = "skills";
2196
2467
  var SKILL_FILE = "SKILL.md";
2197
2468
  var SKILLS_INDEX_MAX_CHARS = 4e3;
@@ -2238,11 +2509,11 @@ var SkillStore = class {
2238
2509
  const out = [];
2239
2510
  if (this.projectRoot) {
2240
2511
  out.push({
2241
- dir: join4(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2512
+ dir: join5(this.projectRoot, ".reasonix", SKILLS_DIRNAME),
2242
2513
  scope: "project"
2243
2514
  });
2244
2515
  }
2245
- out.push({ dir: join4(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2516
+ out.push({ dir: join5(this.homeDir, ".reasonix", SKILLS_DIRNAME), scope: "global" });
2246
2517
  return out;
2247
2518
  }
2248
2519
  /**
@@ -2278,11 +2549,11 @@ var SkillStore = class {
2278
2549
  if (!isValidSkillName(name)) return null;
2279
2550
  for (const { dir, scope } of this.roots()) {
2280
2551
  if (!existsSync4(dir)) continue;
2281
- const dirCandidate = join4(dir, name, SKILL_FILE);
2552
+ const dirCandidate = join5(dir, name, SKILL_FILE);
2282
2553
  if (existsSync4(dirCandidate) && statSync2(dirCandidate).isFile()) {
2283
2554
  return this.parse(dirCandidate, name, scope);
2284
2555
  }
2285
- const flatCandidate = join4(dir, `${name}.md`);
2556
+ const flatCandidate = join5(dir, `${name}.md`);
2286
2557
  if (existsSync4(flatCandidate) && statSync2(flatCandidate).isFile()) {
2287
2558
  return this.parse(flatCandidate, name, scope);
2288
2559
  }
@@ -2297,21 +2568,21 @@ var SkillStore = class {
2297
2568
  readEntry(dir, scope, entry) {
2298
2569
  if (entry.isDirectory()) {
2299
2570
  if (!isValidSkillName(entry.name)) return null;
2300
- const file = join4(dir, entry.name, SKILL_FILE);
2571
+ const file = join5(dir, entry.name, SKILL_FILE);
2301
2572
  if (!existsSync4(file)) return null;
2302
2573
  return this.parse(file, entry.name, scope);
2303
2574
  }
2304
2575
  if (entry.isFile() && entry.name.endsWith(".md")) {
2305
2576
  const stem = entry.name.slice(0, -3);
2306
2577
  if (!isValidSkillName(stem)) return null;
2307
- return this.parse(join4(dir, entry.name), stem, scope);
2578
+ return this.parse(join5(dir, entry.name), stem, scope);
2308
2579
  }
2309
2580
  return null;
2310
2581
  }
2311
2582
  parse(path, stem, scope) {
2312
2583
  let raw;
2313
2584
  try {
2314
- raw = readFileSync4(path, "utf8");
2585
+ raw = readFileSync5(path, "utf8");
2315
2586
  } catch {
2316
2587
  return null;
2317
2588
  }
@@ -2443,12 +2714,12 @@ function projectHash(rootDir) {
2443
2714
  }
2444
2715
  function scopeDir(opts) {
2445
2716
  if (opts.scope === "global") {
2446
- return join5(opts.homeDir, USER_MEMORY_DIR, "global");
2717
+ return join6(opts.homeDir, USER_MEMORY_DIR, "global");
2447
2718
  }
2448
2719
  if (!opts.projectRoot) {
2449
2720
  throw new Error("scope=project requires a projectRoot on MemoryStore");
2450
2721
  }
2451
- return join5(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2722
+ return join6(opts.homeDir, USER_MEMORY_DIR, projectHash(opts.projectRoot));
2452
2723
  }
2453
2724
  function ensureDir(p) {
2454
2725
  if (!existsSync5(p)) mkdirSync2(p, { recursive: true });
@@ -2496,7 +2767,7 @@ var MemoryStore = class {
2496
2767
  homeDir;
2497
2768
  projectRoot;
2498
2769
  constructor(opts = {}) {
2499
- this.homeDir = opts.homeDir ?? join5(homedir4(), ".reasonix");
2770
+ this.homeDir = opts.homeDir ?? join6(homedir4(), ".reasonix");
2500
2771
  this.projectRoot = opts.projectRoot ? resolve2(opts.projectRoot) : void 0;
2501
2772
  }
2502
2773
  /** Directory this store writes `scope` files into, creating it if needed. */
@@ -2507,7 +2778,7 @@ var MemoryStore = class {
2507
2778
  }
2508
2779
  /** Absolute path to a memory file (no existence check). */
2509
2780
  pathFor(scope, name) {
2510
- return join5(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2781
+ return join6(this.dir(scope), `${sanitizeMemoryName(name)}.md`);
2511
2782
  }
2512
2783
  /** True iff this store is configured with a project scope available. */
2513
2784
  hasProjectScope() {
@@ -2519,14 +2790,14 @@ var MemoryStore = class {
2519
2790
  */
2520
2791
  loadIndex(scope) {
2521
2792
  if (scope === "project" && !this.projectRoot) return null;
2522
- const file = join5(
2793
+ const file = join6(
2523
2794
  scopeDir({ homeDir: this.homeDir, scope, projectRoot: this.projectRoot }),
2524
2795
  MEMORY_INDEX_FILE
2525
2796
  );
2526
2797
  if (!existsSync5(file)) return null;
2527
2798
  let raw;
2528
2799
  try {
2529
- raw = readFileSync5(file, "utf8");
2800
+ raw = readFileSync6(file, "utf8");
2530
2801
  } catch {
2531
2802
  return null;
2532
2803
  }
@@ -2544,7 +2815,7 @@ var MemoryStore = class {
2544
2815
  if (!existsSync5(file)) {
2545
2816
  throw new Error(`memory not found: scope=${scope} name=${name}`);
2546
2817
  }
2547
- const raw = readFileSync5(file, "utf8");
2818
+ const raw = readFileSync6(file, "utf8");
2548
2819
  const { data, body } = parseFrontmatter2(raw);
2549
2820
  return {
2550
2821
  name: data.name ?? name,
@@ -2606,7 +2877,7 @@ var MemoryStore = class {
2606
2877
  createdAt: todayIso()
2607
2878
  };
2608
2879
  const dir = this.dir(input.scope);
2609
- const file = join5(dir, `${name}.md`);
2880
+ const file = join6(dir, `${name}.md`);
2610
2881
  const content = `${formatFrontmatter(entry)}${body}
2611
2882
  `;
2612
2883
  writeFileSync2(file, content, "utf8");
@@ -2640,7 +2911,7 @@ var MemoryStore = class {
2640
2911
  return;
2641
2912
  }
2642
2913
  const mdFiles = files.filter((f) => f !== MEMORY_INDEX_FILE && f.endsWith(".md")).sort((a, b) => a.localeCompare(b));
2643
- const indexPath = join5(dir, MEMORY_INDEX_FILE);
2914
+ const indexPath = join6(dir, MEMORY_INDEX_FILE);
2644
2915
  if (mdFiles.length === 0) {
2645
2916
  if (existsSync5(indexPath)) unlinkSync2(indexPath);
2646
2917
  return;
@@ -3673,6 +3944,50 @@ function tokenizeCommand(cmd) {
3673
3944
  if (cur.length > 0) out.push(cur);
3674
3945
  return out;
3675
3946
  }
3947
+ function detectShellOperator(cmd) {
3948
+ const opPrefix = /^(?:2>&1|&>|\|{1,2}|&{1,2}|2>{1,2}|>{1,2}|<{1,2})/;
3949
+ let cur = "";
3950
+ let curQuoted = false;
3951
+ let quote = null;
3952
+ const check = () => {
3953
+ if (cur.length === 0 && !curQuoted) return null;
3954
+ if (!curQuoted) {
3955
+ const m = opPrefix.exec(cur);
3956
+ if (m) return m[0] ?? null;
3957
+ }
3958
+ return null;
3959
+ };
3960
+ for (let i = 0; i < cmd.length; i++) {
3961
+ const ch = cmd[i];
3962
+ if (quote) {
3963
+ if (ch === quote) {
3964
+ quote = null;
3965
+ } else if (ch === "\\" && quote === '"' && i + 1 < cmd.length) {
3966
+ cur += cmd[++i];
3967
+ curQuoted = true;
3968
+ } else {
3969
+ cur += ch;
3970
+ curQuoted = true;
3971
+ }
3972
+ continue;
3973
+ }
3974
+ if (ch === '"' || ch === "'") {
3975
+ quote = ch;
3976
+ curQuoted = true;
3977
+ continue;
3978
+ }
3979
+ if (ch === " " || ch === " ") {
3980
+ const op = check();
3981
+ if (op) return op;
3982
+ cur = "";
3983
+ curQuoted = false;
3984
+ continue;
3985
+ }
3986
+ cur += ch;
3987
+ }
3988
+ if (quote) return null;
3989
+ return check();
3990
+ }
3676
3991
  function isAllowed(cmd, extra = []) {
3677
3992
  const normalized = cmd.trim().replace(/\s+/g, " ");
3678
3993
  const allowlist = [...BUILTIN_ALLOWLIST, ...extra];
@@ -3685,6 +4000,12 @@ function isAllowed(cmd, extra = []) {
3685
4000
  async function runCommand(cmd, opts) {
3686
4001
  const argv = tokenizeCommand(cmd);
3687
4002
  if (argv.length === 0) throw new Error("run_command: empty command");
4003
+ const operator = detectShellOperator(cmd);
4004
+ if (operator !== null) {
4005
+ throw new Error(
4006
+ `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.`
4007
+ );
4008
+ }
3688
4009
  const timeoutMs = (opts.timeoutSec ?? DEFAULT_TIMEOUT_SEC) * 1e3;
3689
4010
  const maxChars = opts.maxOutputChars ?? DEFAULT_MAX_OUTPUT_CHARS;
3690
4011
  const spawnOpts = {
@@ -3862,7 +4183,7 @@ function registerShellTools(registry, opts) {
3862
4183
  properties: {
3863
4184
  command: {
3864
4185
  type: "string",
3865
- description: "Full command line. Tokenized with POSIX-ish quoting; no shell expansion, no pipes, no redirects."
4186
+ 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`.'
3866
4187
  },
3867
4188
  timeoutSec: {
3868
4189
  type: "integer",
@@ -4081,12 +4402,12 @@ ${i + 1}. ${r.title}`);
4081
4402
  }
4082
4403
 
4083
4404
  // src/env.ts
4084
- import { readFileSync as readFileSync6 } from "fs";
4405
+ import { readFileSync as readFileSync7 } from "fs";
4085
4406
  import { resolve as resolve5 } from "path";
4086
4407
  function loadDotenv(path = ".env") {
4087
4408
  let raw;
4088
4409
  try {
4089
- raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
4410
+ raw = readFileSync7(resolve5(process.cwd(), path), "utf8");
4090
4411
  } catch {
4091
4412
  return;
4092
4413
  }
@@ -4105,7 +4426,7 @@ function loadDotenv(path = ".env") {
4105
4426
  }
4106
4427
 
4107
4428
  // src/transcript.ts
4108
- import { createWriteStream, readFileSync as readFileSync7 } from "fs";
4429
+ import { createWriteStream, readFileSync as readFileSync8 } from "fs";
4109
4430
  function recordFromLoopEvent(ev, extra) {
4110
4431
  const rec = {
4111
4432
  ts: (/* @__PURE__ */ new Date()).toISOString(),
@@ -4156,7 +4477,7 @@ function openTranscriptFile(path, meta) {
4156
4477
  return stream;
4157
4478
  }
4158
4479
  function readTranscript(path) {
4159
- const raw = readFileSync7(path, "utf8");
4480
+ const raw = readFileSync8(path, "utf8");
4160
4481
  return parseTranscript(raw);
4161
4482
  }
4162
4483
  function isPlanStateEmptyShape(s) {
@@ -5211,8 +5532,8 @@ async function trySection(load) {
5211
5532
  }
5212
5533
 
5213
5534
  // src/code/edit-blocks.ts
5214
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
5215
- import { dirname as dirname3, resolve as resolve6 } from "path";
5535
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, unlinkSync as unlinkSync3, writeFileSync as writeFileSync3 } from "fs";
5536
+ import { dirname as dirname4, resolve as resolve6 } from "path";
5216
5537
  var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
5217
5538
  function parseEditBlocks(text) {
5218
5539
  const out = [];
@@ -5250,11 +5571,11 @@ function applyEditBlock(block, rootDir) {
5250
5571
  message: "file does not exist; to create it, use an empty SEARCH block"
5251
5572
  };
5252
5573
  }
5253
- mkdirSync3(dirname3(absTarget), { recursive: true });
5574
+ mkdirSync3(dirname4(absTarget), { recursive: true });
5254
5575
  writeFileSync3(absTarget, block.replace, "utf8");
5255
5576
  return { path: block.path, status: "created" };
5256
5577
  }
5257
- const content = readFileSync8(absTarget, "utf8");
5578
+ const content = readFileSync9(absTarget, "utf8");
5258
5579
  if (searchEmpty) {
5259
5580
  return {
5260
5581
  path: block.path,
@@ -5293,7 +5614,7 @@ function snapshotBeforeEdits(blocks, rootDir) {
5293
5614
  continue;
5294
5615
  }
5295
5616
  try {
5296
- snapshots.push({ path: b.path, prevContent: readFileSync8(abs, "utf8") });
5617
+ snapshots.push({ path: b.path, prevContent: readFileSync9(abs, "utf8") });
5297
5618
  } catch {
5298
5619
  snapshots.push({ path: b.path, prevContent: null });
5299
5620
  }
@@ -5336,8 +5657,8 @@ function sep() {
5336
5657
  }
5337
5658
 
5338
5659
  // src/code/prompt.ts
5339
- import { existsSync as existsSync8, readFileSync as readFileSync9 } from "fs";
5340
- import { join as join7 } from "path";
5660
+ import { existsSync as existsSync8, readFileSync as readFileSync10 } from "fs";
5661
+ import { join as join8 } from "path";
5341
5662
  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.
5342
5663
 
5343
5664
  # Cite or shut up \u2014 non-negotiable
@@ -5458,11 +5779,11 @@ Two different rules depending on which tool:
5458
5779
  `;
5459
5780
  function codeSystemPrompt(rootDir) {
5460
5781
  const withMemory = applyMemoryStack(CODE_SYSTEM_PROMPT, rootDir);
5461
- const gitignorePath = join7(rootDir, ".gitignore");
5782
+ const gitignorePath = join8(rootDir, ".gitignore");
5462
5783
  if (!existsSync8(gitignorePath)) return withMemory;
5463
5784
  let content;
5464
5785
  try {
5465
- content = readFileSync9(gitignorePath, "utf8");
5786
+ content = readFileSync10(gitignorePath, "utf8");
5466
5787
  } catch {
5467
5788
  return withMemory;
5468
5789
  }
@@ -5482,15 +5803,15 @@ ${truncated}
5482
5803
  }
5483
5804
 
5484
5805
  // src/config.ts
5485
- import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync10, writeFileSync as writeFileSync4 } from "fs";
5806
+ import { chmodSync as chmodSync2, mkdirSync as mkdirSync4, readFileSync as readFileSync11, writeFileSync as writeFileSync4 } from "fs";
5486
5807
  import { homedir as homedir5 } from "os";
5487
- import { dirname as dirname4, join as join8 } from "path";
5808
+ import { dirname as dirname5, join as join9 } from "path";
5488
5809
  function defaultConfigPath() {
5489
- return join8(homedir5(), ".reasonix", "config.json");
5810
+ return join9(homedir5(), ".reasonix", "config.json");
5490
5811
  }
5491
5812
  function readConfig(path = defaultConfigPath()) {
5492
5813
  try {
5493
- const raw = readFileSync10(path, "utf8");
5814
+ const raw = readFileSync11(path, "utf8");
5494
5815
  const parsed = JSON.parse(raw);
5495
5816
  if (parsed && typeof parsed === "object") return parsed;
5496
5817
  } catch {
@@ -5498,7 +5819,7 @@ function readConfig(path = defaultConfigPath()) {
5498
5819
  return {};
5499
5820
  }
5500
5821
  function writeConfig(cfg, path = defaultConfigPath()) {
5501
- mkdirSync4(dirname4(path), { recursive: true });
5822
+ mkdirSync4(dirname5(path), { recursive: true });
5502
5823
  writeFileSync4(path, JSON.stringify(cfg, null, 2), "utf8");
5503
5824
  try {
5504
5825
  chmodSync2(path, 384);
@@ -5525,25 +5846,25 @@ function redactKey(key) {
5525
5846
  }
5526
5847
 
5527
5848
  // src/version.ts
5528
- import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync11, writeFileSync as writeFileSync5 } from "fs";
5849
+ import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync5 } from "fs";
5529
5850
  import { homedir as homedir6 } from "os";
5530
- import { dirname as dirname5, join as join9 } from "path";
5531
- import { fileURLToPath } from "url";
5851
+ import { dirname as dirname6, join as join10 } from "path";
5852
+ import { fileURLToPath as fileURLToPath2 } from "url";
5532
5853
  var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
5533
5854
  var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
5534
5855
  var LATEST_FETCH_TIMEOUT_MS = 2e3;
5535
5856
  function readPackageVersion() {
5536
5857
  try {
5537
- let dir = dirname5(fileURLToPath(import.meta.url));
5858
+ let dir = dirname6(fileURLToPath2(import.meta.url));
5538
5859
  for (let i = 0; i < 6; i++) {
5539
- const p = join9(dir, "package.json");
5860
+ const p = join10(dir, "package.json");
5540
5861
  if (existsSync9(p)) {
5541
- const pkg = JSON.parse(readFileSync11(p, "utf8"));
5862
+ const pkg = JSON.parse(readFileSync12(p, "utf8"));
5542
5863
  if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
5543
5864
  return pkg.version;
5544
5865
  }
5545
5866
  }
5546
- const parent = dirname5(dir);
5867
+ const parent = dirname6(dir);
5547
5868
  if (parent === dir) break;
5548
5869
  dir = parent;
5549
5870
  }
@@ -5553,11 +5874,11 @@ function readPackageVersion() {
5553
5874
  }
5554
5875
  var VERSION = readPackageVersion();
5555
5876
  function cachePath(homeDirOverride) {
5556
- return join9(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
5877
+ return join10(homeDirOverride ?? homedir6(), ".reasonix", "version-cache.json");
5557
5878
  }
5558
5879
  function readCache(homeDirOverride) {
5559
5880
  try {
5560
- const raw = readFileSync11(cachePath(homeDirOverride), "utf8");
5881
+ const raw = readFileSync12(cachePath(homeDirOverride), "utf8");
5561
5882
  const parsed = JSON.parse(raw);
5562
5883
  if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
5563
5884
  return parsed;
@@ -5569,7 +5890,7 @@ function readCache(homeDirOverride) {
5569
5890
  function writeCache(entry, homeDirOverride) {
5570
5891
  try {
5571
5892
  const p = cachePath(homeDirOverride);
5572
- mkdirSync5(dirname5(p), { recursive: true });
5893
+ mkdirSync5(dirname6(p), { recursive: true });
5573
5894
  writeFileSync5(p, JSON.stringify(entry), "utf8");
5574
5895
  } catch {
5575
5896
  }
@@ -5577,8 +5898,8 @@ function writeCache(entry, homeDirOverride) {
5577
5898
  async function getLatestVersion(opts = {}) {
5578
5899
  const ttl = opts.ttlMs ?? LATEST_CACHE_TTL_MS;
5579
5900
  if (!opts.force) {
5580
- const cached = readCache(opts.homeDir);
5581
- if (cached && Date.now() - cached.checkedAt < ttl) return cached.version;
5901
+ const cached2 = readCache(opts.homeDir);
5902
+ if (cached2 && Date.now() - cached2.checkedAt < ttl) return cached2.version;
5582
5903
  }
5583
5904
  const fetchImpl = opts.fetchImpl ?? globalThis.fetch;
5584
5905
  if (!fetchImpl) return null;
@@ -5626,11 +5947,11 @@ function isNpxInstall() {
5626
5947
  }
5627
5948
 
5628
5949
  // src/usage.ts
5629
- import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync12, statSync as statSync4 } from "fs";
5950
+ import { appendFileSync as appendFileSync2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync13, statSync as statSync4 } from "fs";
5630
5951
  import { homedir as homedir7 } from "os";
5631
- import { dirname as dirname6, join as join10 } from "path";
5952
+ import { dirname as dirname7, join as join11 } from "path";
5632
5953
  function defaultUsageLogPath(homeDirOverride) {
5633
- return join10(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5954
+ return join11(homeDirOverride ?? homedir7(), ".reasonix", "usage.jsonl");
5634
5955
  }
5635
5956
  function appendUsage(input) {
5636
5957
  const record = {
@@ -5646,7 +5967,7 @@ function appendUsage(input) {
5646
5967
  };
5647
5968
  const path = input.path ?? defaultUsageLogPath();
5648
5969
  try {
5649
- mkdirSync6(dirname6(path), { recursive: true });
5970
+ mkdirSync6(dirname7(path), { recursive: true });
5650
5971
  appendFileSync2(path, `${JSON.stringify(record)}
5651
5972
  `, "utf8");
5652
5973
  } catch {
@@ -5657,7 +5978,7 @@ function readUsageLog(path = defaultUsageLogPath()) {
5657
5978
  if (!existsSync10(path)) return [];
5658
5979
  let raw;
5659
5980
  try {
5660
- raw = readFileSync12(path, "utf8");
5981
+ raw = readFileSync13(path, "utf8");
5661
5982
  } catch {
5662
5983
  return [];
5663
5984
  }
@@ -5755,6 +6076,7 @@ export {
5755
6076
  CODE_SYSTEM_PROMPT,
5756
6077
  CacheFirstLoop,
5757
6078
  DEFAULT_MAX_RESULT_CHARS,
6079
+ DEFAULT_MAX_RESULT_TOKENS,
5758
6080
  DeepSeekClient,
5759
6081
  HOOK_EVENTS,
5760
6082
  HOOK_SETTINGS_DIRNAME,
@@ -5804,6 +6126,7 @@ export {
5804
6126
  defaultSelector,
5805
6127
  defaultUsageLogPath,
5806
6128
  deleteSession,
6129
+ detectShellOperator,
5807
6130
  diffTranscripts,
5808
6131
  emptyPlanState,
5809
6132
  fetchWithRetry,
@@ -5878,6 +6201,7 @@ export {
5878
6201
  stripHallucinatedToolMarkup,
5879
6202
  tokenizeCommand,
5880
6203
  truncateForModel,
6204
+ truncateForModelByTokens,
5881
6205
  webFetch,
5882
6206
  webSearch,
5883
6207
  withUtf8Codepage,