reasonix 0.0.6 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -7
- package/dist/cli/index.js +801 -24
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +211 -2
- package/dist/index.js +446 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1289,7 +1289,13 @@ var CacheFirstLoop = class {
|
|
|
1289
1289
|
name,
|
|
1290
1290
|
content: result
|
|
1291
1291
|
});
|
|
1292
|
-
yield {
|
|
1292
|
+
yield {
|
|
1293
|
+
turn: this._turn,
|
|
1294
|
+
role: "tool",
|
|
1295
|
+
content: result,
|
|
1296
|
+
toolName: name,
|
|
1297
|
+
toolArgs: args
|
|
1298
|
+
};
|
|
1293
1299
|
}
|
|
1294
1300
|
}
|
|
1295
1301
|
yield { turn: this._turn, role: "done", content: "[max_tool_iters reached]" };
|
|
@@ -1342,8 +1348,462 @@ function loadDotenv(path = ".env") {
|
|
|
1342
1348
|
}
|
|
1343
1349
|
}
|
|
1344
1350
|
|
|
1351
|
+
// src/transcript.ts
|
|
1352
|
+
import { createWriteStream, readFileSync as readFileSync3 } from "fs";
|
|
1353
|
+
function recordFromLoopEvent(ev, extra) {
|
|
1354
|
+
const rec = {
|
|
1355
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1356
|
+
turn: ev.turn,
|
|
1357
|
+
role: ev.role,
|
|
1358
|
+
content: ev.content
|
|
1359
|
+
};
|
|
1360
|
+
if (ev.toolName !== void 0) rec.tool = ev.toolName;
|
|
1361
|
+
if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
|
|
1362
|
+
if (ev.error !== void 0) rec.error = ev.error;
|
|
1363
|
+
if (ev.stats) {
|
|
1364
|
+
rec.usage = {
|
|
1365
|
+
prompt_tokens: ev.stats.usage.promptTokens,
|
|
1366
|
+
completion_tokens: ev.stats.usage.completionTokens,
|
|
1367
|
+
total_tokens: ev.stats.usage.totalTokens,
|
|
1368
|
+
prompt_cache_hit_tokens: ev.stats.usage.promptCacheHitTokens,
|
|
1369
|
+
prompt_cache_miss_tokens: ev.stats.usage.promptCacheMissTokens
|
|
1370
|
+
};
|
|
1371
|
+
rec.cost = ev.stats.cost;
|
|
1372
|
+
rec.model = ev.stats.model;
|
|
1373
|
+
rec.prefixHash = extra.prefixHash;
|
|
1374
|
+
} else if (ev.role === "assistant_final") {
|
|
1375
|
+
rec.model = extra.model;
|
|
1376
|
+
rec.prefixHash = extra.prefixHash;
|
|
1377
|
+
}
|
|
1378
|
+
return rec;
|
|
1379
|
+
}
|
|
1380
|
+
function writeRecord(stream, record) {
|
|
1381
|
+
stream.write(`${JSON.stringify(record)}
|
|
1382
|
+
`);
|
|
1383
|
+
}
|
|
1384
|
+
function writeMeta(stream, meta) {
|
|
1385
|
+
const line = { role: "_meta", meta };
|
|
1386
|
+
stream.write(`${JSON.stringify(line)}
|
|
1387
|
+
`);
|
|
1388
|
+
}
|
|
1389
|
+
function openTranscriptFile(path, meta) {
|
|
1390
|
+
const stream = createWriteStream(path, { flags: "a" });
|
|
1391
|
+
writeMeta(stream, meta);
|
|
1392
|
+
return stream;
|
|
1393
|
+
}
|
|
1394
|
+
function readTranscript(path) {
|
|
1395
|
+
const raw = readFileSync3(path, "utf8");
|
|
1396
|
+
return parseTranscript(raw);
|
|
1397
|
+
}
|
|
1398
|
+
function parseTranscript(raw) {
|
|
1399
|
+
const out = { meta: null, records: [] };
|
|
1400
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1401
|
+
const trimmed = line.trim();
|
|
1402
|
+
if (!trimmed) continue;
|
|
1403
|
+
let obj;
|
|
1404
|
+
try {
|
|
1405
|
+
obj = JSON.parse(trimmed);
|
|
1406
|
+
} catch {
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
if (!obj || typeof obj !== "object") continue;
|
|
1410
|
+
const rec = obj;
|
|
1411
|
+
if (rec.role === "_meta" && rec.meta && typeof rec.meta === "object") {
|
|
1412
|
+
out.meta = rec.meta;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (typeof rec.ts === "string" && typeof rec.turn === "number" && typeof rec.role === "string" && typeof rec.content === "string") {
|
|
1416
|
+
out.records.push(rec);
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return out;
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
// src/replay.ts
|
|
1423
|
+
function groupRecordsByTurn(records) {
|
|
1424
|
+
const byTurn = /* @__PURE__ */ new Map();
|
|
1425
|
+
for (const rec of records) {
|
|
1426
|
+
const list = byTurn.get(rec.turn);
|
|
1427
|
+
if (list) list.push(rec);
|
|
1428
|
+
else byTurn.set(rec.turn, [rec]);
|
|
1429
|
+
}
|
|
1430
|
+
return [...byTurn.entries()].sort(([a], [b]) => a - b).map(([turn, records2]) => ({ turn, records: records2 }));
|
|
1431
|
+
}
|
|
1432
|
+
function computeCumulativeStats(pages, upToIdx) {
|
|
1433
|
+
if (upToIdx < 0) return computeReplayStats([]);
|
|
1434
|
+
const flat = [];
|
|
1435
|
+
for (let i = 0; i <= upToIdx && i < pages.length; i++) {
|
|
1436
|
+
const records = pages[i]?.records;
|
|
1437
|
+
if (records) flat.push(...records);
|
|
1438
|
+
}
|
|
1439
|
+
return computeReplayStats(flat);
|
|
1440
|
+
}
|
|
1441
|
+
function replayFromFile(path) {
|
|
1442
|
+
const parsed = readTranscript(path);
|
|
1443
|
+
return { parsed, stats: computeReplayStats(parsed.records) };
|
|
1444
|
+
}
|
|
1445
|
+
function computeReplayStats(records) {
|
|
1446
|
+
const turns = [];
|
|
1447
|
+
const models = /* @__PURE__ */ new Set();
|
|
1448
|
+
const prefixHashes = /* @__PURE__ */ new Set();
|
|
1449
|
+
let userTurns = 0;
|
|
1450
|
+
let toolCalls = 0;
|
|
1451
|
+
for (const rec of records) {
|
|
1452
|
+
if (rec.role === "user") userTurns++;
|
|
1453
|
+
else if (rec.role === "tool") toolCalls++;
|
|
1454
|
+
else if (rec.role === "assistant_final") {
|
|
1455
|
+
if (rec.model) models.add(rec.model);
|
|
1456
|
+
if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
|
|
1457
|
+
if (rec.usage && rec.model) {
|
|
1458
|
+
const u = new Usage(
|
|
1459
|
+
rec.usage.prompt_tokens ?? 0,
|
|
1460
|
+
rec.usage.completion_tokens ?? 0,
|
|
1461
|
+
rec.usage.total_tokens ?? 0,
|
|
1462
|
+
rec.usage.prompt_cache_hit_tokens ?? 0,
|
|
1463
|
+
rec.usage.prompt_cache_miss_tokens ?? 0
|
|
1464
|
+
);
|
|
1465
|
+
turns.push({
|
|
1466
|
+
turn: rec.turn,
|
|
1467
|
+
model: rec.model,
|
|
1468
|
+
usage: u,
|
|
1469
|
+
// `rec.cost` wins when present — honors whatever the writer computed
|
|
1470
|
+
// even if pricing tables have since changed. Only recompute when
|
|
1471
|
+
// the transcript didn't record it (old format).
|
|
1472
|
+
cost: rec.cost ?? costUsd(rec.model, u),
|
|
1473
|
+
cacheHitRatio: u.cacheHitRatio
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
return {
|
|
1479
|
+
perTurn: turns,
|
|
1480
|
+
models: [...models],
|
|
1481
|
+
prefixHashes: [...prefixHashes],
|
|
1482
|
+
userTurns,
|
|
1483
|
+
toolCalls,
|
|
1484
|
+
...summarizeTurns(turns)
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
function summarizeTurns(turns) {
|
|
1488
|
+
const totalCost = turns.reduce((s, t) => s + t.cost, 0);
|
|
1489
|
+
const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
|
|
1490
|
+
let hit = 0;
|
|
1491
|
+
let miss = 0;
|
|
1492
|
+
for (const t of turns) {
|
|
1493
|
+
hit += t.usage.promptCacheHitTokens;
|
|
1494
|
+
miss += t.usage.promptCacheMissTokens;
|
|
1495
|
+
}
|
|
1496
|
+
const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
|
|
1497
|
+
const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
|
|
1498
|
+
return {
|
|
1499
|
+
turns: turns.length,
|
|
1500
|
+
totalCostUsd: round2(totalCost, 6),
|
|
1501
|
+
claudeEquivalentUsd: round2(totalClaude, 6),
|
|
1502
|
+
savingsVsClaudePct: round2(savingsVsClaude * 100, 2),
|
|
1503
|
+
cacheHitRatio: round2(cacheHitRatio, 4)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function round2(n, digits) {
|
|
1507
|
+
const f = 10 ** digits;
|
|
1508
|
+
return Math.round(n * f) / f;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
// src/diff.ts
|
|
1512
|
+
function findNextDivergence(pairs, fromIdx) {
|
|
1513
|
+
for (let i = fromIdx + 1; i < pairs.length; i++) {
|
|
1514
|
+
if (pairs[i].kind !== "match") return i;
|
|
1515
|
+
}
|
|
1516
|
+
return -1;
|
|
1517
|
+
}
|
|
1518
|
+
function findPrevDivergence(pairs, fromIdx) {
|
|
1519
|
+
const start = Math.min(fromIdx - 1, pairs.length - 1);
|
|
1520
|
+
for (let i = start; i >= 0; i--) {
|
|
1521
|
+
if (pairs[i].kind !== "match") return i;
|
|
1522
|
+
}
|
|
1523
|
+
return -1;
|
|
1524
|
+
}
|
|
1525
|
+
function diffTranscripts(a, b) {
|
|
1526
|
+
const aSide = {
|
|
1527
|
+
label: a.label,
|
|
1528
|
+
meta: a.parsed.meta,
|
|
1529
|
+
records: a.parsed.records,
|
|
1530
|
+
stats: computeReplayStats(a.parsed.records)
|
|
1531
|
+
};
|
|
1532
|
+
const bSide = {
|
|
1533
|
+
label: b.label,
|
|
1534
|
+
meta: b.parsed.meta,
|
|
1535
|
+
records: b.parsed.records,
|
|
1536
|
+
stats: computeReplayStats(b.parsed.records)
|
|
1537
|
+
};
|
|
1538
|
+
const aByTurn = groupByTurn(a.parsed.records);
|
|
1539
|
+
const bByTurn = groupByTurn(b.parsed.records);
|
|
1540
|
+
const turns = [.../* @__PURE__ */ new Set([...aByTurn.keys(), ...bByTurn.keys()])].sort((x, y) => x - y);
|
|
1541
|
+
const pairs = [];
|
|
1542
|
+
let firstDivergenceTurn = null;
|
|
1543
|
+
for (const turn of turns) {
|
|
1544
|
+
const aGroup = aByTurn.get(turn) ?? { assistant: void 0, tools: [] };
|
|
1545
|
+
const bGroup = bByTurn.get(turn) ?? { assistant: void 0, tools: [] };
|
|
1546
|
+
const aAssistant = aGroup.assistant;
|
|
1547
|
+
const bAssistant = bGroup.assistant;
|
|
1548
|
+
const aTools = aGroup.tools;
|
|
1549
|
+
const bTools = bGroup.tools;
|
|
1550
|
+
let kind;
|
|
1551
|
+
let divergenceNote;
|
|
1552
|
+
if (!aAssistant && bAssistant) kind = "only_in_b";
|
|
1553
|
+
else if (aAssistant && !bAssistant) kind = "only_in_a";
|
|
1554
|
+
else if (!aAssistant && !bAssistant)
|
|
1555
|
+
kind = "diverge";
|
|
1556
|
+
else {
|
|
1557
|
+
divergenceNote = classifyDivergence(aAssistant, bAssistant, aTools, bTools);
|
|
1558
|
+
kind = divergenceNote ? "diverge" : "match";
|
|
1559
|
+
}
|
|
1560
|
+
if (kind !== "match" && firstDivergenceTurn === null) firstDivergenceTurn = turn;
|
|
1561
|
+
pairs.push({ turn, aAssistant, bAssistant, aTools, bTools, kind, divergenceNote });
|
|
1562
|
+
}
|
|
1563
|
+
return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
|
|
1564
|
+
}
|
|
1565
|
+
function classifyDivergence(a, b, aTools, bTools) {
|
|
1566
|
+
const aNames = aTools.map((t) => t.tool ?? "").sort();
|
|
1567
|
+
const bNames = bTools.map((t) => t.tool ?? "").sort();
|
|
1568
|
+
if (aNames.join(",") !== bNames.join(",")) {
|
|
1569
|
+
return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
|
|
1570
|
+
}
|
|
1571
|
+
for (let i = 0; i < aTools.length; i++) {
|
|
1572
|
+
const at = aTools[i];
|
|
1573
|
+
const bt = bTools[i];
|
|
1574
|
+
if (at.tool !== bt.tool) continue;
|
|
1575
|
+
if ((at.args ?? "") !== (bt.args ?? "")) {
|
|
1576
|
+
return `"${at.tool}" args differ`;
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
const simRatio = similarity(a.content, b.content);
|
|
1580
|
+
if (simRatio < 0.75) return `text similarity ${(simRatio * 100).toFixed(0)}%`;
|
|
1581
|
+
return void 0;
|
|
1582
|
+
}
|
|
1583
|
+
function similarity(a, b) {
|
|
1584
|
+
if (a === b) return 1;
|
|
1585
|
+
if (!a && !b) return 1;
|
|
1586
|
+
if (!a || !b) return 0;
|
|
1587
|
+
const maxLen = Math.max(a.length, b.length);
|
|
1588
|
+
if (maxLen > 2e3) return tokenOverlap(a, b);
|
|
1589
|
+
const dist = levenshtein(a, b);
|
|
1590
|
+
return 1 - dist / maxLen;
|
|
1591
|
+
}
|
|
1592
|
+
function tokenOverlap(a, b) {
|
|
1593
|
+
const ta = new Set(a.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1594
|
+
const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
|
|
1595
|
+
if (ta.size === 0 && tb.size === 0) return 1;
|
|
1596
|
+
let shared = 0;
|
|
1597
|
+
for (const t of ta) if (tb.has(t)) shared++;
|
|
1598
|
+
return 2 * shared / (ta.size + tb.size);
|
|
1599
|
+
}
|
|
1600
|
+
function levenshtein(a, b) {
|
|
1601
|
+
const m = a.length;
|
|
1602
|
+
const n = b.length;
|
|
1603
|
+
if (m === 0) return n;
|
|
1604
|
+
if (n === 0) return m;
|
|
1605
|
+
let prev = new Array(n + 1);
|
|
1606
|
+
let curr = new Array(n + 1);
|
|
1607
|
+
for (let j = 0; j <= n; j++) prev[j] = j;
|
|
1608
|
+
for (let i = 1; i <= m; i++) {
|
|
1609
|
+
curr[0] = i;
|
|
1610
|
+
for (let j = 1; j <= n; j++) {
|
|
1611
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
1612
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
1613
|
+
}
|
|
1614
|
+
[prev, curr] = [curr, prev];
|
|
1615
|
+
}
|
|
1616
|
+
return prev[n];
|
|
1617
|
+
}
|
|
1618
|
+
function groupByTurn(records) {
|
|
1619
|
+
const out = /* @__PURE__ */ new Map();
|
|
1620
|
+
for (const rec of records) {
|
|
1621
|
+
if (rec.role === "user") continue;
|
|
1622
|
+
const g = out.get(rec.turn) ?? { tools: [] };
|
|
1623
|
+
if (rec.role === "assistant_final") g.assistant = rec;
|
|
1624
|
+
else if (rec.role === "tool") g.tools.push(rec);
|
|
1625
|
+
out.set(rec.turn, g);
|
|
1626
|
+
}
|
|
1627
|
+
return out;
|
|
1628
|
+
}
|
|
1629
|
+
function renderSummaryTable(report, _opts = {}) {
|
|
1630
|
+
const a = report.a;
|
|
1631
|
+
const b = report.b;
|
|
1632
|
+
const lines = [];
|
|
1633
|
+
lines.push("Comparing:");
|
|
1634
|
+
lines.push(` A ${a.label}`);
|
|
1635
|
+
lines.push(` B ${b.label}`);
|
|
1636
|
+
lines.push("");
|
|
1637
|
+
lines.push(row(["", "A", "B", "\u0394"], [20, 14, 14, 14]));
|
|
1638
|
+
lines.push(
|
|
1639
|
+
row(["\u2500".repeat(20), "\u2500".repeat(14), "\u2500".repeat(14), "\u2500".repeat(14)], [20, 14, 14, 14])
|
|
1640
|
+
);
|
|
1641
|
+
lines.push(statRow("model calls", a.stats.turns, b.stats.turns));
|
|
1642
|
+
lines.push(statRow("user turns", a.stats.userTurns, b.stats.userTurns));
|
|
1643
|
+
lines.push(statRow("tool calls", a.stats.toolCalls, b.stats.toolCalls));
|
|
1644
|
+
lines.push(
|
|
1645
|
+
row(
|
|
1646
|
+
[
|
|
1647
|
+
"cache hit",
|
|
1648
|
+
`${pct(a.stats.cacheHitRatio)}`,
|
|
1649
|
+
`${pct(b.stats.cacheHitRatio)}`,
|
|
1650
|
+
signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)
|
|
1651
|
+
],
|
|
1652
|
+
[20, 14, 14, 14]
|
|
1653
|
+
)
|
|
1654
|
+
);
|
|
1655
|
+
lines.push(
|
|
1656
|
+
row(
|
|
1657
|
+
[
|
|
1658
|
+
"cost (USD)",
|
|
1659
|
+
`$${a.stats.totalCostUsd.toFixed(6)}`,
|
|
1660
|
+
`$${b.stats.totalCostUsd.toFixed(6)}`,
|
|
1661
|
+
costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)
|
|
1662
|
+
],
|
|
1663
|
+
[20, 14, 14, 14]
|
|
1664
|
+
)
|
|
1665
|
+
);
|
|
1666
|
+
lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
|
|
1667
|
+
lines.push("");
|
|
1668
|
+
const aPrefixStable = a.stats.prefixHashes.length <= 1;
|
|
1669
|
+
const bPrefixStable = b.stats.prefixHashes.length <= 1;
|
|
1670
|
+
if (aPrefixStable !== bPrefixStable) {
|
|
1671
|
+
const stable = aPrefixStable ? "A" : "B";
|
|
1672
|
+
const churn = aPrefixStable ? "B" : "A";
|
|
1673
|
+
const churnCount = aPrefixStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
|
|
1674
|
+
lines.push(
|
|
1675
|
+
`prefix stability: ${stable} stayed byte-stable across ${Math.max(
|
|
1676
|
+
a.stats.turns,
|
|
1677
|
+
b.stats.turns
|
|
1678
|
+
)} turns; ${churn} churned ${churnCount} distinct prefixes.`
|
|
1679
|
+
);
|
|
1680
|
+
lines.push("");
|
|
1681
|
+
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
1682
|
+
lines.push(
|
|
1683
|
+
`prefix: A and B share the same prefix hash (${a.stats.prefixHashes[0].slice(0, 12)}\u2026) \u2014 cache delta is attributable to log stability, not prompt change.`
|
|
1684
|
+
);
|
|
1685
|
+
lines.push("");
|
|
1686
|
+
}
|
|
1687
|
+
if (report.firstDivergenceTurn !== null) {
|
|
1688
|
+
const p = report.pairs.find((p2) => p2.turn === report.firstDivergenceTurn);
|
|
1689
|
+
lines.push(
|
|
1690
|
+
`first divergence: turn ${report.firstDivergenceTurn} \u2014 ${p?.divergenceNote ?? "?"}`
|
|
1691
|
+
);
|
|
1692
|
+
if (p?.aAssistant) lines.push(` A \u2192 ${truncate(p.aAssistant.content, 100)}`);
|
|
1693
|
+
if (p?.bAssistant) lines.push(` B \u2192 ${truncate(p.bAssistant.content, 100)}`);
|
|
1694
|
+
} else {
|
|
1695
|
+
lines.push("no material divergence detected (texts within similarity threshold).");
|
|
1696
|
+
}
|
|
1697
|
+
return lines.join("\n");
|
|
1698
|
+
}
|
|
1699
|
+
function renderMarkdown(report) {
|
|
1700
|
+
const a = report.a;
|
|
1701
|
+
const b = report.b;
|
|
1702
|
+
const out = [];
|
|
1703
|
+
out.push(`# Transcript diff: ${a.label} vs ${b.label}`);
|
|
1704
|
+
out.push("");
|
|
1705
|
+
if (a.meta || b.meta) {
|
|
1706
|
+
out.push("## Meta");
|
|
1707
|
+
out.push("");
|
|
1708
|
+
out.push(`| | ${a.label} | ${b.label} |`);
|
|
1709
|
+
out.push("|---|---|---|");
|
|
1710
|
+
out.push(`| source | ${a.meta?.source ?? "\u2014"} | ${b.meta?.source ?? "\u2014"} |`);
|
|
1711
|
+
out.push(`| model | ${a.meta?.model ?? "\u2014"} | ${b.meta?.model ?? "\u2014"} |`);
|
|
1712
|
+
out.push(`| task | ${a.meta?.task ?? "\u2014"} | ${b.meta?.task ?? "\u2014"} |`);
|
|
1713
|
+
out.push(`| startedAt | ${a.meta?.startedAt ?? "\u2014"} | ${b.meta?.startedAt ?? "\u2014"} |`);
|
|
1714
|
+
out.push("");
|
|
1715
|
+
}
|
|
1716
|
+
out.push("## Summary");
|
|
1717
|
+
out.push("");
|
|
1718
|
+
out.push(`| metric | ${a.label} | ${b.label} | delta |`);
|
|
1719
|
+
out.push("|---|---:|---:|---:|");
|
|
1720
|
+
out.push(
|
|
1721
|
+
`| model calls | ${a.stats.turns} | ${b.stats.turns} | ${signed(b.stats.turns - a.stats.turns)} |`
|
|
1722
|
+
);
|
|
1723
|
+
out.push(
|
|
1724
|
+
`| user turns | ${a.stats.userTurns} | ${b.stats.userTurns} | ${signed(b.stats.userTurns - a.stats.userTurns)} |`
|
|
1725
|
+
);
|
|
1726
|
+
out.push(
|
|
1727
|
+
`| tool calls | ${a.stats.toolCalls} | ${b.stats.toolCalls} | ${signed(b.stats.toolCalls - a.stats.toolCalls)} |`
|
|
1728
|
+
);
|
|
1729
|
+
out.push(
|
|
1730
|
+
`| cache hit | ${pct(a.stats.cacheHitRatio)} | ${pct(b.stats.cacheHitRatio)} | **${signPct(b.stats.cacheHitRatio - a.stats.cacheHitRatio)}** |`
|
|
1731
|
+
);
|
|
1732
|
+
out.push(
|
|
1733
|
+
`| cost (USD) | $${a.stats.totalCostUsd.toFixed(6)} | $${b.stats.totalCostUsd.toFixed(6)} | ${costDelta(a.stats.totalCostUsd, b.stats.totalCostUsd)} |`
|
|
1734
|
+
);
|
|
1735
|
+
out.push(
|
|
1736
|
+
`| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
|
|
1737
|
+
);
|
|
1738
|
+
out.push("");
|
|
1739
|
+
out.push("## Turn-by-turn");
|
|
1740
|
+
out.push("");
|
|
1741
|
+
out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
|
|
1742
|
+
out.push("|---:|:---:|---|---|---|");
|
|
1743
|
+
for (const p of report.pairs) {
|
|
1744
|
+
const aTools = p.aTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
|
|
1745
|
+
const bTools = p.bTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
|
|
1746
|
+
out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
|
|
1747
|
+
}
|
|
1748
|
+
out.push("");
|
|
1749
|
+
if (report.firstDivergenceTurn !== null) {
|
|
1750
|
+
const p = report.pairs.find((x) => x.turn === report.firstDivergenceTurn);
|
|
1751
|
+
out.push(`## First divergence (turn ${report.firstDivergenceTurn})`);
|
|
1752
|
+
out.push("");
|
|
1753
|
+
out.push(p?.divergenceNote ?? "");
|
|
1754
|
+
out.push("");
|
|
1755
|
+
if (p?.aAssistant) {
|
|
1756
|
+
out.push(`**${a.label}:**`);
|
|
1757
|
+
out.push("");
|
|
1758
|
+
out.push("```");
|
|
1759
|
+
out.push(p.aAssistant.content);
|
|
1760
|
+
out.push("```");
|
|
1761
|
+
out.push("");
|
|
1762
|
+
}
|
|
1763
|
+
if (p?.bAssistant) {
|
|
1764
|
+
out.push(`**${b.label}:**`);
|
|
1765
|
+
out.push("");
|
|
1766
|
+
out.push("```");
|
|
1767
|
+
out.push(p.bAssistant.content);
|
|
1768
|
+
out.push("```");
|
|
1769
|
+
out.push("");
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
return out.join("\n");
|
|
1773
|
+
}
|
|
1774
|
+
function row(cols, widths) {
|
|
1775
|
+
return cols.map((c, i) => padRight(c, widths[i] ?? c.length)).join(" ");
|
|
1776
|
+
}
|
|
1777
|
+
function statRow(label, av, bv) {
|
|
1778
|
+
return row([label, `${av}`, `${bv}`, signed(bv - av)], [20, 14, 14, 14]);
|
|
1779
|
+
}
|
|
1780
|
+
function padRight(s, w) {
|
|
1781
|
+
return s.length >= w ? s : s + " ".repeat(w - s.length);
|
|
1782
|
+
}
|
|
1783
|
+
function signed(n) {
|
|
1784
|
+
if (n === 0) return "0";
|
|
1785
|
+
return `${n > 0 ? "+" : ""}${n}`;
|
|
1786
|
+
}
|
|
1787
|
+
function signPct(diff) {
|
|
1788
|
+
if (diff === 0) return "0pp";
|
|
1789
|
+
const s = (diff * 100).toFixed(1);
|
|
1790
|
+
return `${diff > 0 ? "+" : ""}${s}pp`;
|
|
1791
|
+
}
|
|
1792
|
+
function pct(x) {
|
|
1793
|
+
return `${(x * 100).toFixed(1)}%`;
|
|
1794
|
+
}
|
|
1795
|
+
function costDelta(a, b) {
|
|
1796
|
+
if (a === 0 && b === 0) return "\u2014";
|
|
1797
|
+
if (a === 0) return "new";
|
|
1798
|
+
const pctChange = (b - a) / a * 100;
|
|
1799
|
+
return `${pctChange > 0 ? "+" : ""}${pctChange.toFixed(1)}%`;
|
|
1800
|
+
}
|
|
1801
|
+
function truncate(s, n) {
|
|
1802
|
+
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1345
1805
|
// src/config.ts
|
|
1346
|
-
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as
|
|
1806
|
+
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1347
1807
|
import { homedir as homedir2 } from "os";
|
|
1348
1808
|
import { dirname as dirname2, join as join2 } from "path";
|
|
1349
1809
|
function defaultConfigPath() {
|
|
@@ -1351,7 +1811,7 @@ function defaultConfigPath() {
|
|
|
1351
1811
|
}
|
|
1352
1812
|
function readConfig(path = defaultConfigPath()) {
|
|
1353
1813
|
try {
|
|
1354
|
-
const raw =
|
|
1814
|
+
const raw = readFileSync4(path, "utf8");
|
|
1355
1815
|
const parsed = JSON.parse(raw);
|
|
1356
1816
|
if (parsed && typeof parsed === "object") return parsed;
|
|
1357
1817
|
} catch {
|
|
@@ -1386,14 +1846,13 @@ function redactKey(key) {
|
|
|
1386
1846
|
}
|
|
1387
1847
|
|
|
1388
1848
|
// src/index.ts
|
|
1389
|
-
var VERSION = "0.
|
|
1849
|
+
var VERSION = "0.2.2";
|
|
1390
1850
|
|
|
1391
1851
|
// src/cli/commands/chat.tsx
|
|
1392
1852
|
import { render } from "ink";
|
|
1393
1853
|
import React7, { useState as useState4 } from "react";
|
|
1394
1854
|
|
|
1395
1855
|
// src/cli/ui/App.tsx
|
|
1396
|
-
import { createWriteStream } from "fs";
|
|
1397
1856
|
import { Box as Box5, Static, Text as Text5, useApp } from "ink";
|
|
1398
1857
|
import React5, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
1399
1858
|
|
|
@@ -1601,7 +2060,7 @@ var EventRow = React2.memo(function EventRow2({ event }) {
|
|
|
1601
2060
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React2.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React2.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React2.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React2.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React2.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React2.createElement(Text2, { color: "magenta" }, event.repair) : null);
|
|
1602
2061
|
}
|
|
1603
2062
|
if (event.role === "tool") {
|
|
1604
|
-
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ",
|
|
2063
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", truncate2(event.text, 400)));
|
|
1605
2064
|
}
|
|
1606
2065
|
if (event.role === "error") {
|
|
1607
2066
|
return /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "red" }, event.text));
|
|
@@ -1650,8 +2109,8 @@ function StreamingAssistant({ event }) {
|
|
|
1650
2109
|
if (p.completed === 0) {
|
|
1651
2110
|
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
|
|
1652
2111
|
}
|
|
1653
|
-
const
|
|
1654
|
-
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (",
|
|
2112
|
+
const pct2 = Math.round(p.completed / p.total * 100);
|
|
2113
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React2.createElement(Text2, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct2, "%)", " "), /* @__PURE__ */ React2.createElement(Elapsed, null)), /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " latest #", p.latestIndex, " T=", p.latestTemperature.toFixed(1), " u=", p.latestUncertainties, p.completed < p.total ? " \xB7 waiting for other samples\u2026" : " \xB7 selecting winner\u2026"));
|
|
1655
2114
|
}
|
|
1656
2115
|
const tail = lastLine(event.text, 140);
|
|
1657
2116
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
@@ -1666,7 +2125,7 @@ function StatsLine({ stats }) {
|
|
|
1666
2125
|
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
1667
2126
|
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
|
|
1668
2127
|
}
|
|
1669
|
-
function
|
|
2128
|
+
function truncate2(s, max) {
|
|
1670
2129
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
1671
2130
|
}
|
|
1672
2131
|
|
|
@@ -1855,7 +2314,12 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
1855
2314
|
});
|
|
1856
2315
|
const transcriptRef = useRef(null);
|
|
1857
2316
|
if (transcript && !transcriptRef.current) {
|
|
1858
|
-
transcriptRef.current =
|
|
2317
|
+
transcriptRef.current = openTranscriptFile(transcript, {
|
|
2318
|
+
version: 1,
|
|
2319
|
+
source: "reasonix chat",
|
|
2320
|
+
model,
|
|
2321
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2322
|
+
});
|
|
1859
2323
|
}
|
|
1860
2324
|
useEffect2(() => {
|
|
1861
2325
|
return () => {
|
|
@@ -1905,18 +2369,14 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
1905
2369
|
}
|
|
1906
2370
|
}, [session, loop]);
|
|
1907
2371
|
const prefixHash = loop.prefix.fingerprint;
|
|
1908
|
-
const writeTranscript = useCallback(
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
})}
|
|
1917
|
-
`
|
|
1918
|
-
);
|
|
1919
|
-
}, []);
|
|
2372
|
+
const writeTranscript = useCallback(
|
|
2373
|
+
(ev) => {
|
|
2374
|
+
const stream = transcriptRef.current;
|
|
2375
|
+
if (!stream) return;
|
|
2376
|
+
writeRecord(stream, recordFromLoopEvent(ev, { model, prefixHash }));
|
|
2377
|
+
},
|
|
2378
|
+
[model, prefixHash]
|
|
2379
|
+
);
|
|
1920
2380
|
const handleSubmit = useCallback(
|
|
1921
2381
|
async (raw) => {
|
|
1922
2382
|
const text = raw.trim();
|
|
@@ -2134,6 +2594,300 @@ async function chatCommand(opts) {
|
|
|
2134
2594
|
await waitUntilExit();
|
|
2135
2595
|
}
|
|
2136
2596
|
|
|
2597
|
+
// src/cli/commands/diff.ts
|
|
2598
|
+
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2599
|
+
import { basename } from "path";
|
|
2600
|
+
import { render as render2 } from "ink";
|
|
2601
|
+
import React10 from "react";
|
|
2602
|
+
|
|
2603
|
+
// src/cli/ui/DiffApp.tsx
|
|
2604
|
+
import { Box as Box8, Static as Static2, Text as Text8, useApp as useApp3, useInput } from "ink";
|
|
2605
|
+
import React9, { useState as useState5 } from "react";
|
|
2606
|
+
|
|
2607
|
+
// src/cli/ui/RecordView.tsx
|
|
2608
|
+
import { Box as Box7, Text as Text7 } from "ink";
|
|
2609
|
+
import React8 from "react";
|
|
2610
|
+
function RecordView({ rec, compact = false }) {
|
|
2611
|
+
const toolArgsMax = compact ? 120 : 200;
|
|
2612
|
+
const toolContentMax = compact ? 200 : 400;
|
|
2613
|
+
if (rec.role === "user") {
|
|
2614
|
+
return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React8.createElement(Text7, null, rec.content));
|
|
2615
|
+
}
|
|
2616
|
+
if (rec.role === "assistant_final") {
|
|
2617
|
+
return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React8.createElement(CacheBadge, { usage: rec.usage }) : null), rec.content ? /* @__PURE__ */ React8.createElement(Text7, null, rec.content) : /* @__PURE__ */ React8.createElement(Text7, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
2618
|
+
}
|
|
2619
|
+
if (rec.role === "tool") {
|
|
2620
|
+
return /* @__PURE__ */ React8.createElement(Box7, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
|
|
2621
|
+
}
|
|
2622
|
+
if (rec.role === "error") {
|
|
2623
|
+
return /* @__PURE__ */ React8.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React8.createElement(Text7, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React8.createElement(Text7, { color: "red" }, rec.error ?? rec.content));
|
|
2624
|
+
}
|
|
2625
|
+
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
2626
|
+
return null;
|
|
2627
|
+
}
|
|
2628
|
+
return /* @__PURE__ */ React8.createElement(Box7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
2629
|
+
}
|
|
2630
|
+
function CacheBadge({ usage }) {
|
|
2631
|
+
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
2632
|
+
const miss = usage.prompt_cache_miss_tokens ?? 0;
|
|
2633
|
+
const total = hit + miss;
|
|
2634
|
+
if (total === 0) return null;
|
|
2635
|
+
const pct2 = hit / total * 100;
|
|
2636
|
+
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
2637
|
+
return /* @__PURE__ */ React8.createElement(Text7, null, /* @__PURE__ */ React8.createElement(Text7, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React8.createElement(Text7, { color }, pct2.toFixed(1), "%"));
|
|
2638
|
+
}
|
|
2639
|
+
function truncate3(s, max) {
|
|
2640
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
2641
|
+
}
|
|
2642
|
+
|
|
2643
|
+
// src/cli/ui/DiffApp.tsx
|
|
2644
|
+
function DiffApp({ report }) {
|
|
2645
|
+
const { exit } = useApp3();
|
|
2646
|
+
const maxIdx = Math.max(0, report.pairs.length - 1);
|
|
2647
|
+
const initialIdx = report.firstDivergenceTurn ? report.pairs.findIndex((p) => p.turn === report.firstDivergenceTurn) : 0;
|
|
2648
|
+
const [idx, setIdx] = useState5(Math.max(0, initialIdx));
|
|
2649
|
+
useInput((input, key) => {
|
|
2650
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
2651
|
+
exit();
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
2655
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
2656
|
+
} else if (input === "k" || key.upArrow) {
|
|
2657
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
2658
|
+
} else if (input === "g") {
|
|
2659
|
+
setIdx(0);
|
|
2660
|
+
} else if (input === "G") {
|
|
2661
|
+
setIdx(maxIdx);
|
|
2662
|
+
} else if (input === "n") {
|
|
2663
|
+
const next = findNextDivergence(report.pairs, idx);
|
|
2664
|
+
if (next !== -1) setIdx(next);
|
|
2665
|
+
} else if (input === "N" || input === "p") {
|
|
2666
|
+
const prev = findPrevDivergence(report.pairs, idx);
|
|
2667
|
+
if (prev !== -1) setIdx(prev);
|
|
2668
|
+
}
|
|
2669
|
+
});
|
|
2670
|
+
const pair = report.pairs[idx];
|
|
2671
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column" }, /* @__PURE__ */ React9.createElement(DiffHeader, { report }), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React9.createElement(Text8, null, pair ? /* @__PURE__ */ React9.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React9.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React9.createElement(Text8, null, pair.divergenceNote)) : null, /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "j"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "k"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "N"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "g"), "/", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React9.createElement(Text8, { bold: true }, "q"), " ", "quit")));
|
|
2672
|
+
}
|
|
2673
|
+
function DiffHeader({ report }) {
|
|
2674
|
+
const a = report.a;
|
|
2675
|
+
const b = report.b;
|
|
2676
|
+
const cacheDelta = b.stats.cacheHitRatio - a.stats.cacheHitRatio;
|
|
2677
|
+
const costDelta2 = a.stats.totalCostUsd > 0 ? (b.stats.totalCostUsd - a.stats.totalCostUsd) / a.stats.totalCostUsd * 100 : 0;
|
|
2678
|
+
const aStable = a.stats.prefixHashes.length <= 1;
|
|
2679
|
+
const bStable = b.stats.prefixHashes.length <= 1;
|
|
2680
|
+
let prefixLine = null;
|
|
2681
|
+
if (aStable !== bStable) {
|
|
2682
|
+
const stableLabel = aStable ? report.a.label : report.b.label;
|
|
2683
|
+
const churnLabel = aStable ? report.b.label : report.a.label;
|
|
2684
|
+
const churnCount = aStable ? b.stats.prefixHashes.length : a.stats.prefixHashes.length;
|
|
2685
|
+
prefixLine = `${stableLabel} stayed byte-stable; ${churnLabel} churned ${churnCount} distinct prefixes.`;
|
|
2686
|
+
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
2687
|
+
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
2688
|
+
}
|
|
2689
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React9.createElement(Box8, { justifyContent: "space-between" }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, a.label), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " vs B="), /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, b.label)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cache "), /* @__PURE__ */ React9.createElement(Text8, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React9.createElement(Text8, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "cost "), /* @__PURE__ */ React9.createElement(Text8, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React9.createElement(Text8, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React9.createElement(Text8, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "model calls "), /* @__PURE__ */ React9.createElement(Text8, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
2690
|
+
}
|
|
2691
|
+
function Pane({
|
|
2692
|
+
label,
|
|
2693
|
+
headerColor,
|
|
2694
|
+
records
|
|
2695
|
+
}) {
|
|
2696
|
+
return /* @__PURE__ */ React9.createElement(
|
|
2697
|
+
Box8,
|
|
2698
|
+
{
|
|
2699
|
+
flexDirection: "column",
|
|
2700
|
+
flexGrow: 1,
|
|
2701
|
+
paddingX: 1,
|
|
2702
|
+
borderStyle: "single",
|
|
2703
|
+
borderColor: headerColor
|
|
2704
|
+
},
|
|
2705
|
+
/* @__PURE__ */ React9.createElement(Text8, { color: headerColor, bold: true }, label),
|
|
2706
|
+
records.length === 0 ? /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React9.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React9.createElement(RecordView, { key, rec, compact: true }))
|
|
2707
|
+
);
|
|
2708
|
+
}
|
|
2709
|
+
function KindBadge({ kind }) {
|
|
2710
|
+
if (kind === "match") {
|
|
2711
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "green" }, "\u2713 match");
|
|
2712
|
+
}
|
|
2713
|
+
if (kind === "diverge") {
|
|
2714
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "\u2605 diverge");
|
|
2715
|
+
}
|
|
2716
|
+
if (kind === "only_in_a") {
|
|
2717
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "blue" }, "\u2190 only in A");
|
|
2718
|
+
}
|
|
2719
|
+
return /* @__PURE__ */ React9.createElement(Text8, { color: "magenta" }, "\u2192 only in B");
|
|
2720
|
+
}
|
|
2721
|
+
function paneRecords(pair, side) {
|
|
2722
|
+
if (!pair) return [];
|
|
2723
|
+
const tools = side === "a" ? pair.aTools : pair.bTools;
|
|
2724
|
+
const assistant = side === "a" ? pair.aAssistant : pair.bAssistant;
|
|
2725
|
+
const out = [...tools];
|
|
2726
|
+
if (assistant) out.push(assistant);
|
|
2727
|
+
return out;
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
// src/cli/commands/diff.ts
|
|
2731
|
+
async function diffCommand(opts) {
|
|
2732
|
+
const aParsed = readTranscript(opts.a);
|
|
2733
|
+
const bParsed = readTranscript(opts.b);
|
|
2734
|
+
const report = diffTranscripts(
|
|
2735
|
+
{ label: opts.labelA ?? basename(opts.a), parsed: aParsed },
|
|
2736
|
+
{ label: opts.labelB ?? basename(opts.b), parsed: bParsed }
|
|
2737
|
+
);
|
|
2738
|
+
const wantMarkdown = !!opts.mdPath;
|
|
2739
|
+
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
2740
|
+
const wantTui = opts.tui || !wantPrint && !wantMarkdown;
|
|
2741
|
+
if (wantMarkdown) {
|
|
2742
|
+
console.log(renderSummaryTable(report));
|
|
2743
|
+
const md = renderMarkdown(report);
|
|
2744
|
+
writeFileSync2(opts.mdPath, md, "utf8");
|
|
2745
|
+
console.log(`
|
|
2746
|
+
markdown report written to ${opts.mdPath}`);
|
|
2747
|
+
return;
|
|
2748
|
+
}
|
|
2749
|
+
if (wantTui) {
|
|
2750
|
+
const { waitUntilExit } = render2(React10.createElement(DiffApp, { report }), {
|
|
2751
|
+
exitOnCtrlC: true
|
|
2752
|
+
});
|
|
2753
|
+
await waitUntilExit();
|
|
2754
|
+
return;
|
|
2755
|
+
}
|
|
2756
|
+
console.log(renderSummaryTable(report));
|
|
2757
|
+
}
|
|
2758
|
+
|
|
2759
|
+
// src/cli/commands/replay.ts
|
|
2760
|
+
import { render as render3 } from "ink";
|
|
2761
|
+
import React12 from "react";
|
|
2762
|
+
|
|
2763
|
+
// src/cli/ui/ReplayApp.tsx
|
|
2764
|
+
import { Box as Box9, Static as Static3, Text as Text9, useApp as useApp4, useInput as useInput2 } from "ink";
|
|
2765
|
+
import React11, { useMemo as useMemo2, useState as useState6 } from "react";
|
|
2766
|
+
function ReplayApp({ meta, pages }) {
|
|
2767
|
+
const { exit } = useApp4();
|
|
2768
|
+
const maxIdx = Math.max(0, pages.length - 1);
|
|
2769
|
+
const [idx, setIdx] = useState6(maxIdx);
|
|
2770
|
+
useInput2((input, key) => {
|
|
2771
|
+
if (input === "q" || key.ctrl && input === "c") {
|
|
2772
|
+
exit();
|
|
2773
|
+
return;
|
|
2774
|
+
}
|
|
2775
|
+
if (input === "j" || key.downArrow || input === " " || key.return) {
|
|
2776
|
+
setIdx((i) => Math.min(maxIdx, i + 1));
|
|
2777
|
+
} else if (input === "k" || key.upArrow) {
|
|
2778
|
+
setIdx((i) => Math.max(0, i - 1));
|
|
2779
|
+
} else if (input === "g") {
|
|
2780
|
+
setIdx(0);
|
|
2781
|
+
} else if (input === "G") {
|
|
2782
|
+
setIdx(maxIdx);
|
|
2783
|
+
} else if (input === "h" || key.leftArrow) {
|
|
2784
|
+
setIdx(0);
|
|
2785
|
+
} else if (input === "l" || key.rightArrow) {
|
|
2786
|
+
setIdx(maxIdx);
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
const cumStats = useMemo2(() => computeCumulativeStats(pages, idx), [pages, idx]);
|
|
2790
|
+
const summary = {
|
|
2791
|
+
turns: cumStats.turns,
|
|
2792
|
+
totalCostUsd: cumStats.totalCostUsd,
|
|
2793
|
+
claudeEquivalentUsd: cumStats.claudeEquivalentUsd,
|
|
2794
|
+
savingsVsClaudePct: cumStats.savingsVsClaudePct,
|
|
2795
|
+
cacheHitRatio: cumStats.cacheHitRatio
|
|
2796
|
+
};
|
|
2797
|
+
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
2798
|
+
const currentPage = pages[idx];
|
|
2799
|
+
const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
|
|
2800
|
+
return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React11.createElement(
|
|
2801
|
+
StatsPanel,
|
|
2802
|
+
{
|
|
2803
|
+
summary,
|
|
2804
|
+
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
2805
|
+
prefixHash
|
|
2806
|
+
}
|
|
2807
|
+
), /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React11.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React11.createElement(Text9, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React11.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React11.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React11.createElement(Text9, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React11.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React11.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React11.createElement(Text9, { bold: true }, "q"), " quit")));
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
// src/cli/commands/replay.ts
|
|
2811
|
+
async function replayCommand(opts) {
|
|
2812
|
+
const wantPrint = opts.print || !process.stdout.isTTY || opts.head !== void 0 || opts.tail !== void 0;
|
|
2813
|
+
if (wantPrint) {
|
|
2814
|
+
printReplay(opts);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
const { parsed } = replayFromFile(opts.path);
|
|
2818
|
+
const pages = groupRecordsByTurn(parsed.records);
|
|
2819
|
+
const { waitUntilExit } = render3(React12.createElement(ReplayApp, { meta: parsed.meta, pages }), {
|
|
2820
|
+
exitOnCtrlC: true
|
|
2821
|
+
});
|
|
2822
|
+
await waitUntilExit();
|
|
2823
|
+
}
|
|
2824
|
+
function printReplay(opts) {
|
|
2825
|
+
const { parsed, stats } = replayFromFile(opts.path);
|
|
2826
|
+
if (parsed.meta) {
|
|
2827
|
+
const m = parsed.meta;
|
|
2828
|
+
const bits = [`source=${m.source}`];
|
|
2829
|
+
if (m.model) bits.push(`model=${m.model}`);
|
|
2830
|
+
if (m.task) bits.push(`task=${m.task}`);
|
|
2831
|
+
if (m.mode) bits.push(`mode=${m.mode}`);
|
|
2832
|
+
if (m.repeat !== void 0) bits.push(`repeat=${m.repeat}`);
|
|
2833
|
+
bits.push(`started=${m.startedAt}`);
|
|
2834
|
+
console.log(`[meta] ${bits.join(" ")}`);
|
|
2835
|
+
console.log("");
|
|
2836
|
+
}
|
|
2837
|
+
const records = sliceRecords(parsed.records, opts);
|
|
2838
|
+
for (const rec of records) {
|
|
2839
|
+
renderRecord(rec);
|
|
2840
|
+
}
|
|
2841
|
+
console.log("");
|
|
2842
|
+
console.log("\u2500\u2500 summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2843
|
+
console.log(`model calls: ${stats.turns}`);
|
|
2844
|
+
console.log(`user turns: ${stats.userTurns}`);
|
|
2845
|
+
console.log(`tool calls: ${stats.toolCalls}`);
|
|
2846
|
+
console.log(`cache hit: ${(stats.cacheHitRatio * 100).toFixed(1)}%`);
|
|
2847
|
+
console.log(`cost: $${stats.totalCostUsd.toFixed(6)}`);
|
|
2848
|
+
console.log(`claude equivalent: $${stats.claudeEquivalentUsd.toFixed(6)}`);
|
|
2849
|
+
console.log(`savings vs claude: ${stats.savingsVsClaudePct.toFixed(1)}%`);
|
|
2850
|
+
console.log(`models: ${stats.models.join(", ") || "\u2014"}`);
|
|
2851
|
+
console.log(`prefix hashes: ${stats.prefixHashes.length} distinct`);
|
|
2852
|
+
if (stats.prefixHashes.length === 1) {
|
|
2853
|
+
console.log(` (byte-stable prefix: ${stats.prefixHashes[0]?.slice(0, 16)}\u2026)`);
|
|
2854
|
+
} else if (stats.prefixHashes.length > 1) {
|
|
2855
|
+
console.log(" (prefix churned \u2014 cache-hostile session)");
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
function sliceRecords(records, opts) {
|
|
2859
|
+
if (opts.head !== void 0 && opts.head > 0) return records.slice(0, opts.head);
|
|
2860
|
+
if (opts.tail !== void 0 && opts.tail > 0) return records.slice(-opts.tail);
|
|
2861
|
+
return records;
|
|
2862
|
+
}
|
|
2863
|
+
function renderRecord(rec) {
|
|
2864
|
+
const turn = `[t${rec.turn}]`;
|
|
2865
|
+
if (rec.role === "user") {
|
|
2866
|
+
console.log(`${turn} USER: ${oneLine(rec.content)}`);
|
|
2867
|
+
} else if (rec.role === "assistant_final") {
|
|
2868
|
+
const cost = rec.cost !== void 0 ? ` $${rec.cost.toFixed(6)}` : "";
|
|
2869
|
+
const cache = rec.usage && (rec.usage.prompt_cache_hit_tokens !== void 0 || rec.usage.prompt_cache_miss_tokens !== void 0) ? (() => {
|
|
2870
|
+
const hit = rec.usage.prompt_cache_hit_tokens ?? 0;
|
|
2871
|
+
const miss = rec.usage.prompt_cache_miss_tokens ?? 0;
|
|
2872
|
+
const total = hit + miss;
|
|
2873
|
+
return total > 0 ? ` cache=${(hit / total * 100).toFixed(1)}%` : "";
|
|
2874
|
+
})() : "";
|
|
2875
|
+
console.log(`${turn} AGENT:${cost}${cache} ${oneLine(rec.content)}`);
|
|
2876
|
+
} else if (rec.role === "tool") {
|
|
2877
|
+
const args = rec.args ? ` args=${oneLine(rec.args, 80)}` : "";
|
|
2878
|
+
console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine(rec.content, 120)}`);
|
|
2879
|
+
} else if (rec.role === "error") {
|
|
2880
|
+
console.log(`${turn} ERROR: ${rec.error ?? rec.content}`);
|
|
2881
|
+
} else if (rec.role === "done") {
|
|
2882
|
+
} else {
|
|
2883
|
+
console.log(`${turn} ${rec.role}: ${oneLine(rec.content)}`);
|
|
2884
|
+
}
|
|
2885
|
+
}
|
|
2886
|
+
function oneLine(s, max = 200) {
|
|
2887
|
+
const collapsed = s.replace(/\s+/g, " ").trim();
|
|
2888
|
+
return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
|
|
2889
|
+
}
|
|
2890
|
+
|
|
2137
2891
|
// src/cli/commands/run.ts
|
|
2138
2892
|
import { stdin, stdout } from "process";
|
|
2139
2893
|
import { createInterface } from "readline/promises";
|
|
@@ -2194,13 +2948,13 @@ async function runCommand(opts) {
|
|
|
2194
2948
|
}
|
|
2195
2949
|
|
|
2196
2950
|
// src/cli/commands/stats.ts
|
|
2197
|
-
import { existsSync as existsSync2, readFileSync as
|
|
2951
|
+
import { existsSync as existsSync2, readFileSync as readFileSync5 } from "fs";
|
|
2198
2952
|
function statsCommand(opts) {
|
|
2199
2953
|
if (!existsSync2(opts.transcript)) {
|
|
2200
2954
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
2201
2955
|
process.exit(1);
|
|
2202
2956
|
}
|
|
2203
|
-
const lines =
|
|
2957
|
+
const lines = readFileSync5(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
2204
2958
|
let assistantTurns = 0;
|
|
2205
2959
|
let toolCalls = 0;
|
|
2206
2960
|
let lastTurn = 0;
|
|
@@ -2262,6 +3016,29 @@ program.command("run <task>").description("Run a single task non-interactively,
|
|
|
2262
3016
|
program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
|
|
2263
3017
|
statsCommand({ transcript });
|
|
2264
3018
|
});
|
|
3019
|
+
program.command("replay <transcript>").description(
|
|
3020
|
+
"Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
|
|
3021
|
+
).option("--print", "Dump to stdout instead of mounting the TUI (auto when piped)").option("--head <n>", "stdout mode only \u2014 show first N records", (v) => Number.parseInt(v, 10)).option("--tail <n>", "stdout mode only \u2014 show last N records", (v) => Number.parseInt(v, 10)).action(async (transcript, opts) => {
|
|
3022
|
+
await replayCommand({
|
|
3023
|
+
path: transcript,
|
|
3024
|
+
print: !!opts.print,
|
|
3025
|
+
head: Number.isFinite(opts.head) ? opts.head : void 0,
|
|
3026
|
+
tail: Number.isFinite(opts.tail) ? opts.tail : void 0
|
|
3027
|
+
});
|
|
3028
|
+
});
|
|
3029
|
+
program.command("diff <a> <b>").description(
|
|
3030
|
+
"Compare two transcripts in a split-pane Ink TUI (default) or stdout table. Use n/N to jump across divergences."
|
|
3031
|
+
).option("--md <path>", "Write a markdown report (blog-ready) to this path").option("--print", "Force stdout table instead of the TUI (auto when piped)").option("--tui", "Force the TUI even when piped (rare)").option("--label-a <label>", "Display label for transcript A (default: filename)").option("--label-b <label>", "Display label for transcript B (default: filename)").action(async (a, b, opts) => {
|
|
3032
|
+
await diffCommand({
|
|
3033
|
+
a,
|
|
3034
|
+
b,
|
|
3035
|
+
mdPath: opts.md,
|
|
3036
|
+
labelA: opts.labelA,
|
|
3037
|
+
labelB: opts.labelB,
|
|
3038
|
+
print: !!opts.print,
|
|
3039
|
+
tui: !!opts.tui
|
|
3040
|
+
});
|
|
3041
|
+
});
|
|
2265
3042
|
program.command("version").description("Print Reasonix version.").action(versionCommand);
|
|
2266
3043
|
program.parseAsync(process.argv).catch((err) => {
|
|
2267
3044
|
console.error(err);
|