reasonix 0.3.2 → 0.4.1
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 +32 -0
- package/dist/cli/chunk-2P2MZLCE.js +81 -0
- package/dist/cli/chunk-2P2MZLCE.js.map +1 -0
- package/dist/cli/index.js +460 -39
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/prompt-MMANQ36Z.js +10 -0
- package/dist/cli/prompt-MMANQ36Z.js.map +1 -0
- package/dist/index.d.ts +128 -3
- package/dist/index.js +268 -25
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import "./chunk-2P2MZLCE.js";
|
|
2
3
|
|
|
3
4
|
// src/cli/index.ts
|
|
4
5
|
import { Command } from "commander";
|
|
@@ -95,8 +96,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
95
96
|
}
|
|
96
97
|
function sleep(ms, signal) {
|
|
97
98
|
if (ms <= 0) return Promise.resolve();
|
|
98
|
-
return new Promise((
|
|
99
|
-
const timer = setTimeout(
|
|
99
|
+
return new Promise((resolve4, reject) => {
|
|
100
|
+
const timer = setTimeout(resolve4, ms);
|
|
100
101
|
if (signal) {
|
|
101
102
|
const onAbort = () => {
|
|
102
103
|
clearTimeout(timer);
|
|
@@ -1166,7 +1167,7 @@ var CacheFirstLoop = class {
|
|
|
1166
1167
|
this.prefix = opts.prefix;
|
|
1167
1168
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1168
1169
|
this.model = opts.model ?? "deepseek-chat";
|
|
1169
|
-
this.maxToolIters = opts.maxToolIters ??
|
|
1170
|
+
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1170
1171
|
if (typeof opts.branch === "number") {
|
|
1171
1172
|
this.branchOptions = { budget: opts.branch };
|
|
1172
1173
|
} else if (opts.branch && typeof opts.branch === "object") {
|
|
@@ -1355,8 +1356,8 @@ var CacheFirstLoop = class {
|
|
|
1355
1356
|
}
|
|
1356
1357
|
);
|
|
1357
1358
|
for (let k = 0; k < budget; k++) {
|
|
1358
|
-
const sample = queue.shift() ?? await new Promise((
|
|
1359
|
-
waiter =
|
|
1359
|
+
const sample = queue.shift() ?? await new Promise((resolve4) => {
|
|
1360
|
+
waiter = resolve4;
|
|
1360
1361
|
});
|
|
1361
1362
|
yield {
|
|
1362
1363
|
turn: this._turn,
|
|
@@ -1476,9 +1477,28 @@ var CacheFirstLoop = class {
|
|
|
1476
1477
|
yield { turn: this._turn, role: "done", content: assistantContent };
|
|
1477
1478
|
return;
|
|
1478
1479
|
}
|
|
1480
|
+
const ctxMax = DEEPSEEK_CONTEXT_TOKENS[this.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
1481
|
+
if (usage && usage.promptTokens / ctxMax > 0.8) {
|
|
1482
|
+
yield {
|
|
1483
|
+
turn: this._turn,
|
|
1484
|
+
role: "warning",
|
|
1485
|
+
content: `context ${usage.promptTokens}/${ctxMax} (${Math.round(
|
|
1486
|
+
usage.promptTokens / ctxMax * 100
|
|
1487
|
+
)}%) \u2014 more tools would overflow. Forcing summary from what was gathered.`
|
|
1488
|
+
};
|
|
1489
|
+
yield* this.forceSummaryAfterIterLimit({ reason: "context-guard" });
|
|
1490
|
+
return;
|
|
1491
|
+
}
|
|
1479
1492
|
for (const call of repairedCalls) {
|
|
1480
1493
|
const name = call.function?.name ?? "";
|
|
1481
1494
|
const args = call.function?.arguments ?? "{}";
|
|
1495
|
+
yield {
|
|
1496
|
+
turn: this._turn,
|
|
1497
|
+
role: "tool_start",
|
|
1498
|
+
content: "",
|
|
1499
|
+
toolName: name,
|
|
1500
|
+
toolArgs: args
|
|
1501
|
+
};
|
|
1482
1502
|
const result = await this.tools.dispatch(name, args);
|
|
1483
1503
|
this.appendAndPersist({
|
|
1484
1504
|
role: "tool",
|
|
@@ -1506,7 +1526,7 @@ var CacheFirstLoop = class {
|
|
|
1506
1526
|
// no tools → model is forced to answer in text
|
|
1507
1527
|
});
|
|
1508
1528
|
const summary = resp.content?.trim() || "(model returned no text; try a narrower question or raise --max-tool-iters)";
|
|
1509
|
-
const reasonPrefix = opts.reason
|
|
1529
|
+
const reasonPrefix = reasonPrefixFor(opts.reason, this.maxToolIters);
|
|
1510
1530
|
const annotated = `${reasonPrefix}
|
|
1511
1531
|
|
|
1512
1532
|
${summary}`;
|
|
@@ -1516,11 +1536,12 @@ ${summary}`;
|
|
|
1516
1536
|
turn: this._turn,
|
|
1517
1537
|
role: "assistant_final",
|
|
1518
1538
|
content: annotated,
|
|
1519
|
-
stats: summaryStats
|
|
1539
|
+
stats: summaryStats,
|
|
1540
|
+
forcedSummary: true
|
|
1520
1541
|
};
|
|
1521
1542
|
yield { turn: this._turn, role: "done", content: summary };
|
|
1522
1543
|
} catch (err) {
|
|
1523
|
-
const label = opts.reason
|
|
1544
|
+
const label = errorLabelFor(opts.reason, this.maxToolIters);
|
|
1524
1545
|
yield {
|
|
1525
1546
|
turn: this._turn,
|
|
1526
1547
|
role: "error",
|
|
@@ -1545,6 +1566,18 @@ ${summary}`;
|
|
|
1545
1566
|
return msg;
|
|
1546
1567
|
}
|
|
1547
1568
|
};
|
|
1569
|
+
function reasonPrefixFor(reason, iterCap) {
|
|
1570
|
+
if (reason === "aborted") return "[aborted by user (Esc) \u2014 summarizing what I found so far]";
|
|
1571
|
+
if (reason === "context-guard") {
|
|
1572
|
+
return "[context budget running low \u2014 summarizing before the next call would overflow]";
|
|
1573
|
+
}
|
|
1574
|
+
return `[tool-call budget (${iterCap}) reached \u2014 forcing summary from what I found]`;
|
|
1575
|
+
}
|
|
1576
|
+
function errorLabelFor(reason, iterCap) {
|
|
1577
|
+
if (reason === "aborted") return "aborted by user";
|
|
1578
|
+
if (reason === "context-guard") return "context-guard triggered (prompt > 80% of window)";
|
|
1579
|
+
return `tool-call budget (${iterCap}) reached`;
|
|
1580
|
+
}
|
|
1548
1581
|
function summarizeBranch(chosen, samples) {
|
|
1549
1582
|
return {
|
|
1550
1583
|
budget: samples.length,
|
|
@@ -2198,7 +2231,7 @@ var McpClient = class {
|
|
|
2198
2231
|
async request(method, params) {
|
|
2199
2232
|
const id = this.nextId++;
|
|
2200
2233
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
2201
|
-
const promise = new Promise((
|
|
2234
|
+
const promise = new Promise((resolve4, reject) => {
|
|
2202
2235
|
const timeout = setTimeout(() => {
|
|
2203
2236
|
this.pending.delete(id);
|
|
2204
2237
|
reject(
|
|
@@ -2206,7 +2239,7 @@ var McpClient = class {
|
|
|
2206
2239
|
);
|
|
2207
2240
|
}, this.requestTimeoutMs);
|
|
2208
2241
|
this.pending.set(id, {
|
|
2209
|
-
resolve:
|
|
2242
|
+
resolve: resolve4,
|
|
2210
2243
|
reject,
|
|
2211
2244
|
timeout
|
|
2212
2245
|
});
|
|
@@ -2290,12 +2323,12 @@ var StdioTransport = class {
|
|
|
2290
2323
|
}
|
|
2291
2324
|
async send(message) {
|
|
2292
2325
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
2293
|
-
return new Promise((
|
|
2326
|
+
return new Promise((resolve4, reject) => {
|
|
2294
2327
|
const line = `${JSON.stringify(message)}
|
|
2295
2328
|
`;
|
|
2296
2329
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
2297
2330
|
if (err) reject(err);
|
|
2298
|
-
else
|
|
2331
|
+
else resolve4();
|
|
2299
2332
|
});
|
|
2300
2333
|
});
|
|
2301
2334
|
}
|
|
@@ -2306,8 +2339,8 @@ var StdioTransport = class {
|
|
|
2306
2339
|
continue;
|
|
2307
2340
|
}
|
|
2308
2341
|
if (this.closed) return;
|
|
2309
|
-
const next = await new Promise((
|
|
2310
|
-
this.waiters.push(
|
|
2342
|
+
const next = await new Promise((resolve4) => {
|
|
2343
|
+
this.waiters.push(resolve4);
|
|
2311
2344
|
});
|
|
2312
2345
|
if (next === null) return;
|
|
2313
2346
|
yield next;
|
|
@@ -2373,8 +2406,8 @@ var SseTransport = class {
|
|
|
2373
2406
|
constructor(opts) {
|
|
2374
2407
|
this.url = opts.url;
|
|
2375
2408
|
this.headers = opts.headers ?? {};
|
|
2376
|
-
this.endpointReady = new Promise((
|
|
2377
|
-
this.resolveEndpoint =
|
|
2409
|
+
this.endpointReady = new Promise((resolve4, reject) => {
|
|
2410
|
+
this.resolveEndpoint = resolve4;
|
|
2378
2411
|
this.rejectEndpoint = reject;
|
|
2379
2412
|
});
|
|
2380
2413
|
this.endpointReady.catch(() => void 0);
|
|
@@ -2401,8 +2434,8 @@ var SseTransport = class {
|
|
|
2401
2434
|
continue;
|
|
2402
2435
|
}
|
|
2403
2436
|
if (this.closed) return;
|
|
2404
|
-
const next = await new Promise((
|
|
2405
|
-
this.waiters.push(
|
|
2437
|
+
const next = await new Promise((resolve4) => {
|
|
2438
|
+
this.waiters.push(resolve4);
|
|
2406
2439
|
});
|
|
2407
2440
|
if (next === null) return;
|
|
2408
2441
|
yield next;
|
|
@@ -2570,8 +2603,133 @@ function parseMcpSpec(input) {
|
|
|
2570
2603
|
return { transport: "stdio", name, command, args };
|
|
2571
2604
|
}
|
|
2572
2605
|
|
|
2606
|
+
// src/code/edit-blocks.ts
|
|
2607
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync3, readFileSync as readFileSync5, unlinkSync as unlinkSync2, writeFileSync as writeFileSync3 } from "fs";
|
|
2608
|
+
import { dirname as dirname3, resolve as resolve2 } from "path";
|
|
2609
|
+
var BLOCK_RE = /^(\S[^\n]*)\n<{7} SEARCH\n([\s\S]*?)\n?={7}\n([\s\S]*?)\n?>{7} REPLACE/gm;
|
|
2610
|
+
function parseEditBlocks(text) {
|
|
2611
|
+
const out = [];
|
|
2612
|
+
BLOCK_RE.lastIndex = 0;
|
|
2613
|
+
let m = BLOCK_RE.exec(text);
|
|
2614
|
+
while (m !== null) {
|
|
2615
|
+
out.push({
|
|
2616
|
+
path: m[1].trim(),
|
|
2617
|
+
search: m[2],
|
|
2618
|
+
replace: m[3],
|
|
2619
|
+
offset: m.index
|
|
2620
|
+
});
|
|
2621
|
+
m = BLOCK_RE.exec(text);
|
|
2622
|
+
}
|
|
2623
|
+
return out;
|
|
2624
|
+
}
|
|
2625
|
+
function applyEditBlock(block, rootDir) {
|
|
2626
|
+
const absRoot = resolve2(rootDir);
|
|
2627
|
+
const absTarget = resolve2(absRoot, block.path);
|
|
2628
|
+
if (absTarget !== absRoot && !absTarget.startsWith(`${absRoot}${sep()}`)) {
|
|
2629
|
+
return {
|
|
2630
|
+
path: block.path,
|
|
2631
|
+
status: "path-escape",
|
|
2632
|
+
message: `resolved path ${absTarget} is outside rootDir ${absRoot}`
|
|
2633
|
+
};
|
|
2634
|
+
}
|
|
2635
|
+
const searchEmpty = block.search.length === 0;
|
|
2636
|
+
const exists = existsSync2(absTarget);
|
|
2637
|
+
try {
|
|
2638
|
+
if (!exists) {
|
|
2639
|
+
if (!searchEmpty) {
|
|
2640
|
+
return {
|
|
2641
|
+
path: block.path,
|
|
2642
|
+
status: "file-missing",
|
|
2643
|
+
message: "file does not exist; to create it, use an empty SEARCH block"
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
mkdirSync3(dirname3(absTarget), { recursive: true });
|
|
2647
|
+
writeFileSync3(absTarget, block.replace, "utf8");
|
|
2648
|
+
return { path: block.path, status: "created" };
|
|
2649
|
+
}
|
|
2650
|
+
const content = readFileSync5(absTarget, "utf8");
|
|
2651
|
+
if (searchEmpty) {
|
|
2652
|
+
return {
|
|
2653
|
+
path: block.path,
|
|
2654
|
+
status: "not-found",
|
|
2655
|
+
message: "empty SEARCH only creates new files \u2014 this file already exists"
|
|
2656
|
+
};
|
|
2657
|
+
}
|
|
2658
|
+
const idx = content.indexOf(block.search);
|
|
2659
|
+
if (idx === -1) {
|
|
2660
|
+
return {
|
|
2661
|
+
path: block.path,
|
|
2662
|
+
status: "not-found",
|
|
2663
|
+
message: "SEARCH text does not match the current file content exactly"
|
|
2664
|
+
};
|
|
2665
|
+
}
|
|
2666
|
+
const replaced = `${content.slice(0, idx)}${block.replace}${content.slice(idx + block.search.length)}`;
|
|
2667
|
+
writeFileSync3(absTarget, replaced, "utf8");
|
|
2668
|
+
return { path: block.path, status: "applied" };
|
|
2669
|
+
} catch (err) {
|
|
2670
|
+
return { path: block.path, status: "error", message: err.message };
|
|
2671
|
+
}
|
|
2672
|
+
}
|
|
2673
|
+
function applyEditBlocks(blocks, rootDir) {
|
|
2674
|
+
return blocks.map((b) => applyEditBlock(b, rootDir));
|
|
2675
|
+
}
|
|
2676
|
+
function snapshotBeforeEdits(blocks, rootDir) {
|
|
2677
|
+
const absRoot = resolve2(rootDir);
|
|
2678
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2679
|
+
const snapshots = [];
|
|
2680
|
+
for (const b of blocks) {
|
|
2681
|
+
if (seen.has(b.path)) continue;
|
|
2682
|
+
seen.add(b.path);
|
|
2683
|
+
const abs = resolve2(absRoot, b.path);
|
|
2684
|
+
if (!existsSync2(abs)) {
|
|
2685
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2686
|
+
continue;
|
|
2687
|
+
}
|
|
2688
|
+
try {
|
|
2689
|
+
snapshots.push({ path: b.path, prevContent: readFileSync5(abs, "utf8") });
|
|
2690
|
+
} catch {
|
|
2691
|
+
snapshots.push({ path: b.path, prevContent: null });
|
|
2692
|
+
}
|
|
2693
|
+
}
|
|
2694
|
+
return snapshots;
|
|
2695
|
+
}
|
|
2696
|
+
function restoreSnapshots(snapshots, rootDir) {
|
|
2697
|
+
const absRoot = resolve2(rootDir);
|
|
2698
|
+
return snapshots.map((snap) => {
|
|
2699
|
+
const abs = resolve2(absRoot, snap.path);
|
|
2700
|
+
if (abs !== absRoot && !abs.startsWith(`${absRoot}${sep()}`)) {
|
|
2701
|
+
return {
|
|
2702
|
+
path: snap.path,
|
|
2703
|
+
status: "path-escape",
|
|
2704
|
+
message: "snapshot path escapes rootDir \u2014 refusing to restore"
|
|
2705
|
+
};
|
|
2706
|
+
}
|
|
2707
|
+
try {
|
|
2708
|
+
if (snap.prevContent === null) {
|
|
2709
|
+
if (existsSync2(abs)) unlinkSync2(abs);
|
|
2710
|
+
return {
|
|
2711
|
+
path: snap.path,
|
|
2712
|
+
status: "applied",
|
|
2713
|
+
message: "removed (the edit had created it)"
|
|
2714
|
+
};
|
|
2715
|
+
}
|
|
2716
|
+
writeFileSync3(abs, snap.prevContent, "utf8");
|
|
2717
|
+
return {
|
|
2718
|
+
path: snap.path,
|
|
2719
|
+
status: "applied",
|
|
2720
|
+
message: "restored to pre-edit content"
|
|
2721
|
+
};
|
|
2722
|
+
} catch (err) {
|
|
2723
|
+
return { path: snap.path, status: "error", message: err.message };
|
|
2724
|
+
}
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
function sep() {
|
|
2728
|
+
return process.platform === "win32" ? "\\" : "/";
|
|
2729
|
+
}
|
|
2730
|
+
|
|
2573
2731
|
// src/index.ts
|
|
2574
|
-
var VERSION = "0.
|
|
2732
|
+
var VERSION = "0.4.1";
|
|
2575
2733
|
|
|
2576
2734
|
// src/cli/commands/chat.tsx
|
|
2577
2735
|
import { render } from "ink";
|
|
@@ -2702,8 +2860,39 @@ function parseBlocks(raw) {
|
|
|
2702
2860
|
listBuf = null;
|
|
2703
2861
|
}
|
|
2704
2862
|
};
|
|
2705
|
-
for (
|
|
2863
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2864
|
+
const rawLine = lines[i];
|
|
2706
2865
|
const line = rawLine.replace(/\s+$/g, "");
|
|
2866
|
+
if (!inCode && /^<{7} SEARCH\s*$/.test(line)) {
|
|
2867
|
+
const filename = para.pop()?.trim();
|
|
2868
|
+
if (filename) {
|
|
2869
|
+
flushPara();
|
|
2870
|
+
flushList();
|
|
2871
|
+
let j = i + 1;
|
|
2872
|
+
const searchLines = [];
|
|
2873
|
+
while (j < lines.length && !/^={7}\s*$/.test(lines[j])) {
|
|
2874
|
+
searchLines.push(lines[j]);
|
|
2875
|
+
j++;
|
|
2876
|
+
}
|
|
2877
|
+
const replaceLines = [];
|
|
2878
|
+
let k = j + 1;
|
|
2879
|
+
while (k < lines.length && !/^>{7} REPLACE\s*$/.test(lines[k])) {
|
|
2880
|
+
replaceLines.push(lines[k]);
|
|
2881
|
+
k++;
|
|
2882
|
+
}
|
|
2883
|
+
if (j < lines.length && k < lines.length) {
|
|
2884
|
+
out.push({
|
|
2885
|
+
kind: "edit-block",
|
|
2886
|
+
filename,
|
|
2887
|
+
search: searchLines.join("\n"),
|
|
2888
|
+
replace: replaceLines.join("\n")
|
|
2889
|
+
});
|
|
2890
|
+
i = k;
|
|
2891
|
+
continue;
|
|
2892
|
+
}
|
|
2893
|
+
para.push(filename);
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2707
2896
|
const fence = line.match(/^```(\w*)/);
|
|
2708
2897
|
if (fence) {
|
|
2709
2898
|
if (inCode) {
|
|
@@ -2781,10 +2970,18 @@ function BlockView({ block }) {
|
|
|
2781
2970
|
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 }))));
|
|
2782
2971
|
case "code":
|
|
2783
2972
|
return /* @__PURE__ */ React2.createElement(Box2, { borderStyle: "single", borderColor: "gray", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: "yellow" }, block.text));
|
|
2973
|
+
case "edit-block":
|
|
2974
|
+
return /* @__PURE__ */ React2.createElement(EditBlockRow, { block });
|
|
2784
2975
|
case "hr":
|
|
2785
2976
|
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");
|
|
2786
2977
|
}
|
|
2787
2978
|
}
|
|
2979
|
+
function EditBlockRow({ block }) {
|
|
2980
|
+
const isNewFile = block.search.length === 0;
|
|
2981
|
+
const searchLines = block.search.split("\n");
|
|
2982
|
+
const replaceLines = block.replace.split("\n");
|
|
2983
|
+
return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { bold: true, color: "cyan" }, block.filename), isNewFile ? /* @__PURE__ */ React2.createElement(Text2, { color: "green", bold: true }, " (new file)") : null), isNewFile ? null : /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: 1 }, searchLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `s-${i}-${line.length}`, color: "red" }, `- ${line}`))), /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", marginTop: isNewFile ? 1 : 0 }, replaceLines.map((line, i) => /* @__PURE__ */ React2.createElement(Text2, { key: `r-${i}-${line.length}`, color: "green" }, `+ ${line}`))));
|
|
2984
|
+
}
|
|
2788
2985
|
function Markdown({ text }) {
|
|
2789
2986
|
const cleaned = stripMath(text);
|
|
2790
2987
|
const blocks = React2.useMemo(() => parseBlocks(cleaned), [cleaned]);
|
|
@@ -2850,7 +3047,17 @@ function StreamingAssistant({ event }) {
|
|
|
2850
3047
|
}
|
|
2851
3048
|
const tail = lastLine(event.text, 140);
|
|
2852
3049
|
const reasoningTail = event.reasoning ? lastLine(event.reasoning, 120) : "";
|
|
2853
|
-
|
|
3050
|
+
const reasoningOnly = !event.text && !!event.reasoning;
|
|
3051
|
+
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(Pulse, null), /* @__PURE__ */ React3.createElement(Text3, { dimColor: true }, " ", "(", reasoningOnly ? "reasoning" : "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) : reasoningOnly ? /* @__PURE__ */ React3.createElement(Text3, { color: "yellow", dimColor: true }, " R1 is thinking before it speaks \u2014 body text starts when reasoning completes (typically 20-90s).") : /* @__PURE__ */ React3.createElement(Text3, { dimColor: true, italic: true }, " (waiting for first byte \u2014 connection is open)"));
|
|
3052
|
+
}
|
|
3053
|
+
function Pulse() {
|
|
3054
|
+
const [tick, setTick] = useState(0);
|
|
3055
|
+
useEffect(() => {
|
|
3056
|
+
const id = setInterval(() => setTick((t) => t + 1), 500);
|
|
3057
|
+
return () => clearInterval(id);
|
|
3058
|
+
}, []);
|
|
3059
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3060
|
+
return /* @__PURE__ */ React3.createElement(Text3, { color: "cyan" }, frames[tick % frames.length]);
|
|
2854
3061
|
}
|
|
2855
3062
|
function lastLine(s, maxChars) {
|
|
2856
3063
|
const flat = s.replace(/\s+/g, " ").trim();
|
|
@@ -2914,6 +3121,7 @@ function formatTokens(n) {
|
|
|
2914
3121
|
}
|
|
2915
3122
|
|
|
2916
3123
|
// src/cli/ui/slash.ts
|
|
3124
|
+
import { spawnSync } from "child_process";
|
|
2917
3125
|
function parseSlash(text) {
|
|
2918
3126
|
if (!text.startsWith("/")) return null;
|
|
2919
3127
|
const parts = text.slice(1).trim().split(/\s+/);
|
|
@@ -2942,6 +3150,10 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2942
3150
|
" /mcp list MCP servers + tools attached to this session",
|
|
2943
3151
|
" /setup (exit + reconfigure) \u2192 run `reasonix setup`",
|
|
2944
3152
|
" /compact [cap] shrink large tool results in history (default 4k/result)",
|
|
3153
|
+
" /apply (code mode) commit the pending edit blocks to disk",
|
|
3154
|
+
" /discard (code mode) drop pending edits without writing",
|
|
3155
|
+
" /undo (code mode) roll back the last applied edit batch",
|
|
3156
|
+
' /commit "msg" (code mode) git add -A && git commit -m "msg"',
|
|
2945
3157
|
" /sessions list saved sessions (current is marked with \u25B8)",
|
|
2946
3158
|
" /forget delete the current session from disk",
|
|
2947
3159
|
" /clear clear displayed history (log + session kept)",
|
|
@@ -2983,6 +3195,45 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
2983
3195
|
return {
|
|
2984
3196
|
info: "To reconfigure (preset, MCP servers, API key), exit this chat and run `reasonix setup`. Changes take effect on next launch."
|
|
2985
3197
|
};
|
|
3198
|
+
case "undo": {
|
|
3199
|
+
if (!ctx.codeUndo) {
|
|
3200
|
+
return {
|
|
3201
|
+
info: "/undo is only available inside `reasonix code` \u2014 chat mode doesn't apply edits."
|
|
3202
|
+
};
|
|
3203
|
+
}
|
|
3204
|
+
return { info: ctx.codeUndo() };
|
|
3205
|
+
}
|
|
3206
|
+
case "apply": {
|
|
3207
|
+
if (!ctx.codeApply) {
|
|
3208
|
+
return {
|
|
3209
|
+
info: "/apply is only available inside `reasonix code` (nothing to apply here)."
|
|
3210
|
+
};
|
|
3211
|
+
}
|
|
3212
|
+
return { info: ctx.codeApply() };
|
|
3213
|
+
}
|
|
3214
|
+
case "discard": {
|
|
3215
|
+
if (!ctx.codeDiscard) {
|
|
3216
|
+
return {
|
|
3217
|
+
info: "/discard is only available inside `reasonix code`."
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
return { info: ctx.codeDiscard() };
|
|
3221
|
+
}
|
|
3222
|
+
case "commit": {
|
|
3223
|
+
if (!ctx.codeRoot) {
|
|
3224
|
+
return {
|
|
3225
|
+
info: "/commit is only available inside `reasonix code` (needs a rooted git repo)."
|
|
3226
|
+
};
|
|
3227
|
+
}
|
|
3228
|
+
const raw = args.join(" ").trim();
|
|
3229
|
+
const message = stripOuterQuotes(raw);
|
|
3230
|
+
if (!message) {
|
|
3231
|
+
return {
|
|
3232
|
+
info: `usage: /commit "your commit message" \u2014 runs \`git add -A && git commit -m "\u2026"\` in ${ctx.codeRoot}`
|
|
3233
|
+
};
|
|
3234
|
+
}
|
|
3235
|
+
return runGitCommit(ctx.codeRoot, message);
|
|
3236
|
+
}
|
|
2986
3237
|
case "compact": {
|
|
2987
3238
|
const tight = Number.parseInt(args[0] ?? "", 10);
|
|
2988
3239
|
const cap = Number.isFinite(tight) && tight >= 500 ? tight : 4e3;
|
|
@@ -3082,6 +3333,38 @@ function handleSlash(cmd, args, loop, ctx = {}) {
|
|
|
3082
3333
|
return { unknown: true, info: `unknown command: /${cmd} (try /help)` };
|
|
3083
3334
|
}
|
|
3084
3335
|
}
|
|
3336
|
+
function stripOuterQuotes(s) {
|
|
3337
|
+
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
|
|
3338
|
+
return s.slice(1, -1);
|
|
3339
|
+
}
|
|
3340
|
+
return s;
|
|
3341
|
+
}
|
|
3342
|
+
function runGitCommit(rootDir, message) {
|
|
3343
|
+
const add = spawnSync("git", ["add", "-A"], { cwd: rootDir, encoding: "utf8" });
|
|
3344
|
+
if (add.error || add.status !== 0) {
|
|
3345
|
+
return { info: `git add failed (${add.status ?? "?"}):
|
|
3346
|
+
${gitTail(add)}` };
|
|
3347
|
+
}
|
|
3348
|
+
const commit = spawnSync("git", ["commit", "-m", message], {
|
|
3349
|
+
cwd: rootDir,
|
|
3350
|
+
encoding: "utf8"
|
|
3351
|
+
});
|
|
3352
|
+
if (commit.error || commit.status !== 0) {
|
|
3353
|
+
return { info: `git commit failed (${commit.status ?? "?"}):
|
|
3354
|
+
${gitTail(commit)}` };
|
|
3355
|
+
}
|
|
3356
|
+
const firstLine = (commit.stdout || "").split(/\r?\n/)[0] ?? "";
|
|
3357
|
+
return { info: `\u25B8 committed: ${message}${firstLine ? `
|
|
3358
|
+
${firstLine}` : ""}` };
|
|
3359
|
+
}
|
|
3360
|
+
function gitTail(res) {
|
|
3361
|
+
const stderr = res.stderr ?? "";
|
|
3362
|
+
const stdout2 = res.stdout ?? "";
|
|
3363
|
+
const body = stderr.trim() || stdout2.trim();
|
|
3364
|
+
if (body) return body;
|
|
3365
|
+
if (res.error) return res.error.message;
|
|
3366
|
+
return "(no output from git)";
|
|
3367
|
+
}
|
|
3085
3368
|
|
|
3086
3369
|
// src/cli/ui/App.tsx
|
|
3087
3370
|
var FLUSH_INTERVAL_MS = 60;
|
|
@@ -3093,7 +3376,8 @@ function App({
|
|
|
3093
3376
|
branch,
|
|
3094
3377
|
session,
|
|
3095
3378
|
tools,
|
|
3096
|
-
mcpSpecs
|
|
3379
|
+
mcpSpecs,
|
|
3380
|
+
codeMode
|
|
3097
3381
|
}) {
|
|
3098
3382
|
const { exit } = useApp();
|
|
3099
3383
|
const [historical, setHistorical] = useState2([]);
|
|
@@ -3101,6 +3385,9 @@ function App({
|
|
|
3101
3385
|
const [input, setInput] = useState2("");
|
|
3102
3386
|
const [busy, setBusy] = useState2(false);
|
|
3103
3387
|
const abortedThisTurn = useRef(false);
|
|
3388
|
+
const [ongoingTool, setOngoingTool] = useState2(null);
|
|
3389
|
+
const lastEditSnapshots = useRef(null);
|
|
3390
|
+
const pendingEdits = useRef([]);
|
|
3104
3391
|
const [summary, setSummary] = useState2({
|
|
3105
3392
|
turns: 0,
|
|
3106
3393
|
totalCostUsd: 0,
|
|
@@ -3175,6 +3462,35 @@ function App({
|
|
|
3175
3462
|
abortedThisTurn.current = true;
|
|
3176
3463
|
loop.abort();
|
|
3177
3464
|
});
|
|
3465
|
+
const codeUndo = useCallback(() => {
|
|
3466
|
+
if (!codeMode) return "not in code mode";
|
|
3467
|
+
const snaps = lastEditSnapshots.current;
|
|
3468
|
+
if (!snaps || snaps.length === 0) {
|
|
3469
|
+
return "nothing to undo \u2014 no recent edit batch to restore";
|
|
3470
|
+
}
|
|
3471
|
+
const results = restoreSnapshots(snaps, codeMode.rootDir);
|
|
3472
|
+
lastEditSnapshots.current = null;
|
|
3473
|
+
return formatUndoResults(results);
|
|
3474
|
+
}, [codeMode]);
|
|
3475
|
+
const codeApply = useCallback(() => {
|
|
3476
|
+
if (!codeMode) return "not in code mode";
|
|
3477
|
+
const blocks = pendingEdits.current;
|
|
3478
|
+
if (blocks.length === 0) {
|
|
3479
|
+
return "nothing pending \u2014 the assistant hasn't proposed edits since the last /apply or /discard.";
|
|
3480
|
+
}
|
|
3481
|
+
const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
|
|
3482
|
+
const results = applyEditBlocks(blocks, codeMode.rootDir);
|
|
3483
|
+
const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
|
|
3484
|
+
if (anyApplied) lastEditSnapshots.current = snaps;
|
|
3485
|
+
pendingEdits.current = [];
|
|
3486
|
+
return formatEditResults(results);
|
|
3487
|
+
}, [codeMode]);
|
|
3488
|
+
const codeDiscard = useCallback(() => {
|
|
3489
|
+
const count = pendingEdits.current.length;
|
|
3490
|
+
if (count === 0) return "nothing pending to discard.";
|
|
3491
|
+
pendingEdits.current = [];
|
|
3492
|
+
return `\u25B8 discarded ${count} pending edit block(s). Nothing was written to disk.`;
|
|
3493
|
+
}, []);
|
|
3178
3494
|
const prefixHash = loop.prefix.fingerprint;
|
|
3179
3495
|
const writeTranscript = useCallback(
|
|
3180
3496
|
(ev) => {
|
|
@@ -3191,7 +3507,13 @@ function App({
|
|
|
3191
3507
|
setInput("");
|
|
3192
3508
|
const slash = parseSlash(text);
|
|
3193
3509
|
if (slash) {
|
|
3194
|
-
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3510
|
+
const result = handleSlash(slash.cmd, slash.args, loop, {
|
|
3511
|
+
mcpSpecs,
|
|
3512
|
+
codeUndo: codeMode ? codeUndo : void 0,
|
|
3513
|
+
codeApply: codeMode ? codeApply : void 0,
|
|
3514
|
+
codeDiscard: codeMode ? codeDiscard : void 0,
|
|
3515
|
+
codeRoot: codeMode?.rootDir
|
|
3516
|
+
});
|
|
3195
3517
|
if (result.exit) {
|
|
3196
3518
|
transcriptRef.current?.end();
|
|
3197
3519
|
exit();
|
|
@@ -3263,12 +3585,13 @@ function App({
|
|
|
3263
3585
|
flush();
|
|
3264
3586
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
3265
3587
|
setStreaming(null);
|
|
3588
|
+
const finalText = ev.content || streamRef.text;
|
|
3266
3589
|
setHistorical((prev) => [
|
|
3267
3590
|
...prev,
|
|
3268
3591
|
{
|
|
3269
3592
|
id: assistantId,
|
|
3270
3593
|
role: "assistant",
|
|
3271
|
-
text:
|
|
3594
|
+
text: finalText,
|
|
3272
3595
|
reasoning: streamRef.reasoning || void 0,
|
|
3273
3596
|
planState: ev.planState,
|
|
3274
3597
|
branch: ev.branch,
|
|
@@ -3277,8 +3600,25 @@ function App({
|
|
|
3277
3600
|
streaming: false
|
|
3278
3601
|
}
|
|
3279
3602
|
]);
|
|
3603
|
+
if (codeMode && finalText && !ev.forcedSummary) {
|
|
3604
|
+
const blocks = parseEditBlocks(finalText);
|
|
3605
|
+
if (blocks.length > 0) {
|
|
3606
|
+
pendingEdits.current = blocks;
|
|
3607
|
+
setHistorical((prev) => [
|
|
3608
|
+
...prev,
|
|
3609
|
+
{
|
|
3610
|
+
id: `pending-${Date.now()}`,
|
|
3611
|
+
role: "info",
|
|
3612
|
+
text: formatPendingPreview(blocks)
|
|
3613
|
+
}
|
|
3614
|
+
]);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
} else if (ev.role === "tool_start") {
|
|
3618
|
+
setOngoingTool({ name: ev.toolName ?? "?", args: ev.toolArgs });
|
|
3280
3619
|
} else if (ev.role === "tool") {
|
|
3281
3620
|
flush();
|
|
3621
|
+
setOngoingTool(null);
|
|
3282
3622
|
setHistorical((prev) => [
|
|
3283
3623
|
...prev,
|
|
3284
3624
|
{
|
|
@@ -3304,11 +3644,12 @@ function App({
|
|
|
3304
3644
|
} finally {
|
|
3305
3645
|
clearInterval(timer);
|
|
3306
3646
|
setStreaming(null);
|
|
3647
|
+
setOngoingTool(null);
|
|
3307
3648
|
setSummary(loop.stats.summary());
|
|
3308
3649
|
setBusy(false);
|
|
3309
3650
|
}
|
|
3310
3651
|
},
|
|
3311
|
-
[busy, exit, loop, mcpSpecs, writeTranscript]
|
|
3652
|
+
[busy, codeApply, codeDiscard, codeMode, codeUndo, exit, loop, mcpSpecs, writeTranscript]
|
|
3312
3653
|
);
|
|
3313
3654
|
return /* @__PURE__ */ React6.createElement(Box6, { flexDirection: "column" }, /* @__PURE__ */ React6.createElement(
|
|
3314
3655
|
StatsPanel,
|
|
@@ -3319,10 +3660,53 @@ function App({
|
|
|
3319
3660
|
harvestOn: loop.harvestEnabled,
|
|
3320
3661
|
branchBudget: loop.branchOptions.budget
|
|
3321
3662
|
}
|
|
3322
|
-
), /* @__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,
|
|
3663
|
+
), /* @__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, ongoingTool ? /* @__PURE__ */ React6.createElement(OngoingToolRow, { tool: ongoingTool }) : null, /* @__PURE__ */ React6.createElement(PromptInput, { value: input, onChange: setInput, onSubmit: handleSubmit, disabled: busy }), /* @__PURE__ */ React6.createElement(CommandStrip, { codeMode: !!codeMode }));
|
|
3323
3664
|
}
|
|
3324
|
-
function
|
|
3325
|
-
|
|
3665
|
+
function OngoingToolRow({ tool }) {
|
|
3666
|
+
const [tick, setTick] = useState2(0);
|
|
3667
|
+
useEffect2(() => {
|
|
3668
|
+
const id = setInterval(() => setTick((t) => t + 1), 120);
|
|
3669
|
+
return () => clearInterval(id);
|
|
3670
|
+
}, []);
|
|
3671
|
+
const frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
3672
|
+
const argsPreview = tool.args && tool.args.length > 0 && tool.args !== "{}" ? ` ${tool.args.length > 60 ? `${tool.args.slice(0, 60)}\u2026` : tool.args}` : "";
|
|
3673
|
+
return /* @__PURE__ */ React6.createElement(Box6, { marginY: 1 }, /* @__PURE__ */ React6.createElement(Text6, { color: "cyan" }, frames[tick % frames.length]), /* @__PURE__ */ React6.createElement(Text6, { color: "yellow" }, ` tool<${tool.name}> running\u2026`), argsPreview ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, argsPreview) : null);
|
|
3674
|
+
}
|
|
3675
|
+
function CommandStrip({ codeMode }) {
|
|
3676
|
+
return /* @__PURE__ */ React6.createElement(Box6, { paddingX: 2, flexDirection: "column" }, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "/help \xB7 /preset ", "<fast|smart|max>", " \xB7 /mcp \xB7 /compact \xB7 /sessions \xB7 /setup \xB7 /clear \xB7 /exit"), codeMode ? /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, 'code mode: /apply \xB7 /discard \xB7 /undo \xB7 /commit "msg" \u2014 edits NEVER write without /apply') : null, /* @__PURE__ */ React6.createElement(Text6, { dimColor: true }, "Esc (while thinking) \u2014 abort & summarize what was found so far"));
|
|
3677
|
+
}
|
|
3678
|
+
function formatEditResults(results) {
|
|
3679
|
+
const lines = results.map((r) => {
|
|
3680
|
+
const mark = r.status === "applied" || r.status === "created" ? "\u2713" : "\u2717";
|
|
3681
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3682
|
+
return ` ${mark} ${r.status.padEnd(11)} ${r.path}${detail}`;
|
|
3683
|
+
});
|
|
3684
|
+
const ok = results.filter((r) => r.status === "applied" || r.status === "created").length;
|
|
3685
|
+
const total = results.length;
|
|
3686
|
+
const header = `\u25B8 edit blocks: ${ok}/${total} applied \u2014 /undo to roll back, or \`git diff\` to review`;
|
|
3687
|
+
return [header, ...lines].join("\n");
|
|
3688
|
+
}
|
|
3689
|
+
function formatPendingPreview(blocks) {
|
|
3690
|
+
const lines = blocks.map((b) => {
|
|
3691
|
+
const removed = b.search === "" ? 0 : countLines2(b.search);
|
|
3692
|
+
const added = countLines2(b.replace);
|
|
3693
|
+
const tag = b.search === "" ? "NEW " : " ";
|
|
3694
|
+
return ` ${tag}${b.path} (-${removed} +${added} lines)`;
|
|
3695
|
+
});
|
|
3696
|
+
const header = `\u25B8 ${blocks.length} pending edit block(s) \u2014 /apply to commit to disk, /discard to drop`;
|
|
3697
|
+
return [header, ...lines].join("\n");
|
|
3698
|
+
}
|
|
3699
|
+
function countLines2(s) {
|
|
3700
|
+
if (s.length === 0) return 0;
|
|
3701
|
+
return (s.match(/\n/g)?.length ?? 0) + 1;
|
|
3702
|
+
}
|
|
3703
|
+
function formatUndoResults(results) {
|
|
3704
|
+
const lines = results.map((r) => {
|
|
3705
|
+
const mark = r.status === "applied" ? "\u2713" : "\u2717";
|
|
3706
|
+
const detail = r.message ? ` (${r.message})` : "";
|
|
3707
|
+
return ` ${mark} ${r.path}${detail}`;
|
|
3708
|
+
});
|
|
3709
|
+
return [`\u25B8 undo: restored ${results.length} file(s) to pre-edit state`, ...lines].join("\n");
|
|
3326
3710
|
}
|
|
3327
3711
|
function describeRepair(repair) {
|
|
3328
3712
|
const parts = [];
|
|
@@ -3396,7 +3780,8 @@ function Root({ initialKey, tools, mcpSpecs, ...appProps }) {
|
|
|
3396
3780
|
branch: appProps.branch,
|
|
3397
3781
|
session: appProps.session,
|
|
3398
3782
|
tools,
|
|
3399
|
-
mcpSpecs
|
|
3783
|
+
mcpSpecs,
|
|
3784
|
+
codeMode: appProps.codeMode
|
|
3400
3785
|
}
|
|
3401
3786
|
);
|
|
3402
3787
|
}
|
|
@@ -3452,9 +3837,35 @@ async function chatCommand(opts) {
|
|
|
3452
3837
|
}
|
|
3453
3838
|
}
|
|
3454
3839
|
|
|
3840
|
+
// src/cli/commands/code.tsx
|
|
3841
|
+
import { basename, resolve as resolve3 } from "path";
|
|
3842
|
+
async function codeCommand(opts = {}) {
|
|
3843
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-MMANQ36Z.js");
|
|
3844
|
+
const rootDir = resolve3(opts.dir ?? process.cwd());
|
|
3845
|
+
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename(rootDir))}`;
|
|
3846
|
+
const fsSpec = `filesystem=npx -y @modelcontextprotocol/server-filesystem ${quoteIfNeeded(rootDir)}`;
|
|
3847
|
+
process.stderr.write(
|
|
3848
|
+
`\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}"
|
|
3849
|
+
`
|
|
3850
|
+
);
|
|
3851
|
+
await chatCommand({
|
|
3852
|
+
model: opts.model ?? "deepseek-reasoner",
|
|
3853
|
+
harvest: true,
|
|
3854
|
+
// smart preset's harvest setting, always on for code
|
|
3855
|
+
system: codeSystemPrompt2(rootDir),
|
|
3856
|
+
transcript: opts.transcript,
|
|
3857
|
+
session,
|
|
3858
|
+
mcp: [fsSpec],
|
|
3859
|
+
codeMode: { rootDir }
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
function quoteIfNeeded(s) {
|
|
3863
|
+
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3455
3866
|
// src/cli/commands/diff.ts
|
|
3456
|
-
import { writeFileSync as
|
|
3457
|
-
import { basename } from "path";
|
|
3867
|
+
import { writeFileSync as writeFileSync4 } from "fs";
|
|
3868
|
+
import { basename as basename2 } from "path";
|
|
3458
3869
|
import { render as render2 } from "ink";
|
|
3459
3870
|
import React11 from "react";
|
|
3460
3871
|
|
|
@@ -3590,8 +4001,8 @@ async function diffCommand(opts) {
|
|
|
3590
4001
|
const aParsed = readTranscript(opts.a);
|
|
3591
4002
|
const bParsed = readTranscript(opts.b);
|
|
3592
4003
|
const report = diffTranscripts(
|
|
3593
|
-
{ label: opts.labelA ??
|
|
3594
|
-
{ label: opts.labelB ??
|
|
4004
|
+
{ label: opts.labelA ?? basename2(opts.a), parsed: aParsed },
|
|
4005
|
+
{ label: opts.labelB ?? basename2(opts.b), parsed: bParsed }
|
|
3595
4006
|
);
|
|
3596
4007
|
const wantMarkdown = !!opts.mdPath;
|
|
3597
4008
|
const wantPrint = opts.print || !process.stdout.isTTY;
|
|
@@ -3599,7 +4010,7 @@ async function diffCommand(opts) {
|
|
|
3599
4010
|
if (wantMarkdown) {
|
|
3600
4011
|
console.log(renderSummaryTable(report));
|
|
3601
4012
|
const md = renderMarkdown(report);
|
|
3602
|
-
|
|
4013
|
+
writeFileSync4(opts.mdPath, md, "utf8");
|
|
3603
4014
|
console.log(`
|
|
3604
4015
|
markdown report written to ${opts.mdPath}`);
|
|
3605
4016
|
return;
|
|
@@ -4408,10 +4819,10 @@ function buildSpec(name, argsByName) {
|
|
|
4408
4819
|
const entry = CATALOG_BY_NAME.get(name);
|
|
4409
4820
|
if (!entry) return name;
|
|
4410
4821
|
const userArg = entry.userArgs ? argsByName[name] : void 0;
|
|
4411
|
-
const tail = userArg ? ` ${
|
|
4822
|
+
const tail = userArg ? ` ${quoteIfNeeded2(userArg)}` : "";
|
|
4412
4823
|
return `${entry.name}=npx -y ${entry.package}${tail}`;
|
|
4413
4824
|
}
|
|
4414
|
-
function
|
|
4825
|
+
function quoteIfNeeded2(s) {
|
|
4415
4826
|
return /\s|"/.test(s) ? `"${s.replace(/"/g, '\\"')}"` : s;
|
|
4416
4827
|
}
|
|
4417
4828
|
|
|
@@ -4439,13 +4850,13 @@ async function setupCommand(_opts = {}) {
|
|
|
4439
4850
|
}
|
|
4440
4851
|
|
|
4441
4852
|
// src/cli/commands/stats.ts
|
|
4442
|
-
import { existsSync as
|
|
4853
|
+
import { existsSync as existsSync3, readFileSync as readFileSync6 } from "fs";
|
|
4443
4854
|
function statsCommand(opts) {
|
|
4444
|
-
if (!
|
|
4855
|
+
if (!existsSync3(opts.transcript)) {
|
|
4445
4856
|
console.error(`no such transcript: ${opts.transcript}`);
|
|
4446
4857
|
process.exit(1);
|
|
4447
4858
|
}
|
|
4448
|
-
const lines =
|
|
4859
|
+
const lines = readFileSync6(opts.transcript, "utf8").split(/\r?\n/).filter(Boolean);
|
|
4449
4860
|
let assistantTurns = 0;
|
|
4450
4861
|
let toolCalls = 0;
|
|
4451
4862
|
let lastTurn = 0;
|
|
@@ -4526,6 +4937,16 @@ program.action(async () => {
|
|
|
4526
4937
|
program.command("setup").description("Interactive wizard \u2014 API key, preset, MCP servers. Re-run any time to reconfigure.").action(async () => {
|
|
4527
4938
|
await setupCommand({});
|
|
4528
4939
|
});
|
|
4940
|
+
program.command("code [dir]").description(
|
|
4941
|
+
"Code-editing chat \u2014 filesystem MCP auto-bridged at <dir> (default: cwd), coding system prompt, smart preset. Model proposes SEARCH/REPLACE blocks; Reasonix applies them to disk."
|
|
4942
|
+
).option("-m, --model <id>", "Override default reasoner model").option("--no-session", "Disable session persistence for this run").option("--transcript <path>", "Write a JSONL transcript to this path").action(async (dir, opts) => {
|
|
4943
|
+
await codeCommand({
|
|
4944
|
+
dir,
|
|
4945
|
+
model: opts.model,
|
|
4946
|
+
noSession: opts.session === false,
|
|
4947
|
+
transcript: opts.transcript
|
|
4948
|
+
});
|
|
4949
|
+
});
|
|
4529
4950
|
program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
|
|
4530
4951
|
"--preset <name>",
|
|
4531
4952
|
"Bundle of model + harvest + branch. One of: fast, smart, max. Overrides config.preset."
|