reasonix 0.2.2 → 0.3.0-alpha.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 +33 -0
- package/dist/cli/index.js +734 -105
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +277 -2
- package/dist/index.js +347 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1360,6 +1360,14 @@ function recordFromLoopEvent(ev, extra) {
|
|
|
1360
1360
|
if (ev.toolName !== void 0) rec.tool = ev.toolName;
|
|
1361
1361
|
if (ev.toolArgs !== void 0) rec.args = ev.toolArgs;
|
|
1362
1362
|
if (ev.error !== void 0) rec.error = ev.error;
|
|
1363
|
+
if (ev.planState && !isPlanStateEmptyShape(ev.planState)) {
|
|
1364
|
+
rec.planState = {
|
|
1365
|
+
subgoals: [...ev.planState.subgoals],
|
|
1366
|
+
hypotheses: [...ev.planState.hypotheses],
|
|
1367
|
+
uncertainties: [...ev.planState.uncertainties],
|
|
1368
|
+
rejectedPaths: [...ev.planState.rejectedPaths]
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1363
1371
|
if (ev.stats) {
|
|
1364
1372
|
rec.usage = {
|
|
1365
1373
|
prompt_tokens: ev.stats.usage.promptTokens,
|
|
@@ -1395,6 +1403,9 @@ function readTranscript(path) {
|
|
|
1395
1403
|
const raw = readFileSync3(path, "utf8");
|
|
1396
1404
|
return parseTranscript(raw);
|
|
1397
1405
|
}
|
|
1406
|
+
function isPlanStateEmptyShape(s) {
|
|
1407
|
+
return s.subgoals.length === 0 && s.hypotheses.length === 0 && s.uncertainties.length === 0 && s.rejectedPaths.length === 0;
|
|
1408
|
+
}
|
|
1398
1409
|
function parseTranscript(raw) {
|
|
1399
1410
|
const out = { meta: null, records: [] };
|
|
1400
1411
|
for (const line of raw.split(/\r?\n/)) {
|
|
@@ -1448,12 +1459,20 @@ function computeReplayStats(records) {
|
|
|
1448
1459
|
const prefixHashes = /* @__PURE__ */ new Set();
|
|
1449
1460
|
let userTurns = 0;
|
|
1450
1461
|
let toolCalls = 0;
|
|
1462
|
+
let harvestedTurns = 0;
|
|
1463
|
+
let totalUncertainties = 0;
|
|
1464
|
+
let totalSubgoals = 0;
|
|
1451
1465
|
for (const rec of records) {
|
|
1452
1466
|
if (rec.role === "user") userTurns++;
|
|
1453
1467
|
else if (rec.role === "tool") toolCalls++;
|
|
1454
1468
|
else if (rec.role === "assistant_final") {
|
|
1455
1469
|
if (rec.model) models.add(rec.model);
|
|
1456
1470
|
if (rec.prefixHash) prefixHashes.add(rec.prefixHash);
|
|
1471
|
+
if (rec.planState) {
|
|
1472
|
+
harvestedTurns++;
|
|
1473
|
+
totalUncertainties += rec.planState.uncertainties.length;
|
|
1474
|
+
totalSubgoals += rec.planState.subgoals.length;
|
|
1475
|
+
}
|
|
1457
1476
|
if (rec.usage && rec.model) {
|
|
1458
1477
|
const u = new Usage(
|
|
1459
1478
|
rec.usage.prompt_tokens ?? 0,
|
|
@@ -1481,6 +1500,9 @@ function computeReplayStats(records) {
|
|
|
1481
1500
|
prefixHashes: [...prefixHashes],
|
|
1482
1501
|
userTurns,
|
|
1483
1502
|
toolCalls,
|
|
1503
|
+
harvestedTurns,
|
|
1504
|
+
totalUncertainties,
|
|
1505
|
+
totalSubgoals,
|
|
1484
1506
|
...summarizeTurns(turns)
|
|
1485
1507
|
};
|
|
1486
1508
|
}
|
|
@@ -1664,6 +1686,41 @@ function renderSummaryTable(report, _opts = {}) {
|
|
|
1664
1686
|
)
|
|
1665
1687
|
);
|
|
1666
1688
|
lines.push(statRow("prefix hashes", a.stats.prefixHashes.length, b.stats.prefixHashes.length));
|
|
1689
|
+
if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
|
|
1690
|
+
lines.push(
|
|
1691
|
+
row(
|
|
1692
|
+
[
|
|
1693
|
+
"harvest turns",
|
|
1694
|
+
`${a.stats.harvestedTurns}`,
|
|
1695
|
+
`${b.stats.harvestedTurns}`,
|
|
1696
|
+
signed(b.stats.harvestedTurns - a.stats.harvestedTurns)
|
|
1697
|
+
],
|
|
1698
|
+
[20, 14, 14, 14]
|
|
1699
|
+
)
|
|
1700
|
+
);
|
|
1701
|
+
lines.push(
|
|
1702
|
+
row(
|
|
1703
|
+
[
|
|
1704
|
+
" subgoals",
|
|
1705
|
+
`${a.stats.totalSubgoals}`,
|
|
1706
|
+
`${b.stats.totalSubgoals}`,
|
|
1707
|
+
signed(b.stats.totalSubgoals - a.stats.totalSubgoals)
|
|
1708
|
+
],
|
|
1709
|
+
[20, 14, 14, 14]
|
|
1710
|
+
)
|
|
1711
|
+
);
|
|
1712
|
+
lines.push(
|
|
1713
|
+
row(
|
|
1714
|
+
[
|
|
1715
|
+
" uncertainties",
|
|
1716
|
+
`${a.stats.totalUncertainties}`,
|
|
1717
|
+
`${b.stats.totalUncertainties}`,
|
|
1718
|
+
signed(b.stats.totalUncertainties - a.stats.totalUncertainties)
|
|
1719
|
+
],
|
|
1720
|
+
[20, 14, 14, 14]
|
|
1721
|
+
)
|
|
1722
|
+
);
|
|
1723
|
+
}
|
|
1667
1724
|
lines.push("");
|
|
1668
1725
|
const aPrefixStable = a.stats.prefixHashes.length <= 1;
|
|
1669
1726
|
const bPrefixStable = b.stats.prefixHashes.length <= 1;
|
|
@@ -1735,6 +1792,17 @@ function renderMarkdown(report) {
|
|
|
1735
1792
|
out.push(
|
|
1736
1793
|
`| prefix hashes | ${a.stats.prefixHashes.length} | ${b.stats.prefixHashes.length} | \u2014 |`
|
|
1737
1794
|
);
|
|
1795
|
+
if (a.stats.harvestedTurns > 0 || b.stats.harvestedTurns > 0) {
|
|
1796
|
+
out.push(
|
|
1797
|
+
`| harvest turns | ${a.stats.harvestedTurns} | ${b.stats.harvestedTurns} | ${signed(b.stats.harvestedTurns - a.stats.harvestedTurns)} |`
|
|
1798
|
+
);
|
|
1799
|
+
out.push(
|
|
1800
|
+
`| harvest subgoals | ${a.stats.totalSubgoals} | ${b.stats.totalSubgoals} | ${signed(b.stats.totalSubgoals - a.stats.totalSubgoals)} |`
|
|
1801
|
+
);
|
|
1802
|
+
out.push(
|
|
1803
|
+
`| harvest uncertainties | ${a.stats.totalUncertainties} | ${b.stats.totalUncertainties} | ${signed(b.stats.totalUncertainties - a.stats.totalUncertainties)} |`
|
|
1804
|
+
);
|
|
1805
|
+
}
|
|
1738
1806
|
out.push("");
|
|
1739
1807
|
out.push("## Turn-by-turn");
|
|
1740
1808
|
out.push("");
|
|
@@ -1802,6 +1870,278 @@ function truncate(s, n) {
|
|
|
1802
1870
|
return s.length > n ? `${s.slice(0, n)}\u2026` : s;
|
|
1803
1871
|
}
|
|
1804
1872
|
|
|
1873
|
+
// src/mcp/types.ts
|
|
1874
|
+
var MCP_PROTOCOL_VERSION = "2024-11-05";
|
|
1875
|
+
function isJsonRpcError(msg) {
|
|
1876
|
+
return "error" in msg;
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
// src/mcp/client.ts
|
|
1880
|
+
var McpClient = class {
|
|
1881
|
+
transport;
|
|
1882
|
+
clientInfo;
|
|
1883
|
+
requestTimeoutMs;
|
|
1884
|
+
pending = /* @__PURE__ */ new Map();
|
|
1885
|
+
nextId = 1;
|
|
1886
|
+
readerStarted = false;
|
|
1887
|
+
initialized = false;
|
|
1888
|
+
_serverCapabilities = {};
|
|
1889
|
+
constructor(opts) {
|
|
1890
|
+
this.transport = opts.transport;
|
|
1891
|
+
this.clientInfo = opts.clientInfo ?? { name: "reasonix", version: "0.3.0-dev" };
|
|
1892
|
+
this.requestTimeoutMs = opts.requestTimeoutMs ?? 6e4;
|
|
1893
|
+
}
|
|
1894
|
+
/** Server's advertised capabilities, available after initialize(). */
|
|
1895
|
+
get serverCapabilities() {
|
|
1896
|
+
return this._serverCapabilities;
|
|
1897
|
+
}
|
|
1898
|
+
/**
|
|
1899
|
+
* Complete the initialize → initialized handshake. Must be called
|
|
1900
|
+
* before any other method (otherwise compliant servers reject).
|
|
1901
|
+
*/
|
|
1902
|
+
async initialize() {
|
|
1903
|
+
if (this.initialized) throw new Error("MCP client already initialized");
|
|
1904
|
+
this.startReaderIfNeeded();
|
|
1905
|
+
const result = await this.request("initialize", {
|
|
1906
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
1907
|
+
capabilities: { tools: {} },
|
|
1908
|
+
clientInfo: this.clientInfo
|
|
1909
|
+
});
|
|
1910
|
+
this._serverCapabilities = result.capabilities ?? {};
|
|
1911
|
+
await this.transport.send({
|
|
1912
|
+
jsonrpc: "2.0",
|
|
1913
|
+
method: "notifications/initialized"
|
|
1914
|
+
});
|
|
1915
|
+
this.initialized = true;
|
|
1916
|
+
return result;
|
|
1917
|
+
}
|
|
1918
|
+
/** List tools the server exposes. */
|
|
1919
|
+
async listTools() {
|
|
1920
|
+
this.assertInitialized();
|
|
1921
|
+
return this.request("tools/list", {});
|
|
1922
|
+
}
|
|
1923
|
+
/** Invoke a tool by name. Returns the raw MCP result (caller unwraps content). */
|
|
1924
|
+
async callTool(name, args) {
|
|
1925
|
+
this.assertInitialized();
|
|
1926
|
+
return this.request("tools/call", {
|
|
1927
|
+
name,
|
|
1928
|
+
arguments: args ?? {}
|
|
1929
|
+
});
|
|
1930
|
+
}
|
|
1931
|
+
/** Close the transport and reject any outstanding requests. */
|
|
1932
|
+
async close() {
|
|
1933
|
+
for (const [, pending] of this.pending) {
|
|
1934
|
+
clearTimeout(pending.timeout);
|
|
1935
|
+
pending.reject(new Error("MCP client closed"));
|
|
1936
|
+
}
|
|
1937
|
+
this.pending.clear();
|
|
1938
|
+
await this.transport.close();
|
|
1939
|
+
}
|
|
1940
|
+
// ---------- internals ----------
|
|
1941
|
+
assertInitialized() {
|
|
1942
|
+
if (!this.initialized) throw new Error("MCP client not initialized \u2014 call initialize() first");
|
|
1943
|
+
}
|
|
1944
|
+
async request(method, params) {
|
|
1945
|
+
const id = this.nextId++;
|
|
1946
|
+
const frame = { jsonrpc: "2.0", id, method, params };
|
|
1947
|
+
const promise = new Promise((resolve2, reject) => {
|
|
1948
|
+
const timeout = setTimeout(() => {
|
|
1949
|
+
this.pending.delete(id);
|
|
1950
|
+
reject(
|
|
1951
|
+
new Error(`MCP request ${method} (id=${id}) timed out after ${this.requestTimeoutMs}ms`)
|
|
1952
|
+
);
|
|
1953
|
+
}, this.requestTimeoutMs);
|
|
1954
|
+
this.pending.set(id, {
|
|
1955
|
+
resolve: resolve2,
|
|
1956
|
+
reject,
|
|
1957
|
+
timeout
|
|
1958
|
+
});
|
|
1959
|
+
});
|
|
1960
|
+
await this.transport.send(frame);
|
|
1961
|
+
return promise;
|
|
1962
|
+
}
|
|
1963
|
+
startReaderIfNeeded() {
|
|
1964
|
+
if (this.readerStarted) return;
|
|
1965
|
+
this.readerStarted = true;
|
|
1966
|
+
void this.readLoop();
|
|
1967
|
+
}
|
|
1968
|
+
async readLoop() {
|
|
1969
|
+
try {
|
|
1970
|
+
for await (const msg of this.transport.messages()) {
|
|
1971
|
+
this.dispatch(msg);
|
|
1972
|
+
}
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
for (const [, pending] of this.pending) {
|
|
1975
|
+
clearTimeout(pending.timeout);
|
|
1976
|
+
pending.reject(err);
|
|
1977
|
+
}
|
|
1978
|
+
this.pending.clear();
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
dispatch(msg) {
|
|
1982
|
+
if (!("id" in msg) || msg.id === null || msg.id === void 0) return;
|
|
1983
|
+
if (!("result" in msg) && !("error" in msg)) return;
|
|
1984
|
+
const pending = this.pending.get(msg.id);
|
|
1985
|
+
if (!pending) return;
|
|
1986
|
+
this.pending.delete(msg.id);
|
|
1987
|
+
clearTimeout(pending.timeout);
|
|
1988
|
+
const resp = msg;
|
|
1989
|
+
if (isJsonRpcError(resp)) {
|
|
1990
|
+
pending.reject(new Error(`MCP ${resp.error.code}: ${resp.error.message}`));
|
|
1991
|
+
} else {
|
|
1992
|
+
pending.resolve(resp.result);
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
|
|
1997
|
+
// src/mcp/stdio.ts
|
|
1998
|
+
import { spawn } from "child_process";
|
|
1999
|
+
var StdioTransport = class {
|
|
2000
|
+
child;
|
|
2001
|
+
queue = [];
|
|
2002
|
+
waiters = [];
|
|
2003
|
+
closed = false;
|
|
2004
|
+
stdoutBuffer = "";
|
|
2005
|
+
constructor(opts) {
|
|
2006
|
+
const env = opts.replaceEnv ? { ...opts.env ?? {} } : { ...process.env, ...opts.env ?? {} };
|
|
2007
|
+
const shell = opts.shell ?? process.platform === "win32";
|
|
2008
|
+
if (shell) {
|
|
2009
|
+
const line = [
|
|
2010
|
+
opts.command,
|
|
2011
|
+
...(opts.args ?? []).map((a) => quoteArg(a, process.platform === "win32"))
|
|
2012
|
+
].join(" ");
|
|
2013
|
+
this.child = spawn(line, [], {
|
|
2014
|
+
env,
|
|
2015
|
+
cwd: opts.cwd,
|
|
2016
|
+
stdio: ["pipe", "pipe", "inherit"],
|
|
2017
|
+
shell: true
|
|
2018
|
+
});
|
|
2019
|
+
} else {
|
|
2020
|
+
this.child = spawn(opts.command, opts.args ?? [], {
|
|
2021
|
+
env,
|
|
2022
|
+
cwd: opts.cwd,
|
|
2023
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
2024
|
+
});
|
|
2025
|
+
}
|
|
2026
|
+
this.child.stdout.setEncoding("utf8");
|
|
2027
|
+
this.child.stdout.on("data", (chunk) => this.onStdout(chunk));
|
|
2028
|
+
this.child.on("close", () => this.onClose());
|
|
2029
|
+
this.child.on("error", (err) => {
|
|
2030
|
+
this.push({
|
|
2031
|
+
jsonrpc: "2.0",
|
|
2032
|
+
id: null,
|
|
2033
|
+
error: { code: -32e3, message: `transport error: ${err.message}` }
|
|
2034
|
+
});
|
|
2035
|
+
});
|
|
2036
|
+
}
|
|
2037
|
+
async send(message) {
|
|
2038
|
+
if (this.closed) throw new Error("MCP transport is closed");
|
|
2039
|
+
return new Promise((resolve2, reject) => {
|
|
2040
|
+
const line = `${JSON.stringify(message)}
|
|
2041
|
+
`;
|
|
2042
|
+
this.child.stdin.write(line, "utf8", (err) => {
|
|
2043
|
+
if (err) reject(err);
|
|
2044
|
+
else resolve2();
|
|
2045
|
+
});
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
async *messages() {
|
|
2049
|
+
while (true) {
|
|
2050
|
+
if (this.queue.length > 0) {
|
|
2051
|
+
yield this.queue.shift();
|
|
2052
|
+
continue;
|
|
2053
|
+
}
|
|
2054
|
+
if (this.closed) return;
|
|
2055
|
+
const next = await new Promise((resolve2) => {
|
|
2056
|
+
this.waiters.push(resolve2);
|
|
2057
|
+
});
|
|
2058
|
+
if (next === null) return;
|
|
2059
|
+
yield next;
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async close() {
|
|
2063
|
+
if (this.closed) return;
|
|
2064
|
+
this.closed = true;
|
|
2065
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2066
|
+
try {
|
|
2067
|
+
this.child.stdin.end();
|
|
2068
|
+
} catch {
|
|
2069
|
+
}
|
|
2070
|
+
if (this.child.exitCode === null && !this.child.killed) {
|
|
2071
|
+
this.child.kill("SIGTERM");
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
/** Parse incoming stdout chunks into NDJSON messages. */
|
|
2075
|
+
onStdout(chunk) {
|
|
2076
|
+
this.stdoutBuffer += chunk;
|
|
2077
|
+
let newlineIdx;
|
|
2078
|
+
while ((newlineIdx = this.stdoutBuffer.indexOf("\n")) !== -1) {
|
|
2079
|
+
const line = this.stdoutBuffer.slice(0, newlineIdx).trim();
|
|
2080
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIdx + 1);
|
|
2081
|
+
if (!line) continue;
|
|
2082
|
+
try {
|
|
2083
|
+
const msg = JSON.parse(line);
|
|
2084
|
+
this.push(msg);
|
|
2085
|
+
} catch {
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
onClose() {
|
|
2090
|
+
this.closed = true;
|
|
2091
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
2092
|
+
}
|
|
2093
|
+
push(msg) {
|
|
2094
|
+
const waiter = this.waiters.shift();
|
|
2095
|
+
if (waiter) waiter(msg);
|
|
2096
|
+
else this.queue.push(msg);
|
|
2097
|
+
}
|
|
2098
|
+
};
|
|
2099
|
+
function quoteArg(s, windows) {
|
|
2100
|
+
if (!windows) {
|
|
2101
|
+
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
2102
|
+
}
|
|
2103
|
+
return `"${s.replace(/"/g, '""')}"`;
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
// src/mcp/registry.ts
|
|
2107
|
+
async function bridgeMcpTools(client, opts = {}) {
|
|
2108
|
+
const registry = opts.registry ?? new ToolRegistry({ autoFlatten: opts.autoFlatten });
|
|
2109
|
+
const prefix = opts.namePrefix ?? "";
|
|
2110
|
+
const result = { registry, registeredNames: [], skipped: [] };
|
|
2111
|
+
const listed = await client.listTools();
|
|
2112
|
+
for (const mcpTool of listed.tools) {
|
|
2113
|
+
if (!mcpTool.name) {
|
|
2114
|
+
result.skipped.push({ name: "?", reason: "empty tool name" });
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const registeredName = `${prefix}${mcpTool.name}`;
|
|
2118
|
+
registry.register({
|
|
2119
|
+
name: registeredName,
|
|
2120
|
+
description: mcpTool.description ?? "",
|
|
2121
|
+
parameters: mcpTool.inputSchema,
|
|
2122
|
+
fn: async (args) => {
|
|
2123
|
+
const toolResult = await client.callTool(mcpTool.name, args);
|
|
2124
|
+
return flattenMcpResult(toolResult);
|
|
2125
|
+
}
|
|
2126
|
+
});
|
|
2127
|
+
result.registeredNames.push(registeredName);
|
|
2128
|
+
}
|
|
2129
|
+
return result;
|
|
2130
|
+
}
|
|
2131
|
+
function flattenMcpResult(result) {
|
|
2132
|
+
const parts = result.content.map(blockToString);
|
|
2133
|
+
const joined = parts.join("\n").trim();
|
|
2134
|
+
if (result.isError) {
|
|
2135
|
+
return `ERROR: ${joined || "(no error message from server)"}`;
|
|
2136
|
+
}
|
|
2137
|
+
return joined;
|
|
2138
|
+
}
|
|
2139
|
+
function blockToString(block) {
|
|
2140
|
+
if (block.type === "text") return block.text;
|
|
2141
|
+
if (block.type === "image") return `[image ${block.mimeType}, ${block.data.length} chars base64]`;
|
|
2142
|
+
return `[unknown block: ${JSON.stringify(block)}]`;
|
|
2143
|
+
}
|
|
2144
|
+
|
|
1805
2145
|
// src/config.ts
|
|
1806
2146
|
import { chmodSync as chmodSync2, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync } from "fs";
|
|
1807
2147
|
import { homedir as homedir2 } from "os";
|
|
@@ -1846,23 +2186,93 @@ function redactKey(key) {
|
|
|
1846
2186
|
}
|
|
1847
2187
|
|
|
1848
2188
|
// src/index.ts
|
|
1849
|
-
var VERSION = "0.
|
|
2189
|
+
var VERSION = "0.3.0-alpha.2";
|
|
1850
2190
|
|
|
1851
2191
|
// src/cli/commands/chat.tsx
|
|
1852
2192
|
import { render } from "ink";
|
|
1853
|
-
import
|
|
2193
|
+
import React8, { useState as useState4 } from "react";
|
|
2194
|
+
|
|
2195
|
+
// src/mcp/shell-split.ts
|
|
2196
|
+
function shellSplit(input) {
|
|
2197
|
+
const tokens = [];
|
|
2198
|
+
let cur = "";
|
|
2199
|
+
let quote = null;
|
|
2200
|
+
let i = 0;
|
|
2201
|
+
const s = input;
|
|
2202
|
+
while (i < s.length) {
|
|
2203
|
+
const ch = s[i];
|
|
2204
|
+
if (quote) {
|
|
2205
|
+
if (ch === quote) {
|
|
2206
|
+
quote = null;
|
|
2207
|
+
i++;
|
|
2208
|
+
continue;
|
|
2209
|
+
}
|
|
2210
|
+
if (ch === "\\" && quote === '"' && i + 1 < s.length) {
|
|
2211
|
+
cur += s[i + 1];
|
|
2212
|
+
i += 2;
|
|
2213
|
+
continue;
|
|
2214
|
+
}
|
|
2215
|
+
cur += ch;
|
|
2216
|
+
i++;
|
|
2217
|
+
continue;
|
|
2218
|
+
}
|
|
2219
|
+
if (ch === '"' || ch === "'") {
|
|
2220
|
+
quote = ch;
|
|
2221
|
+
i++;
|
|
2222
|
+
continue;
|
|
2223
|
+
}
|
|
2224
|
+
if (ch === "\\" && i + 1 < s.length) {
|
|
2225
|
+
cur += s[i + 1];
|
|
2226
|
+
i += 2;
|
|
2227
|
+
continue;
|
|
2228
|
+
}
|
|
2229
|
+
if (ch === " " || ch === " ") {
|
|
2230
|
+
if (cur.length > 0) {
|
|
2231
|
+
tokens.push(cur);
|
|
2232
|
+
cur = "";
|
|
2233
|
+
}
|
|
2234
|
+
i++;
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
cur += ch;
|
|
2238
|
+
i++;
|
|
2239
|
+
}
|
|
2240
|
+
if (quote) {
|
|
2241
|
+
throw new Error(
|
|
2242
|
+
`shellSplit: unterminated ${quote === '"' ? "double" : "single"} quote in input`
|
|
2243
|
+
);
|
|
2244
|
+
}
|
|
2245
|
+
if (cur.length > 0) tokens.push(cur);
|
|
2246
|
+
return tokens;
|
|
2247
|
+
}
|
|
1854
2248
|
|
|
1855
2249
|
// src/cli/ui/App.tsx
|
|
1856
|
-
import { Box as
|
|
1857
|
-
import
|
|
2250
|
+
import { Box as Box6, Static, Text as Text6, useApp } from "ink";
|
|
2251
|
+
import React6, { useCallback, useEffect as useEffect2, useMemo, useRef, useState as useState2 } from "react";
|
|
1858
2252
|
|
|
1859
2253
|
// src/cli/ui/EventLog.tsx
|
|
1860
|
-
import { Box as
|
|
1861
|
-
import
|
|
2254
|
+
import { Box as Box3, Text as Text3 } from "ink";
|
|
2255
|
+
import React3, { useEffect, useState } from "react";
|
|
1862
2256
|
|
|
1863
|
-
// src/cli/ui/
|
|
2257
|
+
// src/cli/ui/PlanStateBlock.tsx
|
|
1864
2258
|
import { Box, Text } from "ink";
|
|
1865
2259
|
import React from "react";
|
|
2260
|
+
function PlanStateBlock({ planState }) {
|
|
2261
|
+
const fields = [];
|
|
2262
|
+
if (planState.subgoals.length) fields.push(["subgoals", planState.subgoals, "cyan", false]);
|
|
2263
|
+
if (planState.hypotheses.length)
|
|
2264
|
+
fields.push(["hypotheses", planState.hypotheses, "green", false]);
|
|
2265
|
+
if (planState.uncertainties.length)
|
|
2266
|
+
fields.push(["uncertainties", planState.uncertainties, "yellow", false]);
|
|
2267
|
+
if (planState.rejectedPaths.length)
|
|
2268
|
+
fields.push(["rejected", planState.rejectedPaths, "red", true]);
|
|
2269
|
+
if (fields.length === 0) return null;
|
|
2270
|
+
return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", marginBottom: 1 }, fields.map(([label, items, color, dim]) => /* @__PURE__ */ React.createElement(Text, { key: label }, /* @__PURE__ */ React.createElement(Text, { color, bold: true, dimColor: dim }, "\u2039 ", label), /* @__PURE__ */ React.createElement(Text, { dimColor: true }, ` (${items.length})`), /* @__PURE__ */ React.createElement(Text, null, `: ${items.join(" \xB7 ")}`))));
|
|
2271
|
+
}
|
|
2272
|
+
|
|
2273
|
+
// src/cli/ui/markdown.tsx
|
|
2274
|
+
import { Box as Box2, Text as Text2 } from "ink";
|
|
2275
|
+
import React2 from "react";
|
|
1866
2276
|
var SUPERSCRIPT = {
|
|
1867
2277
|
"0": "\u2070",
|
|
1868
2278
|
"1": "\xB9",
|
|
@@ -1919,27 +2329,27 @@ function InlineMd({ text }) {
|
|
|
1919
2329
|
for (const m of text.matchAll(INLINE_RE)) {
|
|
1920
2330
|
const start = m.index ?? 0;
|
|
1921
2331
|
if (start > last) {
|
|
1922
|
-
parts.push(/* @__PURE__ */
|
|
2332
|
+
parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last, start)));
|
|
1923
2333
|
}
|
|
1924
2334
|
if (m[2] !== void 0) {
|
|
1925
2335
|
parts.push(
|
|
1926
|
-
/* @__PURE__ */
|
|
2336
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `b${idx++}`, bold: true }, m[2])
|
|
1927
2337
|
);
|
|
1928
2338
|
} else if (m[3] !== void 0) {
|
|
1929
2339
|
parts.push(
|
|
1930
|
-
/* @__PURE__ */
|
|
2340
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `c${idx++}`, color: "yellow" }, m[3])
|
|
1931
2341
|
);
|
|
1932
2342
|
} else if (m[4] !== void 0) {
|
|
1933
2343
|
parts.push(
|
|
1934
|
-
/* @__PURE__ */
|
|
2344
|
+
/* @__PURE__ */ React2.createElement(Text2, { key: `i${idx++}`, italic: true }, m[4])
|
|
1935
2345
|
);
|
|
1936
2346
|
}
|
|
1937
2347
|
last = start + m[0].length;
|
|
1938
2348
|
}
|
|
1939
2349
|
if (last < text.length) {
|
|
1940
|
-
parts.push(/* @__PURE__ */
|
|
2350
|
+
parts.push(/* @__PURE__ */ React2.createElement(Text2, { key: `t${idx++}` }, text.slice(last)));
|
|
1941
2351
|
}
|
|
1942
|
-
return /* @__PURE__ */
|
|
2352
|
+
return /* @__PURE__ */ React2.createElement(Text2, null, parts);
|
|
1943
2353
|
}
|
|
1944
2354
|
function parseBlocks(raw) {
|
|
1945
2355
|
const lines = raw.split(/\r?\n/);
|
|
@@ -2033,42 +2443,42 @@ function parseBlocks(raw) {
|
|
|
2033
2443
|
function BlockView({ block }) {
|
|
2034
2444
|
switch (block.kind) {
|
|
2035
2445
|
case "heading":
|
|
2036
|
-
return /* @__PURE__ */
|
|
2446
|
+
return /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text }));
|
|
2037
2447
|
case "paragraph":
|
|
2038
|
-
return /* @__PURE__ */
|
|
2448
|
+
return /* @__PURE__ */ React2.createElement(InlineMd, { text: block.text });
|
|
2039
2449
|
case "bullet":
|
|
2040
|
-
return /* @__PURE__ */
|
|
2450
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column" }, block.items.map((item, i) => /* @__PURE__ */ React2.createElement(Box2, { key: `${i}-${item.slice(0, 24)}` }, /* @__PURE__ */ React2.createElement(Text2, { color: "cyan" }, block.ordered ? ` ${block.start + i}. ` : " \u2022 "), /* @__PURE__ */ React2.createElement(InlineMd, { text: item }))));
|
|
2041
2451
|
case "code":
|
|
2042
|
-
return /* @__PURE__ */
|
|
2452
|
+
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
2043
2453
|
case "hr":
|
|
2044
|
-
return /* @__PURE__ */
|
|
2454
|
+
return /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
|
|
2045
2455
|
}
|
|
2046
2456
|
}
|
|
2047
2457
|
function Markdown({ text }) {
|
|
2048
2458
|
const cleaned = stripMath(text);
|
|
2049
|
-
const blocks =
|
|
2050
|
-
return /* @__PURE__ */
|
|
2459
|
+
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
2460
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", gap: 1 }, blocks.map((b, i) => /* @__PURE__ */ React2.createElement(BlockView, { key: `${i}-${b.kind}`, block: b })));
|
|
2051
2461
|
}
|
|
2052
2462
|
|
|
2053
2463
|
// src/cli/ui/EventLog.tsx
|
|
2054
|
-
var EventRow =
|
|
2464
|
+
var EventRow = React3.memo(function EventRow2({ event }) {
|
|
2055
2465
|
if (event.role === "user") {
|
|
2056
|
-
return /* @__PURE__ */
|
|
2466
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
2057
2467
|
}
|
|
2058
2468
|
if (event.role === "assistant") {
|
|
2059
|
-
if (event.streaming) return /* @__PURE__ */
|
|
2060
|
-
return /* @__PURE__ */
|
|
2469
|
+
if (event.streaming) return /* @__PURE__ */ React3.createElement(StreamingAssistant, { event });
|
|
2470
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant")), event.branch ? /* @__PURE__ */ React3.createElement(BranchBlock, { branch: event.branch }) : null, event.reasoning ? /* @__PURE__ */ React3.createElement(ReasoningBlock, { reasoning: event.reasoning }) : null, !isPlanStateEmpty(event.planState) ? /* @__PURE__ */ React3.createElement(PlanStateBlock, { planState: event.planState }) : null, event.text ? /* @__PURE__ */ React3.createElement(Markdown, { text: event.text }) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(no content)"), event.stats ? /* @__PURE__ */ React3.createElement(StatsLine, { stats: event.stats }) : null, event.repair ? /* @__PURE__ */ React3.createElement(Text3, { color: "magenta" }, event.repair) : null);
|
|
2061
2471
|
}
|
|
2062
2472
|
if (event.role === "tool") {
|
|
2063
|
-
return /* @__PURE__ */
|
|
2473
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "yellow" }, `tool<${event.toolName ?? "?"}> \u2192`), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", truncate2(event.text, 400)));
|
|
2064
2474
|
}
|
|
2065
2475
|
if (event.role === "error") {
|
|
2066
|
-
return /* @__PURE__ */
|
|
2476
|
+
return /* @__PURE__ */ React3.createElement(Box3, { marginTop: 1 }, /* @__PURE__ */ React3.createElement(Text3, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "red" }, event.text));
|
|
2067
2477
|
}
|
|
2068
2478
|
if (event.role === "info") {
|
|
2069
|
-
return /* @__PURE__ */
|
|
2479
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, event.text));
|
|
2070
2480
|
}
|
|
2071
|
-
return /* @__PURE__ */
|
|
2481
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, null, event.text));
|
|
2072
2482
|
});
|
|
2073
2483
|
function BranchBlock({ branch }) {
|
|
2074
2484
|
const per = branch.uncertainties.map((u, i) => {
|
|
@@ -2076,21 +2486,13 @@ function BranchBlock({ branch }) {
|
|
|
2076
2486
|
const t = (branch.temperatures[i] ?? 0).toFixed(1);
|
|
2077
2487
|
return `${marker} #${i} T=${t} u=${u}`;
|
|
2078
2488
|
}).join(" ");
|
|
2079
|
-
return /* @__PURE__ */
|
|
2080
|
-
}
|
|
2081
|
-
function PlanStateBlock({ planState }) {
|
|
2082
|
-
const lines = [];
|
|
2083
|
-
if (planState.subgoals.length) lines.push(["subgoals", planState.subgoals]);
|
|
2084
|
-
if (planState.hypotheses.length) lines.push(["hypotheses", planState.hypotheses]);
|
|
2085
|
-
if (planState.uncertainties.length) lines.push(["uncertainties", planState.uncertainties]);
|
|
2086
|
-
if (planState.rejectedPaths.length) lines.push(["rejected", planState.rejectedPaths]);
|
|
2087
|
-
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginBottom: 1 }, lines.map(([label, items]) => /* @__PURE__ */ React2.createElement(Text2, { key: label, color: "magenta" }, "\u2039 ", /* @__PURE__ */ React2.createElement(Text2, { bold: true }, label), ` (${items.length}): ${items.join(" \xB7 ")}`)));
|
|
2489
|
+
return /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branched ", /* @__PURE__ */ React3.createElement(Text3, { bold: true }, branch.budget), ` samples \u2192 picked #${branch.chosenIndex} `, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, per)));
|
|
2088
2490
|
}
|
|
2089
2491
|
function ReasoningBlock({ reasoning }) {
|
|
2090
2492
|
const max = 220;
|
|
2091
2493
|
const flat = reasoning.replace(/\s+/g, " ").trim();
|
|
2092
2494
|
const preview = flat.length <= max ? flat : `${flat.slice(0, max)}\u2026 (+${flat.length - max} chars)`;
|
|
2093
|
-
return /* @__PURE__ */
|
|
2495
|
+
return /* @__PURE__ */ React3.createElement(Box3, { marginBottom: 1 }, /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", preview));
|
|
2094
2496
|
}
|
|
2095
2497
|
function Elapsed() {
|
|
2096
2498
|
const [s, setS] = useState(0);
|
|
@@ -2101,20 +2503,20 @@ function Elapsed() {
|
|
|
2101
2503
|
}, []);
|
|
2102
2504
|
const mm = String(Math.floor(s / 60)).padStart(2, "0");
|
|
2103
2505
|
const ss = String(s % 60).padStart(2, "0");
|
|
2104
|
-
return /* @__PURE__ */
|
|
2506
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, `${mm}:${ss}`);
|
|
2105
2507
|
}
|
|
2106
2508
|
function StreamingAssistant({ event }) {
|
|
2107
2509
|
if (event.branchProgress) {
|
|
2108
2510
|
const p = event.branchProgress;
|
|
2109
2511
|
if (p.completed === 0) {
|
|
2110
|
-
return /* @__PURE__ */
|
|
2512
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} launching ", p.total, " parallel samples (R1 thinking in parallel)\u2026", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", "spread across T=0.0/0.5/1.0 \xB7 typical wait 30-90s for reasoner"));
|
|
2111
2513
|
}
|
|
2112
2514
|
const pct2 = Math.round(p.completed / p.total * 100);
|
|
2113
|
-
return /* @__PURE__ */
|
|
2515
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { color: "blue" }, "\u{1F500} branching ", p.completed, "/", p.total, " (", pct2, "%)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), /* @__PURE__ */ React3.createElement(Text3, { 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"));
|
|
2114
2516
|
}
|
|
2115
2517
|
const tail = lastLine(event.text, 140);
|
|
2116
2518
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
2117
|
-
return /* @__PURE__ */
|
|
2519
|
+
return /* @__PURE__ */ React3.createElement(Box3, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React3.createElement(Box3, null, /* @__PURE__ */ React3.createElement(Text3, { bold: true, color: "green" }, "assistant", " "), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "(streaming \xB7 ", event.text.length, event.reasoning ? ` + think ${event.reasoning.length}` : "", " chars)", " "), /* @__PURE__ */ React3.createElement(Elapsed, null)), reasoningTail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, "\u21B3 thinking: ", reasoningTail) : null, tail ? /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, "\u25B8 ", tail) : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first token\u2026)"));
|
|
2118
2520
|
}
|
|
2119
2521
|
function lastLine(s, maxChars) {
|
|
2120
2522
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2123,16 +2525,16 @@ function lastLine(s, maxChars) {
|
|
|
2123
2525
|
}
|
|
2124
2526
|
function StatsLine({ stats }) {
|
|
2125
2527
|
const hit = (stats.cacheHitRatio * 100).toFixed(1);
|
|
2126
|
-
return /* @__PURE__ */
|
|
2528
|
+
return /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " \u21B3 cache ", hit, "% \xB7 tokens ", stats.usage.promptTokens, "\u2192", stats.usage.completionTokens, " \xB7 $", stats.cost.toFixed(6));
|
|
2127
2529
|
}
|
|
2128
2530
|
function truncate2(s, max) {
|
|
2129
2531
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
2130
2532
|
}
|
|
2131
2533
|
|
|
2132
2534
|
// src/cli/ui/PromptInput.tsx
|
|
2133
|
-
import { Box as
|
|
2535
|
+
import { Box as Box4, Text as Text4 } from "ink";
|
|
2134
2536
|
import TextInput from "ink-text-input";
|
|
2135
|
-
import
|
|
2537
|
+
import React4 from "react";
|
|
2136
2538
|
function PromptInput({
|
|
2137
2539
|
value,
|
|
2138
2540
|
onChange,
|
|
@@ -2141,7 +2543,7 @@ function PromptInput({
|
|
|
2141
2543
|
placeholder
|
|
2142
2544
|
}) {
|
|
2143
2545
|
const effectivePlaceholder = disabled ? placeholder ?? "\u2026waiting for response\u2026" : placeholder ?? "type a message, or /command";
|
|
2144
|
-
return /* @__PURE__ */
|
|
2546
|
+
return /* @__PURE__ */ React4.createElement(Box4, { borderStyle: "round", borderColor: disabled ? "gray" : "cyan", paddingX: 1 }, /* @__PURE__ */ React4.createElement(Text4, { bold: true, color: disabled ? "gray" : "cyan" }, "you \u203A", " "), /* @__PURE__ */ React4.createElement(
|
|
2145
2547
|
TextInput,
|
|
2146
2548
|
{
|
|
2147
2549
|
value,
|
|
@@ -2154,8 +2556,8 @@ function PromptInput({
|
|
|
2154
2556
|
}
|
|
2155
2557
|
|
|
2156
2558
|
// src/cli/ui/StatsPanel.tsx
|
|
2157
|
-
import { Box as
|
|
2158
|
-
import
|
|
2559
|
+
import { Box as Box5, Text as Text5 } from "ink";
|
|
2560
|
+
import React5 from "react";
|
|
2159
2561
|
function StatsPanel({
|
|
2160
2562
|
summary,
|
|
2161
2563
|
model,
|
|
@@ -2166,7 +2568,7 @@ function StatsPanel({
|
|
|
2166
2568
|
const hitPct = (summary.cacheHitRatio * 100).toFixed(1);
|
|
2167
2569
|
const hitColor = summary.cacheHitRatio >= 0.7 ? "green" : summary.cacheHitRatio >= 0.4 ? "yellow" : "red";
|
|
2168
2570
|
const branchOn = (branchBudget ?? 1) > 1;
|
|
2169
|
-
return /* @__PURE__ */
|
|
2571
|
+
return /* @__PURE__ */ React5.createElement(Box5, { borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1 }, /* @__PURE__ */ React5.createElement(Box5, { justifyContent: "space-between" }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { color: "cyan", bold: true }, "Reasonix"), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 model "), /* @__PURE__ */ React5.createElement(Text5, { color: "yellow" }, model), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, " \xB7 prefix "), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, prefixHash), harvestOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "magenta" }, " \xB7 harvest") : null, branchOn ? /* @__PURE__ */ React5.createElement(Text5, { color: "blue" }, " \xB7 branch", branchBudget) : null), /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "turns ", summary.turns, " \xB7 type /help")), /* @__PURE__ */ React5.createElement(Box5, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cache hit "), /* @__PURE__ */ React5.createElement(Text5, { color: hitColor, bold: true }, hitPct, "%")), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "cost "), /* @__PURE__ */ React5.createElement(Text5, { color: "green" }, "$", summary.totalCostUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "vs Claude "), /* @__PURE__ */ React5.createElement(Text5, null, "$", summary.claudeEquivalentUsd.toFixed(6))), /* @__PURE__ */ React5.createElement(Text5, null, /* @__PURE__ */ React5.createElement(Text5, { dimColor: true }, "saving "), /* @__PURE__ */ React5.createElement(Text5, { color: "green", bold: true }, summary.savingsVsClaudePct.toFixed(1), "%"))));
|
|
2170
2572
|
}
|
|
2171
2573
|
|
|
2172
2574
|
// src/cli/ui/slash.ts
|
|
@@ -2299,7 +2701,7 @@ function handleSlash(cmd, args, loop) {
|
|
|
2299
2701
|
|
|
2300
2702
|
// src/cli/ui/App.tsx
|
|
2301
2703
|
var FLUSH_INTERVAL_MS = 60;
|
|
2302
|
-
function App({ model, system, transcript, harvest: harvest2, branch, session }) {
|
|
2704
|
+
function App({ model, system, transcript, harvest: harvest2, branch, session, tools }) {
|
|
2303
2705
|
const { exit } = useApp();
|
|
2304
2706
|
const [historical, setHistorical] = useState2([]);
|
|
2305
2707
|
const [streaming, setStreaming] = useState2(null);
|
|
@@ -2330,11 +2732,14 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2330
2732
|
const loop = useMemo(() => {
|
|
2331
2733
|
if (loopRef.current) return loopRef.current;
|
|
2332
2734
|
const client = new DeepSeekClient();
|
|
2333
|
-
const prefix = new ImmutablePrefix({
|
|
2334
|
-
|
|
2735
|
+
const prefix = new ImmutablePrefix({
|
|
2736
|
+
system,
|
|
2737
|
+
toolSpecs: tools?.specs()
|
|
2738
|
+
});
|
|
2739
|
+
const l = new CacheFirstLoop({ client, prefix, tools, model, harvest: harvest2, branch, session });
|
|
2335
2740
|
loopRef.current = l;
|
|
2336
2741
|
return l;
|
|
2337
|
-
}, [model, system, harvest2, branch, session]);
|
|
2742
|
+
}, [model, system, harvest2, branch, session, tools]);
|
|
2338
2743
|
const sessionBannerShown = useRef(false);
|
|
2339
2744
|
useEffect2(() => {
|
|
2340
2745
|
if (sessionBannerShown.current) return;
|
|
@@ -2497,7 +2902,7 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2497
2902
|
},
|
|
2498
2903
|
[busy, exit, loop, writeTranscript]
|
|
2499
2904
|
);
|
|
2500
|
-
return /* @__PURE__ */
|
|
2905
|
+
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
2501
2906
|
StatsPanel,
|
|
2502
2907
|
{
|
|
2503
2908
|
summary,
|
|
@@ -2506,10 +2911,10 @@ function App({ model, system, transcript, harvest: harvest2, branch, session })
|
|
|
2506
2911
|
harvestOn: loop.harvestEnabled,
|
|
2507
2912
|
branchBudget: loop.branchOptions.budget
|
|
2508
2913
|
}
|
|
2509
|
-
), /* @__PURE__ */
|
|
2914
|
+
), /* @__PURE__ */ React6.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React6.createElement(EventRow, { key: item.id, event: item })), streaming ? /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(EventRow, { event: streaming })) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, null));
|
|
2510
2915
|
}
|
|
2511
2916
|
function CommandStrip() {
|
|
2512
|
-
return /* @__PURE__ */
|
|
2917
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2 }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /sessions \xB7 /model \xB7 /harvest \xB7 /branch \xB7 /clear \xB7 /exit"));
|
|
2513
2918
|
}
|
|
2514
2919
|
function describeRepair(repair) {
|
|
2515
2920
|
const parts = [];
|
|
@@ -2520,9 +2925,9 @@ function describeRepair(repair) {
|
|
|
2520
2925
|
}
|
|
2521
2926
|
|
|
2522
2927
|
// src/cli/ui/Setup.tsx
|
|
2523
|
-
import { Box as
|
|
2928
|
+
import { Box as Box7, Text as Text7, useApp as useApp2 } from "ink";
|
|
2524
2929
|
import TextInput2 from "ink-text-input";
|
|
2525
|
-
import
|
|
2930
|
+
import React7, { useState as useState3 } from "react";
|
|
2526
2931
|
function Setup({ onReady }) {
|
|
2527
2932
|
const [value, setValue] = useState3("");
|
|
2528
2933
|
const [error, setError] = useState3(null);
|
|
@@ -2546,7 +2951,7 @@ function Setup({ onReady }) {
|
|
|
2546
2951
|
}
|
|
2547
2952
|
onReady(trimmed);
|
|
2548
2953
|
};
|
|
2549
|
-
return /* @__PURE__ */
|
|
2954
|
+
return /* @__PURE__ */ React7.createElement(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React7.createElement(
|
|
2550
2955
|
TextInput2,
|
|
2551
2956
|
{
|
|
2552
2957
|
value,
|
|
@@ -2555,14 +2960,14 @@ function Setup({ onReady }) {
|
|
|
2555
2960
|
mask: "\u2022",
|
|
2556
2961
|
placeholder: "sk-..."
|
|
2557
2962
|
}
|
|
2558
|
-
)), error ? /* @__PURE__ */
|
|
2963
|
+
)), error ? /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { color: "red" }, error)) : value ? /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React7.createElement(Box7, { marginTop: 1 }, /* @__PURE__ */ React7.createElement(Text7, { dimColor: true }, "(Type /exit to abort.)")));
|
|
2559
2964
|
}
|
|
2560
2965
|
|
|
2561
2966
|
// src/cli/commands/chat.tsx
|
|
2562
|
-
function Root({ initialKey, ...appProps }) {
|
|
2967
|
+
function Root({ initialKey, tools, ...appProps }) {
|
|
2563
2968
|
const [key, setKey] = useState4(initialKey);
|
|
2564
2969
|
if (!key) {
|
|
2565
|
-
return /* @__PURE__ */
|
|
2970
|
+
return /* @__PURE__ */ React8.createElement(
|
|
2566
2971
|
Setup,
|
|
2567
2972
|
{
|
|
2568
2973
|
onReady: (k) => {
|
|
@@ -2573,7 +2978,7 @@ function Root({ initialKey, ...appProps }) {
|
|
|
2573
2978
|
);
|
|
2574
2979
|
}
|
|
2575
2980
|
process.env.DEEPSEEK_API_KEY = key;
|
|
2576
|
-
return /* @__PURE__ */
|
|
2981
|
+
return /* @__PURE__ */ React8.createElement(
|
|
2577
2982
|
App,
|
|
2578
2983
|
{
|
|
2579
2984
|
model: appProps.model,
|
|
@@ -2581,51 +2986,86 @@ function Root({ initialKey, ...appProps }) {
|
|
|
2581
2986
|
transcript: appProps.transcript,
|
|
2582
2987
|
harvest: appProps.harvest,
|
|
2583
2988
|
branch: appProps.branch,
|
|
2584
|
-
session: appProps.session
|
|
2989
|
+
session: appProps.session,
|
|
2990
|
+
tools
|
|
2585
2991
|
}
|
|
2586
2992
|
);
|
|
2587
2993
|
}
|
|
2588
2994
|
async function chatCommand(opts) {
|
|
2589
2995
|
loadDotenv();
|
|
2590
2996
|
const initialKey = loadApiKey();
|
|
2591
|
-
|
|
2997
|
+
let mcp;
|
|
2998
|
+
let tools;
|
|
2999
|
+
if (opts.mcp) {
|
|
3000
|
+
const argv = shellSplit(opts.mcp);
|
|
3001
|
+
if (argv.length === 0) {
|
|
3002
|
+
process.stderr.write("error: --mcp requires a command\n");
|
|
3003
|
+
process.exit(2);
|
|
3004
|
+
}
|
|
3005
|
+
const [command, ...args] = argv;
|
|
3006
|
+
if (!command) {
|
|
3007
|
+
process.stderr.write("error: --mcp command is empty\n");
|
|
3008
|
+
process.exit(2);
|
|
3009
|
+
}
|
|
3010
|
+
const transport = new StdioTransport({ command, args });
|
|
3011
|
+
mcp = new McpClient({ transport });
|
|
3012
|
+
try {
|
|
3013
|
+
await mcp.initialize();
|
|
3014
|
+
const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
|
|
3015
|
+
tools = bridge.registry;
|
|
3016
|
+
process.stderr.write(
|
|
3017
|
+
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
3018
|
+
`
|
|
3019
|
+
);
|
|
3020
|
+
} catch (err) {
|
|
3021
|
+
process.stderr.write(`MCP setup failed: ${err.message}
|
|
3022
|
+
`);
|
|
3023
|
+
await mcp.close();
|
|
3024
|
+
process.exit(1);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
const { waitUntilExit } = render(/* @__PURE__ */ React8.createElement(Root, { initialKey, tools, ...opts }), {
|
|
2592
3028
|
exitOnCtrlC: true
|
|
2593
3029
|
});
|
|
2594
|
-
|
|
3030
|
+
try {
|
|
3031
|
+
await waitUntilExit();
|
|
3032
|
+
} finally {
|
|
3033
|
+
await mcp?.close();
|
|
3034
|
+
}
|
|
2595
3035
|
}
|
|
2596
3036
|
|
|
2597
3037
|
// src/cli/commands/diff.ts
|
|
2598
3038
|
import { writeFileSync as writeFileSync2 } from "fs";
|
|
2599
3039
|
import { basename } from "path";
|
|
2600
3040
|
import { render as render2 } from "ink";
|
|
2601
|
-
import
|
|
3041
|
+
import React11 from "react";
|
|
2602
3042
|
|
|
2603
3043
|
// src/cli/ui/DiffApp.tsx
|
|
2604
|
-
import { Box as
|
|
2605
|
-
import
|
|
3044
|
+
import { Box as Box9, Static as Static2, Text as Text9, useApp as useApp3, useInput } from "ink";
|
|
3045
|
+
import React10, { useState as useState5 } from "react";
|
|
2606
3046
|
|
|
2607
3047
|
// src/cli/ui/RecordView.tsx
|
|
2608
|
-
import { Box as
|
|
2609
|
-
import
|
|
3048
|
+
import { Box as Box8, Text as Text8 } from "ink";
|
|
3049
|
+
import React9 from "react";
|
|
2610
3050
|
function RecordView({ rec, compact = false }) {
|
|
2611
3051
|
const toolArgsMax = compact ? 120 : 200;
|
|
2612
3052
|
const toolContentMax = compact ? 200 : 400;
|
|
2613
3053
|
if (rec.role === "user") {
|
|
2614
|
-
return /* @__PURE__ */
|
|
3054
|
+
return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React9.createElement(Text8, null, rec.content));
|
|
2615
3055
|
}
|
|
2616
3056
|
if (rec.role === "assistant_final") {
|
|
2617
|
-
return /* @__PURE__ */
|
|
3057
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React9.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React9.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React9.createElement(Text8, null, rec.content) : /* @__PURE__ */ React9.createElement(Text8, { dimColor: true, italic: true }, "(tool-call response only)"));
|
|
2618
3058
|
}
|
|
2619
3059
|
if (rec.role === "tool") {
|
|
2620
|
-
return /* @__PURE__ */
|
|
3060
|
+
return /* @__PURE__ */ React9.createElement(Box8, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " args: ", truncate3(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \u2192 ", truncate3(rec.content, toolContentMax)));
|
|
2621
3061
|
}
|
|
2622
3062
|
if (rec.role === "error") {
|
|
2623
|
-
return /* @__PURE__ */
|
|
3063
|
+
return /* @__PURE__ */ React9.createElement(Box8, { marginTop: 1 }, /* @__PURE__ */ React9.createElement(Text8, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React9.createElement(Text8, { color: "red" }, rec.error ?? rec.content));
|
|
2624
3064
|
}
|
|
2625
3065
|
if (rec.role === "done" || rec.role === "assistant_delta") {
|
|
2626
3066
|
return null;
|
|
2627
3067
|
}
|
|
2628
|
-
return /* @__PURE__ */
|
|
3068
|
+
return /* @__PURE__ */ React9.createElement(Box8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, "[", rec.role, "] ", rec.content));
|
|
2629
3069
|
}
|
|
2630
3070
|
function CacheBadge({ usage }) {
|
|
2631
3071
|
const hit = usage.prompt_cache_hit_tokens ?? 0;
|
|
@@ -2634,7 +3074,7 @@ function CacheBadge({ usage }) {
|
|
|
2634
3074
|
if (total === 0) return null;
|
|
2635
3075
|
const pct2 = hit / total * 100;
|
|
2636
3076
|
const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
|
|
2637
|
-
return /* @__PURE__ */
|
|
3077
|
+
return /* @__PURE__ */ React9.createElement(Text8, null, /* @__PURE__ */ React9.createElement(Text8, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React9.createElement(Text8, { color }, pct2.toFixed(1), "%"));
|
|
2638
3078
|
}
|
|
2639
3079
|
function truncate3(s, max) {
|
|
2640
3080
|
return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
|
|
@@ -2668,7 +3108,7 @@ function DiffApp({ report }) {
|
|
|
2668
3108
|
}
|
|
2669
3109
|
});
|
|
2670
3110
|
const pair = report.pairs[idx];
|
|
2671
|
-
return /* @__PURE__ */
|
|
3111
|
+
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column" }, /* @__PURE__ */ React10.createElement(DiffHeader, { report }), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React10.createElement(Text9, null, pair ? /* @__PURE__ */ React10.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React10.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React10.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React10.createElement(Text9, null, pair.divergenceNote)) : null, /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "j"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "k"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "N"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "g"), "/", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React10.createElement(Text9, { bold: true }, "q"), " ", "quit")));
|
|
2672
3112
|
}
|
|
2673
3113
|
function DiffHeader({ report }) {
|
|
2674
3114
|
const a = report.a;
|
|
@@ -2686,15 +3126,15 @@ function DiffHeader({ report }) {
|
|
|
2686
3126
|
} else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
|
|
2687
3127
|
prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
|
|
2688
3128
|
}
|
|
2689
|
-
return /* @__PURE__ */
|
|
3129
|
+
return /* @__PURE__ */ React10.createElement(Box9, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React10.createElement(Box9, { justifyContent: "space-between" }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, a.label), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " vs B="), /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, b.label)), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "cache "), /* @__PURE__ */ React10.createElement(Text9, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React10.createElement(Text9, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React10.createElement(Text9, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "cost "), /* @__PURE__ */ React10.createElement(Text9, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React10.createElement(Text9, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React10.createElement(Text9, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React10.createElement(Text9, null, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true }, "model calls "), /* @__PURE__ */ React10.createElement(Text9, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, italic: true }, prefixLine)) : null);
|
|
2690
3130
|
}
|
|
2691
3131
|
function Pane({
|
|
2692
3132
|
label,
|
|
2693
3133
|
headerColor,
|
|
2694
3134
|
records
|
|
2695
3135
|
}) {
|
|
2696
|
-
return /* @__PURE__ */
|
|
2697
|
-
|
|
3136
|
+
return /* @__PURE__ */ React10.createElement(
|
|
3137
|
+
Box9,
|
|
2698
3138
|
{
|
|
2699
3139
|
flexDirection: "column",
|
|
2700
3140
|
flexGrow: 1,
|
|
@@ -2702,21 +3142,21 @@ function Pane({
|
|
|
2702
3142
|
borderStyle: "single",
|
|
2703
3143
|
borderColor: headerColor
|
|
2704
3144
|
},
|
|
2705
|
-
/* @__PURE__ */
|
|
2706
|
-
records.length === 0 ? /* @__PURE__ */
|
|
3145
|
+
/* @__PURE__ */ React10.createElement(Text9, { color: headerColor, bold: true }, label),
|
|
3146
|
+
records.length === 0 ? /* @__PURE__ */ React10.createElement(Box9, { marginTop: 1 }, /* @__PURE__ */ React10.createElement(Text9, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React10.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React10.createElement(RecordView, { key, rec, compact: true }))
|
|
2707
3147
|
);
|
|
2708
3148
|
}
|
|
2709
3149
|
function KindBadge({ kind }) {
|
|
2710
3150
|
if (kind === "match") {
|
|
2711
|
-
return /* @__PURE__ */
|
|
3151
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "green" }, "\u2713 match");
|
|
2712
3152
|
}
|
|
2713
3153
|
if (kind === "diverge") {
|
|
2714
|
-
return /* @__PURE__ */
|
|
3154
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "yellow" }, "\u2605 diverge");
|
|
2715
3155
|
}
|
|
2716
3156
|
if (kind === "only_in_a") {
|
|
2717
|
-
return /* @__PURE__ */
|
|
3157
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "blue" }, "\u2190 only in A");
|
|
2718
3158
|
}
|
|
2719
|
-
return /* @__PURE__ */
|
|
3159
|
+
return /* @__PURE__ */ React10.createElement(Text9, { color: "magenta" }, "\u2192 only in B");
|
|
2720
3160
|
}
|
|
2721
3161
|
function paneRecords(pair, side) {
|
|
2722
3162
|
if (!pair) return [];
|
|
@@ -2747,7 +3187,7 @@ markdown report written to ${opts.mdPath}`);
|
|
|
2747
3187
|
return;
|
|
2748
3188
|
}
|
|
2749
3189
|
if (wantTui) {
|
|
2750
|
-
const { waitUntilExit } = render2(
|
|
3190
|
+
const { waitUntilExit } = render2(React11.createElement(DiffApp, { report }), {
|
|
2751
3191
|
exitOnCtrlC: true
|
|
2752
3192
|
});
|
|
2753
3193
|
await waitUntilExit();
|
|
@@ -2758,11 +3198,11 @@ markdown report written to ${opts.mdPath}`);
|
|
|
2758
3198
|
|
|
2759
3199
|
// src/cli/commands/replay.ts
|
|
2760
3200
|
import { render as render3 } from "ink";
|
|
2761
|
-
import
|
|
3201
|
+
import React13 from "react";
|
|
2762
3202
|
|
|
2763
3203
|
// src/cli/ui/ReplayApp.tsx
|
|
2764
|
-
import { Box as
|
|
2765
|
-
import
|
|
3204
|
+
import { Box as Box10, Static as Static3, Text as Text10, useApp as useApp4, useInput as useInput2 } from "ink";
|
|
3205
|
+
import React12, { useMemo as useMemo2, useState as useState6 } from "react";
|
|
2766
3206
|
function ReplayApp({ meta, pages }) {
|
|
2767
3207
|
const { exit } = useApp4();
|
|
2768
3208
|
const maxIdx = Math.max(0, pages.length - 1);
|
|
@@ -2797,14 +3237,14 @@ function ReplayApp({ meta, pages }) {
|
|
|
2797
3237
|
const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
|
|
2798
3238
|
const currentPage = pages[idx];
|
|
2799
3239
|
const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
|
|
2800
|
-
return /* @__PURE__ */
|
|
3240
|
+
return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(
|
|
2801
3241
|
StatsPanel,
|
|
2802
3242
|
{
|
|
2803
3243
|
summary,
|
|
2804
3244
|
model: cumStats.models[0] ?? meta?.model ?? "?",
|
|
2805
3245
|
prefixHash
|
|
2806
3246
|
}
|
|
2807
|
-
), /* @__PURE__ */
|
|
3247
|
+
), /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React12.createElement(Box10, { justifyContent: "space-between" }, /* @__PURE__ */ React12.createElement(Text10, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React12.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React12.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React12.createElement(Text10, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React12.createElement(Box10, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React12.createElement(Text10, { dimColor: true }, /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "j"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "k"), "/", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React12.createElement(Text10, { bold: true }, "q"), " quit")));
|
|
2808
3248
|
}
|
|
2809
3249
|
|
|
2810
3250
|
// src/cli/commands/replay.ts
|
|
@@ -2816,7 +3256,7 @@ async function replayCommand(opts) {
|
|
|
2816
3256
|
}
|
|
2817
3257
|
const { parsed } = replayFromFile(opts.path);
|
|
2818
3258
|
const pages = groupRecordsByTurn(parsed.records);
|
|
2819
|
-
const { waitUntilExit } = render3(
|
|
3259
|
+
const { waitUntilExit } = render3(React13.createElement(ReplayApp, { meta: parsed.meta, pages }), {
|
|
2820
3260
|
exitOnCtrlC: true
|
|
2821
3261
|
});
|
|
2822
3262
|
await waitUntilExit();
|
|
@@ -2854,6 +3294,11 @@ function printReplay(opts) {
|
|
|
2854
3294
|
} else if (stats.prefixHashes.length > 1) {
|
|
2855
3295
|
console.log(" (prefix churned \u2014 cache-hostile session)");
|
|
2856
3296
|
}
|
|
3297
|
+
if (stats.harvestedTurns > 0) {
|
|
3298
|
+
console.log(`harvest (Pillar 2): ${stats.harvestedTurns} turn(s) produced plan state`);
|
|
3299
|
+
console.log(` subgoals: ${stats.totalSubgoals}`);
|
|
3300
|
+
console.log(` uncertainties: ${stats.totalUncertainties}`);
|
|
3301
|
+
}
|
|
2857
3302
|
}
|
|
2858
3303
|
function sliceRecords(records, opts) {
|
|
2859
3304
|
if (opts.head !== void 0 && opts.head > 0) return records.slice(0, opts.head);
|
|
@@ -2873,6 +3318,21 @@ function renderRecord(rec) {
|
|
|
2873
3318
|
return total > 0 ? ` cache=${(hit / total * 100).toFixed(1)}%` : "";
|
|
2874
3319
|
})() : "";
|
|
2875
3320
|
console.log(`${turn} AGENT:${cost}${cache} ${oneLine(rec.content)}`);
|
|
3321
|
+
if (rec.planState) {
|
|
3322
|
+
const ps = rec.planState;
|
|
3323
|
+
if (ps.subgoals.length)
|
|
3324
|
+
console.log(` \u2039 subgoals (${ps.subgoals.length}): ${ps.subgoals.join(" \xB7 ")}`);
|
|
3325
|
+
if (ps.hypotheses.length)
|
|
3326
|
+
console.log(` \u2039 hypotheses (${ps.hypotheses.length}): ${ps.hypotheses.join(" \xB7 ")}`);
|
|
3327
|
+
if (ps.uncertainties.length)
|
|
3328
|
+
console.log(
|
|
3329
|
+
` \u2039 uncertainties(${ps.uncertainties.length}): ${ps.uncertainties.join(" \xB7 ")}`
|
|
3330
|
+
);
|
|
3331
|
+
if (ps.rejectedPaths.length)
|
|
3332
|
+
console.log(
|
|
3333
|
+
` \u2039 rejected (${ps.rejectedPaths.length}): ${ps.rejectedPaths.join(" \xB7 ")}`
|
|
3334
|
+
);
|
|
3335
|
+
}
|
|
2876
3336
|
} else if (rec.role === "tool") {
|
|
2877
3337
|
const args = rec.args ? ` args=${oneLine(rec.args, 80)}` : "";
|
|
2878
3338
|
console.log(`${turn} TOOL ${rec.tool ?? "?"}:${args} \u2192 ${oneLine(rec.content, 120)}`);
|
|
@@ -2926,18 +3386,76 @@ async function runCommand(opts) {
|
|
|
2926
3386
|
loadDotenv();
|
|
2927
3387
|
const apiKey = await ensureApiKey();
|
|
2928
3388
|
process.env.DEEPSEEK_API_KEY = apiKey;
|
|
3389
|
+
let mcp;
|
|
3390
|
+
let tools;
|
|
3391
|
+
if (opts.mcp) {
|
|
3392
|
+
const argv = shellSplit(opts.mcp);
|
|
3393
|
+
const [command, ...args] = argv;
|
|
3394
|
+
if (!command) {
|
|
3395
|
+
process.stderr.write("error: --mcp command is empty\n");
|
|
3396
|
+
process.exit(2);
|
|
3397
|
+
}
|
|
3398
|
+
mcp = new McpClient({ transport: new StdioTransport({ command, args }) });
|
|
3399
|
+
try {
|
|
3400
|
+
await mcp.initialize();
|
|
3401
|
+
const bridge = await bridgeMcpTools(mcp, { namePrefix: opts.mcpPrefix });
|
|
3402
|
+
tools = bridge.registry;
|
|
3403
|
+
process.stderr.write(
|
|
3404
|
+
`\u25B8 MCP: ${bridge.registeredNames.length} tool(s) from ${argv.join(" ")}
|
|
3405
|
+
`
|
|
3406
|
+
);
|
|
3407
|
+
} catch (err) {
|
|
3408
|
+
process.stderr.write(`MCP setup failed: ${err.message}
|
|
3409
|
+
`);
|
|
3410
|
+
await mcp.close();
|
|
3411
|
+
process.exit(1);
|
|
3412
|
+
}
|
|
3413
|
+
}
|
|
2929
3414
|
const client = new DeepSeekClient();
|
|
2930
|
-
const prefix = new ImmutablePrefix({
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
3415
|
+
const prefix = new ImmutablePrefix({
|
|
3416
|
+
system: opts.system,
|
|
3417
|
+
toolSpecs: tools?.specs()
|
|
3418
|
+
});
|
|
3419
|
+
const loop = new CacheFirstLoop({
|
|
3420
|
+
client,
|
|
3421
|
+
prefix,
|
|
3422
|
+
tools,
|
|
3423
|
+
model: opts.model,
|
|
3424
|
+
harvest: opts.harvest,
|
|
3425
|
+
branch: opts.branch
|
|
3426
|
+
});
|
|
3427
|
+
const prefixHash = prefix.fingerprint;
|
|
3428
|
+
let transcriptStream = null;
|
|
3429
|
+
if (opts.transcript) {
|
|
3430
|
+
transcriptStream = openTranscriptFile(opts.transcript, {
|
|
3431
|
+
version: 1,
|
|
3432
|
+
source: "reasonix run",
|
|
3433
|
+
model: opts.model,
|
|
3434
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3435
|
+
});
|
|
3436
|
+
writeRecord(transcriptStream, {
|
|
3437
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3438
|
+
turn: 1,
|
|
3439
|
+
role: "user",
|
|
3440
|
+
content: opts.task
|
|
3441
|
+
});
|
|
3442
|
+
}
|
|
3443
|
+
try {
|
|
3444
|
+
for await (const ev of loop.step(opts.task)) {
|
|
3445
|
+
if (ev.role === "assistant_delta" && ev.content) process.stdout.write(ev.content);
|
|
3446
|
+
if (ev.role === "tool") process.stdout.write(`
|
|
2935
3447
|
[tool ${ev.toolName}] ${ev.content}
|
|
2936
3448
|
`);
|
|
2937
|
-
|
|
3449
|
+
if (ev.role === "error") process.stderr.write(`
|
|
2938
3450
|
[error] ${ev.error}
|
|
2939
3451
|
`);
|
|
2940
|
-
|
|
3452
|
+
if (ev.role === "done") process.stdout.write("\n");
|
|
3453
|
+
if (transcriptStream && ev.role !== "assistant_delta") {
|
|
3454
|
+
writeRecord(transcriptStream, recordFromLoopEvent(ev, { model: opts.model, prefixHash }));
|
|
3455
|
+
}
|
|
3456
|
+
}
|
|
3457
|
+
} finally {
|
|
3458
|
+
transcriptStream?.end();
|
|
2941
3459
|
}
|
|
2942
3460
|
const s = loop.stats.summary();
|
|
2943
3461
|
process.stdout.write(
|
|
@@ -2945,6 +3463,90 @@ async function runCommand(opts) {
|
|
|
2945
3463
|
\u2014 turns:${s.turns} cache:${(s.cacheHitRatio * 100).toFixed(1)}% cost:$${s.totalCostUsd.toFixed(6)} save-vs-claude:${s.savingsVsClaudePct.toFixed(1)}%
|
|
2946
3464
|
`
|
|
2947
3465
|
);
|
|
3466
|
+
if (opts.transcript) {
|
|
3467
|
+
process.stdout.write(`
|
|
3468
|
+
transcript: ${opts.transcript}
|
|
3469
|
+
`);
|
|
3470
|
+
process.stdout.write(` \u2192 npx reasonix replay ${opts.transcript}
|
|
3471
|
+
`);
|
|
3472
|
+
}
|
|
3473
|
+
await mcp?.close();
|
|
3474
|
+
}
|
|
3475
|
+
|
|
3476
|
+
// src/cli/commands/sessions.ts
|
|
3477
|
+
function sessionsCommand(opts) {
|
|
3478
|
+
if (opts.name) {
|
|
3479
|
+
inspectSession(opts.name, !!opts.verbose);
|
|
3480
|
+
} else {
|
|
3481
|
+
listAll();
|
|
3482
|
+
}
|
|
3483
|
+
}
|
|
3484
|
+
function listAll() {
|
|
3485
|
+
const items = listSessions();
|
|
3486
|
+
if (items.length === 0) {
|
|
3487
|
+
console.log(
|
|
3488
|
+
"no saved sessions yet \u2014 run `reasonix chat` (sessions are auto-saved unless --no-session)."
|
|
3489
|
+
);
|
|
3490
|
+
return;
|
|
3491
|
+
}
|
|
3492
|
+
console.log("Saved sessions (~/.reasonix/sessions/):");
|
|
3493
|
+
console.log("");
|
|
3494
|
+
console.log(` ${"name".padEnd(22)} ${"msgs".padStart(6)} ${"size".padStart(8)} modified`);
|
|
3495
|
+
console.log(` ${"\u2500".repeat(60)}`);
|
|
3496
|
+
for (const s of items) {
|
|
3497
|
+
const sizeKb = `${(s.size / 1024).toFixed(1)} KB`;
|
|
3498
|
+
const when = s.mtime.toISOString().replace("T", " ").slice(0, 16);
|
|
3499
|
+
console.log(
|
|
3500
|
+
` ${s.name.padEnd(22)} ${String(s.messageCount).padStart(6)} ${sizeKb.padStart(8)} ${when}`
|
|
3501
|
+
);
|
|
3502
|
+
}
|
|
3503
|
+
console.log("");
|
|
3504
|
+
console.log("Inspect: reasonix sessions <name>");
|
|
3505
|
+
console.log("Resume: reasonix chat --session <name>");
|
|
3506
|
+
}
|
|
3507
|
+
function inspectSession(name, verbose) {
|
|
3508
|
+
const path = sessionPath(name);
|
|
3509
|
+
const messages = loadSessionMessages(name);
|
|
3510
|
+
if (messages.length === 0) {
|
|
3511
|
+
console.error(`no session named "${name}" (or it's empty).`);
|
|
3512
|
+
console.error(`looked at: ${path}`);
|
|
3513
|
+
process.exit(1);
|
|
3514
|
+
}
|
|
3515
|
+
console.log(`[session] ${name} ${messages.length} messages ${path}`);
|
|
3516
|
+
console.log("");
|
|
3517
|
+
let turnIndex = 0;
|
|
3518
|
+
for (const msg of messages) {
|
|
3519
|
+
renderMessage(msg, turnIndex, verbose);
|
|
3520
|
+
if (msg.role === "user") turnIndex++;
|
|
3521
|
+
}
|
|
3522
|
+
}
|
|
3523
|
+
function renderMessage(msg, turnIdx, verbose) {
|
|
3524
|
+
const turn = turnIdx > 0 ? `[t${turnIdx}]` : "[start]";
|
|
3525
|
+
const content = typeof msg.content === "string" ? msg.content : "";
|
|
3526
|
+
const flat = oneLine2(content);
|
|
3527
|
+
if (msg.role === "user") {
|
|
3528
|
+
console.log(`${turn} USER: ${flat}`);
|
|
3529
|
+
} else if (msg.role === "assistant") {
|
|
3530
|
+
console.log(`${turn} AGENT: ${flat || "(tool call only)"}`);
|
|
3531
|
+
if (verbose && msg.tool_calls?.length) {
|
|
3532
|
+
for (const tc of msg.tool_calls) {
|
|
3533
|
+
console.log(
|
|
3534
|
+
` \u2192 call ${tc.function?.name} ${truncate4(tc.function?.arguments ?? "", 80)}`
|
|
3535
|
+
);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
} else if (msg.role === "tool") {
|
|
3539
|
+
console.log(`${turn} TOOL ${msg.name ?? "?"}: ${truncate4(flat, 160)}`);
|
|
3540
|
+
} else if (msg.role === "system") {
|
|
3541
|
+
if (verbose) console.log(`${turn} SYSTEM: ${truncate4(flat, 160)}`);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
function oneLine2(s, max = 200) {
|
|
3545
|
+
const collapsed = s.replace(/\s+/g, " ").trim();
|
|
3546
|
+
return collapsed.length > max ? `${collapsed.slice(0, max)}\u2026` : collapsed;
|
|
3547
|
+
}
|
|
3548
|
+
function truncate4(s, max) {
|
|
3549
|
+
return s.length <= max ? s : `${s.slice(0, max)}\u2026`;
|
|
2948
3550
|
}
|
|
2949
3551
|
|
|
2950
3552
|
// src/cli/commands/stats.ts
|
|
@@ -2992,7 +3594,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
2992
3594
|
).option(
|
|
2993
3595
|
"--session <name>",
|
|
2994
3596
|
"Use a named session (default: 'default'). Resume the same session next time."
|
|
2995
|
-
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").
|
|
3597
|
+
).option("--no-session", "Disable session persistence for this run (ephemeral chat)").option(
|
|
3598
|
+
"--mcp <command>",
|
|
3599
|
+
'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
|
|
3600
|
+
).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (opts) => {
|
|
2996
3601
|
let session;
|
|
2997
3602
|
if (opts.session === false) {
|
|
2998
3603
|
session = void 0;
|
|
@@ -3007,15 +3612,39 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
|
|
|
3007
3612
|
transcript: opts.transcript,
|
|
3008
3613
|
harvest: !!opts.harvest,
|
|
3009
3614
|
branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
|
|
3010
|
-
session
|
|
3615
|
+
session,
|
|
3616
|
+
mcp: opts.mcp,
|
|
3617
|
+
mcpPrefix: opts.mcpPrefix
|
|
3011
3618
|
});
|
|
3012
3619
|
});
|
|
3013
|
-
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).
|
|
3014
|
-
|
|
3620
|
+
program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id", "deepseek-chat").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option(
|
|
3621
|
+
"--harvest",
|
|
3622
|
+
"Extract typed plan state from R1 reasoning (Pillar 2, adds a cheap V3 call per turn)"
|
|
3623
|
+
).option(
|
|
3624
|
+
"--branch <n>",
|
|
3625
|
+
"Self-consistency: run N parallel samples per turn and pick the most confident",
|
|
3626
|
+
(v) => Number.parseInt(v, 10)
|
|
3627
|
+
).option("--transcript <path>", "Write a JSONL transcript to this path for replay/diff").option(
|
|
3628
|
+
"--mcp <command>",
|
|
3629
|
+
'Spawn an MCP server and bridge its tools. Shell-quoted: --mcp "npx -y @scope/server-x /path"'
|
|
3630
|
+
).option("--mcp-prefix <str>", "Prefix prepended to every MCP tool name (avoid collisions)").action(async (task, opts) => {
|
|
3631
|
+
await runCommand({
|
|
3632
|
+
task,
|
|
3633
|
+
model: opts.model,
|
|
3634
|
+
system: opts.system,
|
|
3635
|
+
harvest: !!opts.harvest,
|
|
3636
|
+
branch: Number.isFinite(opts.branch) && opts.branch > 1 ? opts.branch : void 0,
|
|
3637
|
+
transcript: opts.transcript,
|
|
3638
|
+
mcp: opts.mcp,
|
|
3639
|
+
mcpPrefix: opts.mcpPrefix
|
|
3640
|
+
});
|
|
3015
3641
|
});
|
|
3016
3642
|
program.command("stats <transcript>").description("Summarize a JSONL transcript produced by `reasonix chat --transcript`.").action((transcript) => {
|
|
3017
3643
|
statsCommand({ transcript });
|
|
3018
3644
|
});
|
|
3645
|
+
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) => {
|
|
3646
|
+
sessionsCommand({ name, verbose: !!opts.verbose });
|
|
3647
|
+
});
|
|
3019
3648
|
program.command("replay <transcript>").description(
|
|
3020
3649
|
"Interactive Ink TUI to scrub through a transcript + rebuild its session summary (cost, cache, prefix stability). No API calls."
|
|
3021
3650
|
).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) => {
|