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 +23 -0
- package/dist/cli/index.js +299 -53
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +123 -1
- package/dist/index.js +133 -0
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
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 `${
|
|
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
|
|
3008
|
+
const header2 = r.timedOut ? `$ ${cmd}
|
|
3009
3009
|
[killed after timeout]` : `$ ${cmd}
|
|
3010
3010
|
[exit ${r.exitCode ?? "?"}]`;
|
|
3011
|
-
return r.output ? `${
|
|
3012
|
-
${r.output}` :
|
|
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
|
|
3177
|
+
const header2 = page.title ? `${page.title}
|
|
3178
3178
|
${page.url}` : page.url;
|
|
3179
|
-
return `${
|
|
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
|
|
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
|
|
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 `${
|
|
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
|
|
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
|
|
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" },
|
|
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}` },
|
|
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
|
|
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 = `${
|
|
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
|
|
6630
|
+
const header2 = `Tool calls in this session (${total}, most recent first):`;
|
|
6372
6631
|
const shown = Math.min(total, 10);
|
|
6373
|
-
const lines = [
|
|
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
|
|
7272
|
-
return [
|
|
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
|
|
7282
|
-
return [
|
|
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 =
|
|
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(` ${
|
|
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
|
|
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
|
|
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) => {
|