reasonix 0.4.22 → 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 +77 -0
- package/dist/cli/index.js +738 -96
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +329 -1
- package/dist/index.js +452 -88
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -506,6 +506,174 @@ function resolveTemperatures(budget, custom) {
|
|
|
506
506
|
return out;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
// src/hooks.ts
|
|
510
|
+
import { spawn } from "child_process";
|
|
511
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
512
|
+
import { homedir as homedir2 } from "os";
|
|
513
|
+
import { join as join2 } from "path";
|
|
514
|
+
var HOOK_EVENTS = [
|
|
515
|
+
"PreToolUse",
|
|
516
|
+
"PostToolUse",
|
|
517
|
+
"UserPromptSubmit",
|
|
518
|
+
"Stop"
|
|
519
|
+
];
|
|
520
|
+
var BLOCKING_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "UserPromptSubmit"]);
|
|
521
|
+
var DEFAULT_TIMEOUTS_MS = {
|
|
522
|
+
PreToolUse: 5e3,
|
|
523
|
+
UserPromptSubmit: 5e3,
|
|
524
|
+
PostToolUse: 3e4,
|
|
525
|
+
Stop: 3e4
|
|
526
|
+
};
|
|
527
|
+
var HOOK_SETTINGS_FILENAME = "settings.json";
|
|
528
|
+
var HOOK_SETTINGS_DIRNAME = ".reasonix";
|
|
529
|
+
function globalSettingsPath(homeDirOverride) {
|
|
530
|
+
return join2(homeDirOverride ?? homedir2(), HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
531
|
+
}
|
|
532
|
+
function projectSettingsPath(projectRoot) {
|
|
533
|
+
return join2(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
|
|
534
|
+
}
|
|
535
|
+
function readSettingsFile(path) {
|
|
536
|
+
if (!existsSync(path)) return null;
|
|
537
|
+
try {
|
|
538
|
+
const raw = readFileSync2(path, "utf8");
|
|
539
|
+
const parsed = JSON.parse(raw);
|
|
540
|
+
if (parsed && typeof parsed === "object") return parsed;
|
|
541
|
+
} catch {
|
|
542
|
+
}
|
|
543
|
+
return null;
|
|
544
|
+
}
|
|
545
|
+
function loadHooks(opts = {}) {
|
|
546
|
+
const out = [];
|
|
547
|
+
if (opts.projectRoot) {
|
|
548
|
+
const projPath = projectSettingsPath(opts.projectRoot);
|
|
549
|
+
const settings2 = readSettingsFile(projPath);
|
|
550
|
+
if (settings2) appendResolved(out, settings2, "project", projPath);
|
|
551
|
+
}
|
|
552
|
+
const globalPath = globalSettingsPath(opts.homeDir);
|
|
553
|
+
const settings = readSettingsFile(globalPath);
|
|
554
|
+
if (settings) appendResolved(out, settings, "global", globalPath);
|
|
555
|
+
return out;
|
|
556
|
+
}
|
|
557
|
+
function appendResolved(out, settings, scope, source) {
|
|
558
|
+
if (!settings.hooks) return;
|
|
559
|
+
for (const event of HOOK_EVENTS) {
|
|
560
|
+
const list = settings.hooks[event];
|
|
561
|
+
if (!Array.isArray(list)) continue;
|
|
562
|
+
for (const cfg of list) {
|
|
563
|
+
if (!cfg || typeof cfg.command !== "string" || cfg.command.trim() === "") continue;
|
|
564
|
+
out.push({ ...cfg, event, scope, source });
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
function matchesTool(hook, toolName) {
|
|
569
|
+
if (hook.event !== "PreToolUse" && hook.event !== "PostToolUse") return true;
|
|
570
|
+
const m = hook.match;
|
|
571
|
+
if (!m || m === "*") return true;
|
|
572
|
+
try {
|
|
573
|
+
const re = new RegExp(`^(?:${m})$`);
|
|
574
|
+
return re.test(toolName);
|
|
575
|
+
} catch {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
function defaultSpawner(input) {
|
|
580
|
+
return new Promise((resolve6) => {
|
|
581
|
+
const child = spawn(input.command, {
|
|
582
|
+
cwd: input.cwd,
|
|
583
|
+
shell: true,
|
|
584
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
585
|
+
});
|
|
586
|
+
let stdout2 = "";
|
|
587
|
+
let stderr = "";
|
|
588
|
+
let timedOut = false;
|
|
589
|
+
const timer = setTimeout(() => {
|
|
590
|
+
timedOut = true;
|
|
591
|
+
child.kill("SIGTERM");
|
|
592
|
+
setTimeout(() => {
|
|
593
|
+
try {
|
|
594
|
+
child.kill("SIGKILL");
|
|
595
|
+
} catch {
|
|
596
|
+
}
|
|
597
|
+
}, 500);
|
|
598
|
+
}, input.timeoutMs);
|
|
599
|
+
child.stdout.on("data", (chunk) => {
|
|
600
|
+
stdout2 += chunk.toString("utf8");
|
|
601
|
+
});
|
|
602
|
+
child.stderr.on("data", (chunk) => {
|
|
603
|
+
stderr += chunk.toString("utf8");
|
|
604
|
+
});
|
|
605
|
+
child.once("error", (err) => {
|
|
606
|
+
clearTimeout(timer);
|
|
607
|
+
resolve6({
|
|
608
|
+
exitCode: null,
|
|
609
|
+
stdout: stdout2,
|
|
610
|
+
stderr,
|
|
611
|
+
timedOut: false,
|
|
612
|
+
spawnError: err
|
|
613
|
+
});
|
|
614
|
+
});
|
|
615
|
+
child.once("close", (code) => {
|
|
616
|
+
clearTimeout(timer);
|
|
617
|
+
resolve6({
|
|
618
|
+
exitCode: code,
|
|
619
|
+
stdout: stdout2.trim(),
|
|
620
|
+
stderr: stderr.trim(),
|
|
621
|
+
timedOut
|
|
622
|
+
});
|
|
623
|
+
});
|
|
624
|
+
try {
|
|
625
|
+
child.stdin.write(input.stdin);
|
|
626
|
+
child.stdin.end();
|
|
627
|
+
} catch {
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function formatHookOutcomeMessage(outcome) {
|
|
632
|
+
if (outcome.decision === "pass") return "";
|
|
633
|
+
const detail = (outcome.stderr || outcome.stdout || "").trim();
|
|
634
|
+
const tag = `${outcome.hook.scope}/${outcome.hook.event}`;
|
|
635
|
+
const cmd = outcome.hook.command.length > 60 ? `${outcome.hook.command.slice(0, 60)}\u2026` : outcome.hook.command;
|
|
636
|
+
const head = `hook ${tag} \`${cmd}\` ${outcome.decision}`;
|
|
637
|
+
return detail ? `${head}: ${detail}` : head;
|
|
638
|
+
}
|
|
639
|
+
function decideOutcome(event, raw) {
|
|
640
|
+
if (raw.spawnError) return "error";
|
|
641
|
+
if (raw.timedOut) return BLOCKING_EVENTS.has(event) ? "block" : "warn";
|
|
642
|
+
if (raw.exitCode === 0) return "pass";
|
|
643
|
+
if (raw.exitCode === 2 && BLOCKING_EVENTS.has(event)) return "block";
|
|
644
|
+
return "warn";
|
|
645
|
+
}
|
|
646
|
+
async function runHooks(opts) {
|
|
647
|
+
const spawner = opts.spawner ?? defaultSpawner;
|
|
648
|
+
const event = opts.payload.event;
|
|
649
|
+
const toolName = opts.payload.toolName ?? "";
|
|
650
|
+
const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
|
|
651
|
+
const outcomes = [];
|
|
652
|
+
let blocked = false;
|
|
653
|
+
const stdin2 = `${JSON.stringify(opts.payload)}
|
|
654
|
+
`;
|
|
655
|
+
for (const hook of matching) {
|
|
656
|
+
const start = Date.now();
|
|
657
|
+
const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
|
|
658
|
+
const cwd = hook.cwd ?? opts.payload.cwd;
|
|
659
|
+
const raw = await spawner({ command: hook.command, cwd, stdin: stdin2, timeoutMs });
|
|
660
|
+
const decision = decideOutcome(event, raw);
|
|
661
|
+
outcomes.push({
|
|
662
|
+
hook,
|
|
663
|
+
decision,
|
|
664
|
+
exitCode: raw.exitCode,
|
|
665
|
+
stdout: raw.stdout,
|
|
666
|
+
stderr: raw.stderr || (raw.spawnError ? raw.spawnError.message : "") || (raw.timedOut ? `hook timed out after ${timeoutMs}ms` : ""),
|
|
667
|
+
durationMs: Date.now() - start
|
|
668
|
+
});
|
|
669
|
+
if (decision === "block") {
|
|
670
|
+
blocked = true;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return { event, outcomes, blocked };
|
|
675
|
+
}
|
|
676
|
+
|
|
509
677
|
// src/repair/flatten.ts
|
|
510
678
|
function analyzeSchema(schema) {
|
|
511
679
|
if (!schema) return { shouldFlatten: false, leafCount: 0, maxDepth: 0 };
|
|
@@ -1128,21 +1296,21 @@ function signature2(call) {
|
|
|
1128
1296
|
import {
|
|
1129
1297
|
appendFileSync,
|
|
1130
1298
|
chmodSync as chmodSync2,
|
|
1131
|
-
existsSync,
|
|
1299
|
+
existsSync as existsSync2,
|
|
1132
1300
|
mkdirSync as mkdirSync2,
|
|
1133
|
-
readFileSync as
|
|
1301
|
+
readFileSync as readFileSync3,
|
|
1134
1302
|
readdirSync,
|
|
1135
1303
|
statSync,
|
|
1136
1304
|
unlinkSync,
|
|
1137
1305
|
writeFileSync as writeFileSync2
|
|
1138
1306
|
} from "fs";
|
|
1139
|
-
import { homedir as
|
|
1140
|
-
import { dirname as dirname2, join as
|
|
1307
|
+
import { homedir as homedir3 } from "os";
|
|
1308
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1141
1309
|
function sessionsDir() {
|
|
1142
|
-
return
|
|
1310
|
+
return join3(homedir3(), ".reasonix", "sessions");
|
|
1143
1311
|
}
|
|
1144
1312
|
function sessionPath(name) {
|
|
1145
|
-
return
|
|
1313
|
+
return join3(sessionsDir(), `${sanitizeName(name)}.jsonl`);
|
|
1146
1314
|
}
|
|
1147
1315
|
function sanitizeName(name) {
|
|
1148
1316
|
const cleaned = name.replace(/[^\w\-\u4e00-\u9fa5]/g, "_").slice(0, 64);
|
|
@@ -1150,9 +1318,9 @@ function sanitizeName(name) {
|
|
|
1150
1318
|
}
|
|
1151
1319
|
function loadSessionMessages(name) {
|
|
1152
1320
|
const path = sessionPath(name);
|
|
1153
|
-
if (!
|
|
1321
|
+
if (!existsSync2(path)) return [];
|
|
1154
1322
|
try {
|
|
1155
|
-
const raw =
|
|
1323
|
+
const raw = readFileSync3(path, "utf8");
|
|
1156
1324
|
const out = [];
|
|
1157
1325
|
for (const line of raw.split(/\r?\n/)) {
|
|
1158
1326
|
const trimmed = line.trim();
|
|
@@ -1180,11 +1348,11 @@ function appendSessionMessage(name, message) {
|
|
|
1180
1348
|
}
|
|
1181
1349
|
function listSessions() {
|
|
1182
1350
|
const dir = sessionsDir();
|
|
1183
|
-
if (!
|
|
1351
|
+
if (!existsSync2(dir)) return [];
|
|
1184
1352
|
try {
|
|
1185
1353
|
const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
|
|
1186
1354
|
return files.map((file) => {
|
|
1187
|
-
const path =
|
|
1355
|
+
const path = join3(dir, file);
|
|
1188
1356
|
const stat = statSync(path);
|
|
1189
1357
|
const name = file.replace(/\.jsonl$/, "");
|
|
1190
1358
|
const messageCount = countLines(path);
|
|
@@ -1216,7 +1384,7 @@ function rewriteSession(name, messages) {
|
|
|
1216
1384
|
}
|
|
1217
1385
|
function countLines(path) {
|
|
1218
1386
|
try {
|
|
1219
|
-
const raw =
|
|
1387
|
+
const raw = readFileSync3(path, "utf8");
|
|
1220
1388
|
return raw.split(/\r?\n/).filter((l) => l.trim()).length;
|
|
1221
1389
|
} catch {
|
|
1222
1390
|
return 0;
|
|
@@ -1330,6 +1498,14 @@ var CacheFirstLoop = class {
|
|
|
1330
1498
|
branchEnabled;
|
|
1331
1499
|
branchOptions;
|
|
1332
1500
|
sessionName;
|
|
1501
|
+
/**
|
|
1502
|
+
* Hook list, mutable so `/hooks reload` can swap it without
|
|
1503
|
+
* reconstructing the loop. Default empty — the filter cost on a
|
|
1504
|
+
* tool call is one array length check.
|
|
1505
|
+
*/
|
|
1506
|
+
hooks;
|
|
1507
|
+
/** `cwd` reported to hook stdin. Resolved once at construction. */
|
|
1508
|
+
hookCwd;
|
|
1333
1509
|
/** Number of messages that were pre-loaded from the session file. */
|
|
1334
1510
|
resumedMessageCount;
|
|
1335
1511
|
_turn = 0;
|
|
@@ -1348,6 +1524,8 @@ var CacheFirstLoop = class {
|
|
|
1348
1524
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1349
1525
|
this.model = opts.model ?? "deepseek-chat";
|
|
1350
1526
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1527
|
+
this.hooks = opts.hooks ?? [];
|
|
1528
|
+
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
1351
1529
|
if (typeof opts.branch === "number") {
|
|
1352
1530
|
this.branchOptions = { budget: opts.branch };
|
|
1353
1531
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1811,7 +1989,37 @@ var CacheFirstLoop = class {
|
|
|
1811
1989
|
toolName: name,
|
|
1812
1990
|
toolArgs: args
|
|
1813
1991
|
};
|
|
1814
|
-
const
|
|
1992
|
+
const parsedArgs = safeParseToolArgs(args);
|
|
1993
|
+
const preReport = await runHooks({
|
|
1994
|
+
hooks: this.hooks,
|
|
1995
|
+
payload: {
|
|
1996
|
+
event: "PreToolUse",
|
|
1997
|
+
cwd: this.hookCwd,
|
|
1998
|
+
toolName: name,
|
|
1999
|
+
toolArgs: parsedArgs
|
|
2000
|
+
}
|
|
2001
|
+
});
|
|
2002
|
+
for (const w of hookWarnings(preReport.outcomes, this._turn)) yield w;
|
|
2003
|
+
let result;
|
|
2004
|
+
if (preReport.blocked) {
|
|
2005
|
+
const blocking = preReport.outcomes[preReport.outcomes.length - 1];
|
|
2006
|
+
const reason = (blocking?.stderr || blocking?.stdout || "blocked by PreToolUse hook").trim();
|
|
2007
|
+
result = `[hook block] ${blocking?.hook.command ?? "<unknown>"}
|
|
2008
|
+
${reason}`;
|
|
2009
|
+
} else {
|
|
2010
|
+
result = await this.tools.dispatch(name, args, { signal });
|
|
2011
|
+
const postReport = await runHooks({
|
|
2012
|
+
hooks: this.hooks,
|
|
2013
|
+
payload: {
|
|
2014
|
+
event: "PostToolUse",
|
|
2015
|
+
cwd: this.hookCwd,
|
|
2016
|
+
toolName: name,
|
|
2017
|
+
toolArgs: parsedArgs,
|
|
2018
|
+
toolResult: result
|
|
2019
|
+
}
|
|
2020
|
+
});
|
|
2021
|
+
for (const w of hookWarnings(postReport.outcomes, this._turn)) yield w;
|
|
2022
|
+
}
|
|
1815
2023
|
this.appendAndPersist({
|
|
1816
2024
|
role: "tool",
|
|
1817
2025
|
tool_call_id: call.id ?? "",
|
|
@@ -1898,6 +2106,19 @@ function stripHallucinatedToolMarkup(s) {
|
|
|
1898
2106
|
out = out.replace(/<|DSML|[\s\S]*$/g, "");
|
|
1899
2107
|
return out.trim();
|
|
1900
2108
|
}
|
|
2109
|
+
function safeParseToolArgs(raw) {
|
|
2110
|
+
try {
|
|
2111
|
+
return JSON.parse(raw);
|
|
2112
|
+
} catch {
|
|
2113
|
+
return raw;
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
function* hookWarnings(outcomes, turn) {
|
|
2117
|
+
for (const o of outcomes) {
|
|
2118
|
+
if (o.decision === "pass") continue;
|
|
2119
|
+
yield { turn, role: "warning", content: formatHookOutcomeMessage(o) };
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
1901
2122
|
function reasonPrefixFor(reason, iterCap) {
|
|
1902
2123
|
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1903
2124
|
if (reason === "context-guard") {
|
|
@@ -2238,10 +2459,10 @@ function registerFilesystemTools(registry, opts) {
|
|
|
2238
2459
|
const after = before.slice(0, firstIdx) + args.replace + before.slice(firstIdx + args.search.length);
|
|
2239
2460
|
await fs.writeFile(abs, after, "utf8");
|
|
2240
2461
|
const rel = pathMod.relative(rootDir, abs);
|
|
2241
|
-
const
|
|
2462
|
+
const header2 = `edited ${rel} (${args.search.length}\u2192${args.replace.length} chars)`;
|
|
2242
2463
|
const startLine = before.slice(0, firstIdx).split(/\r?\n/).length;
|
|
2243
2464
|
const diff = renderEditDiff(args.search, args.replace, startLine);
|
|
2244
|
-
return `${
|
|
2465
|
+
return `${header2}
|
|
2245
2466
|
${diff}`;
|
|
2246
2467
|
}
|
|
2247
2468
|
});
|
|
@@ -2493,8 +2714,8 @@ function registerPlanTool(registry, opts = {}) {
|
|
|
2493
2714
|
}
|
|
2494
2715
|
|
|
2495
2716
|
// src/tools/shell.ts
|
|
2496
|
-
import { spawn } from "child_process";
|
|
2497
|
-
import { existsSync as
|
|
2717
|
+
import { spawn as spawn2 } from "child_process";
|
|
2718
|
+
import { existsSync as existsSync3, statSync as statSync2 } from "fs";
|
|
2498
2719
|
import * as pathMod2 from "path";
|
|
2499
2720
|
var DEFAULT_TIMEOUT_SEC = 60;
|
|
2500
2721
|
var DEFAULT_MAX_OUTPUT_CHARS = 32e3;
|
|
@@ -2617,7 +2838,7 @@ async function runCommand(cmd, opts) {
|
|
|
2617
2838
|
return await new Promise((resolve6, reject) => {
|
|
2618
2839
|
let child;
|
|
2619
2840
|
try {
|
|
2620
|
-
child =
|
|
2841
|
+
child = spawn2(bin, args, effectiveSpawnOpts);
|
|
2621
2842
|
} catch (err) {
|
|
2622
2843
|
reject(err);
|
|
2623
2844
|
return;
|
|
@@ -2672,7 +2893,7 @@ function resolveExecutable(cmd, opts = {}) {
|
|
|
2672
2893
|
}
|
|
2673
2894
|
function defaultIsFile(full) {
|
|
2674
2895
|
try {
|
|
2675
|
-
return
|
|
2896
|
+
return existsSync3(full) && statSync2(full).isFile();
|
|
2676
2897
|
} catch {
|
|
2677
2898
|
return false;
|
|
2678
2899
|
}
|
|
@@ -2784,11 +3005,11 @@ function registerShellTools(registry, opts) {
|
|
|
2784
3005
|
return registry;
|
|
2785
3006
|
}
|
|
2786
3007
|
function formatCommandResult(cmd, r) {
|
|
2787
|
-
const
|
|
3008
|
+
const header2 = r.timedOut ? `$ ${cmd}
|
|
2788
3009
|
[killed after timeout]` : `$ ${cmd}
|
|
2789
3010
|
[exit ${r.exitCode ?? "?"}]`;
|
|
2790
|
-
return r.output ? `${
|
|
2791
|
-
${r.output}` :
|
|
3011
|
+
return r.output ? `${header2}
|
|
3012
|
+
${r.output}` : header2;
|
|
2792
3013
|
}
|
|
2793
3014
|
|
|
2794
3015
|
// src/tools/web.ts
|
|
@@ -2953,9 +3174,9 @@ function registerWebTools(registry, opts = {}) {
|
|
|
2953
3174
|
throw new Error("web_fetch: url must start with http:// or https://");
|
|
2954
3175
|
}
|
|
2955
3176
|
const page = await webFetch(args.url, { maxChars: maxFetchChars, signal: ctx?.signal });
|
|
2956
|
-
const
|
|
3177
|
+
const header2 = page.title ? `${page.title}
|
|
2957
3178
|
${page.url}` : page.url;
|
|
2958
|
-
return `${
|
|
3179
|
+
return `${header2}
|
|
2959
3180
|
|
|
2960
3181
|
${page.text}`;
|
|
2961
3182
|
}
|
|
@@ -2975,12 +3196,12 @@ ${i + 1}. ${r.title}`);
|
|
|
2975
3196
|
}
|
|
2976
3197
|
|
|
2977
3198
|
// src/env.ts
|
|
2978
|
-
import { readFileSync as
|
|
3199
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
2979
3200
|
import { resolve as resolve3 } from "path";
|
|
2980
3201
|
function loadDotenv(path = ".env") {
|
|
2981
3202
|
let raw;
|
|
2982
3203
|
try {
|
|
2983
|
-
raw =
|
|
3204
|
+
raw = readFileSync4(resolve3(process.cwd(), path), "utf8");
|
|
2984
3205
|
} catch {
|
|
2985
3206
|
return;
|
|
2986
3207
|
}
|
|
@@ -2999,7 +3220,7 @@ function loadDotenv(path = ".env") {
|
|
|
2999
3220
|
}
|
|
3000
3221
|
|
|
3001
3222
|
// src/transcript.ts
|
|
3002
|
-
import { createWriteStream, readFileSync as
|
|
3223
|
+
import { createWriteStream, readFileSync as readFileSync5 } from "fs";
|
|
3003
3224
|
function recordFromLoopEvent(ev, extra) {
|
|
3004
3225
|
const rec = {
|
|
3005
3226
|
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
@@ -3050,7 +3271,7 @@ function openTranscriptFile(path, meta) {
|
|
|
3050
3271
|
return stream;
|
|
3051
3272
|
}
|
|
3052
3273
|
function readTranscript(path) {
|
|
3053
|
-
const raw =
|
|
3274
|
+
const raw = readFileSync5(path, "utf8");
|
|
3054
3275
|
return parseTranscript(raw);
|
|
3055
3276
|
}
|
|
3056
3277
|
function isPlanStateEmptyShape(s) {
|
|
@@ -3784,7 +4005,7 @@ var McpClient = class {
|
|
|
3784
4005
|
};
|
|
3785
4006
|
|
|
3786
4007
|
// src/mcp/stdio.ts
|
|
3787
|
-
import { spawn as
|
|
4008
|
+
import { spawn as spawn3 } from "child_process";
|
|
3788
4009
|
var StdioTransport = class {
|
|
3789
4010
|
child;
|
|
3790
4011
|
queue = [];
|
|
@@ -3799,14 +4020,14 @@ var StdioTransport = class {
|
|
|
3799
4020
|
opts.command,
|
|
3800
4021
|
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
3801
4022
|
].join(" ");
|
|
3802
|
-
this.child =
|
|
4023
|
+
this.child = spawn3(line, [], {
|
|
3803
4024
|
env,
|
|
3804
4025
|
cwd: opts.cwd,
|
|
3805
4026
|
stdio: ["pipe", "pipe", "inherit"],
|
|
3806
4027
|
shell: true
|
|
3807
4028
|
});
|
|
3808
4029
|
} else {
|
|
3809
|
-
this.child =
|
|
4030
|
+
this.child = spawn3(opts.command, opts.args ?? [], {
|
|
3810
4031
|
env,
|
|
3811
4032
|
cwd: opts.cwd,
|
|
3812
4033
|
stdio: ["pipe", "pipe", "inherit"]
|
|
@@ -4136,7 +4357,7 @@ async function trySection(load) {
|
|
|
4136
4357
|
}
|
|
4137
4358
|
|
|
4138
4359
|
// src/code/edit-blocks.ts
|
|
4139
|
-
import { existsSync as
|
|
4360
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync6, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
4140
4361
|
import { dirname as dirname4, resolve as resolve4 } from "path";
|
|
4141
4362
|
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
4142
4363
|
function parseEditBlocks(text) {
|
|
@@ -4165,7 +4386,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4165
4386
|
};
|
|
4166
4387
|
}
|
|
4167
4388
|
const searchEmpty = block.search.length === 0;
|
|
4168
|
-
const exists =
|
|
4389
|
+
const exists = existsSync4(absTarget);
|
|
4169
4390
|
try {
|
|
4170
4391
|
if (!exists) {
|
|
4171
4392
|
if (!searchEmpty) {
|
|
@@ -4179,7 +4400,7 @@ function applyEditBlock(block, rootDir) {
|
|
|
4179
4400
|
writeFileSync3(absTarget, block.replace, "utf8");
|
|
4180
4401
|
return { path: block.path, status: "created" };
|
|
4181
4402
|
}
|
|
4182
|
-
const content =
|
|
4403
|
+
const content = readFileSync6(absTarget, "utf8");
|
|
4183
4404
|
if (searchEmpty) {
|
|
4184
4405
|
return {
|
|
4185
4406
|
path: block.path,
|
|
@@ -4213,12 +4434,12 @@ function snapshotBeforeEdits(blocks, rootDir) {
|
|
|
4213
4434
|
if (seen.has(b.path)) continue;
|
|
4214
4435
|
seen.add(b.path);
|
|
4215
4436
|
const abs = resolve4(absRoot, b.path);
|
|
4216
|
-
if (!
|
|
4437
|
+
if (!existsSync4(abs)) {
|
|
4217
4438
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4218
4439
|
continue;
|
|
4219
4440
|
}
|
|
4220
4441
|
try {
|
|
4221
|
-
snapshots.push({ path: b.path, prevContent:
|
|
4442
|
+
snapshots.push({ path: b.path, prevContent: readFileSync6(abs, "utf8") });
|
|
4222
4443
|
} catch {
|
|
4223
4444
|
snapshots.push({ path: b.path, prevContent: null });
|
|
4224
4445
|
}
|
|
@@ -4238,7 +4459,7 @@ function restoreSnapshots(snapshots, rootDir) {
|
|
|
4238
4459
|
}
|
|
4239
4460
|
try {
|
|
4240
4461
|
if (snap.prevContent === null) {
|
|
4241
|
-
if (
|
|
4462
|
+
if (existsSync4(abs)) unlinkSync2(abs);
|
|
4242
4463
|
return {
|
|
4243
4464
|
path: snap.path,
|
|
4244
4465
|
status: "applied",
|
|
@@ -4261,9 +4482,9 @@ function sep() {
|
|
|
4261
4482
|
}
|
|
4262
4483
|
|
|
4263
4484
|
// src/version.ts
|
|
4264
|
-
import { existsSync as
|
|
4265
|
-
import { homedir as
|
|
4266
|
-
import { dirname as dirname5, join as
|
|
4485
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
|
|
4486
|
+
import { homedir as homedir4 } from "os";
|
|
4487
|
+
import { dirname as dirname5, join as join5 } from "path";
|
|
4267
4488
|
import { fileURLToPath } from "url";
|
|
4268
4489
|
var REGISTRY_URL = "https://registry.npmjs.org/reasonix/latest";
|
|
4269
4490
|
var LATEST_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -4272,9 +4493,9 @@ function readPackageVersion() {
|
|
|
4272
4493
|
try {
|
|
4273
4494
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
4274
4495
|
for (let i = 0; i < 6; i++) {
|
|
4275
|
-
const p =
|
|
4276
|
-
if (
|
|
4277
|
-
const pkg = JSON.parse(
|
|
4496
|
+
const p = join5(dir, "package.json");
|
|
4497
|
+
if (existsSync5(p)) {
|
|
4498
|
+
const pkg = JSON.parse(readFileSync7(p, "utf8"));
|
|
4278
4499
|
if (pkg?.name === "reasonix" && typeof pkg.version === "string") {
|
|
4279
4500
|
return pkg.version;
|
|
4280
4501
|
}
|
|
@@ -4289,11 +4510,11 @@ function readPackageVersion() {
|
|
|
4289
4510
|
}
|
|
4290
4511
|
var VERSION = readPackageVersion();
|
|
4291
4512
|
function cachePath(homeDirOverride) {
|
|
4292
|
-
return
|
|
4513
|
+
return join5(homeDirOverride ?? homedir4(), ".reasonix", "version-cache.json");
|
|
4293
4514
|
}
|
|
4294
4515
|
function readCache(homeDirOverride) {
|
|
4295
4516
|
try {
|
|
4296
|
-
const raw =
|
|
4517
|
+
const raw = readFileSync7(cachePath(homeDirOverride), "utf8");
|
|
4297
4518
|
const parsed = JSON.parse(raw);
|
|
4298
4519
|
if (parsed && typeof parsed.version === "string" && typeof parsed.checkedAt === "number") {
|
|
4299
4520
|
return parsed;
|
|
@@ -4361,8 +4582,134 @@ function isNpxInstall() {
|
|
|
4361
4582
|
return false;
|
|
4362
4583
|
}
|
|
4363
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
|
+
|
|
4364
4711
|
// src/cli/commands/chat.tsx
|
|
4365
|
-
import { existsSync as
|
|
4712
|
+
import { existsSync as existsSync8, statSync as statSync4 } from "fs";
|
|
4366
4713
|
import { render } from "ink";
|
|
4367
4714
|
import React15, { useState as useState7 } from "react";
|
|
4368
4715
|
|
|
@@ -4401,7 +4748,7 @@ function registerSkillTools(registry, opts = {}) {
|
|
|
4401
4748
|
});
|
|
4402
4749
|
}
|
|
4403
4750
|
const rawArgs = typeof args.arguments === "string" ? args.arguments.trim() : "";
|
|
4404
|
-
const
|
|
4751
|
+
const header2 = [
|
|
4405
4752
|
`# Skill: ${skill.name}`,
|
|
4406
4753
|
skill.description ? `> ${skill.description}` : "",
|
|
4407
4754
|
`(scope: ${skill.scope} \xB7 ${skill.path})`
|
|
@@ -4409,7 +4756,7 @@ function registerSkillTools(registry, opts = {}) {
|
|
|
4409
4756
|
const argsBlock = rawArgs ? `
|
|
4410
4757
|
|
|
4411
4758
|
Arguments: ${rawArgs}` : "";
|
|
4412
|
-
return `${
|
|
4759
|
+
return `${header2}
|
|
4413
4760
|
|
|
4414
4761
|
${skill.body}${argsBlock}`;
|
|
4415
4762
|
}
|
|
@@ -4638,7 +4985,7 @@ function parseBlocks(raw) {
|
|
|
4638
4985
|
if (/^\|?\s*:?-{2,}:?\s*(\|\s*:?-{2,}:?\s*)+\|?\s*$/.test(next)) {
|
|
4639
4986
|
flushPara();
|
|
4640
4987
|
flushList();
|
|
4641
|
-
const
|
|
4988
|
+
const header2 = splitTableRow(line);
|
|
4642
4989
|
const rows = [];
|
|
4643
4990
|
let j = i + 2;
|
|
4644
4991
|
while (j < lines.length) {
|
|
@@ -4647,7 +4994,7 @@ function parseBlocks(raw) {
|
|
|
4647
4994
|
rows.push(splitTableRow(r));
|
|
4648
4995
|
j++;
|
|
4649
4996
|
}
|
|
4650
|
-
out.push({ kind: "table", header, rows });
|
|
4997
|
+
out.push({ kind: "table", header: header2, rows });
|
|
4651
4998
|
i = j - 1;
|
|
4652
4999
|
continue;
|
|
4653
5000
|
}
|
|
@@ -4714,7 +5061,7 @@ function TableBlockRow({ block }) {
|
|
|
4714
5061
|
for (const r of block.rows) cellLengths.push(displayWidth(r[c] ?? ""));
|
|
4715
5062
|
widths.push(Math.min(40, Math.max(3, ...cellLengths)));
|
|
4716
5063
|
}
|
|
4717
|
-
const
|
|
5064
|
+
const pad3 = (s, w) => {
|
|
4718
5065
|
const dw = displayWidth(s);
|
|
4719
5066
|
if (dw >= w) return s;
|
|
4720
5067
|
return s + " ".repeat(w - dw);
|
|
@@ -4722,12 +5069,12 @@ function TableBlockRow({ block }) {
|
|
|
4722
5069
|
const separator = widths.map((w) => "\u2500".repeat(w)).join("\u2500\u253C\u2500");
|
|
4723
5070
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, /* @__PURE__ */ React2.createElement(Box2, null, block.header.map((cell, ci) => (
|
|
4724
5071
|
// biome-ignore lint/suspicious/noArrayIndexKey: table columns never reorder — derived from a static header array
|
|
4725
|
-
/* @__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 " : "")
|
|
4726
5073
|
))), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, separator), block.rows.map((row2, ri) => (
|
|
4727
5074
|
// biome-ignore lint/suspicious/noArrayIndexKey: table rows render in source order and don't reorder
|
|
4728
5075
|
/* @__PURE__ */ React2.createElement(Box2, { key: `r-${ri}` }, Array.from({ length: colCount }).map((_, ci) => (
|
|
4729
5076
|
// biome-ignore lint/suspicious/noArrayIndexKey: same — column axis is fixed by the table shape
|
|
4730
|
-
/* @__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 " : "")
|
|
4731
5078
|
)))
|
|
4732
5079
|
)));
|
|
4733
5080
|
}
|
|
@@ -5372,6 +5719,114 @@ function formatTokens(n) {
|
|
|
5372
5719
|
|
|
5373
5720
|
// src/cli/ui/slash.ts
|
|
5374
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
|
|
5375
5830
|
var SLASH_COMMANDS = [
|
|
5376
5831
|
{ cmd: "help", summary: "show the full command reference" },
|
|
5377
5832
|
{ cmd: "status", summary: "current model, flags, context, session" },
|
|
@@ -5395,6 +5850,19 @@ var SLASH_COMMANDS = [
|
|
|
5395
5850
|
argsHint: "[list|show <name>|<name> [args]]",
|
|
5396
5851
|
summary: "list / run user skills (<project>/.reasonix/skills + ~/.reasonix/skills)"
|
|
5397
5852
|
},
|
|
5853
|
+
{
|
|
5854
|
+
cmd: "hooks",
|
|
5855
|
+
argsHint: "[reload]",
|
|
5856
|
+
summary: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk"
|
|
5857
|
+
},
|
|
5858
|
+
{
|
|
5859
|
+
cmd: "update",
|
|
5860
|
+
summary: "show current vs latest version + the shell command to upgrade"
|
|
5861
|
+
},
|
|
5862
|
+
{
|
|
5863
|
+
cmd: "stats",
|
|
5864
|
+
summary: "cross-session cost dashboard (today / week / month / all-time \xB7 cache hit \xB7 vs Claude)"
|
|
5865
|
+
},
|
|
5398
5866
|
{ cmd: "think", summary: "dump the last turn's full R1 reasoning (reasoner only)" },
|
|
5399
5867
|
{ cmd: "retry", summary: "truncate & resend your last message (fresh sample)" },
|
|
5400
5868
|
{ cmd: "compact", argsHint: "[cap]", summary: "shrink oversized tool results in the log" },
|
|
@@ -5568,6 +6036,16 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
5568
6036
|
case "skills": {
|
|
5569
6037
|
return handleSkillSlash(args, ctx);
|
|
5570
6038
|
}
|
|
6039
|
+
case "hook":
|
|
6040
|
+
case "hooks": {
|
|
6041
|
+
return handleHooksSlash(args, loop, ctx);
|
|
6042
|
+
}
|
|
6043
|
+
case "update": {
|
|
6044
|
+
return handleUpdateSlash(ctx);
|
|
6045
|
+
}
|
|
6046
|
+
case "stats": {
|
|
6047
|
+
return handleStatsSlash();
|
|
6048
|
+
}
|
|
5571
6049
|
case "think":
|
|
5572
6050
|
case "reasoning": {
|
|
5573
6051
|
const raw = loop.scratch.reasoning;
|
|
@@ -5804,6 +6282,111 @@ ${entry.text}`
|
|
|
5804
6282
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
5805
6283
|
}
|
|
5806
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
|
+
}
|
|
6303
|
+
function handleUpdateSlash(ctx) {
|
|
6304
|
+
const latest = ctx.latestVersion ?? null;
|
|
6305
|
+
const lines = [`current: reasonix ${VERSION}`];
|
|
6306
|
+
if (latest === null) {
|
|
6307
|
+
ctx.refreshLatestVersion?.();
|
|
6308
|
+
lines.push(
|
|
6309
|
+
"latest: (not yet resolved \u2014 background check in flight or offline)",
|
|
6310
|
+
"",
|
|
6311
|
+
"triggered a fresh registry fetch \u2014 retry `/update` in a few seconds,",
|
|
6312
|
+
"or run `reasonix update` in another terminal to force it synchronously."
|
|
6313
|
+
);
|
|
6314
|
+
return { info: lines.join("\n") };
|
|
6315
|
+
}
|
|
6316
|
+
lines.push(`latest: reasonix ${latest}`);
|
|
6317
|
+
const diff = compareVersions(VERSION, latest);
|
|
6318
|
+
if (diff >= 0) {
|
|
6319
|
+
lines.push("", "you're on the latest. nothing to do.");
|
|
6320
|
+
return { info: lines.join("\n") };
|
|
6321
|
+
}
|
|
6322
|
+
if (isNpxInstall()) {
|
|
6323
|
+
lines.push(
|
|
6324
|
+
"",
|
|
6325
|
+
"you're running via npx \u2014 the next `npx reasonix ...` launch will auto-fetch.",
|
|
6326
|
+
"to force a refresh sooner: `npm cache clean --force`."
|
|
6327
|
+
);
|
|
6328
|
+
} else {
|
|
6329
|
+
lines.push(
|
|
6330
|
+
"",
|
|
6331
|
+
"to upgrade, exit this session and run:",
|
|
6332
|
+
" reasonix update (interactive, dry-run supported via --dry-run)",
|
|
6333
|
+
" npm install -g reasonix@latest (direct)",
|
|
6334
|
+
"",
|
|
6335
|
+
"in-session install is deliberately disabled \u2014 the npm spawn would",
|
|
6336
|
+
"corrupt this TUI's rendering and Windows can lock the running binary."
|
|
6337
|
+
);
|
|
6338
|
+
}
|
|
6339
|
+
return { info: lines.join("\n") };
|
|
6340
|
+
}
|
|
6341
|
+
function handleHooksSlash(args, loop, ctx) {
|
|
6342
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
6343
|
+
if (sub === "reload") {
|
|
6344
|
+
if (!ctx.reloadHooks) {
|
|
6345
|
+
return {
|
|
6346
|
+
info: "/hooks reload is not available in this context (no reload callback wired)."
|
|
6347
|
+
};
|
|
6348
|
+
}
|
|
6349
|
+
const count = ctx.reloadHooks();
|
|
6350
|
+
return { info: `\u25B8 reloaded hooks \xB7 ${count} active` };
|
|
6351
|
+
}
|
|
6352
|
+
if (sub !== "" && sub !== "list" && sub !== "ls") {
|
|
6353
|
+
return {
|
|
6354
|
+
info: "usage: /hooks list active hooks\n /hooks reload re-read settings.json files"
|
|
6355
|
+
};
|
|
6356
|
+
}
|
|
6357
|
+
const hooks = loop.hooks;
|
|
6358
|
+
const projPath = ctx.codeRoot ? projectSettingsPath(ctx.codeRoot) : void 0;
|
|
6359
|
+
const globPath = globalSettingsPath();
|
|
6360
|
+
if (hooks.length === 0) {
|
|
6361
|
+
const lines2 = [
|
|
6362
|
+
"no hooks configured.",
|
|
6363
|
+
"",
|
|
6364
|
+
"drop a settings.json with a `hooks` key into either of:",
|
|
6365
|
+
ctx.codeRoot ? ` \xB7 ${projPath} (project)` : " \xB7 <project>/.reasonix/settings.json (project)",
|
|
6366
|
+
` \xB7 ${globPath} (global)`,
|
|
6367
|
+
"",
|
|
6368
|
+
"events: PreToolUse, PostToolUse, UserPromptSubmit, Stop",
|
|
6369
|
+
"exit 0 = pass \xB7 exit 2 = block (Pre*) \xB7 other = warn"
|
|
6370
|
+
];
|
|
6371
|
+
return { info: lines2.join("\n") };
|
|
6372
|
+
}
|
|
6373
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
6374
|
+
for (const event of HOOK_EVENTS) grouped.set(event, []);
|
|
6375
|
+
for (const h of hooks) grouped.get(h.event)?.push(h);
|
|
6376
|
+
const lines = [`\u25B8 ${hooks.length} hook(s) loaded`];
|
|
6377
|
+
for (const event of HOOK_EVENTS) {
|
|
6378
|
+
const list = grouped.get(event) ?? [];
|
|
6379
|
+
if (list.length === 0) continue;
|
|
6380
|
+
lines.push("", `${event}:`);
|
|
6381
|
+
for (const h of list) {
|
|
6382
|
+
const match = h.match && h.match !== "*" ? ` match=${h.match}` : "";
|
|
6383
|
+
const desc = h.description ? ` \u2014 ${h.description}` : "";
|
|
6384
|
+
lines.push(` [${h.scope}]${match} ${h.command}${desc}`);
|
|
6385
|
+
}
|
|
6386
|
+
}
|
|
6387
|
+
lines.push("", `sources: project=${projPath ?? "(none \u2014 chat mode)"} \xB7 global=${globPath}`);
|
|
6388
|
+
return { info: lines.join("\n") };
|
|
6389
|
+
}
|
|
5807
6390
|
function handleSkillSlash(args, ctx) {
|
|
5808
6391
|
const store = new SkillStore({ projectRoot: ctx.codeRoot });
|
|
5809
6392
|
const sub = (args[0] ?? "").toLowerCase();
|
|
@@ -5861,12 +6444,12 @@ function handleSkillSlash(args, ctx) {
|
|
|
5861
6444
|
};
|
|
5862
6445
|
}
|
|
5863
6446
|
const extra = args.slice(1).join(" ").trim();
|
|
5864
|
-
const
|
|
6447
|
+
const header2 = `# Skill: ${skill.name}${skill.description ? `
|
|
5865
6448
|
> ${skill.description}` : ""}`;
|
|
5866
6449
|
const argsLine = extra ? `
|
|
5867
6450
|
|
|
5868
6451
|
Arguments: ${extra}` : "";
|
|
5869
|
-
const payload = `${
|
|
6452
|
+
const payload = `${header2}
|
|
5870
6453
|
|
|
5871
6454
|
${skill.body}${argsLine}`;
|
|
5872
6455
|
return {
|
|
@@ -6044,9 +6627,9 @@ function appendSection(lines, label, section) {
|
|
|
6044
6627
|
}
|
|
6045
6628
|
function formatToolList(history) {
|
|
6046
6629
|
const total = history.length;
|
|
6047
|
-
const
|
|
6630
|
+
const header2 = `Tool calls in this session (${total}, most recent first):`;
|
|
6048
6631
|
const shown = Math.min(total, 10);
|
|
6049
|
-
const lines = [
|
|
6632
|
+
const lines = [header2];
|
|
6050
6633
|
for (let i = 0; i < shown; i++) {
|
|
6051
6634
|
const entry = history[total - 1 - i];
|
|
6052
6635
|
if (!entry) continue;
|
|
@@ -6129,7 +6712,12 @@ function App({
|
|
|
6129
6712
|
const [toolProgress, setToolProgress] = useState5(null);
|
|
6130
6713
|
const [statusLine, setStatusLine] = useState5(null);
|
|
6131
6714
|
const [balance, setBalance] = useState5(null);
|
|
6132
|
-
const [
|
|
6715
|
+
const [latestVersion, setLatestVersion] = useState5(null);
|
|
6716
|
+
const updateAvailable = latestVersion && compareVersions(VERSION, latestVersion) < 0 ? latestVersion : null;
|
|
6717
|
+
const [hookList, setHookList] = useState5(
|
|
6718
|
+
() => loadHooks({ projectRoot: codeMode?.rootDir })
|
|
6719
|
+
);
|
|
6720
|
+
const hookCwd = codeMode?.rootDir ?? process.cwd();
|
|
6133
6721
|
const lastEditSnapshots = useRef2(null);
|
|
6134
6722
|
const pendingEdits = useRef2([]);
|
|
6135
6723
|
const [pendingShell, setPendingShell] = useState5(null);
|
|
@@ -6185,10 +6773,23 @@ function App({
|
|
|
6185
6773
|
system,
|
|
6186
6774
|
toolSpecs: tools?.specs()
|
|
6187
6775
|
});
|
|
6188
|
-
const l = new CacheFirstLoop({
|
|
6776
|
+
const l = new CacheFirstLoop({
|
|
6777
|
+
client,
|
|
6778
|
+
prefix,
|
|
6779
|
+
tools,
|
|
6780
|
+
model,
|
|
6781
|
+
harvest: harvest2,
|
|
6782
|
+
branch,
|
|
6783
|
+
session,
|
|
6784
|
+
hooks: hookList,
|
|
6785
|
+
hookCwd
|
|
6786
|
+
});
|
|
6189
6787
|
loopRef.current = l;
|
|
6190
6788
|
return l;
|
|
6191
6789
|
}, [model, system, harvest2, branch, session, tools]);
|
|
6790
|
+
useEffect2(() => {
|
|
6791
|
+
loop.hooks = hookList;
|
|
6792
|
+
}, [loop, hookList]);
|
|
6192
6793
|
useEffect2(() => {
|
|
6193
6794
|
let cancelled = false;
|
|
6194
6795
|
void (async () => {
|
|
@@ -6206,7 +6807,7 @@ function App({
|
|
|
6206
6807
|
void (async () => {
|
|
6207
6808
|
const latest = await getLatestVersion();
|
|
6208
6809
|
if (cancelled || !latest) return;
|
|
6209
|
-
|
|
6810
|
+
setLatestVersion(latest);
|
|
6210
6811
|
})();
|
|
6211
6812
|
return () => {
|
|
6212
6813
|
cancelled = true;
|
|
@@ -6383,7 +6984,19 @@ function App({
|
|
|
6383
6984
|
memoryRoot: codeMode?.rootDir ?? process.cwd(),
|
|
6384
6985
|
planMode,
|
|
6385
6986
|
setPlanMode: codeMode ? togglePlanMode : void 0,
|
|
6386
|
-
clearPendingPlan: codeMode ? clearPendingPlan : void 0
|
|
6987
|
+
clearPendingPlan: codeMode ? clearPendingPlan : void 0,
|
|
6988
|
+
reloadHooks: () => {
|
|
6989
|
+
const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
|
|
6990
|
+
setHookList(fresh);
|
|
6991
|
+
return fresh.length;
|
|
6992
|
+
},
|
|
6993
|
+
latestVersion,
|
|
6994
|
+
refreshLatestVersion: () => {
|
|
6995
|
+
void (async () => {
|
|
6996
|
+
const fresh = await getLatestVersion({ force: true });
|
|
6997
|
+
if (fresh) setLatestVersion(fresh);
|
|
6998
|
+
})();
|
|
6999
|
+
}
|
|
6387
7000
|
});
|
|
6388
7001
|
if (result.exit) {
|
|
6389
7002
|
transcriptRef.current?.end();
|
|
@@ -6421,6 +7034,23 @@ function App({
|
|
|
6421
7034
|
return;
|
|
6422
7035
|
}
|
|
6423
7036
|
}
|
|
7037
|
+
if (hookList.some((h) => h.event === "UserPromptSubmit")) {
|
|
7038
|
+
const promptReport = await runHooks({
|
|
7039
|
+
hooks: hookList,
|
|
7040
|
+
payload: { event: "UserPromptSubmit", cwd: hookCwd, prompt: text }
|
|
7041
|
+
});
|
|
7042
|
+
if (promptReport.outcomes.length > 0) {
|
|
7043
|
+
setHistorical((prev) => [
|
|
7044
|
+
...prev,
|
|
7045
|
+
...promptReport.outcomes.filter((o) => o.decision !== "pass").map((o) => ({
|
|
7046
|
+
id: `hp-${Date.now()}-${Math.random()}`,
|
|
7047
|
+
role: "warning",
|
|
7048
|
+
text: formatHookOutcomeMessage(o)
|
|
7049
|
+
}))
|
|
7050
|
+
]);
|
|
7051
|
+
}
|
|
7052
|
+
if (promptReport.blocked) return;
|
|
7053
|
+
}
|
|
6424
7054
|
promptHistory.current.push(text);
|
|
6425
7055
|
setHistorical((prev) => [...prev, { id: `u-${Date.now()}`, role: "user", text }]);
|
|
6426
7056
|
const assistantId = `a-${Date.now()}`;
|
|
@@ -6493,6 +7123,13 @@ function App({
|
|
|
6493
7123
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
6494
7124
|
setStreaming(null);
|
|
6495
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
|
+
}
|
|
6496
7133
|
const finalText = ev.content || streamRef.text;
|
|
6497
7134
|
const iterReasoning = streamRef.reasoning || void 0;
|
|
6498
7135
|
const iterId = `${assistantId}-i${assistantIterCounter.current++}`;
|
|
@@ -6581,6 +7218,28 @@ function App({
|
|
|
6581
7218
|
}
|
|
6582
7219
|
}
|
|
6583
7220
|
flush();
|
|
7221
|
+
if (hookList.some((h) => h.event === "Stop")) {
|
|
7222
|
+
const stopReport = await runHooks({
|
|
7223
|
+
hooks: hookList,
|
|
7224
|
+
payload: {
|
|
7225
|
+
event: "Stop",
|
|
7226
|
+
cwd: hookCwd,
|
|
7227
|
+
lastAssistantText: streamRef.text,
|
|
7228
|
+
turn: loop.stats.summary().turns
|
|
7229
|
+
}
|
|
7230
|
+
});
|
|
7231
|
+
for (const o of stopReport.outcomes) {
|
|
7232
|
+
if (o.decision === "pass") continue;
|
|
7233
|
+
setHistorical((prev) => [
|
|
7234
|
+
...prev,
|
|
7235
|
+
{
|
|
7236
|
+
id: `hs-${Date.now()}-${Math.random()}`,
|
|
7237
|
+
role: "warning",
|
|
7238
|
+
text: formatHookOutcomeMessage(o)
|
|
7239
|
+
}
|
|
7240
|
+
]);
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
6584
7243
|
} finally {
|
|
6585
7244
|
if (timer) clearInterval(timer);
|
|
6586
7245
|
setStreaming(null);
|
|
@@ -6606,10 +7265,14 @@ function App({
|
|
|
6606
7265
|
codeMode,
|
|
6607
7266
|
codeUndo,
|
|
6608
7267
|
exit,
|
|
7268
|
+
hookCwd,
|
|
7269
|
+
hookList,
|
|
6609
7270
|
loop,
|
|
7271
|
+
latestVersion,
|
|
6610
7272
|
mcpSpecs,
|
|
6611
7273
|
mcpServers,
|
|
6612
7274
|
planMode,
|
|
7275
|
+
session,
|
|
6613
7276
|
slashSelected,
|
|
6614
7277
|
togglePlanMode,
|
|
6615
7278
|
writeTranscript
|
|
@@ -6872,8 +7535,8 @@ function formatEditResults(results) {
|
|
|
6872
7535
|
});
|
|
6873
7536
|
const ok = results.filter((r) => r.status === "applied" || r.status === "created").length;
|
|
6874
7537
|
const total = results.length;
|
|
6875
|
-
const
|
|
6876
|
-
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");
|
|
6877
7540
|
}
|
|
6878
7541
|
function formatPendingPreview(blocks) {
|
|
6879
7542
|
const lines = blocks.map((b) => {
|
|
@@ -6882,8 +7545,8 @@ function formatPendingPreview(blocks) {
|
|
|
6882
7545
|
const tag = b.search === "" ? "NEW " : " ";
|
|
6883
7546
|
return ` ${tag}${b.path} (-${removed} +${added} lines)`;
|
|
6884
7547
|
});
|
|
6885
|
-
const
|
|
6886
|
-
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");
|
|
6887
7550
|
}
|
|
6888
7551
|
function countLines2(s) {
|
|
6889
7552
|
if (s.length === 0) return 0;
|
|
@@ -7128,7 +7791,7 @@ async function chatCommand(opts) {
|
|
|
7128
7791
|
const prior = loadSessionMessages(opts.session);
|
|
7129
7792
|
if (prior.length > 0) {
|
|
7130
7793
|
const p = sessionPath(opts.session);
|
|
7131
|
-
const mtime =
|
|
7794
|
+
const mtime = existsSync8(p) ? statSync4(p).mtime : /* @__PURE__ */ new Date();
|
|
7132
7795
|
sessionPreview = { messageCount: prior.length, lastActive: mtime };
|
|
7133
7796
|
}
|
|
7134
7797
|
} else if (opts.session && opts.forceNew) {
|
|
@@ -7471,7 +8134,7 @@ function mcpListCommand(opts) {
|
|
|
7471
8134
|
console.log("Popular MCP servers you can bridge into Reasonix:");
|
|
7472
8135
|
console.log("");
|
|
7473
8136
|
for (const entry of MCP_CATALOG) {
|
|
7474
|
-
console.log(` ${
|
|
8137
|
+
console.log(` ${pad2(entry.name, 12)} ${entry.summary}`);
|
|
7475
8138
|
console.log(` ${mcpCommandFor(entry)}`);
|
|
7476
8139
|
if (entry.note) console.log(` \xB7 ${entry.note}`);
|
|
7477
8140
|
console.log("");
|
|
@@ -7484,7 +8147,7 @@ function mcpListCommand(opts) {
|
|
|
7484
8147
|
" https://mcp.so \u2014 community-maintained catalog"
|
|
7485
8148
|
);
|
|
7486
8149
|
}
|
|
7487
|
-
function
|
|
8150
|
+
function pad2(s, width) {
|
|
7488
8151
|
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
7489
8152
|
}
|
|
7490
8153
|
|
|
@@ -7753,6 +8416,9 @@ async function runCommand2(opts) {
|
|
|
7753
8416
|
[error] ${ev.error}
|
|
7754
8417
|
`);
|
|
7755
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
|
+
}
|
|
7756
8422
|
if (transcriptStream && ev.role !== "assistant_delta") {
|
|
7757
8423
|
writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: opts.model, prefixHash }));
|
|
7758
8424
|
}
|
|
@@ -8149,34 +8815,8 @@ async function setupCommand(_opts = {}) {
|
|
|
8149
8815
|
await waitUntilExit();
|
|
8150
8816
|
}
|
|
8151
8817
|
|
|
8152
|
-
// src/cli/commands/stats.ts
|
|
8153
|
-
import { existsSync as existsSync6, readFileSync as readFileSync7 } from "fs";
|
|
8154
|
-
function statsCommand(opts) {
|
|
8155
|
-
if (!existsSync6(opts.transcript)) {
|
|
8156
|
-
console.error(`no such transcript: ${opts.transcript}`);
|
|
8157
|
-
process.exit(1);
|
|
8158
|
-
}
|
|
8159
|
-
const lines = readFileSync7(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
8160
|
-
let assistantTurns = 0;
|
|
8161
|
-
let toolCalls = 0;
|
|
8162
|
-
let lastTurn = 0;
|
|
8163
|
-
for (const line of lines) {
|
|
8164
|
-
try {
|
|
8165
|
-
const rec = JSON.parse(line);
|
|
8166
|
-
if (rec.role === "assistant_final") assistantTurns++;
|
|
8167
|
-
if (rec.role === "tool") toolCalls++;
|
|
8168
|
-
if (typeof rec.turn === "number") lastTurn = Math.max(lastTurn, rec.turn);
|
|
8169
|
-
} catch {
|
|
8170
|
-
}
|
|
8171
|
-
}
|
|
8172
|
-
console.log(`transcript: ${opts.transcript}`);
|
|
8173
|
-
console.log(`assistant turns: ${assistantTurns}`);
|
|
8174
|
-
console.log(`tool invocations: ${toolCalls}`);
|
|
8175
|
-
console.log(`last turn index: ${lastTurn}`);
|
|
8176
|
-
}
|
|
8177
|
-
|
|
8178
8818
|
// src/cli/commands/update.ts
|
|
8179
|
-
import { spawn as
|
|
8819
|
+
import { spawn as spawn4 } from "child_process";
|
|
8180
8820
|
function planUpdate(input) {
|
|
8181
8821
|
const diff = compareVersions(input.current, input.latest);
|
|
8182
8822
|
if (diff > 0) {
|
|
@@ -8207,7 +8847,7 @@ function planUpdate(input) {
|
|
|
8207
8847
|
}
|
|
8208
8848
|
function defaultSpawn(argv) {
|
|
8209
8849
|
return new Promise((resolve6, reject) => {
|
|
8210
|
-
const child =
|
|
8850
|
+
const child = spawn4(argv[0], argv.slice(1), {
|
|
8211
8851
|
stdio: "inherit",
|
|
8212
8852
|
shell: process.platform === "win32"
|
|
8213
8853
|
});
|
|
@@ -8400,7 +9040,9 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
8400
9040
|
mcpPrefix: opts.mcpPrefix
|
|
8401
9041
|
});
|
|
8402
9042
|
});
|
|
8403
|
-
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) => {
|
|
8404
9046
|
statsCommand({ transcript });
|
|
8405
9047
|
});
|
|
8406
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) => {
|