reasonix 0.4.23 → 0.4.24

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/README.md CHANGED
@@ -148,6 +148,29 @@ reasonix code › /commit "fix: findByEmail case-insensitive"
148
148
  writable).
149
149
  - `npx reasonix code --no-session` — ephemeral; nothing saved.
150
150
 
151
+ ### `reasonix stats` — how much did you actually save?
152
+
153
+ Every turn `reasonix chat|code|run` runs appends a compact record
154
+ (tokens + cost + what Claude Sonnet 4.6 would have charged) to
155
+ `~/.reasonix/usage.jsonl`. `reasonix stats` with no args rolls that
156
+ log into today / week / month / all-time windows:
157
+
158
+ ```
159
+ Reasonix usage — /Users/you/.reasonix/usage.jsonl
160
+
161
+ turns cache hit cost (USD) vs Claude saved
162
+ ----------------------------------------------------------------------
163
+ today 8 95.1% $0.004821 $0.1348 96.4%
164
+ week 34 93.8% $0.023104 $0.6081 96.2%
165
+ month 127 94.2% $0.081530 $2.1452 96.2%
166
+ all-time 342 94.0% $0.210881 $5.8934 96.4%
167
+ ```
168
+
169
+ Privacy: only tokens, costs, and the session name you chose land
170
+ in the file. No prompts, no completions, no tool arguments.
171
+ `reasonix stats <transcript>` keeps the old per-file summary
172
+ (assistant turns + tool calls) for scripts that already use it.
173
+
151
174
  ### Staying current
152
175
 
153
176
  The panel header shows the running version next to `Reasonix` (e.g.
package/dist/cli/index.js CHANGED
@@ -2459,10 +2459,10 @@ function registerFilesystemTools(registry, opts) {
2459
2459
  const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
2460
2460
  await fs.writeFile(abs, after, "utf8");
2461
2461
  const rel = pathMod.relative(rootDir, abs);
2462
- const header = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
2462
+ const header2 = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
2463
2463
  const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
2464
2464
  const diff = renderEditDiff(args.search, args.replace, startLine);
2465
- return `${header}
2465
+ return `${header2}
2466
2466
  ${diff}`;
2467
2467
  }
2468
2468
  });
@@ -3005,11 +3005,11 @@ function registerShellTools(registry, opts) {
3005
3005
  return registry;
3006
3006
  }
3007
3007
  function formatCommandResult(cmd, r) {
3008
- const header = r.timedOut ? `$ ${cmd}
3008
+ const header2 = r.timedOut ? `$ ${cmd}
3009
3009
  [killed after timeout]` : `$ ${cmd}
3010
3010
  [exit ${r.exitCode ?? "?"}]`;
3011
- return r.output ? `${header}
3012
- ${r.output}` : header;
3011
+ return r.output ? `${header2}
3012
+ ${r.output}` : header2;
3013
3013
  }
3014
3014
 
3015
3015
  // src/tools/web.ts
@@ -3174,9 +3174,9 @@ function registerWebTools(registry, opts = {}) {
3174
3174
  throw new Error("web_fetch: url must start with http:// or https://");
3175
3175
  }
3176
3176
  const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
3177
- const header = page.title ? `${page.title}
3177
+ const header2 = page.title ? `${page.title}
3178
3178
  ${page.url}` : page.url;
3179
- return `${header}
3179
+ return `${header2}
3180
3180
 
3181
3181
  ${page.text}`;
3182
3182
  }
@@ -4582,8 +4582,134 @@ function isNpxInstall() {
4582
4582
  return false;
4583
4583
  }
4584
4584
 
