reasonix 0.12.19 → 0.12.21

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/cli/index.js CHANGED
@@ -181,8 +181,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
181
181
  }
182
182
  function sleep(ms, signal) {
183
183
  if (ms <= 0) return Promise.resolve();
184
- return new Promise((resolve13, reject) => {
185
- const timer = setTimeout(resolve13, ms);
184
+ return new Promise((resolve14, reject) => {
185
+ const timer = setTimeout(resolve14, ms);
186
186
  if (signal) {
187
187
  const onAbort = () => {
188
188
  clearTimeout(timer);
@@ -548,8 +548,8 @@ var defaultSelector = (samples) => {
548
548
  })[0];
549
549
  };
550
550
  async function runBranches(client, request, opts = {}) {
551
- const budget = Math.max(1, opts.budget ?? 1);
552
- const temperatures = resolveTemperatures(budget, opts.temperatures);
551
+ const budget2 = Math.max(1, opts.budget ?? 1);
552
+ const temperatures = resolveTemperatures(budget2, opts.temperatures);
553
553
  const selector = opts.selector ?? defaultSelector;
554
554
  const samples = await Promise.all(
555
555
  temperatures.map(async (temperature, index) => {
@@ -586,12 +586,12 @@ function aggregateBranchUsage(samples) {
586
586
  promptCacheMissTokens
587
587
  };
588
588
  }
589
- function resolveTemperatures(budget, custom) {
590
- if (custom && custom.length >= budget) return [...custom.slice(0, budget)];
591
- if (budget === 1) return [0];
589
+ function resolveTemperatures(budget2, custom) {
590
+ if (custom && custom.length >= budget2) return [...custom.slice(0, budget2)];
591
+ if (budget2 === 1) return [0];
592
592
  const out = [];
593
- for (let i = 0; i < budget; i++) {
594
- out.push(Number((i / (budget - 1)).toFixed(2)));
593
+ for (let i = 0; i < budget2; i++) {
594
+ out.push(Number((i / (budget2 - 1)).toFixed(2)));
595
595
  }
596
596
  return out;
597
597
  }
@@ -668,7 +668,7 @@ function matchesTool(hook, toolName) {
668
668
  }
669
669
  var HOOK_OUTPUT_CAP_BYTES = 256 * 1024;
670
670
  function defaultSpawner(input) {
671
- return new Promise((resolve13) => {
671
+ return new Promise((resolve14) => {
672
672
  const child = spawn(input.command, {
673
673
  cwd: input.cwd,
674
674
  shell: true,
@@ -713,7 +713,7 @@ function defaultSpawner(input) {
713
713
  child.stderr.on("data", (chunk) => onChunk("stderr", chunk));
714
714
  child.once("error", (err) => {
715
715
  clearTimeout(timer);
716
- resolve13({
716
+ resolve14({
717
717
  exitCode: null,
718
718
  stdout: Buffer.concat(stdoutChunks).toString("utf8"),
719
719
  stderr: Buffer.concat(stderrChunks).toString("utf8"),
@@ -724,7 +724,7 @@ function defaultSpawner(input) {
724
724
  });
725
725
  child.once("close", (code) => {
726
726
  clearTimeout(timer);
727
- resolve13({
727
+ resolve14({
728
728
  exitCode: code,
729
729
  stdout: Buffer.concat(stdoutChunks).toString("utf8").trim(),
730
730
  stderr: Buffer.concat(stderrChunks).toString("utf8").trim(),
@@ -1276,29 +1276,29 @@ function truncateForModelByTokens(s, maxTokens) {
1276
1276
 
1277
1277
  ${tail}`;
1278
1278
  }
1279
- function sizePrefixToTokens(s, budget) {
1280
- if (budget <= 0 || s.length === 0) return "";
1281
- let size = Math.min(s.length, budget * 4);
1279
+ function sizePrefixToTokens(s, budget2) {
1280
+ if (budget2 <= 0 || s.length === 0) return "";
1281
+ let size = Math.min(s.length, budget2 * 4);
1282
1282
  for (let iter = 0; iter < 6; iter++) {
1283
1283
  if (size <= 0) return "";
1284
1284
  const slice = s.slice(0, size);
1285
1285
  const count = countTokens(slice);
1286
- if (count <= budget) return slice;
1287
- const next = Math.floor(size * (budget / count) * 0.95);
1286
+ if (count <= budget2) return slice;
1287
+ const next = Math.floor(size * (budget2 / count) * 0.95);
1288
1288
  if (next >= size) return s.slice(0, Math.max(0, size - 1));
1289
1289
  size = next;
1290
1290
  }
1291
1291
  return s.slice(0, Math.max(0, size));
1292
1292
  }
1293
- function sizeSuffixToTokens(s, budget) {
1294
- if (budget <= 0 || s.length === 0) return "";
1295
- let size = Math.min(s.length, budget * 4);
1293
+ function sizeSuffixToTokens(s, budget2) {
1294
+ if (budget2 <= 0 || s.length === 0) return "";
1295
+ let size = Math.min(s.length, budget2 * 4);
1296
1296
  for (let iter = 0; iter < 6; iter++) {
1297
1297
  if (size <= 0) return "";
1298
1298
  const slice = s.slice(-size);
1299
1299
  const count = countTokens(slice);
1300
- if (count <= budget) return slice;
1301
- const next = Math.floor(size * (budget / count) * 0.95);
1300
+ if (count <= budget2) return slice;
1301
+ const next = Math.floor(size * (budget2 / count) * 0.95);
1302
1302
  if (next >= size) return s.slice(-Math.max(0, size - 1));
1303
1303
  size = next;
1304
1304
  }
@@ -1999,6 +1999,19 @@ var CacheFirstLoop = class {
1999
1999
  * flip it live alongside `model`.
2000
2000
  */
2001
2001
  autoEscalate = true;
2002
+ /**
2003
+ * Soft USD budget — see {@link CacheFirstLoopOptions.budgetUsd}.
2004
+ * Mutable so `/budget` slash can set / change / clear it mid-session.
2005
+ * `null` (the default) disables all budget checks.
2006
+ */
2007
+ budgetUsd;
2008
+ /**
2009
+ * Set the first time a turn crosses 80% of the budget so the warning
2010
+ * doesn't repeat every turn afterwards. Cleared by `setBudget` (any
2011
+ * change re-arms the warning, including raising the cap above the
2012
+ * current spend).
2013
+ */
2014
+ _budgetWarned = false;
2002
2015
  sessionName;
2003
2016
  /**
2004
2017
  * Hook list, mutable so `/hooks reload` can swap it without
@@ -2064,6 +2077,7 @@ var CacheFirstLoop = class {
2064
2077
  this.model = opts.model ?? "deepseek-v4-flash";
2065
2078
  this.reasoningEffort = opts.reasoningEffort ?? "max";
2066
2079
  if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
2080
+ this.budgetUsd = typeof opts.budgetUsd === "number" && opts.budgetUsd > 0 ? opts.budgetUsd : null;
2067
2081
  this.maxToolIters = opts.maxToolIters ?? 64;
2068
2082
  this.hooks = opts.hooks ?? [];
2069
2083
  this.hookCwd = opts.hookCwd ?? process.cwd();
@@ -2303,6 +2317,16 @@ var CacheFirstLoop = class {
2303
2317
  }
2304
2318
  this.stream = this.branchEnabled ? false : this._streamPreference;
2305
2319
  }
2320
+ /**
2321
+ * Set / change / clear the soft USD budget. `null` (or any non-
2322
+ * positive number) disables the cap entirely. Re-arms the 80%
2323
+ * warning so a user who bumps the cap mid-session sees a fresh
2324
+ * threshold message at the new boundary.
2325
+ */
2326
+ setBudget(usd) {
2327
+ this.budgetUsd = typeof usd === "number" && usd > 0 ? usd : null;
2328
+ this._budgetWarned = false;
2329
+ }
2306
2330
  /**
2307
2331
  * Arm pro for the next turn (consumed at turn start). Called by
2308
2332
  * `/pro`. Idempotent — repeated calls stay armed, `disarmPro()`
@@ -2464,6 +2488,26 @@ var CacheFirstLoop = class {
2464
2488
  return userText;
2465
2489
  }
2466
2490
  async *step(userInput) {
2491
+ if (this.budgetUsd !== null) {
2492
+ const spent = this.stats.totalCost;
2493
+ if (spent >= this.budgetUsd) {
2494
+ yield {
2495
+ turn: this._turn,
2496
+ role: "error",
2497
+ content: "",
2498
+ error: `session budget exhausted \u2014 spent $${spent.toFixed(4)} \u2265 cap $${this.budgetUsd.toFixed(2)}. Bump the cap with /budget <usd>, clear it with /budget off, or end the session.`
2499
+ };
2500
+ return;
2501
+ }
2502
+ if (!this._budgetWarned && spent >= this.budgetUsd * 0.8) {
2503
+ this._budgetWarned = true;
2504
+ yield {
2505
+ turn: this._turn,
2506
+ role: "warning",
2507
+ content: `\u25B2 budget 80% used \u2014 $${spent.toFixed(4)} of $${this.budgetUsd.toFixed(2)}. Next turn or two likely trips the cap.`
2508
+ };
2509
+ }
2510
+ }
2467
2511
  this._turn++;
2468
2512
  this.scratch.reset();
2469
2513
  this.repair.resetStorm();
@@ -2560,14 +2604,14 @@ var CacheFirstLoop = class {
2560
2604
  let preHarvestedPlanState;
2561
2605
  try {
2562
2606
  if (this.branchEnabled) {
2563
- const budget = this.branchOptions.budget ?? 1;
2607
+ const budget2 = this.branchOptions.budget ?? 1;
2564
2608
  yield {
2565
2609
  turn: this._turn,
2566
2610
  role: "branch_start",
2567
2611
  content: "",
2568
2612
  branchProgress: {
2569
2613
  completed: 0,
2570
- total: budget,
2614
+ total: budget2,
2571
2615
  latestIndex: -1,
2572
2616
  latestTemperature: -1,
2573
2617
  latestUncertainties: -1
@@ -2601,9 +2645,9 @@ var CacheFirstLoop = class {
2601
2645
  onSampleDone
2602
2646
  }
2603
2647
  );
2604
- for (let k = 0; k < budget; k++) {
2605
- const sample = queue.shift() ?? await new Promise((resolve13) => {
2606
- waiter = resolve13;
2648
+ for (let k = 0; k < budget2; k++) {
2649
+ const sample = queue.shift() ?? await new Promise((resolve14) => {
2650
+ waiter = resolve14;
2607
2651
  });
2608
2652
  yield {
2609
2653
  turn: this._turn,
@@ -2611,7 +2655,7 @@ var CacheFirstLoop = class {
2611
2655
  content: "",
2612
2656
  branchProgress: {
2613
2657
  completed: k + 1,
2614
- total: budget,
2658
+ total: budget2,
2615
2659
  latestIndex: sample.index,
2616
2660
  latestTemperature: sample.temperature,
2617
2661
  latestUncertainties: sample.planState.uncertainties.length
@@ -5400,7 +5444,7 @@ async function runCommand(cmd, opts) {
5400
5444
  };
5401
5445
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5402
5446
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5403
- return await new Promise((resolve13, reject) => {
5447
+ return await new Promise((resolve14, reject) => {
5404
5448
  let child;
5405
5449
  try {
5406
5450
  child = spawn3(bin, args, effectiveSpawnOpts);
@@ -5445,7 +5489,7 @@ async function runCommand(cmd, opts) {
5445
5489
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5446
5490
 
5447
5491
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5448
- resolve13({ exitCode: code, output, timedOut });
5492
+ resolve14({ exitCode: code, output, timedOut });
5449
5493
  });
5450
5494
  });
5451
5495
  }
@@ -6788,7 +6832,7 @@ var McpClient = class {
6788
6832
  const id = this.nextId++;
6789
6833
  const frame = { jsonrpc: "2.0", id, method, params };
6790
6834
  let abortHandler = null;
6791
- const promise = new Promise((resolve13, reject) => {
6835
+ const promise = new Promise((resolve14, reject) => {
6792
6836
  const timeout = setTimeout(() => {
6793
6837
  this.pending.delete(id);
6794
6838
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -6797,7 +6841,7 @@ var McpClient = class {
6797
6841
  );
6798
6842
  }, this.requestTimeoutMs);
6799
6843
  this.pending.set(id, {
6800
- resolve: resolve13,
6844
+ resolve: resolve14,
6801
6845
  reject,
6802
6846
  timeout
6803
6847
  });
@@ -6920,12 +6964,12 @@ var StdioTransport = class {
6920
6964
  }
6921
6965
  async send(message) {
6922
6966
  if (this.closed) throw new Error("MCP transport is closed");
6923
- return new Promise((resolve13, reject) => {
6967
+ return new Promise((resolve14, reject) => {
6924
6968
  const line = `${JSON.stringify(message)}
6925
6969
  `;
6926
6970
  this.child.stdin.write(line, "utf8", (err) => {
6927
6971
  if (err) reject(err);
6928
- else resolve13();
6972
+ else resolve14();
6929
6973
  });
6930
6974
  });
6931
6975
  }
@@ -6936,8 +6980,8 @@ var StdioTransport = class {
6936
6980
  continue;
6937
6981
  }
6938
6982
  if (this.closed) return;
6939
- const next = await new Promise((resolve13) => {
6940
- this.waiters.push(resolve13);
6983
+ const next = await new Promise((resolve14) => {
6984
+ this.waiters.push(resolve14);
6941
6985
  });
6942
6986
  if (next === null) return;
6943
6987
  yield next;
@@ -7003,8 +7047,8 @@ var SseTransport = class {
7003
7047
  constructor(opts) {
7004
7048
  this.url = opts.url;
7005
7049
  this.headers = opts.headers ?? {};
7006
- this.endpointReady = new Promise((resolve13, reject) => {
7007
- this.resolveEndpoint = resolve13;
7050
+ this.endpointReady = new Promise((resolve14, reject) => {
7051
+ this.resolveEndpoint = resolve14;
7008
7052
  this.rejectEndpoint = reject;
7009
7053
  });
7010
7054
  this.endpointReady.catch(() => void 0);
@@ -7031,8 +7075,8 @@ var SseTransport = class {
7031
7075
  continue;
7032
7076
  }
7033
7077
  if (this.closed) return;
7034
- const next = await new Promise((resolve13) => {
7035
- this.waiters.push(resolve13);
7078
+ const next = await new Promise((resolve14) => {
7079
+ this.waiters.push(resolve14);
7036
7080
  });
7037
7081
  if (next === null) return;
7038
7082
  yield next;
@@ -7219,8 +7263,8 @@ var StreamableHttpTransport = class {
7219
7263
  continue;
7220
7264
  }
7221
7265
  if (this.closed) return;
7222
- const next = await new Promise((resolve13) => {
7223
- this.waiters.push(resolve13);
7266
+ const next = await new Promise((resolve14) => {
7267
+ this.waiters.push(resolve14);
7224
7268
  });
7225
7269
  if (next === null) return;
7226
7270
  yield next;
@@ -9688,7 +9732,7 @@ async function startOllamaDaemon(opts = {}) {
9688
9732
  return { ready: false, pid };
9689
9733
  }
9690
9734
  async function pullOllamaModel(modelName, opts = {}) {
9691
- return new Promise((resolve13) => {
9735
+ return new Promise((resolve14) => {
9692
9736
  const child = spawn5("ollama", ["pull", modelName], {
9693
9737
  stdio: ["ignore", "pipe", "pipe"],
9694
9738
  windowsHide: true
@@ -9700,8 +9744,8 @@ async function pullOllamaModel(modelName, opts = {}) {
9700
9744
  }
9701
9745
  streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
9702
9746
  streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
9703
- child.once("exit", (code) => resolve13(code ?? -1));
9704
- child.once("error", () => resolve13(-1));
9747
+ child.once("exit", (code) => resolve14(code ?? -1));
9748
+ child.once("error", () => resolve14(-1));
9705
9749
  });
9706
9750
  }
9707
9751
  function streamLines(stream, cb) {
@@ -10546,7 +10590,7 @@ var MAX_BODY_BYTES = 256 * 1024;
10546
10590
  async function readBody(req) {
10547
10591
  let total = 0;
10548
10592
  const chunks = [];
10549
- return new Promise((resolve13, reject) => {
10593
+ return new Promise((resolve14, reject) => {
10550
10594
  req.on("data", (chunk) => {
10551
10595
  total += chunk.length;
10552
10596
  if (total > MAX_BODY_BYTES) {
@@ -10556,7 +10600,7 @@ async function readBody(req) {
10556
10600
  }
10557
10601
  chunks.push(chunk);
10558
10602
  });
10559
- req.on("end", () => resolve13(Buffer.concat(chunks).toString("utf8")));
10603
+ req.on("end", () => resolve14(Buffer.concat(chunks).toString("utf8")));
10560
10604
  req.on("error", reject);
10561
10605
  });
10562
10606
  }
@@ -10633,7 +10677,7 @@ function startDashboardServer(ctx, opts = {}) {
10633
10677
  const token = opts.token ?? mintToken();
10634
10678
  const host = opts.host ?? "127.0.0.1";
10635
10679
  const port = opts.port ?? 0;
10636
- return new Promise((resolve13, reject) => {
10680
+ return new Promise((resolve14, reject) => {
10637
10681
  const server = createServer((req, res) => {
10638
10682
  dispatch(req, res, ctx, token).catch((err) => {
10639
10683
  if (!res.headersSent) {
@@ -10654,7 +10698,7 @@ function startDashboardServer(ctx, opts = {}) {
10654
10698
  server.close(() => doneResolve());
10655
10699
  setTimeout(() => server.closeAllConnections?.(), 1e3).unref();
10656
10700
  });
10657
- resolve13({ url, token, port: finalPort, close });
10701
+ resolve14({ url, token, port: finalPort, close });
10658
10702
  });
10659
10703
  });
10660
10704
  }
@@ -11270,8 +11314,8 @@ function SelectRow({
11270
11314
  active,
11271
11315
  marker
11272
11316
  }) {
11273
- const color = item.disabled ? "gray" : active ? "cyan" : void 0;
11274
- return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React4.createElement(Box3, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, item.hint)) : null);
11317
+ const color2 = item.disabled ? "gray" : active ? "cyan" : void 0;
11318
+ return /* @__PURE__ */ React4.createElement(Box3, { flexDirection: "column" }, /* @__PURE__ */ React4.createElement(Box3, null, /* @__PURE__ */ React4.createElement(Text3, { color: color2 }, marker, " ", item.label)), item.hint ? /* @__PURE__ */ React4.createElement(Box3, { paddingLeft: marker.length + 1 }, /* @__PURE__ */ React4.createElement(Text3, { dimColor: true }, item.hint)) : null);
11275
11319
  }
11276
11320
  function findNextEnabled(items, from, step) {
11277
11321
  if (items.length === 0) return 0;
@@ -11401,13 +11445,13 @@ var MIN_DIFF_ROWS = 8;
11401
11445
  function EditConfirm({ block, onChoose }) {
11402
11446
  const { stdout: stdout3 } = useStdout2();
11403
11447
  const rows = stdout3?.rows ?? 40;
11404
- const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
11448
+ const budget2 = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
11405
11449
  const allLines = useMemo(
11406
11450
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
11407
11451
  [block]
11408
11452
  );
11409
11453
  const [scroll, setScroll] = useState2(0);
11410
- const maxScroll = Math.max(0, allLines.length - budget);
11454
+ const maxScroll = Math.max(0, allLines.length - budget2);
11411
11455
  const effectiveScroll = Math.min(scroll, maxScroll);
11412
11456
  useKeystroke((ev) => {
11413
11457
  if (ev.paste) return;
@@ -11438,11 +11482,11 @@ function EditConfirm({ block, onChoose }) {
11438
11482
  return;
11439
11483
  }
11440
11484
  if (key.pageDown || input === " " || input === "f") {
11441
- setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget - 2)));
11485
+ setScroll((s) => Math.min(maxScroll, s + Math.max(1, budget2 - 2)));
11442
11486
  return;
11443
11487
  }
11444
11488
  if (key.pageUp || input === "b") {
11445
- setScroll((s) => Math.max(0, s - Math.max(1, budget - 2)));
11489
+ setScroll((s) => Math.max(0, s - Math.max(1, budget2 - 2)));
11446
11490
  return;
11447
11491
  }
11448
11492
  if (input === "g") {
@@ -11458,9 +11502,9 @@ function EditConfirm({ block, onChoose }) {
11458
11502
  const removed = isNew ? 0 : (block.search.match(/\n/g)?.length ?? 0) + 1;
11459
11503
  const added = block.replace === "" ? 0 : (block.replace.match(/\n/g)?.length ?? 0) + 1;
11460
11504
  const tag = isNew ? "NEW" : "EDIT";
11461
- const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget);
11505
+ const visibleLines = allLines.slice(effectiveScroll, effectiveScroll + budget2);
11462
11506
  const hiddenAbove = effectiveScroll;
11463
- const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget);
11507
+ const hiddenBelow = Math.max(0, allLines.length - effectiveScroll - budget2);
11464
11508
  const totalLines = allLines.length;
11465
11509
  const showScrollHud = hiddenAbove + hiddenBelow > 0;
11466
11510
  const subtitleParts = [`-${removed} +${added} lines`];
@@ -11486,13 +11530,13 @@ function EditConfirm({ block, onChoose }) {
11486
11530
  ) : null,
11487
11531
  /* @__PURE__ */ React6.createElement(Box5, { flexDirection: "column" }, visibleLines.map((line, i) => {
11488
11532
  const trimmed = line.trimStart();
11489
- const color = trimmed.startsWith("+") ? "#4ade80" : trimmed.startsWith("-") ? "#f87171" : void 0;
11490
- const dim = !color;
11533
+ const color2 = trimmed.startsWith("+") ? "#4ade80" : trimmed.startsWith("-") ? "#f87171" : void 0;
11534
+ const dim = !color2;
11491
11535
  return /* @__PURE__ */ React6.createElement(
11492
11536
  Text4,
11493
11537
  {
11494
11538
  key: `diff-${effectiveScroll}-${i}`,
11495
- color,
11539
+ color: color2,
11496
11540
  dimColor: dim
11497
11541
  },
11498
11542
  line
@@ -11526,7 +11570,7 @@ function PlanStateBlock({ planState }) {
11526
11570
  if (planState.rejectedPaths.length)
11527
11571
  fields.push(["rejected", planState.rejectedPaths, "#94a3b8", true]);
11528
11572
  if (fields.length === 0) return null;
11529
- return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React7.createElement(Box6, { key: label }, /* @__PURE__ */ React7.createElement(Text5, { backgroundColor: color, color: "black", bold: true, dimColor: dim }, ` ${label} ${items.length} `), /* @__PURE__ */ React7.createElement(Text5, null, " "), /* @__PURE__ */ React7.createElement(Text5, { dimColor: dim }, items.join(" \xB7 ")))));
11573
+ return /* @__PURE__ */ React7.createElement(Box6, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color2, dim]) => /* @__PURE__ */ React7.createElement(Box6, { key: label }, /* @__PURE__ */ React7.createElement(Text5, { backgroundColor: color2, color: "black", bold: true, dimColor: dim }, ` ${label} ${items.length} `), /* @__PURE__ */ React7.createElement(Text5, null, " "), /* @__PURE__ */ React7.createElement(Text5, { dimColor: dim }, items.join(" \xB7 ")))));
11530
11574
  }
11531
11575
 
11532
11576
  // src/cli/ui/PlanStepList.tsx
@@ -12437,8 +12481,8 @@ function gradientCells(width, glyph = GLYPH.block) {
12437
12481
  const t2 = width === 1 ? 0 : i * last / (width - 1);
12438
12482
  const lo = Math.floor(t2);
12439
12483
  const hi = Math.min(last, lo + 1);
12440
- const color = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
12441
- cells.push({ ch: glyph, color });
12484
+ const color2 = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
12485
+ cells.push({ ch: glyph, color: color2 });
12442
12486
  }
12443
12487
  return cells;
12444
12488
  }
@@ -12615,9 +12659,9 @@ var ROLE_GLYPH = {
12615
12659
  };
12616
12660
  function RoleGlyph({
12617
12661
  glyph,
12618
- color
12662
+ color: color2
12619
12663
  }) {
12620
- return /* @__PURE__ */ React11.createElement(Text8, { color, bold: true }, glyph);
12664
+ return /* @__PURE__ */ React11.createElement(Text8, { color: color2, bold: true }, glyph);
12621
12665
  }
12622
12666
  function ToolPill({ label, status: status2 }) {
12623
12667
  const bg = status2 === "err" ? "red" : "yellow";
@@ -13547,13 +13591,13 @@ function buildViewport(line, cursorCol, visibleCells, pastes) {
13547
13591
  return clipAroundCursor(line, cursorCol, visibleCells, pastes);
13548
13592
  }
13549
13593
  function clipFromLeft(line, visibleCells, pastes) {
13550
- const budget = Math.max(1, visibleCells - 1);
13594
+ const budget2 = Math.max(1, visibleCells - 1);
13551
13595
  let used = 0;
13552
13596
  let end = 0;
13553
13597
  while (end < line.length) {
13554
13598
  const ch = line[end];
13555
13599
  const cw = charCellsAt(line, end, pastes);
13556
- if (used + cw > budget) break;
13600
+ if (used + cw > budget2) break;
13557
13601
  used += cw;
13558
13602
  end++;
13559
13603
  }
@@ -13561,10 +13605,10 @@ function clipFromLeft(line, visibleCells, pastes) {
13561
13605
  return { segments, cursorCell: null, hiddenLeft: false, hiddenRight: end < line.length };
13562
13606
  }
13563
13607
  function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
13564
- let budget = visibleCells;
13608
+ let budget2 = visibleCells;
13565
13609
  const reservedForMarkers = 2;
13566
- budget = Math.max(1, budget - reservedForMarkers);
13567
- const halfBudget = Math.floor(budget / 2);
13610
+ budget2 = Math.max(1, budget2 - reservedForMarkers);
13611
+ const halfBudget = Math.floor(budget2 / 2);
13568
13612
  let start = cursorCol;
13569
13613
  let leftCells = 0;
13570
13614
  while (start > 0 && leftCells < halfBudget) {
@@ -13573,7 +13617,7 @@ function clipAroundCursor(line, cursorCol, visibleCells, pastes) {
13573
13617
  start--;
13574
13618
  leftCells += cw;
13575
13619
  }
13576
- const rightBudget = budget - leftCells;
13620
+ const rightBudget = budget2 - leftCells;
13577
13621
  let end = cursorCol;
13578
13622
  let rightCells = 0;
13579
13623
  const cursorChar = cursorCol < line.length ? charCellsAt(line, cursorCol, pastes) : 1;
@@ -14080,7 +14124,8 @@ function StatsPanel({
14080
14124
  busy,
14081
14125
  proArmed,
14082
14126
  escalated,
14083
- dashboardUrl
14127
+ dashboardUrl,
14128
+ budgetUsd
14084
14129
  }) {
14085
14130
  const branchOn = (branchBudget ?? 1) > 1;
14086
14131
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
@@ -14126,7 +14171,12 @@ function StatsPanel({
14126
14171
  balance,
14127
14172
  coldStart
14128
14173
  }
14129
- ));
14174
+ ), budgetUsd !== null && budgetUsd !== void 0 ? /* @__PURE__ */ React21.createElement(BudgetRow, { spent: summary.totalCostUsd, cap: budgetUsd }) : null);
14175
+ }
14176
+ function BudgetRow({ spent, cap }) {
14177
+ const pct2 = Math.max(0, spent / cap * 100);
14178
+ const color2 = pct2 >= 100 ? "#f87171" : pct2 >= 80 ? "#fbbf24" : "#94a3b8";
14179
+ return /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " budget "), /* @__PURE__ */ React21.createElement(Text17, { color: color2 }, `$${spent.toFixed(4)} / $${cap.toFixed(2)}`, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, ` (${pct2.toFixed(0)}%)`)));
14130
14180
  }
14131
14181
  function Header({
14132
14182
  model: model2,
@@ -14196,9 +14246,9 @@ function ContextCell({
14196
14246
  if (promptTokens === 0) {
14197
14247
  return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info, dimColor: true }, "\u25A3 ctx "), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, "\u2014 (no turns yet)"));
14198
14248
  }
14199
- const color = ratio >= 0.8 ? COLOR.err : ratio >= 0.6 ? COLOR.warn : COLOR.ok;
14249
+ const color2 = ratio >= 0.8 ? COLOR.err : ratio >= 0.6 ? COLOR.warn : COLOR.ok;
14200
14250
  const pct2 = Math.round(ratio * 100);
14201
- return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25A3 ctx "), /* @__PURE__ */ React21.createElement(Bar, { ratio, color, cells: showBar ? 14 : 10 }), /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.err, bold: true }, " \xB7 /compact") : null);
14251
+ return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25A3 ctx "), /* @__PURE__ */ React21.createElement(Bar, { ratio, color: color2, cells: showBar ? 14 : 10 }), /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: color2, bold: true }, formatTokens(promptTokens), "/", formatTokens(ctxMax)), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " (", pct2, "%)"), ratio >= 0.8 ? /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.err, bold: true }, " \xB7 /compact") : null);
14202
14252
  }
14203
14253
  function CacheCell({
14204
14254
  hitRatio,
@@ -14212,8 +14262,8 @@ function CacheCell({
14212
14262
  if (coldStart) {
14213
14263
  return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info, dimColor: true }, "\u232C cache "), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, pct2, "% "), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true, italic: true }, "(cold start)"));
14214
14264
  }
14215
- const color = hitRatio >= 0.7 ? COLOR.ok : hitRatio >= 0.4 ? COLOR.warn : COLOR.err;
14216
- return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u232C cache "), /* @__PURE__ */ React21.createElement(Text17, { color, bold: true }, pct2, "%"));
14265
+ const color2 = hitRatio >= 0.7 ? COLOR.ok : hitRatio >= 0.4 ? COLOR.warn : COLOR.err;
14266
+ return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u232C cache "), /* @__PURE__ */ React21.createElement(Text17, { color: color2, bold: true }, pct2, "%"));
14217
14267
  }
14218
14268
  function turnCostColor(cost) {
14219
14269
  if (cost <= 0) return void 0;
@@ -14239,16 +14289,16 @@ function CostCell({
14239
14289
  return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25F4 turn "), /* @__PURE__ */ React21.createElement(Text17, { color: turnColor, bold: !coldStart, dimColor: coldStart }, "$", summary.lastTurnCostUsd.toFixed(4)), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " \xB7 session "), /* @__PURE__ */ React21.createElement(Text17, { color: sessionColor, bold: !coldStart, dimColor: coldStart }, "$", summary.totalCostUsd.toFixed(4)));
14240
14290
  }
14241
14291
  function BalanceCell({ balance }) {
14242
- const color = balance.total < 1 ? COLOR.err : balance.total < 5 ? COLOR.warn : COLOR.ok;
14243
- return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25D0 balance "), /* @__PURE__ */ React21.createElement(Text17, { color, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
14292
+ const color2 = balance.total < 1 ? COLOR.err : balance.total < 5 ? COLOR.warn : COLOR.ok;
14293
+ return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25D0 balance "), /* @__PURE__ */ React21.createElement(Text17, { color: color2, bold: true }, balance.currency === "USD" ? "$" : "", balance.total.toFixed(2), balance.currency !== "USD" ? ` ${balance.currency}` : ""));
14244
14294
  }
14245
14295
  function Bar({
14246
14296
  ratio,
14247
- color,
14297
+ color: color2,
14248
14298
  cells = 14
14249
14299
  }) {
14250
14300
  const filled = Math.max(0, Math.min(cells, Math.round(ratio * cells)));
14251
- return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color }, "\u25B0".repeat(filled)), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, "\u25B1".repeat(cells - filled)));
14301
+ return /* @__PURE__ */ React21.createElement(Text17, null, /* @__PURE__ */ React21.createElement(Text17, { color: color2 }, "\u25B0".repeat(filled)), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, "\u25B1".repeat(cells - filled)));
14252
14302
  }
14253
14303
  function formatTokens(n) {
14254
14304
  if (n < 1024) return String(n);
@@ -14828,6 +14878,12 @@ var SLASH_COMMANDS = [
14828
14878
  summary: "arm v4-pro for the NEXT turn only (one-shot \xB7 auto-disarms after turn)",
14829
14879
  argCompleter: ["off"]
14830
14880
  },
14881
+ {
14882
+ cmd: "budget",
14883
+ argsHint: "[usd|off]",
14884
+ summary: "session USD cap \u2014 warns at 80%, refuses next turn at 100%. Off by default. /budget alone shows status",
14885
+ argCompleter: ["off", "1", "5", "10", "20", "50"]
14886
+ },
14831
14887
  { cmd: "mcp", summary: "list MCP servers + tools attached to this session" },
14832
14888
  {
14833
14889
  cmd: "resource",
@@ -16317,6 +16373,42 @@ var pro = (args, loop2, ctx) => {
16317
16373
  };
16318
16374
  };
16319
16375
  var ESCALATION_MODEL_ID = "deepseek-v4-pro";
16376
+ var budget = (args, loop2) => {
16377
+ const arg = args[0]?.trim() ?? "";
16378
+ if (arg === "") {
16379
+ if (loop2.budgetUsd === null) {
16380
+ return {
16381
+ info: "no session budget set \u2014 Reasonix will keep going until you stop it. Set one with: /budget <usd> (e.g. /budget 5)"
16382
+ };
16383
+ }
16384
+ const spent2 = loop2.stats.totalCost;
16385
+ const pct2 = spent2 / loop2.budgetUsd * 100;
16386
+ return {
16387
+ info: `budget: $${spent2.toFixed(4)} of $${loop2.budgetUsd.toFixed(2)} (${pct2.toFixed(1)}%) \xB7 /budget off to clear, /budget <usd> to change`
16388
+ };
16389
+ }
16390
+ if (arg === "off" || arg === "none" || arg === "0") {
16391
+ loop2.setBudget(null);
16392
+ return { info: "budget \u2192 off (no cap)" };
16393
+ }
16394
+ const cleaned = arg.replace(/^\$/, "");
16395
+ const usd = Number(cleaned);
16396
+ if (!Number.isFinite(usd) || usd <= 0) {
16397
+ return {
16398
+ info: `usage: /budget <usd> (got "${arg}" \u2014 must be a positive number, e.g. /budget 5 or /budget 12.50)`
16399
+ };
16400
+ }
16401
+ loop2.setBudget(usd);
16402
+ const spent = loop2.stats.totalCost;
16403
+ if (spent >= usd) {
16404
+ return {
16405
+ info: `\u25B2 budget \u2192 $${usd.toFixed(2)} but already spent $${spent.toFixed(4)}. Next turn will be refused \u2014 bump the cap higher to keep going, or end the session.`
16406
+ };
16407
+ }
16408
+ return {
16409
+ info: `budget \u2192 $${usd.toFixed(2)} (so far: $${spent.toFixed(4)} \xB7 warns at 80%, refuses next turn at 100% \xB7 /budget off to clear)`
16410
+ };
16411
+ };
16320
16412
  var handlers9 = {
16321
16413
  model,
16322
16414
  models,
@@ -16324,7 +16416,8 @@ var handlers9 = {
16324
16416
  preset,
16325
16417
  branch,
16326
16418
  effort,
16327
- pro
16419
+ pro,
16420
+ budget
16328
16421
  };
16329
16422
 
16330
16423
  // src/cli/ui/slash/handlers/observability.ts
@@ -17577,6 +17670,7 @@ function App({
17577
17670
  transcript,
17578
17671
  harvest: harvest3,
17579
17672
  branch: branch2,
17673
+ budgetUsd,
17580
17674
  session,
17581
17675
  tools,
17582
17676
  mcpSpecs,
@@ -17790,6 +17884,7 @@ function App({
17790
17884
  model: model2,
17791
17885
  harvest: harvest3,
17792
17886
  branch: branch2,
17887
+ budgetUsd,
17793
17888
  session,
17794
17889
  hooks: hookList,
17795
17890
  hookCwd: currentRootDir,
@@ -17800,7 +17895,7 @@ function App({
17800
17895
  });
17801
17896
  loopRef.current = l;
17802
17897
  return l;
17803
- }, [model2, system, harvest3, branch2, session, tools, codeMode]);
17898
+ }, [model2, system, harvest3, branch2, budgetUsd, session, tools, codeMode]);
17804
17899
  useEffect6(() => {
17805
17900
  loop2.hooks = hookList;
17806
17901
  }, [loop2, hookList]);
@@ -18129,11 +18224,11 @@ function App({
18129
18224
  if (key.escape && busy) {
18130
18225
  if (abortedThisTurn.current) return;
18131
18226
  abortedThisTurn.current = true;
18132
- const resolve13 = editReviewResolveRef.current;
18133
- if (resolve13) {
18227
+ const resolve14 = editReviewResolveRef.current;
18228
+ if (resolve14) {
18134
18229
  editReviewResolveRef.current = null;
18135
18230
  setPendingEditReview(null);
18136
- resolve13("reject");
18231
+ resolve14("reject");
18137
18232
  }
18138
18233
  if (activeLoopRef.current) stopLoop();
18139
18234
  loop2.abort();
@@ -18599,11 +18694,11 @@ function App({
18599
18694
  handleStagedInputSubmitRef.current(text ?? "", { plan: plan2, mode: choice }).catch(() => void 0);
18600
18695
  },
18601
18696
  resolveEditReview: (choice) => {
18602
- const resolve13 = editReviewResolveRef.current;
18603
- if (resolve13) {
18697
+ const resolve14 = editReviewResolveRef.current;
18698
+ if (resolve14) {
18604
18699
  editReviewResolveRef.current = null;
18605
18700
  setPendingEditReview(null);
18606
- resolve13(choice);
18701
+ resolve14(choice);
18607
18702
  }
18608
18703
  },
18609
18704
  resolveWorkspaceConfirm: (choice) => {
@@ -20045,7 +20140,8 @@ Continue executing from the next pending step. Call mark_step_complete after eac
20045
20140
  updateAvailable,
20046
20141
  proArmed,
20047
20142
  escalated: turnOnPro,
20048
- dashboardUrl
20143
+ dashboardUrl,
20144
+ budgetUsd: loop2.budgetUsd
20049
20145
  }
20050
20146
  ), /* @__PURE__ */ React24.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React24.createElement(EventRow, { key: item.id, event: item, projectRoot: currentRootDir })), !historical.some((e) => e.role === "user" || e.role === "assistant") && !busy && !streaming ? /* @__PURE__ */ React24.createElement(WelcomeBanner, { inCodeMode: !!codeMode }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React24.createElement(Box22, { marginY: 1 }, /* @__PURE__ */ React24.createElement(EventRow, { event: streaming, projectRoot: currentRootDir })) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React24.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React24.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React24.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React24.createElement(
20051
20147
  PlanRefineInput,
@@ -20128,10 +20224,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
20128
20224
  {
20129
20225
  block: pendingEditReview,
20130
20226
  onChoose: (choice) => {
20131
- const resolve13 = editReviewResolveRef.current;
20132
- if (resolve13) {
20227
+ const resolve14 = editReviewResolveRef.current;
20228
+ if (resolve14) {
20133
20229
  editReviewResolveRef.current = null;
20134
- resolve13(choice);
20230
+ resolve14(choice);
20135
20231
  }
20136
20232
  }
20137
20233
  }
@@ -20316,6 +20412,7 @@ function Root({
20316
20412
  transcript: appProps.transcript,
20317
20413
  harvest: appProps.harvest,
20318
20414
  branch: appProps.branch,
20415
+ budgetUsd: appProps.budgetUsd,
20319
20416
  session: appProps.session,
20320
20417
  tools,
20321
20418
  mcpSpecs,
@@ -20477,6 +20574,7 @@ async function codeCommand(opts = {}) {
20477
20574
  await chatCommand({
20478
20575
  model: opts.model ?? "deepseek-v4-flash",
20479
20576
  harvest: opts.harvest ?? false,
20577
+ budgetUsd: opts.budgetUsd,
20480
20578
  system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
20481
20579
  transcript: opts.transcript,
20482
20580
  session,
@@ -20528,8 +20626,8 @@ function CacheBadge({ usage }) {
20528
20626
  const total = hit + miss;
20529
20627
  if (total === 0) return null;
20530
20628
  const pct2 = hit / total * 100;
20531
- const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
20532
- return /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React28.createElement(Text23, { color }, pct2.toFixed(1), "%"));
20629
+ const color2 = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
20630
+ return /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React28.createElement(Text23, { color: color2 }, pct2.toFixed(1), "%"));
20533
20631
  }
20534
20632
  function truncate2(s, max) {
20535
20633
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -20652,8 +20750,313 @@ markdown report written to ${opts.mdPath}`);
20652
20750
  console.log(renderSummaryTable(report));
20653
20751
  }
20654
20752
 
20753
+ // src/cli/commands/doctor.ts
20754
+ import { existsSync as existsSync23, statSync as statSync14 } from "fs";
20755
+ import { homedir as homedir10 } from "os";
20756
+ import { dirname as dirname16, join as join21, resolve as resolve12 } from "path";
20757
+ var TTY = process.stdout.isTTY && process.env.TERM !== "dumb";
20758
+ function color(text, code) {
20759
+ if (!TTY) return text;
20760
+ return `\x1B[${code}m${text}\x1B[0m`;
20761
+ }
20762
+ function badge(level) {
20763
+ if (level === "ok") return color("\u2713", "32");
20764
+ if (level === "warn") return color("\u26A0", "33");
20765
+ return color("\u2717", "31");
20766
+ }
20767
+ function tail4(s) {
20768
+ return s.length <= 4 ? s : `\u2026${s.slice(-4)}`;
20769
+ }
20770
+ function fmtBytes(n) {
20771
+ if (n < 1024) return `${n} B`;
20772
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
20773
+ return `${(n / 1024 / 1024).toFixed(1)} MB`;
20774
+ }
20775
+ async function checkApiKey() {
20776
+ const fromEnv = process.env.DEEPSEEK_API_KEY;
20777
+ if (fromEnv) {
20778
+ return {
20779
+ label: "api key ",
20780
+ level: "ok",
20781
+ detail: `set via env DEEPSEEK_API_KEY (${tail4(fromEnv)})`
20782
+ };
20783
+ }
20784
+ try {
20785
+ const cfg = readConfig();
20786
+ if (cfg.apiKey) {
20787
+ return {
20788
+ label: "api key ",
20789
+ level: "ok",
20790
+ detail: `from ${defaultConfigPath()} (${tail4(cfg.apiKey)})`
20791
+ };
20792
+ }
20793
+ } catch {
20794
+ }
20795
+ return {
20796
+ label: "api key ",
20797
+ level: "fail",
20798
+ detail: "not set \u2014 `reasonix setup` to save one, or export DEEPSEEK_API_KEY. Get a key at https://platform.deepseek.com/api_keys"
20799
+ };
20800
+ }
20801
+ async function checkConfig() {
20802
+ const path5 = defaultConfigPath();
20803
+ if (!existsSync23(path5)) {
20804
+ return {
20805
+ label: "config ",
20806
+ level: "warn",
20807
+ detail: "missing \u2014 running with library defaults. `reasonix setup` writes one."
20808
+ };
20809
+ }
20810
+ try {
20811
+ const cfg = readConfig(path5);
20812
+ const parts = [];
20813
+ if (cfg.preset) parts.push(`preset=${cfg.preset}`);
20814
+ if (cfg.editMode) parts.push(`editMode=${cfg.editMode}`);
20815
+ if (cfg.mcp && cfg.mcp.length > 0) parts.push(`mcp=${cfg.mcp.length}`);
20816
+ return {
20817
+ label: "config ",
20818
+ level: "ok",
20819
+ detail: `${path5}${parts.length ? ` (${parts.join(", ")})` : ""}`
20820
+ };
20821
+ } catch (err) {
20822
+ return {
20823
+ label: "config ",
20824
+ level: "fail",
20825
+ detail: `${path5} unreadable \u2014 ${err.message}`
20826
+ };
20827
+ }
20828
+ }
20829
+ async function checkApiReach() {
20830
+ const key = process.env.DEEPSEEK_API_KEY ?? readConfig().apiKey;
20831
+ if (!key) {
20832
+ return {
20833
+ label: "api reach ",
20834
+ level: "warn",
20835
+ detail: "skipped \u2014 no api key to test with"
20836
+ };
20837
+ }
20838
+ try {
20839
+ const client = new DeepSeekClient({ apiKey: key });
20840
+ const ctl = new AbortController();
20841
+ const timer = setTimeout(() => ctl.abort(), 8e3);
20842
+ let balance;
20843
+ try {
20844
+ balance = await client.getBalance({ signal: ctl.signal });
20845
+ } finally {
20846
+ clearTimeout(timer);
20847
+ }
20848
+ if (!balance) {
20849
+ return {
20850
+ label: "api reach ",
20851
+ level: "fail",
20852
+ detail: "/user/balance returned null \u2014 auth failed or network blocked"
20853
+ };
20854
+ }
20855
+ if (!balance.is_available) {
20856
+ const info2 = balance.balance_infos[0];
20857
+ return {
20858
+ label: "api reach ",
20859
+ level: "warn",
20860
+ detail: `account flagged not-available${info2 ? ` (${info2.total_balance} ${info2.currency})` : ""} \u2014 top up or check your dashboard`
20861
+ };
20862
+ }
20863
+ const info = balance.balance_infos[0];
20864
+ return {
20865
+ label: "api reach ",
20866
+ level: "ok",
20867
+ detail: info ? `/user/balance ok \u2014 ${info.total_balance} ${info.currency}` : "/user/balance ok"
20868
+ };
20869
+ } catch (err) {
20870
+ return {
20871
+ label: "api reach ",
20872
+ level: "fail",
20873
+ detail: `${err.message}`
20874
+ };
20875
+ }
20876
+ }
20877
+ async function checkTokenizer() {
20878
+ const candidates = [
20879
+ join21(
20880
+ dirname16(new URL(import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, "$1")),
20881
+ "..",
20882
+ "..",
20883
+ "..",
20884
+ "data",
20885
+ "deepseek-tokenizer.json.gz"
20886
+ ),
20887
+ join21(process.cwd(), "data", "deepseek-tokenizer.json.gz")
20888
+ ];
20889
+ for (const p of candidates) {
20890
+ if (existsSync23(p)) {
20891
+ try {
20892
+ const stat2 = statSync14(p);
20893
+ return {
20894
+ label: "tokenizer ",
20895
+ level: "ok",
20896
+ detail: `${p} (${fmtBytes(stat2.size)})`
20897
+ };
20898
+ } catch {
20899
+ }
20900
+ }
20901
+ }
20902
+ return {
20903
+ label: "tokenizer ",
20904
+ level: "warn",
20905
+ detail: "data/deepseek-tokenizer.json.gz not found \u2014 token counts will fall back to char heuristics"
20906
+ };
20907
+ }
20908
+ async function checkSessions() {
20909
+ try {
20910
+ const list = listSessions();
20911
+ if (list.length === 0) {
20912
+ return {
20913
+ label: "sessions ",
20914
+ level: "ok",
20915
+ detail: "0 saved"
20916
+ };
20917
+ }
20918
+ const totalBytes = list.reduce((s, e) => s + e.size, 0);
20919
+ const oldest = list[list.length - 1];
20920
+ const ageDays = Math.floor((Date.now() - oldest.mtime.getTime()) / (24 * 60 * 60 * 1e3));
20921
+ const stale = list.filter(
20922
+ (e) => Date.now() - e.mtime.getTime() >= 90 * 24 * 60 * 60 * 1e3
20923
+ ).length;
20924
+ const detail = `${list.length} saved \xB7 ${fmtBytes(totalBytes)} \xB7 oldest ${ageDays}d`;
20925
+ if (stale > 0) {
20926
+ return {
20927
+ label: "sessions ",
20928
+ level: "warn",
20929
+ detail: `${detail} \xB7 ${stale} idle \u226590d (run /prune-sessions)`
20930
+ };
20931
+ }
20932
+ return { label: "sessions ", level: "ok", detail };
20933
+ } catch (err) {
20934
+ return {
20935
+ label: "sessions ",
20936
+ level: "warn",
20937
+ detail: `cannot list \u2014 ${err.message}`
20938
+ };
20939
+ }
20940
+ }
20941
+ async function checkHooks(projectRoot) {
20942
+ try {
20943
+ const all = loadHooks({ projectRoot });
20944
+ const global = all.filter((h) => h.scope === "global").length;
20945
+ const project = all.filter((h) => h.scope === "project").length;
20946
+ return {
20947
+ label: "hooks ",
20948
+ level: "ok",
20949
+ detail: `${global} global, ${project} project`
20950
+ };
20951
+ } catch (err) {
20952
+ return {
20953
+ label: "hooks ",
20954
+ level: "warn",
20955
+ detail: `couldn't parse settings.json \u2014 ${err.message}`
20956
+ };
20957
+ }
20958
+ }
20959
+ async function checkOllama(projectRoot) {
20960
+ let exists = false;
20961
+ try {
20962
+ exists = await indexExists(projectRoot);
20963
+ } catch {
20964
+ }
20965
+ if (!exists) {
20966
+ return {
20967
+ label: "ollama ",
20968
+ level: "ok",
20969
+ detail: "not in use (no semantic index built; `reasonix index` to enable)"
20970
+ };
20971
+ }
20972
+ try {
20973
+ const status2 = await checkOllamaStatus(process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text");
20974
+ if (!status2.binaryFound) {
20975
+ return {
20976
+ label: "ollama ",
20977
+ level: "warn",
20978
+ detail: "binary not on PATH \u2014 semantic_search will fail; install from https://ollama.com"
20979
+ };
20980
+ }
20981
+ if (!status2.daemonRunning) {
20982
+ return {
20983
+ label: "ollama ",
20984
+ level: "warn",
20985
+ detail: "daemon not running \u2014 `ollama serve` (or just call /semantic in TUI to auto-start)"
20986
+ };
20987
+ }
20988
+ if (!status2.modelPulled) {
20989
+ return {
20990
+ label: "ollama ",
20991
+ level: "warn",
20992
+ detail: `model ${status2.modelName} not pulled \u2014 \`ollama pull ${status2.modelName}\``
20993
+ };
20994
+ }
20995
+ return {
20996
+ label: "ollama ",
20997
+ level: "ok",
20998
+ detail: `daemon up \xB7 model ${status2.modelName} ready`
20999
+ };
21000
+ } catch (err) {
21001
+ return {
21002
+ label: "ollama ",
21003
+ level: "warn",
21004
+ detail: `probe failed \u2014 ${err.message}`
21005
+ };
21006
+ }
21007
+ }
21008
+ async function checkProject(projectRoot) {
21009
+ const markers = [".git", "REASONIX.md", "package.json", "pyproject.toml", "Cargo.toml", "go.mod"];
21010
+ const found = markers.filter((m) => existsSync23(join21(projectRoot, m)));
21011
+ if (found.length === 0) {
21012
+ return {
21013
+ label: "project ",
21014
+ level: "warn",
21015
+ detail: `${projectRoot} has none of: ${markers.slice(0, 3).join(", ")} \u2026 \u2014 \`reasonix code\` will still run, but @-mentions and project memory have nothing to anchor`
21016
+ };
21017
+ }
21018
+ return {
21019
+ label: "project ",
21020
+ level: "ok",
21021
+ detail: `${projectRoot} (${found.join(", ")})`
21022
+ };
21023
+ }
21024
+ async function doctorCommand() {
21025
+ loadDotenv();
21026
+ const projectRoot = resolve12(process.cwd());
21027
+ console.log(`${color(`reasonix ${VERSION} \xB7 doctor`, "1")} (cwd: ${projectRoot})`);
21028
+ console.log(` home: ${homedir10()}`);
21029
+ console.log("");
21030
+ const checks = await Promise.all([
21031
+ checkApiKey(),
21032
+ checkConfig(),
21033
+ checkApiReach(),
21034
+ checkTokenizer(),
21035
+ checkSessions(),
21036
+ checkHooks(projectRoot),
21037
+ checkOllama(projectRoot),
21038
+ checkProject(projectRoot)
21039
+ ]);
21040
+ for (const c of checks) {
21041
+ console.log(` ${badge(c.level)} ${c.label} ${c.detail}`);
21042
+ }
21043
+ const ok = checks.filter((c) => c.level === "ok").length;
21044
+ const warn = checks.filter((c) => c.level === "warn").length;
21045
+ const fail = checks.filter((c) => c.level === "fail").length;
21046
+ console.log("");
21047
+ const summary = `${ok} ok \xB7 ${warn} warn \xB7 ${fail} fail`;
21048
+ if (fail > 0) {
21049
+ console.log(color(summary, "31"));
21050
+ process.exit(1);
21051
+ } else if (warn > 0) {
21052
+ console.log(color(summary, "33"));
21053
+ } else {
21054
+ console.log(color(summary, "32"));
21055
+ }
21056
+ }
21057
+
20655
21058
  // src/cli/commands/index.ts
20656
- import { resolve as resolve12 } from "path";
21059
+ import { resolve as resolve13 } from "path";
20657
21060
 
20658
21061
  // src/index/semantic/preflight.ts
20659
21062
  import { stdin as stdin2, stdout } from "process";
@@ -20727,7 +21130,7 @@ async function confirm(question, defaultYes) {
20727
21130
 
20728
21131
  // src/cli/commands/index.ts
20729
21132
  async function indexCommand(opts = {}) {
20730
- const root = resolve12(opts.dir ?? process.cwd());
21133
+ const root = resolve13(opts.dir ?? process.cwd());
20731
21134
  const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
20732
21135
  const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
20733
21136
  const preflightOk = await ollamaPreflight({
@@ -21213,7 +21616,8 @@ async function runCommand2(opts) {
21213
21616
  tools,
21214
21617
  model: opts.model,
21215
21618
  harvest: opts.harvest,
21216
- branch: opts.branch
21619
+ branch: opts.branch,
21620
+ budgetUsd: opts.budgetUsd
21217
21621
  });
21218
21622
  const prefixHash = prefix.fingerprint;
21219
21623
  let transcriptStream = null;
@@ -21648,13 +22052,13 @@ function planUpdate(input) {
21648
22052
  };
21649
22053
  }
21650
22054
  function defaultSpawn(argv) {
21651
- return new Promise((resolve13, reject) => {
22055
+ return new Promise((resolve14, reject) => {
21652
22056
  const child = spawn6(argv[0], argv.slice(1), {
21653
22057
  stdio: "inherit",
21654
22058
  shell: process.platform === "win32"
21655
22059
  });
21656
22060
  child.once("error", reject);
21657
- child.once("exit", (code) => resolve13(code ?? 1));
22061
+ child.once("exit", (code) => resolve14(code ?? 1));
21658
22062
  });
21659
22063
  }
21660
22064
  async function updateCommand(opts = {}) {
@@ -21769,6 +22173,17 @@ Your training data has a cutoff. When an answer's correctness depends on somethi
21769
22173
  The signal isn't a topic list \u2014 it's: "if I'm wrong about this, is it because reality moved on?". If yes, ground the answer in fresh evidence; if no (definitions, mechanisms, well-established APIs), answer from memory.
21770
22174
 
21771
22175
  ${ESCALATION_CONTRACT}`;
22176
+ function parseBudgetFlag(raw) {
22177
+ if (raw === void 0) return void 0;
22178
+ if (!Number.isFinite(raw) || raw <= 0) {
22179
+ process.stderr.write(
22180
+ `\u25B2 ignoring --budget=${raw} (must be a positive number) \u2014 running with no cap
22181
+ `
22182
+ );
22183
+ return void 0;
22184
+ }
22185
+ return raw;
22186
+ }
21772
22187
  var program = new Command();
21773
22188
  program.name("reasonix").description("DeepSeek-native agent framework \u2014 built for cache hits and cheap tokens.").version(VERSION).option(
21774
22189
  "-c, --continue",
@@ -21806,6 +22221,10 @@ program.command("code [dir]").description(
21806
22221
  ).option("-m, --model <id>", "Override default model (v4-flash)").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
21807
22222
  "--harvest",
21808
22223
  "Opt-in Pillar-2 plan-state extraction. Adds one flash call per turn; off by default (no preset enables it)."
22224
+ ).option(
22225
+ "--budget <usd>",
22226
+ "Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
22227
+ (v) => Number.parseFloat(v)
21809
22228
  ).option(
21810
22229
  "--no-dashboard",
21811
22230
  "Suppress the auto-launched embedded web dashboard. Default behavior boots it on TUI mount and shows the URL in the status bar (clickable in OSC-8-aware terminals)."
@@ -21818,6 +22237,7 @@ program.command("code [dir]").description(
21818
22237
  forceResume: !!opts.resume,
21819
22238
  forceNew: !!opts.new,
21820
22239
  harvest: !!opts.harvest,
22240
+ budgetUsd: parseBudgetFlag(opts.budget),
21821
22241
  noDashboard: opts.dashboard === false
21822
22242
  });
21823
22243
  });
@@ -21831,6 +22251,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
21831
22251
  "--branch <n>",
21832
22252
  "Self-consistency: run N parallel samples per turn (N\xD7 cost). Manual only \u2014 never auto-enabled.",
21833
22253
  (v) => Number.parseInt(v, 10)
22254
+ ).option(
22255
+ "--budget <usd>",
22256
+ "Soft USD cap on session spend. Off by default. Warns at 80%, refuses next turn at 100%. Mid-session: /budget <usd> or /budget off.",
22257
+ (v) => Number.parseFloat(v)
21834
22258
  ).option("--session <name>", "Use a named session (default: from config, usually 'default').").option("--no-session", "Disable session persistence for this run (ephemeral chat)").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option(
21835
22259
  "-c, --continue",
21836
22260
  "Resume the most-recently-used session (any name) without showing the picker."
@@ -21868,6 +22292,7 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
21868
22292
  transcript: opts.transcript,
21869
22293
  harvest: defaults.harvest,
21870
22294
  branch: defaults.branch,
22295
+ budgetUsd: parseBudgetFlag(opts.budget),
21871
22296
  session: continueOpts.session,
21872
22297
  mcp: defaults.mcp,
21873
22298
  mcpPrefix: opts.mcpPrefix,
@@ -21880,6 +22305,10 @@ program.command("run <task>").description("Run a single task non-interactively,
21880
22305
  "--branch <n>",
21881
22306
  "Self-consistency: run N parallel samples per turn and pick the most confident",
21882
22307
  (v) => Number.parseInt(v, 10)
22308
+ ).option(
22309
+ "--budget <usd>",
22310
+ "Soft USD cap on session spend. Off by default. Refuses to start a new turn once cumulative cost \u2265 cap.",
22311
+ (v) => Number.parseFloat(v)
21883
22312
  ).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
21884
22313
  "--mcp <spec>",
21885
22314
  'MCP server spec; repeatable. "name=cmd args...", "cmd args...", or a URL (http/https \u2192 SSE).',
@@ -21903,6 +22332,7 @@ program.command("run <task>").description("Run a single task non-interactively,
21903
22332
  system: applyMemoryStack(opts.system, process.cwd()),
21904
22333
  harvest: defaults.harvest,
21905
22334
  branch: defaults.branch,
22335
+ budgetUsd: parseBudgetFlag(opts.budget),
21906
22336
  transcript: opts.transcript,
21907
22337
  mcp: defaults.mcp,
21908
22338
  mcpPrefix: opts.mcpPrefix
@@ -21913,6 +22343,11 @@ program.command("stats [transcript]").description(
21913
22343
  ).action((transcript) => {
21914
22344
  statsCommand({ transcript });
21915
22345
  });
22346
+ program.command("doctor").description(
22347
+ "One-command health check \u2014 API key, config, /user/balance reachability, tokenizer, sessions, hooks, Ollama (if used), project markers. Exit 1 on any fail; 0 on warn / clean."
22348
+ ).action(async () => {
22349
+ await doctorCommand();
22350
+ });
21916
22351
  program.command("sessions [name]").description("List saved chat sessions, or inspect one by name.").option("-v, --verbose", "Include system prompts + tool-call metadata when inspecting").action((name, opts) => {
21917
22352
  sessionsCommand({ name, verbose: !!opts.verbose });
21918
22353
  });