4585
+ // src/usage.ts
4586
+ import { appendFileSync as appendFileSync2, existsSync as existsSync6, mkdirSync as mkdirSync5, readFileSync as readFileSync8, statSync as statSync3 } from "fs";
4587
+ import { homedir as homedir5 } from "os";
4588
+ import { dirname as dirname6, join as join6 } from "path";
4589
+ function defaultUsageLogPath(homeDirOverride) {
4590
+ return join6(homeDirOverride ?? homedir5(), ".reasonix", "usage.jsonl");
4591
+ }
4592
+ function appendUsage(input) {
4593
+ const record = {
4594
+ ts: input.now ?? Date.now(),
4595
+ session: input.session,
4596
+ model: input.model,
4597
+ promptTokens: input.usage.promptTokens,
4598
+ completionTokens: input.usage.completionTokens,
4599
+ cacheHitTokens: input.usage.promptCacheHitTokens,
4600
+ cacheMissTokens: input.usage.promptCacheMissTokens,
4601
+ costUsd: costUsd(input.model, input.usage),
4602
+ claudeEquivUsd: claudeEquivalentCost(input.usage)
4603
+ };
4604
+ const path = input.path ?? defaultUsageLogPath();
4605
+ try {
4606
+ mkdirSync5(dirname6(path), { recursive: true });
4607
+ appendFileSync2(path, `${JSON.stringify(record)}
4608
+ `, "utf8");
4609
+ } catch {
4610
+ }
4611
+ return record;
4612
+ }
4613
+ function readUsageLog(path = defaultUsageLogPath()) {
4614
+ if (!existsSync6(path)) return [];
4615
+ let raw;
4616
+ try {
4617
+ raw = readFileSync8(path, "utf8");
4618
+ } catch {
4619
+ return [];
4620
+ }
4621
+ const out = [];
4622
+ for (const line of raw.split(/\r?\n/)) {
4623
+ if (!line.trim()) continue;
4624
+ try {
4625
+ const rec = JSON.parse(line);
4626
+ if (isValidRecord(rec)) out.push(rec);
4627
+ } catch {
4628
+ }
4629
+ }
4630
+ return out;
4631
+ }
4632
+ function isValidRecord(rec) {
4633
+ if (!rec || typeof rec !== "object") return false;
4634
+ const r = rec;
4635
+ return typeof r.ts === "number" && typeof r.model === "string" && typeof r.promptTokens === "number" && typeof r.completionTokens === "number" && typeof r.cacheHitTokens === "number" && typeof r.cacheMissTokens === "number" && typeof r.costUsd === "number" && typeof r.claudeEquivUsd === "number";
4636
+ }
4637
+ function bucketCacheHitRatio(b) {
4638
+ const denom = b.cacheHitTokens + b.cacheMissTokens;
4639
+ return denom > 0 ? b.cacheHitTokens / denom : 0;
4640
+ }
4641
+ function bucketSavingsFraction(b) {
4642
+ return b.claudeEquivUsd > 0 ? 1 - b.costUsd / b.claudeEquivUsd : 0;
4643
+ }
4644
+ function emptyBucket(label, since) {
4645
+ return {
4646
+ label,
4647
+ since,
4648
+ turns: 0,
4649
+ promptTokens: 0,
4650
+ completionTokens: 0,
4651
+ cacheHitTokens: 0,
4652
+ cacheMissTokens: 0,
4653
+ costUsd: 0,
4654
+ claudeEquivUsd: 0
4655
+ };
4656
+ }
4657
+ function addToBucket(b, r) {
4658
+ b.turns += 1;
4659
+ b.promptTokens += r.promptTokens;
4660
+ b.completionTokens += r.completionTokens;
4661
+ b.cacheHitTokens += r.cacheHitTokens;
4662
+ b.cacheMissTokens += r.cacheMissTokens;
4663
+ b.costUsd += r.costUsd;
4664
+ b.claudeEquivUsd += r.claudeEquivUsd;
4665
+ }
4666
+ function aggregateUsage(records, opts = {}) {
4667
+ const now = opts.now ?? Date.now();
4668
+ const day = 24 * 60 * 60 * 1e3;
4669
+ const today = emptyBucket("today", now - day);
4670
+ const week = emptyBucket("week", now - 7 * day);
4671
+ const month = emptyBucket("month", now - 30 * day);
4672
+ const all = emptyBucket("all-time", 0);
4673
+ const modelCounts = /* @__PURE__ */ new Map();
4674
+ const sessionCounts = /* @__PURE__ */ new Map();
4675
+ let firstSeen = null;
4676
+ let lastSeen = null;
4677
+ for (const r of records) {
4678
+ addToBucket(all, r);
4679
+ if (r.ts >= today.since) addToBucket(today, r);
4680
+ if (r.ts >= week.since) addToBucket(week, r);
4681
+ if (r.ts >= month.since) addToBucket(month, r);
4682
+ modelCounts.set(r.model, (modelCounts.get(r.model) ?? 0) + 1);
4683
+ const sessKey = r.session ?? "(ephemeral)";
4684
+ sessionCounts.set(sessKey, (sessionCounts.get(sessKey) ?? 0) + 1);
4685
+ if (firstSeen === null || r.ts < firstSeen) firstSeen = r.ts;
4686
+ if (lastSeen === null || r.ts > lastSeen) lastSeen = r.ts;
4687
+ }
4688
+ const byModel = Array.from(modelCounts.entries()).map(([model, turns]) => ({ model, turns })).sort((a, b) => b.turns - a.turns);
4689
+ const bySession = Array.from(sessionCounts.entries()).map(([session, turns]) => ({ session, turns })).sort((a, b) => b.turns - a.turns);
4690
+ return {
4691
+ buckets: [today, week, month, all],
4692
+ byModel,
4693
+ bySession,
4694
+ firstSeen,
4695
+ lastSeen
4696
+ };
4697
+ }
4698
+ function formatLogSize(path = defaultUsageLogPath()) {
4699
+ if (!existsSync6(path)) return "";
4700
+ try {
4701
+ const s = statSync3(path);
4702
+ const bytes = s.size;
4703
+ if (bytes < 1024) return `${bytes} B`;
4704
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
4705
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
4706
+ } catch {
4707
+ return "";
4708
+ }
4709
+ }
4710
+
4585
4711
  // src/cli/commands/chat.tsx
4586
- import { existsSync as existsSync6, statSync as statSync3 } from "fs";
4712
+ import { existsSync as existsSync8, statSync as statSync4 } from "fs";
4587
4713
  import { render } from "ink";
4588
4714
  import React15, { useState as useState7 } from "react";
4589
4715
 
@@ -4622,7 +4748,7 @@ function registerSkillTools(registry, opts = {}) {
4622
4748
  });
4623
4749
  }
4624
4750
  const rawArgs = typeof args.arguments === "string" ? args.arguments.trim() : "";
4625
- const header = [
4751
+ const header2 = [
4626
4752
  `# Skill: ${skill.name}`,
4627
4753
  skill.description ? `> ${skill.description}` : "",
4628
4754
  `(scope: ${skill.scope} \xB7 ${skill.path})`
@@ -4630,7 +4756,7 @@ function registerSkillTools(registry, opts = {}) {
4630
4756
  const argsBlock = rawArgs ? `
4631
4757
 
4632
4758
  Arguments: ${rawArgs}` : "";
4633
- return `${header}
4759
+ return `${header2}
4634
4760
 
4635
4761
  ${skill.body}${argsBlock}`;
4636
4762
  }
@@ -4859,7 +4985,7 @@ function parseBlocks(raw) {
4859
4985
  if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
4860
4986
  flushPara();
4861
4987
  flushList();
4862
- const header = splitTableRow(line);
4988
+ const header2 = splitTableRow(line);
4863
4989
  const rows = [];
4864
4990
  let j = i + 2;
4865
4991
  while (j < lines.length) {
@@ -4868,7 +4994,7 @@ function parseBlocks(raw) {
4868
4994
  rows.push(splitTableRow(r));
4869
4995
  j++;
4870
4996
  }
4871
- out.push({ kind: "table", header, rows });
4997
+ out.push({ kind: "table", header: header2, rows });
4872
4998
  i = j - 1;
4873
4999
  continue;
4874
5000
  }
@@ -4935,7 +5061,7 @@ function TableBlockRow({ block }) {
4935
5061
  for (const r of block.rows) cellLengths.push(displayWidth(r[c] ?? ""));
4936
5062
  widths.push(Math.min(40, Math.max(3, ...cellLengths)));
4937
5063
  }
4938
- const pad2 = (s, w) => {
5064
+ const pad3 = (s, w) => {
4939
5065
  const dw = displayWidth(s);
4940
5066
  if (dw >= w) return s;
4941
5067
  return s + " ".repeat(w - dw);
@@ -4943,12 +5069,12 @@ function TableBlockRow({ block }) {
4943
5069
  const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
4944
5070
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
4945
5071
  // biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
4946
- /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, pad2(cell, widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
5072
+ /* @__PURE__ */ React2.createElement(Text2, { key: `h-${ci}`, bold: true, color: "cyan" }, pad3(cell, widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
4947
5073
  ))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
4948
5074
  // biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
4949
5075
  /* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
4950
5076
  // biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
4951
- /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, pad2(row2[ci] ?? "", widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
5077
+ /* @__PURE__ */ React2.createElement(Text2, { key: `c-${ri}-${ci}` }, pad3(row2[ci] ?? "", widths[ci] ?? 3), ci < colCount - 1 ? " \u2502 " : "")
4952
5078
  )))
4953
5079
  )));
4954
5080
  }
@@ -5593,6 +5719,114 @@ function formatTokens(n) {
5593
5719
 
5594
5720
  // src/cli/ui/slash.ts
5595
5721
  import { spawnSync } from "child_process";
5722
+
5723
+ // src/cli/commands/stats.ts
5724
+ import { existsSync as existsSync7, readFileSync as readFileSync9 } from "fs";
5725
+ function statsCommand(opts) {
5726
+ if (opts.transcript) {
5727
+ transcriptSummary(opts.transcript);
5728
+ return;
5729
+ }
5730
+ dashboard(opts);
5731
+ }
5732
+ function transcriptSummary(path) {
5733
+ if (!existsSync7(path)) {
5734
+ console.error(`no such transcript: ${path}`);
5735
+ process.exit(1);
5736
+ }
5737
+ const lines = readFileSync9(path, "utf8").split(/\r?\n/).filter(Boolean);
5738
+ let assistantTurns = 0;
5739
+ let toolCalls = 0;
5740
+ let lastTurn = 0;
5741
+ for (const line of lines) {
5742
+ try {
5743
+ const rec = JSON.parse(line);
5744
+ if (rec.role === "assistant_final") assistantTurns++;
5745
+ if (rec.role === "tool") toolCalls++;
5746
+ if (typeof rec.turn === "number") lastTurn = Math.max(lastTurn, rec.turn);
5747
+ } catch {
5748
+ }
5749
+ }
5750
+ console.log(`transcript: ${path}`);
5751
+ console.log(`assistant turns: ${assistantTurns}`);
5752
+ console.log(`tool invocations: ${toolCalls}`);
5753
+ console.log(`last turn index: ${lastTurn}`);
5754
+ }
5755
+ function dashboard(opts) {
5756
+ const path = opts.logPath ?? defaultUsageLogPath();
5757
+ const records = readUsageLog(path);
5758
+ if (records.length === 0) {
5759
+ console.log("no usage data yet.");
5760
+ console.log("");
5761
+ console.log(` ${path}`);
5762
+ console.log("");
5763
+ console.log("run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn");
5764
+ console.log("appends one line to the log and `reasonix stats` will roll it up.");
5765
+ return;
5766
+ }
5767
+ const agg = aggregateUsage(records, { now: opts.now });
5768
+ console.log(renderDashboard(agg, path));
5769
+ }
5770
+ function renderDashboard(agg, logPath) {
5771
+ const lines = [];
5772
+ const size = formatLogSize(logPath);
5773
+ lines.push(`Reasonix usage \u2014 ${logPath}${size ? ` (${size})` : ""}`);
5774
+ lines.push("");
5775
+ lines.push(header());
5776
+ lines.push(divider());
5777
+ for (const b of agg.buckets) {
5778
+ lines.push(bucketRow(b));
5779
+ }
5780
+ lines.push("");
5781
+ if (agg.byModel.length > 0) {
5782
+ const totalTurns = agg.buckets[agg.buckets.length - 1]?.turns ?? 0;
5783
+ const top = agg.byModel[0];
5784
+ if (top && totalTurns > 0) {
5785
+ const pct2 = (top.turns / totalTurns * 100).toFixed(0);
5786
+ lines.push(`most used model: ${top.model} (${pct2}% of turns)`);
5787
+ }
5788
+ }
5789
+ if (agg.bySession.length > 0) {
5790
+ const top = agg.bySession[0];
5791
+ if (top) lines.push(`top session: ${top.session} (${top.turns} turns)`);
5792
+ }
5793
+ if (agg.firstSeen) {
5794
+ lines.push(`tracked since: ${new Date(agg.firstSeen).toISOString().slice(0, 10)}`);
5795
+ }
5796
+ return lines.join("\n");
5797
+ }
5798
+ function header() {
5799
+ return [
5800
+ pad("", 10),
5801
+ pad("turns", 8, "right"),
5802
+ pad("cache hit", 10, "right"),
5803
+ pad("cost (USD)", 14, "right"),
5804
+ pad("vs Claude", 14, "right"),
5805
+ pad("saved", 10, "right")
5806
+ ].join(" ");
5807
+ }
5808
+ function divider() {
5809
+ return "-".repeat(70);
5810
+ }
5811
+ function bucketRow(b) {
5812
+ const hit = bucketCacheHitRatio(b);
5813
+ const savings = bucketSavingsFraction(b);
5814
+ return [
5815
+ pad(b.label, 10),
5816
+ pad(b.turns.toString(), 8, "right"),
5817
+ pad(b.turns > 0 ? `${(hit * 100).toFixed(1)}%` : "\u2014", 10, "right"),
5818
+ pad(b.turns > 0 ? `$${b.costUsd.toFixed(6)}` : "\u2014", 14, "right"),
5819
+ pad(b.turns > 0 ? `$${b.claudeEquivUsd.toFixed(4)}` : "\u2014", 14, "right"),
5820
+ pad(b.turns > 0 && savings > 0 ? `${(savings * 100).toFixed(1)}%` : "\u2014", 10, "right")
5821
+ ].join(" ");
5822
+ }
5823
+ function pad(s, width, align = "left") {
5824
+ if (s.length >= width) return s;
5825
+ const fill = " ".repeat(width - s.length);
5826
+ return align === "right" ? `${fill}${s}` : `${s}${fill}`;
5827
+ }
5828
+
5829
+ // src/cli/ui/slash.ts
5596
5830
  var SLASH_COMMANDS = [
5597
5831
  { cmd: "help", summary: "show the full command reference" },
5598
5832
  { cmd: "status", summary: "current model, flags, context, session" },
@@ -5625,6 +5859,10 @@ var SLASH_COMMANDS = [
5625
5859
  cmd: "update",
5626
5860
  summary: "show current vs latest version + the shell command to upgrade"
5627
5861
  },
5862
+ {
5863
+ cmd: "stats",
5864
+ summary: "cross-session cost dashboard (today / week / month / all-time \xB7 cache hit \xB7 vs Claude)"
5865
+ },
5628
5866
  { cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
5629
5867
  { cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
5630
5868
  { cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
@@ -5805,6 +6043,9 @@ function handleSlash(cmd, args, loop, ctx = {}) {
5805
6043
  case "update": {
5806
6044
  return handleUpdateSlash(ctx);
5807
6045
  }
6046
+ case "stats": {
6047
+ return handleStatsSlash();
6048
+ }
5808
6049
  case "think":
5809
6050
  case "reasoning": {
5810
6051
  const raw = loop.scratch.reasoning;
@@ -6041,6 +6282,24 @@ ${entry.text}`
6041
6282
  return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
6042
6283
  }
6043
6284
  }
6285
+ function handleStatsSlash() {
6286
+ const path = defaultUsageLogPath();
6287
+ const records = readUsageLog(path);
6288
+ if (records.length === 0) {
6289
+ return {
6290
+ info: [
6291
+ "no usage data yet.",
6292
+ "",
6293
+ ` ${path}`,
6294
+ "",
6295
+ "every turn you run here appends one record \u2014 this session's turns",
6296
+ "will show up in the dashboard once you send a message."
6297
+ ].join("\n")
6298
+ };
6299
+ }
6300
+ const agg = aggregateUsage(records);
6301
+ return { info: renderDashboard(agg, path) };
6302
+ }
6044
6303
  function handleUpdateSlash(ctx) {
6045
6304
  const latest = ctx.latestVersion ?? null;
6046
6305
  const lines = [`current: reasonix ${VERSION}`];
@@ -6185,12 +6444,12 @@ function handleSkillSlash(args, ctx) {
6185
6444
  };
6186
6445
  }
6187
6446
  const extra = args.slice(1).join(" ").trim();
6188
- const header = `# Skill: ${skill.name}${skill.description ? `
6447
+ const header2 = `# Skill: ${skill.name}${skill.description ? `
6189
6448
  > ${skill.description}` : ""}`;
6190
6449
  const argsLine = extra ? `
6191
6450
 
6192
6451
  Arguments: ${extra}` : "";
6193
- const payload = `${header}
6452
+ const payload = `${header2}
6194
6453
 
6195
6454
  ${skill.body}${argsLine}`;
6196
6455
  return {
@@ -6368,9 +6627,9 @@ function appendSection(lines, label, section) {
6368
6627
  }
6369
6628
  function formatToolList(history) {
6370
6629
  const total = history.length;
6371
- const header = `Tool calls in this session (${total}, most recent first):`;
6630
+ const header2 = `Tool calls in this session (${total}, most recent first):`;
6372
6631
  const shown = Math.min(total, 10);
6373
- const lines = [header];
6632
+ const lines = [header2];
6374
6633
  for (let i = 0; i < shown; i++) {
6375
6634
  const entry = history[total - 1 - i];
6376
6635
  if (!entry) continue;
@@ -6864,6 +7123,13 @@ function App({
6864
7123
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
6865
7124
  setStreaming(null);
6866
7125
  setSummary(loop.stats.summary());
7126
+ if (ev.stats?.usage) {
7127
+ appendUsage({
7128
+ session: session ?? null,
7129
+ model: ev.stats.model,
7130
+ usage: ev.stats.usage
7131
+ });
7132
+ }
6867
7133
  const finalText = ev.content || streamRef.text;
6868
7134
  const iterReasoning = streamRef.reasoning || void 0;
6869
7135
  const iterId = `${assistantId}-i${assistantIterCounter.current++}`;
@@ -7006,6 +7272,7 @@ function App({
7006
7272
  mcpSpecs,
7007
7273
  mcpServers,
7008
7274
  planMode,
7275
+ session,
7009
7276
  slashSelected,
7010
7277
  togglePlanMode,
7011
7278
  writeTranscript
@@ -7268,8 +7535,8 @@ function formatEditResults(results) {
7268
7535
  });
7269
7536
  const ok = results.filter((r) => r.status === "applied" || r.status === "created").length;
7270
7537
  const total = results.length;
7271
- const header = `\u25B8 edit blocks: ${ok}/${total} applied \u2014 /undo to roll back, or \`git diff\` to review`;
7272
- return [header, ...lines].join("\n");
7538
+ const header2 = `\u25B8 edit blocks: ${ok}/${total} applied \u2014 /undo to roll back, or \`git diff\` to review`;
7539
+ return [header2, ...lines].join("\n");
7273
7540
  }
7274
7541
  function formatPendingPreview(blocks) {
7275
7542
  const lines = blocks.map((b) => {
@@ -7278,8 +7545,8 @@ function formatPendingPreview(blocks) {
7278
7545
  const tag = b.search === "" ? "NEW " : " ";
7279
7546
  return ` ${tag}${b.path} (-${removed} +${added} lines)`;
7280
7547
  });
7281
- const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
7282
- return [header, ...lines].join("\n");
7548
+ const header2 = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply (or y) to commit \xB7 /discard (or n) to drop`;
7549
+ return [header2, ...lines].join("\n");
7283
7550
  }
7284
7551
  function countLines2(s) {
7285
7552
  if (s.length === 0) return 0;
@@ -7524,7 +7791,7 @@ async function chatCommand(opts) {
7524
7791
  const prior = loadSessionMessages(opts.session);
7525
7792
  if (prior.length > 0) {
7526
7793
  const p = sessionPath(opts.session);
7527
- const mtime = existsSync6(p) ? statSync3(p).mtime : /* @__PURE__ */ new Date();
7794
+ const mtime = existsSync8(p) ? statSync4(p).mtime : /* @__PURE__ */ new Date();
7528
7795
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
7529
7796
  }
7530
7797
  } else if (opts.session && opts.forceNew) {
@@ -7867,7 +8134,7 @@ function mcpListCommand(opts) {
7867
8134
  console.log("Popular MCP servers you can bridge into Reasonix:");
7868
8135
  console.log("");
7869
8136
  for (const entry of MCP_CATALOG) {
7870
- console.log(` ${pad(entry.name, 12)} ${entry.summary}`);
8137
+ console.log(` ${pad2(entry.name, 12)} ${entry.summary}`);
7871
8138
  console.log(` ${mcpCommandFor(entry)}`);
7872
8139
  if (entry.note) console.log(` \xB7 ${entry.note}`);
7873
8140
  console.log("");
@@ -7880,7 +8147,7 @@ function mcpListCommand(opts) {
7880
8147
  " https://mcp.so \u2014 community-maintained catalog"
7881
8148
  );
7882
8149
  }
7883
- function pad(s, width) {
8150
+ function pad2(s, width) {
7884
8151
  return s.length >= width ? s : s + " ".repeat(width - s.length);
7885
8152
  }
7886
8153
 
@@ -8149,6 +8416,9 @@ async function runCommand2(opts) {
8149
8416
  [error] ${ev.error}
8150
8417
  `);
8151
8418
  if (ev.role === "done") process.stdout.write("\n");
8419
+ if (ev.role === "assistant_final" && ev.stats?.usage) {
8420
+ appendUsage({ session: null, model: ev.stats.model, usage: ev.stats.usage });
8421
+ }
8152
8422
  if (transcriptStream && ev.role !== "assistant_delta") {
8153
8423
  writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: opts.model, prefixHash }));
8154
8424
  }
@@ -8545,32 +8815,6 @@ async function setupCommand(_opts = {}) {
8545
8815
  await waitUntilExit();
8546
8816
  }
8547
8817
 
8548
- // src/cli/commands/stats.ts
8549
- import { existsSync as existsSync7, readFileSync as readFileSync8 } from "fs";
8550
- function statsCommand(opts) {
8551
- if (!existsSync7(opts.transcript)) {
8552
- console.error(`no such transcript: ${opts.transcript}`);
8553
- process.exit(1);
8554
- }
8555
- const lines = readFileSync8(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
8556
- let assistantTurns = 0;
8557
- let toolCalls = 0;
8558
- let lastTurn = 0;
8559
- for (const line of lines) {
8560
- try {
8561
- const rec = JSON.parse(line);
8562
- if (rec.role === "assistant_final") assistantTurns++;
8563
- if (rec.role === "tool") toolCalls++;
8564
- if (typeof rec.turn === "number") lastTurn = Math.max(lastTurn, rec.turn);
8565
- } catch {
8566
- }
8567
- }
8568
- console.log(`transcript: ${opts.transcript}`);
8569
- console.log(`assistant turns: ${assistantTurns}`);
8570
- console.log(`tool invocations: ${toolCalls}`);
8571
- console.log(`last turn index: ${lastTurn}`);
8572
- }
8573
-
8574
8818
  // src/cli/commands/update.ts
8575
8819
  import { spawn as spawn4 } from "child_process";
8576
8820
  function planUpdate(input) {
@@ -8796,7 +9040,9 @@ program.command("run <task>").description("Run a single task non-interactively,
8796
9040
  mcpPrefix: opts.mcpPrefix
8797
9041
  });
8798
9042
  });
8799
- program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
9043
+ program.command("stats [transcript]").description(
9044
+ "Show usage dashboard (today / week / month / all-time \xB7 turns \xB7 cache hit \xB7 cost \xB7 savings vs Claude). Pass a transcript path to fall back to the per-file summary (assistant turns + tool calls)."
9045
+ ).action((transcript) => {
8800
9046
  statsCommand({ transcript });
8801
9047
  });
8802
9048
  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) => {