reasonix 0.11.2 → 0.12.6
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/dashboard/app.css +2346 -0
- package/dashboard/app.js +3913 -0
- package/dashboard/codemirror.js +36 -0
- package/dashboard/index.html +19 -0
- package/dist/cli/{chunk-JDVY4JDU.js → chunk-PKPWI33U.js} +3 -1
- package/dist/cli/index.js +2646 -191
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/{prompt-YRY4HPMZ.js → prompt-HNDDXDRH.js} +2 -2
- package/dist/index.d.ts +181 -31
- package/dist/index.js +221 -24
- package/dist/index.js.map +1 -1
- package/package.json +102 -76
- /package/dist/cli/{chunk-JDVY4JDU.js.map → chunk-PKPWI33U.js.map} +0 -0
- /package/dist/cli/{prompt-YRY4HPMZ.js.map → prompt-HNDDXDRH.js.map} +0 -0
package/dist/cli/index.js
CHANGED
|
@@ -4,13 +4,15 @@ import {
|
|
|
4
4
|
MemoryStore,
|
|
5
5
|
NEGATIVE_CLAIM_RULE,
|
|
6
6
|
PROJECT_MEMORY_FILE,
|
|
7
|
+
SKILLS_DIRNAME,
|
|
8
|
+
SKILL_FILE,
|
|
7
9
|
SkillStore,
|
|
8
10
|
TUI_FORMATTING_RULES,
|
|
9
11
|
applyMemoryStack,
|
|
10
12
|
memoryEnabled,
|
|
11
13
|
readProjectMemory,
|
|
12
14
|
sanitizeMemoryName
|
|
13
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-PKPWI33U.js";
|
|
14
16
|
|
|
15
17
|
// src/cli/index.ts
|
|
16
18
|
import { Command } from "commander";
|
|
@@ -70,6 +72,29 @@ function addProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
|
|
|
70
72
|
cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
|
|
71
73
|
writeConfig(cfg, path5);
|
|
72
74
|
}
|
|
75
|
+
function removeProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
|
|
76
|
+
const trimmed = prefix.trim();
|
|
77
|
+
if (!trimmed) return false;
|
|
78
|
+
const cfg = readConfig(path5);
|
|
79
|
+
const existing = cfg.projects?.[rootDir]?.shellAllowed ?? [];
|
|
80
|
+
if (!existing.includes(trimmed)) return false;
|
|
81
|
+
const next = existing.filter((p) => p !== trimmed);
|
|
82
|
+
if (!cfg.projects) cfg.projects = {};
|
|
83
|
+
if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
|
|
84
|
+
cfg.projects[rootDir].shellAllowed = next;
|
|
85
|
+
writeConfig(cfg, path5);
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
function clearProjectShellAllowed(rootDir, path5 = defaultConfigPath()) {
|
|
89
|
+
const cfg = readConfig(path5);
|
|
90
|
+
const existing = cfg.projects?.[rootDir]?.shellAllowed ?? [];
|
|
91
|
+
if (existing.length === 0) return 0;
|
|
92
|
+
if (!cfg.projects) cfg.projects = {};
|
|
93
|
+
if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
|
|
94
|
+
cfg.projects[rootDir].shellAllowed = [];
|
|
95
|
+
writeConfig(cfg, path5);
|
|
96
|
+
return existing.length;
|
|
97
|
+
}
|
|
73
98
|
function loadEditMode(path5 = defaultConfigPath()) {
|
|
74
99
|
const v = readConfig(path5).editMode;
|
|
75
100
|
return v === "auto" ? "auto" : "review";
|
|
@@ -156,8 +181,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
|
|
|
156
181
|
}
|
|
157
182
|
function sleep(ms, signal) {
|
|
158
183
|
if (ms <= 0) return Promise.resolve();
|
|
159
|
-
return new Promise((
|
|
160
|
-
const timer = setTimeout(
|
|
184
|
+
return new Promise((resolve13, reject) => {
|
|
185
|
+
const timer = setTimeout(resolve13, ms);
|
|
161
186
|
if (signal) {
|
|
162
187
|
const onAbort = () => {
|
|
163
188
|
clearTimeout(timer);
|
|
@@ -642,7 +667,7 @@ function matchesTool(hook, toolName) {
|
|
|
642
667
|
}
|
|
643
668
|
}
|
|
644
669
|
function defaultSpawner(input) {
|
|
645
|
-
return new Promise((
|
|
670
|
+
return new Promise((resolve13) => {
|
|
646
671
|
const child = spawn(input.command, {
|
|
647
672
|
cwd: input.cwd,
|
|
648
673
|
shell: true,
|
|
@@ -669,7 +694,7 @@ function defaultSpawner(input) {
|
|
|
669
694
|
});
|
|
670
695
|
child.once("error", (err) => {
|
|
671
696
|
clearTimeout(timer);
|
|
672
|
-
|
|
697
|
+
resolve13({
|
|
673
698
|
exitCode: null,
|
|
674
699
|
stdout: stdout3,
|
|
675
700
|
stderr,
|
|
@@ -679,7 +704,7 @@ function defaultSpawner(input) {
|
|
|
679
704
|
});
|
|
680
705
|
child.once("close", (code) => {
|
|
681
706
|
clearTimeout(timer);
|
|
682
|
-
|
|
707
|
+
resolve13({
|
|
683
708
|
exitCode: code,
|
|
684
709
|
stdout: stdout3.trim(),
|
|
685
710
|
stderr: stderr.trim(),
|
|
@@ -1467,25 +1492,32 @@ function coerceToToolCall(candidateJson, allowedNames) {
|
|
|
1467
1492
|
var StormBreaker = class {
|
|
1468
1493
|
windowSize;
|
|
1469
1494
|
threshold;
|
|
1495
|
+
isMutating;
|
|
1470
1496
|
recent = [];
|
|
1471
|
-
constructor(windowSize = 6, threshold = 3) {
|
|
1497
|
+
constructor(windowSize = 6, threshold = 3, isMutating) {
|
|
1472
1498
|
this.windowSize = windowSize;
|
|
1473
1499
|
this.threshold = threshold;
|
|
1500
|
+
this.isMutating = isMutating;
|
|
1474
1501
|
}
|
|
1475
1502
|
inspect(call) {
|
|
1476
|
-
const
|
|
1477
|
-
if (!
|
|
1478
|
-
const
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
)
|
|
1503
|
+
const name = call.function?.name;
|
|
1504
|
+
if (!name) return { suppress: false };
|
|
1505
|
+
const args = call.function?.arguments ?? "";
|
|
1506
|
+
const mutating = this.isMutating ? this.isMutating(call) : false;
|
|
1507
|
+
const readOnly = !mutating;
|
|
1508
|
+
if (mutating) {
|
|
1509
|
+
for (let i = this.recent.length - 1; i >= 0; i--) {
|
|
1510
|
+
if (this.recent[i].readOnly) this.recent.splice(i, 1);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
const count = this.recent.reduce((n, e) => e.name === name && e.args === args ? n + 1 : n, 0);
|
|
1482
1514
|
if (count >= this.threshold - 1) {
|
|
1483
1515
|
return {
|
|
1484
1516
|
suppress: true,
|
|
1485
|
-
reason: `call-storm suppressed: ${
|
|
1517
|
+
reason: `call-storm suppressed: ${name} called with identical args ${count + 1} times within window=${this.windowSize}`
|
|
1486
1518
|
};
|
|
1487
1519
|
}
|
|
1488
|
-
this.recent.push(
|
|
1520
|
+
this.recent.push({ name, args, readOnly });
|
|
1489
1521
|
while (this.recent.length > this.windowSize) this.recent.shift();
|
|
1490
1522
|
return { suppress: false };
|
|
1491
1523
|
}
|
|
@@ -1493,11 +1525,6 @@ var StormBreaker = class {
|
|
|
1493
1525
|
this.recent.length = 0;
|
|
1494
1526
|
}
|
|
1495
1527
|
};
|
|
1496
|
-
function signature(call) {
|
|
1497
|
-
const name = call.function?.name;
|
|
1498
|
-
if (!name) return null;
|
|
1499
|
-
return [name, call.function?.arguments ?? ""];
|
|
1500
|
-
}
|
|
1501
1528
|
|
|
1502
1529
|
// src/repair/truncation.ts
|
|
1503
1530
|
function repairTruncatedJson(input) {
|
|
@@ -1575,7 +1602,7 @@ var ToolCallRepair = class {
|
|
|
1575
1602
|
opts;
|
|
1576
1603
|
constructor(opts) {
|
|
1577
1604
|
this.opts = opts;
|
|
1578
|
-
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
|
|
1605
|
+
this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
|
|
1579
1606
|
}
|
|
1580
1607
|
/**
|
|
1581
1608
|
* Drop the StormBreaker's sliding window of recent (name, args)
|
|
@@ -1599,13 +1626,13 @@ var ToolCallRepair = class {
|
|
|
1599
1626
|
allowedNames: this.opts.allowedToolNames,
|
|
1600
1627
|
maxCalls: this.opts.maxScavenge ?? 4
|
|
1601
1628
|
});
|
|
1602
|
-
const seenSignatures = new Set(declaredCalls.map(
|
|
1629
|
+
const seenSignatures = new Set(declaredCalls.map(signature));
|
|
1603
1630
|
const merged = [...declaredCalls];
|
|
1604
1631
|
for (const sc of scavenged.calls) {
|
|
1605
|
-
if (!seenSignatures.has(
|
|
1632
|
+
if (!seenSignatures.has(signature(sc))) {
|
|
1606
1633
|
merged.push(sc);
|
|
1607
1634
|
report.scavenged++;
|
|
1608
|
-
seenSignatures.add(
|
|
1635
|
+
seenSignatures.add(signature(sc));
|
|
1609
1636
|
}
|
|
1610
1637
|
}
|
|
1611
1638
|
report.notes.push(...scavenged.notes);
|
|
@@ -1631,7 +1658,7 @@ var ToolCallRepair = class {
|
|
|
1631
1658
|
return { calls: filtered, report };
|
|
1632
1659
|
}
|
|
1633
1660
|
};
|
|
1634
|
-
function
|
|
1661
|
+
function signature(call) {
|
|
1635
1662
|
return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
|
|
1636
1663
|
}
|
|
1637
1664
|
|
|
@@ -1770,6 +1797,12 @@ function outputCostUsd(model2, usage) {
|
|
|
1770
1797
|
if (!p) return 0;
|
|
1771
1798
|
return usage.completionTokens * p.output / 1e6;
|
|
1772
1799
|
}
|
|
1800
|
+
function cacheSavingsUsd(model2, hitTokens) {
|
|
1801
|
+
if (hitTokens <= 0) return 0;
|
|
1802
|
+
const p = DEEPSEEK_PRICING[model2];
|
|
1803
|
+
if (!p) return 0;
|
|
1804
|
+
return hitTokens * (p.inputCacheMiss - p.inputCacheHit) / 1e6;
|
|
1805
|
+
}
|
|
1773
1806
|
function claudeEquivalentCost(usage) {
|
|
1774
1807
|
return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
|
|
1775
1808
|
}
|
|
@@ -1860,6 +1893,13 @@ var CacheFirstLoop = class {
|
|
|
1860
1893
|
branchOptions;
|
|
1861
1894
|
/** See ReconfigurableOptions — mutable so `/effort` can flip mid-session. */
|
|
1862
1895
|
reasoningEffort;
|
|
1896
|
+
/**
|
|
1897
|
+
* Auto-escalation toggle. `true` lets the loop self-promote to pro
|
|
1898
|
+
* mid-turn (NEEDS_PRO marker / failure threshold); `false` keeps it
|
|
1899
|
+
* pinned to `model`. Mutable so the dashboard's preset switcher can
|
|
1900
|
+
* flip it live alongside `model`.
|
|
1901
|
+
*/
|
|
1902
|
+
autoEscalate = true;
|
|
1863
1903
|
sessionName;
|
|
1864
1904
|
/**
|
|
1865
1905
|
* Hook list, mutable so `/hooks reload` can swap it without
|
|
@@ -1924,6 +1964,7 @@ var CacheFirstLoop = class {
|
|
|
1924
1964
|
this.tools = opts.tools ?? new ToolRegistry();
|
|
1925
1965
|
this.model = opts.model ?? "deepseek-v4-flash";
|
|
1926
1966
|
this.reasoningEffort = opts.reasoningEffort ?? "max";
|
|
1967
|
+
if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
|
|
1927
1968
|
this.maxToolIters = opts.maxToolIters ?? 64;
|
|
1928
1969
|
this.hooks = opts.hooks ?? [];
|
|
1929
1970
|
this.hookCwd = opts.hookCwd ?? process.cwd();
|
|
@@ -1941,7 +1982,26 @@ var CacheFirstLoop = class {
|
|
|
1941
1982
|
this._streamPreference = opts.stream ?? true;
|
|
1942
1983
|
this.stream = this.branchEnabled ? false : this._streamPreference;
|
|
1943
1984
|
const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
|
|
1944
|
-
|
|
1985
|
+
const registry = this.tools;
|
|
1986
|
+
const isMutating = (call) => {
|
|
1987
|
+
const name = call.function?.name;
|
|
1988
|
+
if (!name) return false;
|
|
1989
|
+
const def = registry.get(name);
|
|
1990
|
+
if (!def) return false;
|
|
1991
|
+
if (def.readOnlyCheck) {
|
|
1992
|
+
let args = {};
|
|
1993
|
+
try {
|
|
1994
|
+
args = JSON.parse(call.function?.arguments ?? "{}") ?? {};
|
|
1995
|
+
} catch {
|
|
1996
|
+
}
|
|
1997
|
+
try {
|
|
1998
|
+
if (def.readOnlyCheck(args)) return false;
|
|
1999
|
+
} catch {
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
2002
|
+
return def.readOnly !== true;
|
|
2003
|
+
};
|
|
2004
|
+
this.repair = new ToolCallRepair({ allowedToolNames: allowedNames, isMutating });
|
|
1945
2005
|
this.sessionName = opts.session ?? null;
|
|
1946
2006
|
if (this.sessionName) {
|
|
1947
2007
|
const prior = loadSessionMessages(this.sessionName);
|
|
@@ -2122,6 +2182,7 @@ var CacheFirstLoop = class {
|
|
|
2122
2182
|
if (opts.model !== void 0) this.model = opts.model;
|
|
2123
2183
|
if (opts.stream !== void 0) this._streamPreference = opts.stream;
|
|
2124
2184
|
if (opts.reasoningEffort !== void 0) this.reasoningEffort = opts.reasoningEffort;
|
|
2185
|
+
if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
|
|
2125
2186
|
if (opts.branch !== void 0) {
|
|
2126
2187
|
if (typeof opts.branch === "number") {
|
|
2127
2188
|
this.branchOptions = { budget: opts.branch };
|
|
@@ -2237,7 +2298,7 @@ var CacheFirstLoop = class {
|
|
|
2237
2298
|
if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
|
|
2238
2299
|
if (repair.stormsBroken > 0) bump("storm-broken", repair.stormsBroken);
|
|
2239
2300
|
}
|
|
2240
|
-
if (bumped && !this._escalateThisTurn && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
|
|
2301
|
+
if (bumped && !this._escalateThisTurn && this.autoEscalate && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
|
|
2241
2302
|
this._escalateThisTurn = true;
|
|
2242
2303
|
return true;
|
|
2243
2304
|
}
|
|
@@ -2442,8 +2503,8 @@ var CacheFirstLoop = class {
|
|
|
2442
2503
|
}
|
|
2443
2504
|
);
|
|
2444
2505
|
for (let k = 0; k < budget; k++) {
|
|
2445
|
-
const sample = queue.shift() ?? await new Promise((
|
|
2446
|
-
waiter =
|
|
2506
|
+
const sample = queue.shift() ?? await new Promise((resolve13) => {
|
|
2507
|
+
waiter = resolve13;
|
|
2447
2508
|
});
|
|
2448
2509
|
yield {
|
|
2449
2510
|
turn: this._turn,
|
|
@@ -2482,7 +2543,7 @@ var CacheFirstLoop = class {
|
|
|
2482
2543
|
const callBuf = /* @__PURE__ */ new Map();
|
|
2483
2544
|
const readyIndices = /* @__PURE__ */ new Set();
|
|
2484
2545
|
const callModel = this.modelForCurrentCall();
|
|
2485
|
-
const bufferForEscalation = callModel !== ESCALATION_MODEL;
|
|
2546
|
+
const bufferForEscalation = this.autoEscalate && callModel !== ESCALATION_MODEL;
|
|
2486
2547
|
let escalationBuf = "";
|
|
2487
2548
|
let escalationBufFlushed = false;
|
|
2488
2549
|
for await (const chunk of this.client.stream({
|
|
@@ -2594,7 +2655,7 @@ var CacheFirstLoop = class {
|
|
|
2594
2655
|
};
|
|
2595
2656
|
return;
|
|
2596
2657
|
}
|
|
2597
|
-
if (this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
|
|
2658
|
+
if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
|
|
2598
2659
|
const { reason } = this.parseEscalationMarker(assistantContent);
|
|
2599
2660
|
this._escalateThisTurn = true;
|
|
2600
2661
|
const reasonSuffix = reason ? ` \u2014 ${reason}` : "";
|
|
@@ -3176,6 +3237,9 @@ var DEFAULT_PICKER_IGNORE_DIRS = [
|
|
|
3176
3237
|
"venv",
|
|
3177
3238
|
"__pycache__"
|
|
3178
3239
|
];
|
|
3240
|
+
function listFilesSync(root, opts = {}) {
|
|
3241
|
+
return listFilesWithStatsSync(root, opts).map((e) => e.path);
|
|
3242
|
+
}
|
|
3179
3243
|
function listFilesWithStatsSync(root, opts = {}) {
|
|
3180
3244
|
const maxResults = Math.max(1, opts.maxResults ?? 500);
|
|
3181
3245
|
const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
|
|
@@ -5173,7 +5237,7 @@ async function runCommand(cmd, opts) {
|
|
|
5173
5237
|
};
|
|
5174
5238
|
const { bin, args, spawnOverrides } = prepareSpawn(argv);
|
|
5175
5239
|
const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
|
|
5176
|
-
return await new Promise((
|
|
5240
|
+
return await new Promise((resolve13, reject) => {
|
|
5177
5241
|
let child;
|
|
5178
5242
|
try {
|
|
5179
5243
|
child = spawn3(bin, args, effectiveSpawnOpts);
|
|
@@ -5218,7 +5282,7 @@ async function runCommand(cmd, opts) {
|
|
|
5218
5282
|
const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
|
|
5219
5283
|
|
|
5220
5284
|
[\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
|
|
5221
|
-
|
|
5285
|
+
resolve13({ exitCode: code, output, timedOut });
|
|
5222
5286
|
});
|
|
5223
5287
|
});
|
|
5224
5288
|
}
|
|
@@ -6422,7 +6486,7 @@ var McpClient = class {
|
|
|
6422
6486
|
const id = this.nextId++;
|
|
6423
6487
|
const frame = { jsonrpc: "2.0", id, method, params };
|
|
6424
6488
|
let abortHandler = null;
|
|
6425
|
-
const promise = new Promise((
|
|
6489
|
+
const promise = new Promise((resolve13, reject) => {
|
|
6426
6490
|
const timeout = setTimeout(() => {
|
|
6427
6491
|
this.pending.delete(id);
|
|
6428
6492
|
if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
|
|
@@ -6431,7 +6495,7 @@ var McpClient = class {
|
|
|
6431
6495
|
);
|
|
6432
6496
|
}, this.requestTimeoutMs);
|
|
6433
6497
|
this.pending.set(id, {
|
|
6434
|
-
resolve:
|
|
6498
|
+
resolve: resolve13,
|
|
6435
6499
|
reject,
|
|
6436
6500
|
timeout
|
|
6437
6501
|
});
|
|
@@ -6554,12 +6618,12 @@ var StdioTransport = class {
|
|
|
6554
6618
|
}
|
|
6555
6619
|
async send(message) {
|
|
6556
6620
|
if (this.closed) throw new Error("MCP transport is closed");
|
|
6557
|
-
return new Promise((
|
|
6621
|
+
return new Promise((resolve13, reject) => {
|
|
6558
6622
|
const line = `${JSON.stringify(message)}
|
|
6559
6623
|
`;
|
|
6560
6624
|
this.child.stdin.write(line, "utf8", (err) => {
|
|
6561
6625
|
if (err) reject(err);
|
|
6562
|
-
else
|
|
6626
|
+
else resolve13();
|
|
6563
6627
|
});
|
|
6564
6628
|
});
|
|
6565
6629
|
}
|
|
@@ -6570,8 +6634,8 @@ var StdioTransport = class {
|
|
|
6570
6634
|
continue;
|
|
6571
6635
|
}
|
|
6572
6636
|
if (this.closed) return;
|
|
6573
|
-
const next = await new Promise((
|
|
6574
|
-
this.waiters.push(
|
|
6637
|
+
const next = await new Promise((resolve13) => {
|
|
6638
|
+
this.waiters.push(resolve13);
|
|
6575
6639
|
});
|
|
6576
6640
|
if (next === null) return;
|
|
6577
6641
|
yield next;
|
|
@@ -6637,8 +6701,8 @@ var SseTransport = class {
|
|
|
6637
6701
|
constructor(opts) {
|
|
6638
6702
|
this.url = opts.url;
|
|
6639
6703
|
this.headers = opts.headers ?? {};
|
|
6640
|
-
this.endpointReady = new Promise((
|
|
6641
|
-
this.resolveEndpoint =
|
|
6704
|
+
this.endpointReady = new Promise((resolve13, reject) => {
|
|
6705
|
+
this.resolveEndpoint = resolve13;
|
|
6642
6706
|
this.rejectEndpoint = reject;
|
|
6643
6707
|
});
|
|
6644
6708
|
this.endpointReady.catch(() => void 0);
|
|
@@ -6665,8 +6729,8 @@ var SseTransport = class {
|
|
|
6665
6729
|
continue;
|
|
6666
6730
|
}
|
|
6667
6731
|
if (this.closed) return;
|
|
6668
|
-
const next = await new Promise((
|
|
6669
|
-
this.waiters.push(
|
|
6732
|
+
const next = await new Promise((resolve13) => {
|
|
6733
|
+
this.waiters.push(resolve13);
|
|
6670
6734
|
});
|
|
6671
6735
|
if (next === null) return;
|
|
6672
6736
|
yield next;
|
|
@@ -6760,6 +6824,159 @@ var SseTransport = class {
|
|
|
6760
6824
|
}
|
|
6761
6825
|
};
|
|
6762
6826
|
|
|
6827
|
+
// src/mcp/streamable-http.ts
|
|
6828
|
+
import { createParser as createParser3 } from "eventsource-parser";
|
|
6829
|
+
var SESSION_HEADER = "mcp-session-id";
|
|
6830
|
+
var StreamableHttpTransport = class {
|
|
6831
|
+
url;
|
|
6832
|
+
extraHeaders;
|
|
6833
|
+
queue = [];
|
|
6834
|
+
waiters = [];
|
|
6835
|
+
controller = new AbortController();
|
|
6836
|
+
/** Session id minted by server on (typically) the initialize response. */
|
|
6837
|
+
sessionId = null;
|
|
6838
|
+
closed = false;
|
|
6839
|
+
/** Background SSE read-loops kicked off by send(); awaited on close(). */
|
|
6840
|
+
streams = /* @__PURE__ */ new Set();
|
|
6841
|
+
constructor(opts) {
|
|
6842
|
+
this.url = opts.url;
|
|
6843
|
+
this.extraHeaders = opts.headers ?? {};
|
|
6844
|
+
}
|
|
6845
|
+
async send(message) {
|
|
6846
|
+
if (this.closed) throw new Error("MCP Streamable HTTP transport is closed");
|
|
6847
|
+
const headers = {
|
|
6848
|
+
"content-type": "application/json",
|
|
6849
|
+
// Both accepted — server picks. application/json first signals a
|
|
6850
|
+
// mild preference for the simpler shape when the response is a
|
|
6851
|
+
// single message.
|
|
6852
|
+
accept: "application/json, text/event-stream",
|
|
6853
|
+
...this.extraHeaders
|
|
6854
|
+
};
|
|
6855
|
+
if (this.sessionId !== null) headers["mcp-session-id"] = this.sessionId;
|
|
6856
|
+
let res;
|
|
6857
|
+
try {
|
|
6858
|
+
res = await fetch(this.url, {
|
|
6859
|
+
method: "POST",
|
|
6860
|
+
headers,
|
|
6861
|
+
body: JSON.stringify(message),
|
|
6862
|
+
signal: this.controller.signal
|
|
6863
|
+
});
|
|
6864
|
+
} catch (err) {
|
|
6865
|
+
throw new Error(`MCP Streamable HTTP POST ${this.url} failed: ${err.message}`);
|
|
6866
|
+
}
|
|
6867
|
+
const serverSessionId = res.headers.get(SESSION_HEADER);
|
|
6868
|
+
if (serverSessionId && this.sessionId === null) {
|
|
6869
|
+
this.sessionId = serverSessionId;
|
|
6870
|
+
}
|
|
6871
|
+
if (res.status === 404 && this.sessionId !== null) {
|
|
6872
|
+
await res.body?.cancel().catch(() => void 0);
|
|
6873
|
+
throw new Error(
|
|
6874
|
+
`MCP Streamable HTTP session expired (server returned 404 with Mcp-Session-Id "${this.sessionId}"). Reinitialize the client.`
|
|
6875
|
+
);
|
|
6876
|
+
}
|
|
6877
|
+
if (!res.ok) {
|
|
6878
|
+
const body = await res.text().catch(() => "");
|
|
6879
|
+
throw new Error(
|
|
6880
|
+
`MCP Streamable HTTP POST ${this.url} \u2192 ${res.status} ${res.statusText}${body ? `: ${body}` : ""}`
|
|
6881
|
+
);
|
|
6882
|
+
}
|
|
6883
|
+
if (res.status === 202) {
|
|
6884
|
+
await res.body?.cancel().catch(() => void 0);
|
|
6885
|
+
return;
|
|
6886
|
+
}
|
|
6887
|
+
const ct = (res.headers.get("content-type") ?? "").toLowerCase();
|
|
6888
|
+
if (ct.includes("application/json")) {
|
|
6889
|
+
let parsed;
|
|
6890
|
+
try {
|
|
6891
|
+
parsed = await res.json();
|
|
6892
|
+
} catch (err) {
|
|
6893
|
+
throw new Error(`MCP Streamable HTTP body wasn't valid JSON: ${err.message}`);
|
|
6894
|
+
}
|
|
6895
|
+
if (Array.isArray(parsed)) {
|
|
6896
|
+
for (const item of parsed) this.pushMessage(item);
|
|
6897
|
+
} else {
|
|
6898
|
+
this.pushMessage(parsed);
|
|
6899
|
+
}
|
|
6900
|
+
return;
|
|
6901
|
+
}
|
|
6902
|
+
if (ct.includes("text/event-stream")) {
|
|
6903
|
+
if (!res.body) {
|
|
6904
|
+
throw new Error("MCP Streamable HTTP SSE response had no body");
|
|
6905
|
+
}
|
|
6906
|
+
const stream = this.consumeStream(res.body);
|
|
6907
|
+
this.streams.add(stream);
|
|
6908
|
+
stream.finally(() => this.streams.delete(stream));
|
|
6909
|
+
return;
|
|
6910
|
+
}
|
|
6911
|
+
await res.body?.cancel().catch(() => void 0);
|
|
6912
|
+
}
|
|
6913
|
+
async *messages() {
|
|
6914
|
+
while (true) {
|
|
6915
|
+
if (this.queue.length > 0) {
|
|
6916
|
+
yield this.queue.shift();
|
|
6917
|
+
continue;
|
|
6918
|
+
}
|
|
6919
|
+
if (this.closed) return;
|
|
6920
|
+
const next = await new Promise((resolve13) => {
|
|
6921
|
+
this.waiters.push(resolve13);
|
|
6922
|
+
});
|
|
6923
|
+
if (next === null) return;
|
|
6924
|
+
yield next;
|
|
6925
|
+
}
|
|
6926
|
+
}
|
|
6927
|
+
async close() {
|
|
6928
|
+
if (this.closed) return;
|
|
6929
|
+
this.closed = true;
|
|
6930
|
+
while (this.waiters.length > 0) this.waiters.shift()(null);
|
|
6931
|
+
try {
|
|
6932
|
+
this.controller.abort();
|
|
6933
|
+
} catch {
|
|
6934
|
+
}
|
|
6935
|
+
await Promise.allSettled(Array.from(this.streams));
|
|
6936
|
+
}
|
|
6937
|
+
/** Visible for tests — confirm session header round-trip. */
|
|
6938
|
+
getSessionId() {
|
|
6939
|
+
return this.sessionId;
|
|
6940
|
+
}
|
|
6941
|
+
// ---------- internals ----------
|
|
6942
|
+
async consumeStream(body) {
|
|
6943
|
+
const parser = createParser3({
|
|
6944
|
+
onEvent: (ev) => {
|
|
6945
|
+
const type = ev.event ?? "message";
|
|
6946
|
+
if (type !== "message") return;
|
|
6947
|
+
try {
|
|
6948
|
+
const parsed = JSON.parse(ev.data);
|
|
6949
|
+
this.pushMessage(parsed);
|
|
6950
|
+
} catch {
|
|
6951
|
+
}
|
|
6952
|
+
}
|
|
6953
|
+
});
|
|
6954
|
+
const decoder = new TextDecoder();
|
|
6955
|
+
try {
|
|
6956
|
+
for await (const chunk of body) {
|
|
6957
|
+
if (this.closed) break;
|
|
6958
|
+
parser.feed(decoder.decode(chunk, { stream: true }));
|
|
6959
|
+
}
|
|
6960
|
+
} catch (err) {
|
|
6961
|
+
if (!this.closed) {
|
|
6962
|
+
this.pushMessage({
|
|
6963
|
+
jsonrpc: "2.0",
|
|
6964
|
+
id: null,
|
|
6965
|
+
error: {
|
|
6966
|
+
code: -32e3,
|
|
6967
|
+
message: `Streamable HTTP stream error: ${err.message}`
|
|
6968
|
+
}
|
|
6969
|
+
});
|
|
6970
|
+
}
|
|
6971
|
+
}
|
|
6972
|
+
}
|
|
6973
|
+
pushMessage(msg) {
|
|
6974
|
+
const waiter = this.waiters.shift();
|
|
6975
|
+
if (waiter) waiter(msg);
|
|
6976
|
+
else this.queue.push(msg);
|
|
6977
|
+
}
|
|
6978
|
+
};
|
|
6979
|
+
|
|
6763
6980
|
// src/mcp/shell-split.ts
|
|
6764
6981
|
function shellSplit(input) {
|
|
6765
6982
|
const tokens = [];
|
|
@@ -6812,6 +7029,7 @@ function shellSplit(input) {
|
|
|
6812
7029
|
// src/mcp/spec.ts
|
|
6813
7030
|
var NAME_PREFIX = /^([a-zA-Z_][a-zA-Z0-9_]*)=(.*)$/;
|
|
6814
7031
|
var HTTP_URL = /^https?:\/\//i;
|
|
7032
|
+
var STREAMABLE_PREFIX = /^streamable\+(https?:\/\/.+)$/i;
|
|
6815
7033
|
function parseMcpSpec(input) {
|
|
6816
7034
|
const trimmed = input.trim();
|
|
6817
7035
|
if (!trimmed) {
|
|
@@ -6823,6 +7041,10 @@ function parseMcpSpec(input) {
|
|
|
6823
7041
|
if (!body) {
|
|
6824
7042
|
throw new Error(`MCP spec has name but no command: ${input}`);
|
|
6825
7043
|
}
|
|
7044
|
+
const streamMatch = STREAMABLE_PREFIX.exec(body);
|
|
7045
|
+
if (streamMatch) {
|
|
7046
|
+
return { transport: "streamable-http", name, url: streamMatch[1] };
|
|
7047
|
+
}
|
|
6826
7048
|
if (HTTP_URL.test(body)) {
|
|
6827
7049
|
return { transport: "sse", name, url: body };
|
|
6828
7050
|
}
|
|
@@ -7173,7 +7395,8 @@ function emptyBucket(label, since) {
|
|
|
7173
7395
|
cacheHitTokens: 0,
|
|
7174
7396
|
cacheMissTokens: 0,
|
|
7175
7397
|
costUsd: 0,
|
|
7176
|
-
claudeEquivUsd: 0
|
|
7398
|
+
claudeEquivUsd: 0,
|
|
7399
|
+
cacheSavingsUsd: 0
|
|
7177
7400
|
};
|
|
7178
7401
|
}
|
|
7179
7402
|
function addToBucket(b, r) {
|
|
@@ -7184,6 +7407,7 @@ function addToBucket(b, r) {
|
|
|
7184
7407
|
b.cacheMissTokens += r.cacheMissTokens;
|
|
7185
7408
|
b.costUsd += r.costUsd;
|
|
7186
7409
|
b.claudeEquivUsd += r.claudeEquivUsd;
|
|
7410
|
+
b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);
|
|
7187
7411
|
}
|
|
7188
7412
|
function aggregateUsage(records, opts = {}) {
|
|
7189
7413
|
const now = opts.now ?? Date.now();
|
|
@@ -7254,7 +7478,7 @@ function formatLogSize(path5 = defaultUsageLogPath()) {
|
|
|
7254
7478
|
}
|
|
7255
7479
|
|
|
7256
7480
|
// src/cli/commands/chat.tsx
|
|
7257
|
-
import { existsSync as
|
|
7481
|
+
import { existsSync as existsSync22, statSync as statSync13 } from "fs";
|
|
7258
7482
|
import { render } from "ink";
|
|
7259
7483
|
import React27, { useState as useState12 } from "react";
|
|
7260
7484
|
|
|
@@ -7457,22 +7681,1660 @@ function listPlanArchives(sessionName) {
|
|
|
7457
7681
|
} catch {
|
|
7458
7682
|
}
|
|
7459
7683
|
}
|
|
7460
|
-
summaries.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
|
|
7461
|
-
return summaries;
|
|
7462
|
-
}
|
|
7463
|
-
function relativeTime(updatedAt, now = Date.now()) {
|
|
7464
|
-
const t2 = Date.parse(updatedAt);
|
|
7465
|
-
if (Number.isNaN(t2)) return updatedAt;
|
|
7466
|
-
const diffMs = Math.max(0, now - t2);
|
|
7467
|
-
const sec = Math.floor(diffMs / 1e3);
|
|
7468
|
-
if (sec < 60) return `${sec}s ago`;
|
|
7469
|
-
const min = Math.floor(sec / 60);
|
|
7470
|
-
if (min < 60) return `${min}m ago`;
|
|
7471
|
-
const hr = Math.floor(min / 60);
|
|
7472
|
-
if (hr < 24) return `${hr}h ago`;
|
|
7473
|
-
const day = Math.floor(hr / 24);
|
|
7474
|
-
if (day < 7) return `${day}d ago`;
|
|
7475
|
-
return updatedAt.slice(0, 10);
|
|
7684
|
+
summaries.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
|
|
7685
|
+
return summaries;
|
|
7686
|
+
}
|
|
7687
|
+
function relativeTime(updatedAt, now = Date.now()) {
|
|
7688
|
+
const t2 = Date.parse(updatedAt);
|
|
7689
|
+
if (Number.isNaN(t2)) return updatedAt;
|
|
7690
|
+
const diffMs = Math.max(0, now - t2);
|
|
7691
|
+
const sec = Math.floor(diffMs / 1e3);
|
|
7692
|
+
if (sec < 60) return `${sec}s ago`;
|
|
7693
|
+
const min = Math.floor(sec / 60);
|
|
7694
|
+
if (min < 60) return `${min}m ago`;
|
|
7695
|
+
const hr = Math.floor(min / 60);
|
|
7696
|
+
if (hr < 24) return `${hr}h ago`;
|
|
7697
|
+
const day = Math.floor(hr / 24);
|
|
7698
|
+
if (day < 7) return `${day}d ago`;
|
|
7699
|
+
return updatedAt.slice(0, 10);
|
|
7700
|
+
}
|
|
7701
|
+
|
|
7702
|
+
// src/server/index.ts
|
|
7703
|
+
import { randomBytes } from "crypto";
|
|
7704
|
+
import { createServer } from "http";
|
|
7705
|
+
|
|
7706
|
+
// src/server/api/events.ts
|
|
7707
|
+
var PING_INTERVAL_MS = 25e3;
|
|
7708
|
+
function handleEvents(req, res, ctx) {
|
|
7709
|
+
if (!ctx.subscribeEvents) {
|
|
7710
|
+
res.writeHead(503, { "content-type": "application/json" });
|
|
7711
|
+
res.end(JSON.stringify({ error: "event stream requires an attached dashboard session." }));
|
|
7712
|
+
return;
|
|
7713
|
+
}
|
|
7714
|
+
res.writeHead(200, {
|
|
7715
|
+
"content-type": "text/event-stream",
|
|
7716
|
+
"cache-control": "no-cache",
|
|
7717
|
+
connection: "keep-alive",
|
|
7718
|
+
"x-accel-buffering": "no"
|
|
7719
|
+
// disable Nginx-style buffering if anything proxies us
|
|
7720
|
+
});
|
|
7721
|
+
const writeEvent = (event) => {
|
|
7722
|
+
if (res.writableEnded) return;
|
|
7723
|
+
try {
|
|
7724
|
+
res.write(`data: ${JSON.stringify(event)}
|
|
7725
|
+
|
|
7726
|
+
`);
|
|
7727
|
+
} catch {
|
|
7728
|
+
}
|
|
7729
|
+
};
|
|
7730
|
+
if (ctx.isBusy) writeEvent({ kind: "busy-change", busy: ctx.isBusy() });
|
|
7731
|
+
const unsubscribe = ctx.subscribeEvents(writeEvent);
|
|
7732
|
+
const ping = setInterval(() => writeEvent({ kind: "ping" }), PING_INTERVAL_MS);
|
|
7733
|
+
ping.unref?.();
|
|
7734
|
+
const cleanup = () => {
|
|
7735
|
+
clearInterval(ping);
|
|
7736
|
+
try {
|
|
7737
|
+
unsubscribe();
|
|
7738
|
+
} catch {
|
|
7739
|
+
}
|
|
7740
|
+
if (!res.writableEnded) {
|
|
7741
|
+
try {
|
|
7742
|
+
res.end();
|
|
7743
|
+
} catch {
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
};
|
|
7747
|
+
req.on("close", cleanup);
|
|
7748
|
+
req.on("error", cleanup);
|
|
7749
|
+
res.on("close", cleanup);
|
|
7750
|
+
}
|
|
7751
|
+
|
|
7752
|
+
// src/server/assets.ts
|
|
7753
|
+
import { readFileSync as readFileSync13 } from "fs";
|
|
7754
|
+
import { dirname as dirname10, join as join11 } from "path";
|
|
7755
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
7756
|
+
function resolveAssetDir() {
|
|
7757
|
+
const here = dirname10(fileURLToPath3(import.meta.url));
|
|
7758
|
+
const candidates = [
|
|
7759
|
+
join11(here, "..", "..", "dashboard"),
|
|
7760
|
+
join11(here, "..", "dashboard"),
|
|
7761
|
+
join11(here, "dashboard")
|
|
7762
|
+
];
|
|
7763
|
+
for (const c of candidates) {
|
|
7764
|
+
try {
|
|
7765
|
+
readFileSync13(join11(c, "index.html"), "utf8");
|
|
7766
|
+
return c;
|
|
7767
|
+
} catch {
|
|
7768
|
+
}
|
|
7769
|
+
}
|
|
7770
|
+
return candidates[0];
|
|
7771
|
+
}
|
|
7772
|
+
var ASSET_DIR = resolveAssetDir();
|
|
7773
|
+
var cachedIndex = null;
|
|
7774
|
+
var cachedApp = null;
|
|
7775
|
+
var cachedCss = null;
|
|
7776
|
+
var cachedCm = null;
|
|
7777
|
+
function loadIndexTemplate() {
|
|
7778
|
+
if (cachedIndex) return cachedIndex;
|
|
7779
|
+
cachedIndex = readFileSync13(join11(ASSET_DIR, "index.html"), "utf8");
|
|
7780
|
+
return cachedIndex;
|
|
7781
|
+
}
|
|
7782
|
+
function loadApp() {
|
|
7783
|
+
if (cachedApp) return cachedApp;
|
|
7784
|
+
cachedApp = readFileSync13(join11(ASSET_DIR, "app.js"), "utf8");
|
|
7785
|
+
return cachedApp;
|
|
7786
|
+
}
|
|
7787
|
+
function loadCss() {
|
|
7788
|
+
if (cachedCss) return cachedCss;
|
|
7789
|
+
cachedCss = readFileSync13(join11(ASSET_DIR, "app.css"), "utf8");
|
|
7790
|
+
return cachedCss;
|
|
7791
|
+
}
|
|
7792
|
+
function loadCm() {
|
|
7793
|
+
if (cachedCm) return cachedCm;
|
|
7794
|
+
cachedCm = readFileSync13(join11(ASSET_DIR, "codemirror.js"), "utf8");
|
|
7795
|
+
return cachedCm;
|
|
7796
|
+
}
|
|
7797
|
+
function renderIndexHtml(token, mode2) {
|
|
7798
|
+
const tpl = loadIndexTemplate();
|
|
7799
|
+
const safeToken = token.replace(/[^a-zA-Z0-9]/g, "");
|
|
7800
|
+
return tpl.replaceAll("__REASONIX_TOKEN__", safeToken).replaceAll("__REASONIX_MODE__", mode2);
|
|
7801
|
+
}
|
|
7802
|
+
function serveAsset(name) {
|
|
7803
|
+
if (name === "app.js") {
|
|
7804
|
+
return { body: loadApp(), contentType: "application/javascript; charset=utf-8" };
|
|
7805
|
+
}
|
|
7806
|
+
if (name === "app.css") {
|
|
7807
|
+
return { body: loadCss(), contentType: "text/css; charset=utf-8" };
|
|
7808
|
+
}
|
|
7809
|
+
if (name === "codemirror.js") {
|
|
7810
|
+
return { body: loadCm(), contentType: "application/javascript; charset=utf-8" };
|
|
7811
|
+
}
|
|
7812
|
+
return null;
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7815
|
+
// src/server/api/abort.ts
|
|
7816
|
+
async function handleAbort(method, _rest, _body, ctx) {
|
|
7817
|
+
if (method !== "POST") {
|
|
7818
|
+
return { status: 405, body: { error: "POST only" } };
|
|
7819
|
+
}
|
|
7820
|
+
if (!ctx.abortTurn) {
|
|
7821
|
+
return {
|
|
7822
|
+
status: 503,
|
|
7823
|
+
body: { error: "abort requires an attached dashboard session." }
|
|
7824
|
+
};
|
|
7825
|
+
}
|
|
7826
|
+
ctx.abortTurn();
|
|
7827
|
+
ctx.audit?.({ ts: Date.now(), action: "abort-turn" });
|
|
7828
|
+
return { status: 202, body: { aborted: true } };
|
|
7829
|
+
}
|
|
7830
|
+
|
|
7831
|
+
// src/server/api/edit-mode.ts
|
|
7832
|
+
function parseBody(raw) {
|
|
7833
|
+
if (!raw) return {};
|
|
7834
|
+
try {
|
|
7835
|
+
const parsed = JSON.parse(raw);
|
|
7836
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
7837
|
+
} catch {
|
|
7838
|
+
return {};
|
|
7839
|
+
}
|
|
7840
|
+
}
|
|
7841
|
+
var VALID = /* @__PURE__ */ new Set(["review", "auto", "yolo"]);
|
|
7842
|
+
async function handleEditMode(method, _rest, body, ctx) {
|
|
7843
|
+
if (method === "GET") {
|
|
7844
|
+
return {
|
|
7845
|
+
status: 200,
|
|
7846
|
+
body: { mode: ctx.getEditMode?.() ?? null }
|
|
7847
|
+
};
|
|
7848
|
+
}
|
|
7849
|
+
if (method === "POST") {
|
|
7850
|
+
if (!ctx.setEditMode) {
|
|
7851
|
+
return {
|
|
7852
|
+
status: 503,
|
|
7853
|
+
body: { error: "edit-mode mutation requires an attached `reasonix code` session." }
|
|
7854
|
+
};
|
|
7855
|
+
}
|
|
7856
|
+
const { mode: mode2 } = parseBody(body);
|
|
7857
|
+
if (typeof mode2 !== "string" || !VALID.has(mode2)) {
|
|
7858
|
+
return { status: 400, body: { error: "mode must be review | auto | yolo" } };
|
|
7859
|
+
}
|
|
7860
|
+
const resolved = ctx.setEditMode(mode2);
|
|
7861
|
+
ctx.audit?.({ ts: Date.now(), action: "set-edit-mode", payload: { mode: resolved } });
|
|
7862
|
+
return { status: 200, body: { mode: resolved } };
|
|
7863
|
+
}
|
|
7864
|
+
return { status: 405, body: { error: "GET or POST only" } };
|
|
7865
|
+
}
|
|
7866
|
+
|
|
7867
|
+
// src/server/api/file.ts
|
|
7868
|
+
import {
|
|
7869
|
+
closeSync,
|
|
7870
|
+
existsSync as existsSync11,
|
|
7871
|
+
mkdirSync as mkdirSync8,
|
|
7872
|
+
openSync,
|
|
7873
|
+
readFileSync as readFileSync14,
|
|
7874
|
+
readSync,
|
|
7875
|
+
statSync as statSync6,
|
|
7876
|
+
writeFileSync as writeFileSync7
|
|
7877
|
+
} from "fs";
|
|
7878
|
+
import { dirname as dirname11, isAbsolute as isAbsolute4, resolve as resolve7, sep as sep2 } from "path";
|
|
7879
|
+
var MAX_BYTES = 4 * 1024 * 1024;
|
|
7880
|
+
var BINARY_PROBE_BYTES = 8 * 1024;
|
|
7881
|
+
function parseBody2(raw) {
|
|
7882
|
+
if (!raw) return {};
|
|
7883
|
+
try {
|
|
7884
|
+
const parsed = JSON.parse(raw);
|
|
7885
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
7886
|
+
} catch {
|
|
7887
|
+
return {};
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
function safeResolve(root, requested) {
|
|
7891
|
+
const rootAbs = resolve7(root);
|
|
7892
|
+
const target = isAbsolute4(requested) ? resolve7(requested) : resolve7(rootAbs, requested);
|
|
7893
|
+
if (target !== rootAbs && !target.startsWith(rootAbs + sep2)) return null;
|
|
7894
|
+
return target;
|
|
7895
|
+
}
|
|
7896
|
+
function looksBinary(path5) {
|
|
7897
|
+
let fd = null;
|
|
7898
|
+
try {
|
|
7899
|
+
fd = openSync(path5, "r");
|
|
7900
|
+
const buf = Buffer.alloc(BINARY_PROBE_BYTES);
|
|
7901
|
+
const len = readSync(fd, buf, 0, BINARY_PROBE_BYTES, 0);
|
|
7902
|
+
for (let i = 0; i < len; i++) {
|
|
7903
|
+
if (buf[i] === 0) return true;
|
|
7904
|
+
}
|
|
7905
|
+
return false;
|
|
7906
|
+
} catch {
|
|
7907
|
+
return false;
|
|
7908
|
+
} finally {
|
|
7909
|
+
if (fd !== null) {
|
|
7910
|
+
try {
|
|
7911
|
+
closeSync(fd);
|
|
7912
|
+
} catch {
|
|
7913
|
+
}
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
}
|
|
7917
|
+
async function handleFiles(method, _rest, _body, ctx) {
|
|
7918
|
+
if (method !== "GET") {
|
|
7919
|
+
return { status: 405, body: { error: "GET only" } };
|
|
7920
|
+
}
|
|
7921
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
7922
|
+
if (!cwd2) {
|
|
7923
|
+
return {
|
|
7924
|
+
status: 503,
|
|
7925
|
+
body: { error: "no project root \u2014 open `/dashboard` from `reasonix code`" }
|
|
7926
|
+
};
|
|
7927
|
+
}
|
|
7928
|
+
const files = listFilesSync(cwd2, { maxResults: 5e3 });
|
|
7929
|
+
return {
|
|
7930
|
+
status: 200,
|
|
7931
|
+
body: {
|
|
7932
|
+
root: cwd2,
|
|
7933
|
+
count: files.length,
|
|
7934
|
+
truncated: files.length === 5e3,
|
|
7935
|
+
files
|
|
7936
|
+
}
|
|
7937
|
+
};
|
|
7938
|
+
}
|
|
7939
|
+
async function handleFile(method, rest, body, ctx) {
|
|
7940
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
7941
|
+
if (!cwd2) {
|
|
7942
|
+
return { status: 503, body: { error: "no project root" } };
|
|
7943
|
+
}
|
|
7944
|
+
const requested = rest.map((s) => decodeURIComponent(s)).join("/");
|
|
7945
|
+
if (!requested) {
|
|
7946
|
+
return { status: 400, body: { error: "path required (use /api/file/<path>)" } };
|
|
7947
|
+
}
|
|
7948
|
+
const target = safeResolve(cwd2, requested);
|
|
7949
|
+
if (!target) {
|
|
7950
|
+
return { status: 403, body: { error: "path escapes project root" } };
|
|
7951
|
+
}
|
|
7952
|
+
if (method === "GET") {
|
|
7953
|
+
if (!existsSync11(target)) {
|
|
7954
|
+
return { status: 404, body: { error: "file not found" } };
|
|
7955
|
+
}
|
|
7956
|
+
const stat = statSync6(target);
|
|
7957
|
+
if (stat.isDirectory()) {
|
|
7958
|
+
return { status: 400, body: { error: "path is a directory" } };
|
|
7959
|
+
}
|
|
7960
|
+
if (stat.size > MAX_BYTES) {
|
|
7961
|
+
return {
|
|
7962
|
+
status: 413,
|
|
7963
|
+
body: { error: `file too large (${stat.size} bytes; cap ${MAX_BYTES})` }
|
|
7964
|
+
};
|
|
7965
|
+
}
|
|
7966
|
+
if (looksBinary(target)) {
|
|
7967
|
+
return {
|
|
7968
|
+
status: 415,
|
|
7969
|
+
body: { error: "file appears to be binary \u2014 editor refuses to load." }
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7972
|
+
const content = readFileSync14(target, "utf8");
|
|
7973
|
+
return {
|
|
7974
|
+
status: 200,
|
|
7975
|
+
body: {
|
|
7976
|
+
path: requested,
|
|
7977
|
+
absolute: target,
|
|
7978
|
+
size: stat.size,
|
|
7979
|
+
mtime: stat.mtime.getTime(),
|
|
7980
|
+
content
|
|
7981
|
+
}
|
|
7982
|
+
};
|
|
7983
|
+
}
|
|
7984
|
+
if (method === "POST") {
|
|
7985
|
+
const { content } = parseBody2(body);
|
|
7986
|
+
if (typeof content !== "string") {
|
|
7987
|
+
return { status: 400, body: { error: "content (string) required" } };
|
|
7988
|
+
}
|
|
7989
|
+
if (Buffer.byteLength(content, "utf8") > MAX_BYTES) {
|
|
7990
|
+
return { status: 413, body: { error: "content exceeds 4 MB cap" } };
|
|
7991
|
+
}
|
|
7992
|
+
if (existsSync11(target) && statSync6(target).isDirectory()) {
|
|
7993
|
+
return { status: 400, body: { error: "path is a directory" } };
|
|
7994
|
+
}
|
|
7995
|
+
const parent = dirname11(target);
|
|
7996
|
+
if (!existsSync11(parent)) {
|
|
7997
|
+
mkdirSync8(parent, { recursive: true });
|
|
7998
|
+
}
|
|
7999
|
+
writeFileSync7(target, content, "utf8");
|
|
8000
|
+
ctx.audit?.({
|
|
8001
|
+
ts: Date.now(),
|
|
8002
|
+
action: "save-file",
|
|
8003
|
+
payload: { path: requested, bytes: Buffer.byteLength(content, "utf8") }
|
|
8004
|
+
});
|
|
8005
|
+
const stat = statSync6(target);
|
|
8006
|
+
return {
|
|
8007
|
+
status: 200,
|
|
8008
|
+
body: {
|
|
8009
|
+
saved: true,
|
|
8010
|
+
path: requested,
|
|
8011
|
+
size: stat.size,
|
|
8012
|
+
mtime: stat.mtime.getTime()
|
|
8013
|
+
}
|
|
8014
|
+
};
|
|
8015
|
+
}
|
|
8016
|
+
return { status: 405, body: { error: "GET or POST only" } };
|
|
8017
|
+
}
|
|
8018
|
+
|
|
8019
|
+
// src/server/api/health.ts
|
|
8020
|
+
import { existsSync as existsSync12, readdirSync as readdirSync4, statSync as statSync7 } from "fs";
|
|
8021
|
+
import { homedir as homedir6 } from "os";
|
|
8022
|
+
import { join as join12 } from "path";
|
|
8023
|
+
function dirSize(path5) {
|
|
8024
|
+
if (!existsSync12(path5)) return { path: path5, exists: false, fileCount: 0, totalBytes: 0 };
|
|
8025
|
+
let fileCount = 0;
|
|
8026
|
+
let totalBytes = 0;
|
|
8027
|
+
try {
|
|
8028
|
+
const entries = readdirSync4(path5);
|
|
8029
|
+
for (const name of entries) {
|
|
8030
|
+
const full = join12(path5, name);
|
|
8031
|
+
try {
|
|
8032
|
+
const s = statSync7(full);
|
|
8033
|
+
if (s.isFile()) {
|
|
8034
|
+
fileCount++;
|
|
8035
|
+
totalBytes += s.size;
|
|
8036
|
+
} else if (s.isDirectory()) {
|
|
8037
|
+
try {
|
|
8038
|
+
const inner = readdirSync4(full);
|
|
8039
|
+
for (const child of inner) {
|
|
8040
|
+
try {
|
|
8041
|
+
const cs = statSync7(join12(full, child));
|
|
8042
|
+
if (cs.isFile()) {
|
|
8043
|
+
fileCount++;
|
|
8044
|
+
totalBytes += cs.size;
|
|
8045
|
+
}
|
|
8046
|
+
} catch {
|
|
8047
|
+
}
|
|
8048
|
+
}
|
|
8049
|
+
} catch {
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
} catch {
|
|
8053
|
+
}
|
|
8054
|
+
}
|
|
8055
|
+
} catch {
|
|
8056
|
+
return { path: path5, exists: true, fileCount: 0, totalBytes: 0 };
|
|
8057
|
+
}
|
|
8058
|
+
return { path: path5, exists: true, fileCount, totalBytes };
|
|
8059
|
+
}
|
|
8060
|
+
async function handleHealth(method, _rest, _body, ctx) {
|
|
8061
|
+
if (method !== "GET") {
|
|
8062
|
+
return { status: 405, body: { error: "GET only" } };
|
|
8063
|
+
}
|
|
8064
|
+
const home = homedir6();
|
|
8065
|
+
const reasonixHome = join12(home, ".reasonix");
|
|
8066
|
+
const sessionsStat = dirSize(join12(reasonixHome, "sessions"));
|
|
8067
|
+
const memoryStat = dirSize(join12(reasonixHome, "memory"));
|
|
8068
|
+
const semanticStat = dirSize(join12(reasonixHome, "semantic"));
|
|
8069
|
+
let usageBytes = 0;
|
|
8070
|
+
if (existsSync12(ctx.usageLogPath)) {
|
|
8071
|
+
try {
|
|
8072
|
+
usageBytes = statSync7(ctx.usageLogPath).size;
|
|
8073
|
+
} catch {
|
|
8074
|
+
}
|
|
8075
|
+
}
|
|
8076
|
+
const sessions2 = listSessions();
|
|
8077
|
+
return {
|
|
8078
|
+
status: 200,
|
|
8079
|
+
body: {
|
|
8080
|
+
version: VERSION,
|
|
8081
|
+
latestVersion: ctx.getLatestVersion?.() ?? null,
|
|
8082
|
+
reasonixHome,
|
|
8083
|
+
sessions: {
|
|
8084
|
+
path: sessionsStat.path,
|
|
8085
|
+
count: sessions2.length,
|
|
8086
|
+
totalBytes: sessionsStat.totalBytes
|
|
8087
|
+
},
|
|
8088
|
+
memory: {
|
|
8089
|
+
path: memoryStat.path,
|
|
8090
|
+
fileCount: memoryStat.fileCount,
|
|
8091
|
+
totalBytes: memoryStat.totalBytes
|
|
8092
|
+
},
|
|
8093
|
+
semantic: {
|
|
8094
|
+
path: semanticStat.path,
|
|
8095
|
+
exists: semanticStat.exists,
|
|
8096
|
+
fileCount: semanticStat.fileCount,
|
|
8097
|
+
totalBytes: semanticStat.totalBytes
|
|
8098
|
+
},
|
|
8099
|
+
usageLog: {
|
|
8100
|
+
path: ctx.usageLogPath,
|
|
8101
|
+
bytes: usageBytes
|
|
8102
|
+
},
|
|
8103
|
+
jobs: ctx.jobs ? ctx.jobs.list().length : null
|
|
8104
|
+
}
|
|
8105
|
+
};
|
|
8106
|
+
}
|
|
8107
|
+
|
|
8108
|
+
// src/server/api/hooks.ts
|
|
8109
|
+
import { existsSync as existsSync13, mkdirSync as mkdirSync9, readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "fs";
|
|
8110
|
+
import { dirname as dirname12 } from "path";
|
|
8111
|
+
function parseBody3(raw) {
|
|
8112
|
+
if (!raw) return {};
|
|
8113
|
+
try {
|
|
8114
|
+
const parsed = JSON.parse(raw);
|
|
8115
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8116
|
+
} catch {
|
|
8117
|
+
return {};
|
|
8118
|
+
}
|
|
8119
|
+
}
|
|
8120
|
+
function readSettingsFile2(path5) {
|
|
8121
|
+
if (!existsSync13(path5)) return {};
|
|
8122
|
+
try {
|
|
8123
|
+
const raw = readFileSync15(path5, "utf8");
|
|
8124
|
+
const parsed = JSON.parse(raw);
|
|
8125
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8126
|
+
} catch {
|
|
8127
|
+
return {};
|
|
8128
|
+
}
|
|
8129
|
+
}
|
|
8130
|
+
function writeSettingsFile(path5, hooksBlock) {
|
|
8131
|
+
const existing = readSettingsFile2(path5);
|
|
8132
|
+
existing.hooks = hooksBlock;
|
|
8133
|
+
mkdirSync9(dirname12(path5), { recursive: true });
|
|
8134
|
+
writeFileSync8(path5, `${JSON.stringify(existing, null, 2)}
|
|
8135
|
+
`, "utf8");
|
|
8136
|
+
}
|
|
8137
|
+
async function handleHooks(method, rest, body, ctx) {
|
|
8138
|
+
if (method === "GET" && rest.length === 0) {
|
|
8139
|
+
const projectPath = ctx.getCurrentCwd ? projectSettingsPath(ctx.getCurrentCwd() ?? "") : null;
|
|
8140
|
+
const globalPath = globalSettingsPath();
|
|
8141
|
+
const projectFile = projectPath ? readSettingsFile2(projectPath) : {};
|
|
8142
|
+
const globalFile = readSettingsFile2(globalPath);
|
|
8143
|
+
const resolved = loadHooks({ projectRoot: ctx.getCurrentCwd?.() });
|
|
8144
|
+
return {
|
|
8145
|
+
status: 200,
|
|
8146
|
+
body: {
|
|
8147
|
+
project: {
|
|
8148
|
+
path: projectPath,
|
|
8149
|
+
hooks: projectFile.hooks ?? {}
|
|
8150
|
+
},
|
|
8151
|
+
global: {
|
|
8152
|
+
path: globalPath,
|
|
8153
|
+
hooks: globalFile.hooks ?? {}
|
|
8154
|
+
},
|
|
8155
|
+
resolved,
|
|
8156
|
+
events: HOOK_EVENTS
|
|
8157
|
+
}
|
|
8158
|
+
};
|
|
8159
|
+
}
|
|
8160
|
+
if (method === "POST" && rest[0] === "save") {
|
|
8161
|
+
const { scope, hooks: hooks2 } = parseBody3(body);
|
|
8162
|
+
if (scope !== "project" && scope !== "global") {
|
|
8163
|
+
return { status: 400, body: { error: "scope must be project | global" } };
|
|
8164
|
+
}
|
|
8165
|
+
if (typeof hooks2 !== "object" || hooks2 === null) {
|
|
8166
|
+
return { status: 400, body: { error: "hooks must be an object keyed by event name" } };
|
|
8167
|
+
}
|
|
8168
|
+
let path5;
|
|
8169
|
+
if (scope === "project") {
|
|
8170
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
8171
|
+
if (!cwd2) {
|
|
8172
|
+
return {
|
|
8173
|
+
status: 503,
|
|
8174
|
+
body: { error: "no active project \u2014 open `/dashboard` from inside `reasonix code`" }
|
|
8175
|
+
};
|
|
8176
|
+
}
|
|
8177
|
+
path5 = projectSettingsPath(cwd2);
|
|
8178
|
+
} else {
|
|
8179
|
+
path5 = globalSettingsPath();
|
|
8180
|
+
}
|
|
8181
|
+
if (!path5) {
|
|
8182
|
+
return { status: 500, body: { error: "could not resolve settings path" } };
|
|
8183
|
+
}
|
|
8184
|
+
writeSettingsFile(path5, hooks2);
|
|
8185
|
+
ctx.audit?.({ ts: Date.now(), action: "save-hooks", payload: { scope, path: path5 } });
|
|
8186
|
+
return { status: 200, body: { saved: true, path: path5 } };
|
|
8187
|
+
}
|
|
8188
|
+
if (method === "POST" && rest[0] === "reload") {
|
|
8189
|
+
if (!ctx.reloadHooks) {
|
|
8190
|
+
return {
|
|
8191
|
+
status: 503,
|
|
8192
|
+
body: { error: "reload requires an attached session \u2014 App.tsx wires the callback" }
|
|
8193
|
+
};
|
|
8194
|
+
}
|
|
8195
|
+
const count = ctx.reloadHooks();
|
|
8196
|
+
ctx.audit?.({ ts: Date.now(), action: "reload-hooks", payload: { count } });
|
|
8197
|
+
return { status: 200, body: { reloaded: true, count } };
|
|
8198
|
+
}
|
|
8199
|
+
return { status: 405, body: { error: `method ${method} not supported on this path` } };
|
|
8200
|
+
}
|
|
8201
|
+
|
|
8202
|
+
// src/server/api/mcp.ts
|
|
8203
|
+
function parseBody4(raw) {
|
|
8204
|
+
if (!raw) return {};
|
|
8205
|
+
try {
|
|
8206
|
+
const parsed = JSON.parse(raw);
|
|
8207
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8208
|
+
} catch {
|
|
8209
|
+
return {};
|
|
8210
|
+
}
|
|
8211
|
+
}
|
|
8212
|
+
async function handleMcp(method, rest, body, ctx) {
|
|
8213
|
+
if (method === "GET" && rest.length === 0) {
|
|
8214
|
+
const servers = (ctx.mcpServers ?? []).map((s) => ({
|
|
8215
|
+
label: s.label,
|
|
8216
|
+
spec: s.spec,
|
|
8217
|
+
toolCount: s.toolCount,
|
|
8218
|
+
protocolVersion: s.report.protocolVersion,
|
|
8219
|
+
serverInfo: s.report.serverInfo,
|
|
8220
|
+
capabilities: s.report.capabilities,
|
|
8221
|
+
tools: s.report.tools.supported ? s.report.tools.items : [],
|
|
8222
|
+
resources: s.report.resources.supported ? s.report.resources.items : [],
|
|
8223
|
+
prompts: s.report.prompts.supported ? s.report.prompts.items : [],
|
|
8224
|
+
instructions: s.report.instructions ?? null
|
|
8225
|
+
}));
|
|
8226
|
+
return {
|
|
8227
|
+
status: 200,
|
|
8228
|
+
body: {
|
|
8229
|
+
servers,
|
|
8230
|
+
canHotReload: Boolean(ctx.reloadMcp),
|
|
8231
|
+
canInvoke: Boolean(ctx.invokeMcpTool)
|
|
8232
|
+
}
|
|
8233
|
+
};
|
|
8234
|
+
}
|
|
8235
|
+
if (method === "GET" && rest[0] === "specs") {
|
|
8236
|
+
const cfg = readConfig(ctx.configPath);
|
|
8237
|
+
return { status: 200, body: { specs: cfg.mcp ?? [] } };
|
|
8238
|
+
}
|
|
8239
|
+
if (method === "POST" && rest[0] === "specs") {
|
|
8240
|
+
const { spec } = parseBody4(body);
|
|
8241
|
+
if (typeof spec !== "string" || !spec.trim()) {
|
|
8242
|
+
return { status: 400, body: { error: "spec (non-empty string) required" } };
|
|
8243
|
+
}
|
|
8244
|
+
const cfg = readConfig(ctx.configPath);
|
|
8245
|
+
const list = cfg.mcp ?? [];
|
|
8246
|
+
if (list.includes(spec)) {
|
|
8247
|
+
return { status: 200, body: { added: false, alreadyPresent: true } };
|
|
8248
|
+
}
|
|
8249
|
+
cfg.mcp = [...list, spec.trim()];
|
|
8250
|
+
writeConfig(cfg, ctx.configPath);
|
|
8251
|
+
ctx.audit?.({ ts: Date.now(), action: "add-mcp-spec", payload: { spec } });
|
|
8252
|
+
return { status: 200, body: { added: true, requiresRestart: !ctx.reloadMcp } };
|
|
8253
|
+
}
|
|
8254
|
+
if (method === "DELETE" && rest[0] === "specs") {
|
|
8255
|
+
const { spec } = parseBody4(body);
|
|
8256
|
+
if (typeof spec !== "string") {
|
|
8257
|
+
return { status: 400, body: { error: "spec (string) required" } };
|
|
8258
|
+
}
|
|
8259
|
+
const cfg = readConfig(ctx.configPath);
|
|
8260
|
+
const list = cfg.mcp ?? [];
|
|
8261
|
+
if (!list.includes(spec)) {
|
|
8262
|
+
return { status: 200, body: { removed: false } };
|
|
8263
|
+
}
|
|
8264
|
+
cfg.mcp = list.filter((s) => s !== spec);
|
|
8265
|
+
writeConfig(cfg, ctx.configPath);
|
|
8266
|
+
ctx.audit?.({ ts: Date.now(), action: "remove-mcp-spec", payload: { spec } });
|
|
8267
|
+
return { status: 200, body: { removed: true, requiresRestart: !ctx.reloadMcp } };
|
|
8268
|
+
}
|
|
8269
|
+
if (method === "POST" && rest[0] === "reload") {
|
|
8270
|
+
if (!ctx.reloadMcp) {
|
|
8271
|
+
return {
|
|
8272
|
+
status: 503,
|
|
8273
|
+
body: {
|
|
8274
|
+
error: "live MCP reload not wired in this session \u2014 restart `reasonix code` to apply spec edits."
|
|
8275
|
+
}
|
|
8276
|
+
};
|
|
8277
|
+
}
|
|
8278
|
+
const count = await ctx.reloadMcp();
|
|
8279
|
+
return { status: 200, body: { reloaded: true, count } };
|
|
8280
|
+
}
|
|
8281
|
+
if (method === "POST" && rest[0] === "invoke") {
|
|
8282
|
+
if (!ctx.invokeMcpTool) {
|
|
8283
|
+
return {
|
|
8284
|
+
status: 503,
|
|
8285
|
+
body: { error: "MCP invocation requires an attached session." }
|
|
8286
|
+
};
|
|
8287
|
+
}
|
|
8288
|
+
const { server, tool: tool2, args } = parseBody4(body);
|
|
8289
|
+
if (typeof server !== "string" || typeof tool2 !== "string") {
|
|
8290
|
+
return { status: 400, body: { error: "server + tool (strings) required" } };
|
|
8291
|
+
}
|
|
8292
|
+
try {
|
|
8293
|
+
const result = await ctx.invokeMcpTool(
|
|
8294
|
+
server,
|
|
8295
|
+
tool2,
|
|
8296
|
+
typeof args === "object" && args !== null ? args : {}
|
|
8297
|
+
);
|
|
8298
|
+
return { status: 200, body: { result } };
|
|
8299
|
+
} catch (err) {
|
|
8300
|
+
return { status: 500, body: { error: err.message } };
|
|
8301
|
+
}
|
|
8302
|
+
}
|
|
8303
|
+
return { status: 405, body: { error: `method ${method} not supported on this path` } };
|
|
8304
|
+
}
|
|
8305
|
+
|
|
8306
|
+
// src/server/api/memory.ts
|
|
8307
|
+
import { createHash as createHash2 } from "crypto";
|
|
8308
|
+
import {
|
|
8309
|
+
existsSync as existsSync14,
|
|
8310
|
+
mkdirSync as mkdirSync10,
|
|
8311
|
+
readFileSync as readFileSync16,
|
|
8312
|
+
readdirSync as readdirSync5,
|
|
8313
|
+
statSync as statSync8,
|
|
8314
|
+
unlinkSync as unlinkSync5,
|
|
8315
|
+
writeFileSync as writeFileSync9
|
|
8316
|
+
} from "fs";
|
|
8317
|
+
import { homedir as homedir7 } from "os";
|
|
8318
|
+
import { dirname as dirname13, join as join13, resolve as resolvePath } from "path";
|
|
8319
|
+
function projectHash2(rootDir) {
|
|
8320
|
+
return createHash2("sha1").update(resolvePath(rootDir)).digest("hex").slice(0, 16);
|
|
8321
|
+
}
|
|
8322
|
+
function globalMemoryDir() {
|
|
8323
|
+
return join13(homedir7(), ".reasonix", "memory", "global");
|
|
8324
|
+
}
|
|
8325
|
+
function projectMemoryDir(rootDir) {
|
|
8326
|
+
return join13(homedir7(), ".reasonix", "memory", projectHash2(rootDir));
|
|
8327
|
+
}
|
|
8328
|
+
function parseBody5(raw) {
|
|
8329
|
+
if (!raw) return {};
|
|
8330
|
+
try {
|
|
8331
|
+
const parsed = JSON.parse(raw);
|
|
8332
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8333
|
+
} catch {
|
|
8334
|
+
return {};
|
|
8335
|
+
}
|
|
8336
|
+
}
|
|
8337
|
+
var SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
8338
|
+
function listMemoryFiles(dir) {
|
|
8339
|
+
if (!existsSync14(dir)) return [];
|
|
8340
|
+
try {
|
|
8341
|
+
return readdirSync5(dir).filter((f) => f.endsWith(".md")).map((f) => {
|
|
8342
|
+
const stat = statSync8(join13(dir, f));
|
|
8343
|
+
return {
|
|
8344
|
+
name: f.replace(/\.md$/, ""),
|
|
8345
|
+
size: stat.size,
|
|
8346
|
+
mtime: stat.mtime.getTime()
|
|
8347
|
+
};
|
|
8348
|
+
}).sort((a, b) => b.mtime - a.mtime);
|
|
8349
|
+
} catch {
|
|
8350
|
+
return [];
|
|
8351
|
+
}
|
|
8352
|
+
}
|
|
8353
|
+
async function handleMemory(method, rest, body, ctx) {
|
|
8354
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
8355
|
+
const globalDir = globalMemoryDir();
|
|
8356
|
+
const projectMemDir = cwd2 ? projectMemoryDir(cwd2) : "";
|
|
8357
|
+
if (method === "GET" && rest.length === 0) {
|
|
8358
|
+
const projectMemoryPath = cwd2 ? join13(cwd2, PROJECT_MEMORY_FILE) : null;
|
|
8359
|
+
const projectMemoryExists = projectMemoryPath ? existsSync14(projectMemoryPath) : false;
|
|
8360
|
+
return {
|
|
8361
|
+
status: 200,
|
|
8362
|
+
body: {
|
|
8363
|
+
project: {
|
|
8364
|
+
path: projectMemoryPath,
|
|
8365
|
+
exists: projectMemoryExists,
|
|
8366
|
+
file: PROJECT_MEMORY_FILE
|
|
8367
|
+
},
|
|
8368
|
+
global: {
|
|
8369
|
+
path: globalDir,
|
|
8370
|
+
files: listMemoryFiles(globalDir)
|
|
8371
|
+
},
|
|
8372
|
+
projectMem: {
|
|
8373
|
+
path: projectMemDir,
|
|
8374
|
+
files: projectMemDir ? listMemoryFiles(projectMemDir) : []
|
|
8375
|
+
}
|
|
8376
|
+
}
|
|
8377
|
+
};
|
|
8378
|
+
}
|
|
8379
|
+
const [scope, ...nameParts] = rest;
|
|
8380
|
+
const name = nameParts.join("/");
|
|
8381
|
+
if (method === "GET") {
|
|
8382
|
+
if (scope === "project") {
|
|
8383
|
+
if (!cwd2) return { status: 503, body: { error: "no active project" } };
|
|
8384
|
+
const path5 = join13(cwd2, PROJECT_MEMORY_FILE);
|
|
8385
|
+
if (!existsSync14(path5)) return { status: 404, body: { error: "REASONIX.md not found" } };
|
|
8386
|
+
return { status: 200, body: { path: path5, body: readFileSync16(path5, "utf8") } };
|
|
8387
|
+
}
|
|
8388
|
+
if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
|
|
8389
|
+
const dir = scope === "global" ? globalDir : projectMemDir;
|
|
8390
|
+
if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
|
|
8391
|
+
const path5 = join13(dir, `${name}.md`);
|
|
8392
|
+
if (!existsSync14(path5)) return { status: 404, body: { error: "not found" } };
|
|
8393
|
+
return { status: 200, body: { path: path5, body: readFileSync16(path5, "utf8") } };
|
|
8394
|
+
}
|
|
8395
|
+
return { status: 400, body: { error: "bad scope or name" } };
|
|
8396
|
+
}
|
|
8397
|
+
if (method === "POST") {
|
|
8398
|
+
const { body: contents } = parseBody5(body);
|
|
8399
|
+
if (typeof contents !== "string") {
|
|
8400
|
+
return { status: 400, body: { error: "body (string) required" } };
|
|
8401
|
+
}
|
|
8402
|
+
if (scope === "project") {
|
|
8403
|
+
if (!cwd2) return { status: 503, body: { error: "no active project" } };
|
|
8404
|
+
const path5 = join13(cwd2, PROJECT_MEMORY_FILE);
|
|
8405
|
+
mkdirSync10(dirname13(path5), { recursive: true });
|
|
8406
|
+
writeFileSync9(path5, contents, "utf8");
|
|
8407
|
+
ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, path: path5 } });
|
|
8408
|
+
return { status: 200, body: { saved: true, path: path5 } };
|
|
8409
|
+
}
|
|
8410
|
+
if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
|
|
8411
|
+
const dir = scope === "global" ? globalDir : projectMemDir;
|
|
8412
|
+
if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
|
|
8413
|
+
mkdirSync10(dir, { recursive: true });
|
|
8414
|
+
const path5 = join13(dir, `${name}.md`);
|
|
8415
|
+
writeFileSync9(path5, contents, "utf8");
|
|
8416
|
+
ctx.audit?.({ ts: Date.now(), action: "save-memory", payload: { scope, name, path: path5 } });
|
|
8417
|
+
return { status: 200, body: { saved: true, path: path5 } };
|
|
8418
|
+
}
|
|
8419
|
+
return { status: 400, body: { error: "bad scope or name" } };
|
|
8420
|
+
}
|
|
8421
|
+
if (method === "DELETE") {
|
|
8422
|
+
if ((scope === "global" || scope === "project-mem") && name && SAFE_NAME.test(name)) {
|
|
8423
|
+
const dir = scope === "global" ? globalDir : projectMemDir;
|
|
8424
|
+
if (!dir) return { status: 503, body: { error: "no project root for project-mem" } };
|
|
8425
|
+
const path5 = join13(dir, `${name}.md`);
|
|
8426
|
+
if (existsSync14(path5)) {
|
|
8427
|
+
unlinkSync5(path5);
|
|
8428
|
+
ctx.audit?.({ ts: Date.now(), action: "delete-memory", payload: { scope, name, path: path5 } });
|
|
8429
|
+
return { status: 200, body: { deleted: true } };
|
|
8430
|
+
}
|
|
8431
|
+
return { status: 404, body: { error: "not found" } };
|
|
8432
|
+
}
|
|
8433
|
+
if (scope === "project") {
|
|
8434
|
+
if (!cwd2) return { status: 503, body: { error: "no active project" } };
|
|
8435
|
+
const path5 = join13(cwd2, PROJECT_MEMORY_FILE);
|
|
8436
|
+
if (existsSync14(path5)) {
|
|
8437
|
+
unlinkSync5(path5);
|
|
8438
|
+
ctx.audit?.({ ts: Date.now(), action: "delete-memory", payload: { scope, path: path5 } });
|
|
8439
|
+
return { status: 200, body: { deleted: true } };
|
|
8440
|
+
}
|
|
8441
|
+
return { status: 404, body: { error: "not found" } };
|
|
8442
|
+
}
|
|
8443
|
+
return { status: 400, body: { error: "bad scope or name" } };
|
|
8444
|
+
}
|
|
8445
|
+
return { status: 405, body: { error: `method ${method} not supported` } };
|
|
8446
|
+
}
|
|
8447
|
+
|
|
8448
|
+
// src/server/api/messages.ts
|
|
8449
|
+
async function handleMessages(method, _rest, _body, ctx) {
|
|
8450
|
+
if (method !== "GET") {
|
|
8451
|
+
return { status: 405, body: { error: "GET only" } };
|
|
8452
|
+
}
|
|
8453
|
+
const messages = ctx.getMessages ? ctx.getMessages() : [];
|
|
8454
|
+
return {
|
|
8455
|
+
status: 200,
|
|
8456
|
+
body: {
|
|
8457
|
+
messages,
|
|
8458
|
+
busy: ctx.isBusy ? ctx.isBusy() : false
|
|
8459
|
+
}
|
|
8460
|
+
};
|
|
8461
|
+
}
|
|
8462
|
+
|
|
8463
|
+
// src/server/api/modal.ts
|
|
8464
|
+
function parseBody6(raw) {
|
|
8465
|
+
if (!raw) return {};
|
|
8466
|
+
try {
|
|
8467
|
+
const parsed = JSON.parse(raw);
|
|
8468
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8469
|
+
} catch {
|
|
8470
|
+
return {};
|
|
8471
|
+
}
|
|
8472
|
+
}
|
|
8473
|
+
async function handleModal(method, rest, body, ctx) {
|
|
8474
|
+
if (method === "GET" && rest.length === 0) {
|
|
8475
|
+
return {
|
|
8476
|
+
status: 200,
|
|
8477
|
+
body: { modal: ctx.getActiveModal ? ctx.getActiveModal() : null }
|
|
8478
|
+
};
|
|
8479
|
+
}
|
|
8480
|
+
if (method === "POST" && rest[0] === "resolve") {
|
|
8481
|
+
const { kind, choice, text } = parseBody6(body);
|
|
8482
|
+
if (kind === "shell") {
|
|
8483
|
+
if (!ctx.resolveShellConfirm) {
|
|
8484
|
+
return { status: 503, body: { error: "shell modal resolution not wired" } };
|
|
8485
|
+
}
|
|
8486
|
+
if (choice !== "run_once" && choice !== "always_allow" && choice !== "deny") {
|
|
8487
|
+
return {
|
|
8488
|
+
status: 400,
|
|
8489
|
+
body: { error: "shell choice must be run_once / always_allow / deny" }
|
|
8490
|
+
};
|
|
8491
|
+
}
|
|
8492
|
+
ctx.resolveShellConfirm(choice);
|
|
8493
|
+
return { status: 200, body: { resolved: true } };
|
|
8494
|
+
}
|
|
8495
|
+
if (kind === "choice") {
|
|
8496
|
+
if (!ctx.resolveChoiceConfirm) {
|
|
8497
|
+
return { status: 503, body: { error: "choice modal resolution not wired" } };
|
|
8498
|
+
}
|
|
8499
|
+
const c = choice;
|
|
8500
|
+
if (!c || typeof c !== "object") {
|
|
8501
|
+
return { status: 400, body: { error: "choice must be an object with a kind field" } };
|
|
8502
|
+
}
|
|
8503
|
+
if (c.kind === "pick" && typeof c.optionId === "string") {
|
|
8504
|
+
ctx.resolveChoiceConfirm({ kind: "pick", optionId: c.optionId });
|
|
8505
|
+
return { status: 200, body: { resolved: true } };
|
|
8506
|
+
}
|
|
8507
|
+
if (c.kind === "custom" && typeof c.text === "string") {
|
|
8508
|
+
ctx.resolveChoiceConfirm({ kind: "custom", text: c.text });
|
|
8509
|
+
return { status: 200, body: { resolved: true } };
|
|
8510
|
+
}
|
|
8511
|
+
if (c.kind === "cancel") {
|
|
8512
|
+
ctx.resolveChoiceConfirm({ kind: "cancel" });
|
|
8513
|
+
return { status: 200, body: { resolved: true } };
|
|
8514
|
+
}
|
|
8515
|
+
return { status: 400, body: { error: "unknown choice resolution shape" } };
|
|
8516
|
+
}
|
|
8517
|
+
if (kind === "plan") {
|
|
8518
|
+
if (!ctx.resolvePlanConfirm) {
|
|
8519
|
+
return { status: 503, body: { error: "plan modal resolution not wired" } };
|
|
8520
|
+
}
|
|
8521
|
+
if (choice !== "approve" && choice !== "refine" && choice !== "cancel") {
|
|
8522
|
+
return { status: 400, body: { error: "plan choice must be approve / refine / cancel" } };
|
|
8523
|
+
}
|
|
8524
|
+
ctx.resolvePlanConfirm(choice, typeof text === "string" && text.trim() ? text : void 0);
|
|
8525
|
+
return { status: 200, body: { resolved: true } };
|
|
8526
|
+
}
|
|
8527
|
+
if (kind === "edit-review") {
|
|
8528
|
+
if (!ctx.resolveEditReview) {
|
|
8529
|
+
return { status: 503, body: { error: "edit-review modal resolution not wired" } };
|
|
8530
|
+
}
|
|
8531
|
+
if (choice !== "apply" && choice !== "reject" && choice !== "apply-rest-of-turn" && choice !== "flip-to-auto") {
|
|
8532
|
+
return { status: 400, body: { error: "edit-review choice invalid" } };
|
|
8533
|
+
}
|
|
8534
|
+
ctx.resolveEditReview(choice);
|
|
8535
|
+
return { status: 200, body: { resolved: true } };
|
|
8536
|
+
}
|
|
8537
|
+
return { status: 400, body: { error: `unknown modal kind: ${String(kind)}` } };
|
|
8538
|
+
}
|
|
8539
|
+
return { status: 405, body: { error: `method ${method} not supported on this path` } };
|
|
8540
|
+
}
|
|
8541
|
+
|
|
8542
|
+
// src/server/api/overview.ts
|
|
8543
|
+
async function handleOverview(method, _rest, _body, ctx) {
|
|
8544
|
+
if (method !== "GET") {
|
|
8545
|
+
return { status: 405, body: { error: "GET only" } };
|
|
8546
|
+
}
|
|
8547
|
+
const cfg = readConfig(ctx.configPath);
|
|
8548
|
+
const overview = {
|
|
8549
|
+
version: VERSION,
|
|
8550
|
+
mode: ctx.mode,
|
|
8551
|
+
latestVersion: ctx.getLatestVersion?.() ?? null,
|
|
8552
|
+
session: ctx.getSessionName?.() ?? null,
|
|
8553
|
+
cwd: ctx.getCurrentCwd?.() ?? null,
|
|
8554
|
+
model: ctx.loop?.model ?? null,
|
|
8555
|
+
editMode: ctx.getEditMode?.() ?? null,
|
|
8556
|
+
planMode: ctx.getPlanMode?.() ?? null,
|
|
8557
|
+
pendingEdits: ctx.getPendingEditCount?.() ?? null,
|
|
8558
|
+
mcpServerCount: ctx.mcpServers?.length ?? null,
|
|
8559
|
+
toolCount: ctx.tools ? ctx.tools.size : null,
|
|
8560
|
+
preset: cfg.preset ?? "auto",
|
|
8561
|
+
reasoningEffort: cfg.reasoningEffort ?? "max",
|
|
8562
|
+
stats: ctx.getStats?.() ?? null
|
|
8563
|
+
};
|
|
8564
|
+
return { status: 200, body: overview };
|
|
8565
|
+
}
|
|
8566
|
+
|
|
8567
|
+
// src/server/api/permissions.ts
|
|
8568
|
+
function parseBody7(raw) {
|
|
8569
|
+
if (!raw) return {};
|
|
8570
|
+
try {
|
|
8571
|
+
const parsed = JSON.parse(raw);
|
|
8572
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8573
|
+
} catch {
|
|
8574
|
+
return {};
|
|
8575
|
+
}
|
|
8576
|
+
}
|
|
8577
|
+
async function handlePermissions(method, rest, body, ctx) {
|
|
8578
|
+
if (method === "GET" && rest.length === 0) {
|
|
8579
|
+
const cwd3 = ctx.getCurrentCwd?.();
|
|
8580
|
+
return {
|
|
8581
|
+
status: 200,
|
|
8582
|
+
body: {
|
|
8583
|
+
currentCwd: cwd3 ?? null,
|
|
8584
|
+
editMode: ctx.getEditMode?.() ?? null,
|
|
8585
|
+
builtin: [...BUILTIN_ALLOWLIST],
|
|
8586
|
+
project: cwd3 ? loadProjectShellAllowed(cwd3, ctx.configPath) : []
|
|
8587
|
+
}
|
|
8588
|
+
};
|
|
8589
|
+
}
|
|
8590
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
8591
|
+
if (!cwd2) {
|
|
8592
|
+
return {
|
|
8593
|
+
status: 503,
|
|
8594
|
+
body: {
|
|
8595
|
+
error: "no active project \u2014 mutations require an attached dashboard session (run `/dashboard` from inside `reasonix code`)."
|
|
8596
|
+
}
|
|
8597
|
+
};
|
|
8598
|
+
}
|
|
8599
|
+
if (method === "POST" && rest.length === 0) {
|
|
8600
|
+
const { prefix } = parseBody7(body);
|
|
8601
|
+
if (typeof prefix !== "string" || !prefix.trim()) {
|
|
8602
|
+
return { status: 400, body: { error: "prefix (string) required" } };
|
|
8603
|
+
}
|
|
8604
|
+
const trimmed = prefix.trim();
|
|
8605
|
+
if (BUILTIN_ALLOWLIST.includes(trimmed)) {
|
|
8606
|
+
return {
|
|
8607
|
+
status: 409,
|
|
8608
|
+
body: {
|
|
8609
|
+
error: `\`${trimmed}\` is already in the builtin allowlist \u2014 no project entry needed.`
|
|
8610
|
+
}
|
|
8611
|
+
};
|
|
8612
|
+
}
|
|
8613
|
+
const before = loadProjectShellAllowed(cwd2, ctx.configPath);
|
|
8614
|
+
if (before.includes(trimmed)) {
|
|
8615
|
+
return { status: 200, body: { added: false, prefix: trimmed, alreadyPresent: true } };
|
|
8616
|
+
}
|
|
8617
|
+
addProjectShellAllowed(cwd2, trimmed, ctx.configPath);
|
|
8618
|
+
ctx.audit?.({
|
|
8619
|
+
ts: Date.now(),
|
|
8620
|
+
action: "add-allowlist",
|
|
8621
|
+
payload: { prefix: trimmed, project: cwd2 }
|
|
8622
|
+
});
|
|
8623
|
+
return { status: 200, body: { added: true, prefix: trimmed } };
|
|
8624
|
+
}
|
|
8625
|
+
if (method === "DELETE" && rest.length === 0) {
|
|
8626
|
+
const { prefix } = parseBody7(body);
|
|
8627
|
+
if (typeof prefix !== "string" || !prefix.trim()) {
|
|
8628
|
+
return { status: 400, body: { error: "prefix (string) required" } };
|
|
8629
|
+
}
|
|
8630
|
+
const trimmed = prefix.trim();
|
|
8631
|
+
if (BUILTIN_ALLOWLIST.includes(trimmed)) {
|
|
8632
|
+
return {
|
|
8633
|
+
status: 409,
|
|
8634
|
+
body: {
|
|
8635
|
+
error: `\`${trimmed}\` is in the builtin allowlist (read-only); builtin entries can't be removed at runtime.`
|
|
8636
|
+
}
|
|
8637
|
+
};
|
|
8638
|
+
}
|
|
8639
|
+
const removed = removeProjectShellAllowed(cwd2, trimmed, ctx.configPath);
|
|
8640
|
+
if (removed) {
|
|
8641
|
+
ctx.audit?.({
|
|
8642
|
+
ts: Date.now(),
|
|
8643
|
+
action: "remove-allowlist",
|
|
8644
|
+
payload: { prefix: trimmed, project: cwd2 }
|
|
8645
|
+
});
|
|
8646
|
+
}
|
|
8647
|
+
return { status: 200, body: { removed, prefix: trimmed } };
|
|
8648
|
+
}
|
|
8649
|
+
if (method === "POST" && rest[0] === "clear") {
|
|
8650
|
+
const { confirm: confirm2 } = parseBody7(body);
|
|
8651
|
+
if (confirm2 !== true) {
|
|
8652
|
+
return {
|
|
8653
|
+
status: 400,
|
|
8654
|
+
body: {
|
|
8655
|
+
error: "clear requires { confirm: true } in the body \u2014 guards against accidental wipe."
|
|
8656
|
+
}
|
|
8657
|
+
};
|
|
8658
|
+
}
|
|
8659
|
+
const dropped = clearProjectShellAllowed(cwd2, ctx.configPath);
|
|
8660
|
+
if (dropped > 0) {
|
|
8661
|
+
ctx.audit?.({
|
|
8662
|
+
ts: Date.now(),
|
|
8663
|
+
action: "clear-allowlist",
|
|
8664
|
+
payload: { dropped, project: cwd2 }
|
|
8665
|
+
});
|
|
8666
|
+
}
|
|
8667
|
+
return { status: 200, body: { dropped } };
|
|
8668
|
+
}
|
|
8669
|
+
return { status: 405, body: { error: `method ${method} not supported on this path` } };
|
|
8670
|
+
}
|
|
8671
|
+
|
|
8672
|
+
// src/server/api/plans.ts
|
|
8673
|
+
async function handlePlans(method, _rest, _body, _ctx) {
|
|
8674
|
+
if (method !== "GET") {
|
|
8675
|
+
return { status: 405, body: { error: "GET only" } };
|
|
8676
|
+
}
|
|
8677
|
+
const out = [];
|
|
8678
|
+
for (const session of listSessions()) {
|
|
8679
|
+
const archives = listPlanArchives(session.name);
|
|
8680
|
+
for (const a of archives) {
|
|
8681
|
+
const total = a.steps.length;
|
|
8682
|
+
const done = a.completedStepIds.length;
|
|
8683
|
+
const row2 = {
|
|
8684
|
+
session: session.name,
|
|
8685
|
+
path: a.path,
|
|
8686
|
+
completedAt: a.completedAt,
|
|
8687
|
+
totalSteps: total,
|
|
8688
|
+
completedSteps: done,
|
|
8689
|
+
completionRatio: total > 0 ? done / total : 0,
|
|
8690
|
+
steps: a.steps,
|
|
8691
|
+
completedStepIds: a.completedStepIds
|
|
8692
|
+
};
|
|
8693
|
+
if (a.summary) row2.summary = a.summary;
|
|
8694
|
+
out.push(row2);
|
|
8695
|
+
}
|
|
8696
|
+
}
|
|
8697
|
+
out.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
|
|
8698
|
+
return { status: 200, body: { plans: out } };
|
|
8699
|
+
}
|
|
8700
|
+
|
|
8701
|
+
// src/server/api/sessions.ts
|
|
8702
|
+
import { existsSync as existsSync15, readFileSync as readFileSync17 } from "fs";
|
|
8703
|
+
function parseTranscript2(path5, maxBytes = 4 * 1024 * 1024) {
|
|
8704
|
+
let raw;
|
|
8705
|
+
try {
|
|
8706
|
+
raw = readFileSync17(path5, "utf8");
|
|
8707
|
+
} catch {
|
|
8708
|
+
return [];
|
|
8709
|
+
}
|
|
8710
|
+
if (raw.length > maxBytes) raw = raw.slice(0, maxBytes);
|
|
8711
|
+
const out = [];
|
|
8712
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
8713
|
+
if (!line.trim()) continue;
|
|
8714
|
+
try {
|
|
8715
|
+
const rec = JSON.parse(line);
|
|
8716
|
+
const role = typeof rec.role === "string" ? rec.role : "unknown";
|
|
8717
|
+
const msg = { role };
|
|
8718
|
+
if (typeof rec.content === "string") msg.content = rec.content;
|
|
8719
|
+
else if (rec.content !== void 0) msg.content = JSON.stringify(rec.content);
|
|
8720
|
+
if (typeof rec.tool_name === "string") msg.toolName = rec.tool_name;
|
|
8721
|
+
if (typeof rec.toolName === "string") msg.toolName = rec.toolName;
|
|
8722
|
+
out.push(msg);
|
|
8723
|
+
} catch {
|
|
8724
|
+
}
|
|
8725
|
+
}
|
|
8726
|
+
return out;
|
|
8727
|
+
}
|
|
8728
|
+
async function handleSessions(method, rest, _body, _ctx) {
|
|
8729
|
+
if (method !== "GET") {
|
|
8730
|
+
return { status: 405, body: { error: "GET only" } };
|
|
8731
|
+
}
|
|
8732
|
+
if (rest.length === 0) {
|
|
8733
|
+
const sessions2 = listSessions();
|
|
8734
|
+
return {
|
|
8735
|
+
status: 200,
|
|
8736
|
+
body: {
|
|
8737
|
+
sessions: sessions2.map((s) => ({
|
|
8738
|
+
name: s.name,
|
|
8739
|
+
path: s.path,
|
|
8740
|
+
size: s.size,
|
|
8741
|
+
messageCount: s.messageCount,
|
|
8742
|
+
mtime: s.mtime.getTime()
|
|
8743
|
+
}))
|
|
8744
|
+
}
|
|
8745
|
+
};
|
|
8746
|
+
}
|
|
8747
|
+
const name = decodeURIComponent(rest[0]);
|
|
8748
|
+
const path5 = sessionPath(name);
|
|
8749
|
+
if (!existsSync15(path5)) {
|
|
8750
|
+
return { status: 404, body: { error: `no such session: ${name}` } };
|
|
8751
|
+
}
|
|
8752
|
+
const messages = parseTranscript2(path5);
|
|
8753
|
+
return {
|
|
8754
|
+
status: 200,
|
|
8755
|
+
body: {
|
|
8756
|
+
name,
|
|
8757
|
+
path: path5,
|
|
8758
|
+
messages,
|
|
8759
|
+
messageCount: messages.length
|
|
8760
|
+
}
|
|
8761
|
+
};
|
|
8762
|
+
}
|
|
8763
|
+
|
|
8764
|
+
// src/server/api/settings.ts
|
|
8765
|
+
function parseBody8(raw) {
|
|
8766
|
+
if (!raw) return {};
|
|
8767
|
+
try {
|
|
8768
|
+
const parsed = JSON.parse(raw);
|
|
8769
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8770
|
+
} catch {
|
|
8771
|
+
return {};
|
|
8772
|
+
}
|
|
8773
|
+
}
|
|
8774
|
+
var VALID_PRESETS = /* @__PURE__ */ new Set(["auto", "flash", "pro", "fast", "smart", "max"]);
|
|
8775
|
+
var VALID_EFFORTS = /* @__PURE__ */ new Set(["high", "max"]);
|
|
8776
|
+
async function handleSettings(method, _rest, body, ctx) {
|
|
8777
|
+
if (method === "GET") {
|
|
8778
|
+
const cfg = readConfig(ctx.configPath);
|
|
8779
|
+
return {
|
|
8780
|
+
status: 200,
|
|
8781
|
+
body: {
|
|
8782
|
+
apiKey: cfg.apiKey ? redactKey(cfg.apiKey) : null,
|
|
8783
|
+
apiKeySet: Boolean(cfg.apiKey),
|
|
8784
|
+
baseUrl: cfg.baseUrl ?? null,
|
|
8785
|
+
preset: cfg.preset ?? "auto",
|
|
8786
|
+
reasoningEffort: cfg.reasoningEffort ?? "max",
|
|
8787
|
+
search: cfg.search !== false,
|
|
8788
|
+
editMode: cfg.editMode ?? "review",
|
|
8789
|
+
session: cfg.session ?? null,
|
|
8790
|
+
model: ctx.loop?.model ?? null,
|
|
8791
|
+
// Hint to the SPA which fields require restart.
|
|
8792
|
+
appliesAt: {
|
|
8793
|
+
apiKey: "next-session",
|
|
8794
|
+
baseUrl: "next-session",
|
|
8795
|
+
preset: "next-session",
|
|
8796
|
+
reasoningEffort: "next-turn",
|
|
8797
|
+
search: "next-session"
|
|
8798
|
+
}
|
|
8799
|
+
}
|
|
8800
|
+
};
|
|
8801
|
+
}
|
|
8802
|
+
if (method === "POST") {
|
|
8803
|
+
const fields = parseBody8(body);
|
|
8804
|
+
const cfg = readConfig(ctx.configPath);
|
|
8805
|
+
const changed = [];
|
|
8806
|
+
if (fields.apiKey !== void 0) {
|
|
8807
|
+
if (typeof fields.apiKey !== "string" || !isPlausibleKey(fields.apiKey)) {
|
|
8808
|
+
return { status: 400, body: { error: "apiKey must be a plausible sk- token" } };
|
|
8809
|
+
}
|
|
8810
|
+
saveApiKey(fields.apiKey, ctx.configPath);
|
|
8811
|
+
changed.push("apiKey");
|
|
8812
|
+
}
|
|
8813
|
+
if (fields.baseUrl !== void 0) {
|
|
8814
|
+
if (typeof fields.baseUrl !== "string" || !fields.baseUrl.trim()) {
|
|
8815
|
+
return { status: 400, body: { error: "baseUrl must be a non-empty string" } };
|
|
8816
|
+
}
|
|
8817
|
+
cfg.baseUrl = fields.baseUrl.trim();
|
|
8818
|
+
writeConfig(cfg, ctx.configPath);
|
|
8819
|
+
changed.push("baseUrl");
|
|
8820
|
+
}
|
|
8821
|
+
if (fields.preset !== void 0) {
|
|
8822
|
+
if (typeof fields.preset !== "string" || !VALID_PRESETS.has(fields.preset)) {
|
|
8823
|
+
return { status: 400, body: { error: "preset must be auto | flash | pro" } };
|
|
8824
|
+
}
|
|
8825
|
+
cfg.preset = fields.preset;
|
|
8826
|
+
writeConfig(cfg, ctx.configPath);
|
|
8827
|
+
ctx.applyPresetLive?.(fields.preset);
|
|
8828
|
+
changed.push("preset");
|
|
8829
|
+
}
|
|
8830
|
+
if (fields.reasoningEffort !== void 0) {
|
|
8831
|
+
if (typeof fields.reasoningEffort !== "string" || !VALID_EFFORTS.has(fields.reasoningEffort)) {
|
|
8832
|
+
return { status: 400, body: { error: "reasoningEffort must be high | max" } };
|
|
8833
|
+
}
|
|
8834
|
+
saveReasoningEffort(fields.reasoningEffort, ctx.configPath);
|
|
8835
|
+
ctx.applyEffortLive?.(fields.reasoningEffort);
|
|
8836
|
+
changed.push("reasoningEffort");
|
|
8837
|
+
}
|
|
8838
|
+
if (fields.search !== void 0) {
|
|
8839
|
+
if (typeof fields.search !== "boolean") {
|
|
8840
|
+
return { status: 400, body: { error: "search must be a boolean" } };
|
|
8841
|
+
}
|
|
8842
|
+
cfg.search = fields.search;
|
|
8843
|
+
writeConfig(cfg, ctx.configPath);
|
|
8844
|
+
changed.push("search");
|
|
8845
|
+
}
|
|
8846
|
+
if (changed.length > 0) {
|
|
8847
|
+
ctx.audit?.({ ts: Date.now(), action: "set-settings", payload: { fields: changed } });
|
|
8848
|
+
}
|
|
8849
|
+
return { status: 200, body: { changed } };
|
|
8850
|
+
}
|
|
8851
|
+
return { status: 405, body: { error: "GET or POST only" } };
|
|
8852
|
+
}
|
|
8853
|
+
|
|
8854
|
+
// src/server/api/skills.ts
|
|
8855
|
+
import {
|
|
8856
|
+
existsSync as existsSync16,
|
|
8857
|
+
mkdirSync as mkdirSync11,
|
|
8858
|
+
readFileSync as readFileSync18,
|
|
8859
|
+
readdirSync as readdirSync6,
|
|
8860
|
+
rmSync,
|
|
8861
|
+
statSync as statSync9,
|
|
8862
|
+
writeFileSync as writeFileSync10
|
|
8863
|
+
} from "fs";
|
|
8864
|
+
import { homedir as homedir8 } from "os";
|
|
8865
|
+
import { dirname as dirname14, join as join14 } from "path";
|
|
8866
|
+
function parseBody9(raw) {
|
|
8867
|
+
if (!raw) return {};
|
|
8868
|
+
try {
|
|
8869
|
+
const parsed = JSON.parse(raw);
|
|
8870
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
8871
|
+
} catch {
|
|
8872
|
+
return {};
|
|
8873
|
+
}
|
|
8874
|
+
}
|
|
8875
|
+
var SAFE_NAME2 = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,63}$/;
|
|
8876
|
+
function globalSkillsDir() {
|
|
8877
|
+
return join14(homedir8(), ".reasonix", SKILLS_DIRNAME);
|
|
8878
|
+
}
|
|
8879
|
+
function projectSkillsDir(rootDir) {
|
|
8880
|
+
return join14(rootDir, ".reasonix", SKILLS_DIRNAME);
|
|
8881
|
+
}
|
|
8882
|
+
function parseFrontmatterDescription(raw) {
|
|
8883
|
+
const lines = raw.split(/\r?\n/);
|
|
8884
|
+
if (lines[0] !== "---") return void 0;
|
|
8885
|
+
for (let i = 1; i < lines.length; i++) {
|
|
8886
|
+
if (lines[i] === "---") break;
|
|
8887
|
+
const m = lines[i].match(/^description:\s*(.*)$/);
|
|
8888
|
+
if (m) return m[1].trim();
|
|
8889
|
+
}
|
|
8890
|
+
return void 0;
|
|
8891
|
+
}
|
|
8892
|
+
function listSkills(dir, scope) {
|
|
8893
|
+
if (!existsSync16(dir)) return [];
|
|
8894
|
+
const out = [];
|
|
8895
|
+
try {
|
|
8896
|
+
for (const entry of readdirSync6(dir)) {
|
|
8897
|
+
if (!SAFE_NAME2.test(entry)) continue;
|
|
8898
|
+
const skillPath = join14(dir, entry, SKILL_FILE);
|
|
8899
|
+
if (!existsSync16(skillPath)) continue;
|
|
8900
|
+
try {
|
|
8901
|
+
const stat = statSync9(skillPath);
|
|
8902
|
+
const raw = readFileSync18(skillPath, "utf8");
|
|
8903
|
+
const item = {
|
|
8904
|
+
name: entry,
|
|
8905
|
+
scope,
|
|
8906
|
+
path: skillPath,
|
|
8907
|
+
size: stat.size,
|
|
8908
|
+
mtime: stat.mtime.getTime()
|
|
8909
|
+
};
|
|
8910
|
+
const desc = parseFrontmatterDescription(raw);
|
|
8911
|
+
if (desc) item.description = desc;
|
|
8912
|
+
out.push(item);
|
|
8913
|
+
} catch {
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
} catch {
|
|
8917
|
+
}
|
|
8918
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
8919
|
+
}
|
|
8920
|
+
async function handleSkills(method, rest, body, ctx) {
|
|
8921
|
+
const cwd2 = ctx.getCurrentCwd?.();
|
|
8922
|
+
if (method === "GET" && rest.length === 0) {
|
|
8923
|
+
return {
|
|
8924
|
+
status: 200,
|
|
8925
|
+
body: {
|
|
8926
|
+
global: listSkills(globalSkillsDir(), "global"),
|
|
8927
|
+
project: cwd2 ? listSkills(projectSkillsDir(cwd2), "project") : [],
|
|
8928
|
+
builtin: [
|
|
8929
|
+
{ name: "explore", scope: "builtin", description: "subagent \u2014 broad codebase survey" },
|
|
8930
|
+
{
|
|
8931
|
+
name: "research",
|
|
8932
|
+
scope: "builtin",
|
|
8933
|
+
description: "subagent \u2014 deep web + repo research"
|
|
8934
|
+
}
|
|
8935
|
+
],
|
|
8936
|
+
paths: {
|
|
8937
|
+
global: globalSkillsDir(),
|
|
8938
|
+
project: cwd2 ? projectSkillsDir(cwd2) : null
|
|
8939
|
+
}
|
|
8940
|
+
}
|
|
8941
|
+
};
|
|
8942
|
+
}
|
|
8943
|
+
const [scope, ...nameParts] = rest;
|
|
8944
|
+
const name = nameParts.join("/");
|
|
8945
|
+
if (!scope || !name || !SAFE_NAME2.test(name)) {
|
|
8946
|
+
return { status: 400, body: { error: "expected /api/skills/<scope>/<name>" } };
|
|
8947
|
+
}
|
|
8948
|
+
if (scope !== "project" && scope !== "global") {
|
|
8949
|
+
return {
|
|
8950
|
+
status: 400,
|
|
8951
|
+
body: { error: "scope must be project | global (builtin is read-only)" }
|
|
8952
|
+
};
|
|
8953
|
+
}
|
|
8954
|
+
let dir;
|
|
8955
|
+
if (scope === "project") {
|
|
8956
|
+
if (!cwd2) {
|
|
8957
|
+
return {
|
|
8958
|
+
status: 503,
|
|
8959
|
+
body: { error: "no active project \u2014 open `/dashboard` from `reasonix code`" }
|
|
8960
|
+
};
|
|
8961
|
+
}
|
|
8962
|
+
dir = projectSkillsDir(cwd2);
|
|
8963
|
+
} else {
|
|
8964
|
+
dir = globalSkillsDir();
|
|
8965
|
+
}
|
|
8966
|
+
const skillPath = join14(dir, name, SKILL_FILE);
|
|
8967
|
+
if (method === "GET") {
|
|
8968
|
+
if (!existsSync16(skillPath)) return { status: 404, body: { error: "skill not found" } };
|
|
8969
|
+
return { status: 200, body: { path: skillPath, body: readFileSync18(skillPath, "utf8") } };
|
|
8970
|
+
}
|
|
8971
|
+
if (method === "POST") {
|
|
8972
|
+
const { body: contents } = parseBody9(body);
|
|
8973
|
+
if (typeof contents !== "string") {
|
|
8974
|
+
return { status: 400, body: { error: "body (string) required" } };
|
|
8975
|
+
}
|
|
8976
|
+
mkdirSync11(dirname14(skillPath), { recursive: true });
|
|
8977
|
+
writeFileSync10(skillPath, contents, "utf8");
|
|
8978
|
+
ctx.audit?.({
|
|
8979
|
+
ts: Date.now(),
|
|
8980
|
+
action: "save-skill",
|
|
8981
|
+
payload: { scope, name, path: skillPath }
|
|
8982
|
+
});
|
|
8983
|
+
return { status: 200, body: { saved: true, path: skillPath } };
|
|
8984
|
+
}
|
|
8985
|
+
if (method === "DELETE") {
|
|
8986
|
+
if (!existsSync16(skillPath)) return { status: 404, body: { error: "skill not found" } };
|
|
8987
|
+
rmSync(dirname14(skillPath), { recursive: true, force: true });
|
|
8988
|
+
ctx.audit?.({ ts: Date.now(), action: "delete-skill", payload: { scope, name } });
|
|
8989
|
+
return { status: 200, body: { deleted: true } };
|
|
8990
|
+
}
|
|
8991
|
+
return { status: 405, body: { error: `method ${method} not supported` } };
|
|
8992
|
+
}
|
|
8993
|
+
|
|
8994
|
+
// src/server/api/submit.ts
|
|
8995
|
+
function parseBody10(raw) {
|
|
8996
|
+
if (!raw) return {};
|
|
8997
|
+
try {
|
|
8998
|
+
const parsed = JSON.parse(raw);
|
|
8999
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
9000
|
+
} catch {
|
|
9001
|
+
return {};
|
|
9002
|
+
}
|
|
9003
|
+
}
|
|
9004
|
+
async function handleSubmit(method, _rest, body, ctx) {
|
|
9005
|
+
if (method !== "POST") {
|
|
9006
|
+
return { status: 405, body: { error: "POST only" } };
|
|
9007
|
+
}
|
|
9008
|
+
if (!ctx.submitPrompt) {
|
|
9009
|
+
return {
|
|
9010
|
+
status: 503,
|
|
9011
|
+
body: {
|
|
9012
|
+
error: "submit requires an attached dashboard session \u2014 open `/dashboard` from inside `reasonix code` or `reasonix chat`."
|
|
9013
|
+
}
|
|
9014
|
+
};
|
|
9015
|
+
}
|
|
9016
|
+
const { prompt } = parseBody10(body);
|
|
9017
|
+
if (typeof prompt !== "string" || !prompt.trim()) {
|
|
9018
|
+
return { status: 400, body: { error: "prompt (non-empty string) required" } };
|
|
9019
|
+
}
|
|
9020
|
+
const result = ctx.submitPrompt(prompt);
|
|
9021
|
+
if (!result.accepted) {
|
|
9022
|
+
return {
|
|
9023
|
+
status: 409,
|
|
9024
|
+
body: { accepted: false, reason: result.reason ?? "loop is busy" }
|
|
9025
|
+
};
|
|
9026
|
+
}
|
|
9027
|
+
ctx.audit?.({
|
|
9028
|
+
ts: Date.now(),
|
|
9029
|
+
action: "submit-prompt",
|
|
9030
|
+
payload: { length: prompt.length }
|
|
9031
|
+
});
|
|
9032
|
+
return { status: 202, body: { accepted: true } };
|
|
9033
|
+
}
|
|
9034
|
+
|
|
9035
|
+
// src/server/api/tools.ts
|
|
9036
|
+
async function handleTools(method, _rest, _body, ctx) {
|
|
9037
|
+
if (method !== "GET") {
|
|
9038
|
+
return { status: 405, body: { error: "GET only" } };
|
|
9039
|
+
}
|
|
9040
|
+
if (!ctx.tools) {
|
|
9041
|
+
return {
|
|
9042
|
+
status: 503,
|
|
9043
|
+
body: {
|
|
9044
|
+
error: "live tools view requires an attached session \u2014 run `/dashboard` from inside `reasonix code` instead of standalone `reasonix dashboard`.",
|
|
9045
|
+
available: false
|
|
9046
|
+
}
|
|
9047
|
+
};
|
|
9048
|
+
}
|
|
9049
|
+
const specs = ctx.tools.specs();
|
|
9050
|
+
const items = specs.map((s) => {
|
|
9051
|
+
const def = ctx.tools.get(s.function.name);
|
|
9052
|
+
return {
|
|
9053
|
+
name: s.function.name,
|
|
9054
|
+
description: s.function.description,
|
|
9055
|
+
schema: s.function.parameters,
|
|
9056
|
+
readOnly: Boolean(def?.readOnly),
|
|
9057
|
+
flattened: ctx.tools.wasFlattened(s.function.name)
|
|
9058
|
+
};
|
|
9059
|
+
});
|
|
9060
|
+
return {
|
|
9061
|
+
status: 200,
|
|
9062
|
+
body: {
|
|
9063
|
+
planMode: ctx.tools.planMode,
|
|
9064
|
+
total: items.length,
|
|
9065
|
+
tools: items
|
|
9066
|
+
}
|
|
9067
|
+
};
|
|
9068
|
+
}
|
|
9069
|
+
|
|
9070
|
+
// src/server/api/usage.ts
|
|
9071
|
+
function dayKey(ts) {
|
|
9072
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
9073
|
+
}
|
|
9074
|
+
function buildSeries(records) {
|
|
9075
|
+
const map = /* @__PURE__ */ new Map();
|
|
9076
|
+
for (const r of records) {
|
|
9077
|
+
const day = dayKey(r.ts);
|
|
9078
|
+
let b = map.get(day);
|
|
9079
|
+
if (!b) {
|
|
9080
|
+
b = {
|
|
9081
|
+
day,
|
|
9082
|
+
turns: 0,
|
|
9083
|
+
promptTokens: 0,
|
|
9084
|
+
completionTokens: 0,
|
|
9085
|
+
cacheHitTokens: 0,
|
|
9086
|
+
cacheMissTokens: 0,
|
|
9087
|
+
costUsd: 0,
|
|
9088
|
+
cacheSavingsUsd: 0
|
|
9089
|
+
};
|
|
9090
|
+
map.set(day, b);
|
|
9091
|
+
}
|
|
9092
|
+
b.turns += 1;
|
|
9093
|
+
b.promptTokens += r.promptTokens;
|
|
9094
|
+
b.completionTokens += r.completionTokens;
|
|
9095
|
+
b.cacheHitTokens += r.cacheHitTokens;
|
|
9096
|
+
b.cacheMissTokens += r.cacheMissTokens;
|
|
9097
|
+
b.costUsd += r.costUsd;
|
|
9098
|
+
b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);
|
|
9099
|
+
}
|
|
9100
|
+
return Array.from(map.values()).sort((a, b) => a.day.localeCompare(b.day));
|
|
9101
|
+
}
|
|
9102
|
+
async function handleUsage(method, rest, _body, ctx) {
|
|
9103
|
+
if (method !== "GET") {
|
|
9104
|
+
return { status: 405, body: { error: "GET only" } };
|
|
9105
|
+
}
|
|
9106
|
+
const records = readUsageLog(ctx.usageLogPath);
|
|
9107
|
+
if (rest[0] === "series") {
|
|
9108
|
+
return {
|
|
9109
|
+
status: 200,
|
|
9110
|
+
body: {
|
|
9111
|
+
days: buildSeries(records),
|
|
9112
|
+
recordCount: records.length
|
|
9113
|
+
}
|
|
9114
|
+
};
|
|
9115
|
+
}
|
|
9116
|
+
const agg = aggregateUsage(records);
|
|
9117
|
+
return {
|
|
9118
|
+
status: 200,
|
|
9119
|
+
body: {
|
|
9120
|
+
logPath: ctx.usageLogPath,
|
|
9121
|
+
logSize: formatLogSize(ctx.usageLogPath),
|
|
9122
|
+
recordCount: records.length,
|
|
9123
|
+
buckets: agg.buckets,
|
|
9124
|
+
byModel: agg.byModel,
|
|
9125
|
+
bySession: agg.bySession,
|
|
9126
|
+
firstSeen: agg.firstSeen,
|
|
9127
|
+
lastSeen: agg.lastSeen,
|
|
9128
|
+
subagents: agg.subagents ?? null
|
|
9129
|
+
}
|
|
9130
|
+
};
|
|
9131
|
+
}
|
|
9132
|
+
|
|
9133
|
+
// src/server/router.ts
|
|
9134
|
+
async function handleApi(pathTail, method, body, ctx) {
|
|
9135
|
+
const normalized = pathTail.replace(/\/+$/, "");
|
|
9136
|
+
const [head, ...rest] = normalized.split("/");
|
|
9137
|
+
try {
|
|
9138
|
+
switch (head) {
|
|
9139
|
+
case "overview":
|
|
9140
|
+
return await handleOverview(method, rest, body, ctx);
|
|
9141
|
+
case "usage":
|
|
9142
|
+
return await handleUsage(method, rest, body, ctx);
|
|
9143
|
+
case "tools":
|
|
9144
|
+
return await handleTools(method, rest, body, ctx);
|
|
9145
|
+
case "permissions":
|
|
9146
|
+
return await handlePermissions(method, rest, body, ctx);
|
|
9147
|
+
case "messages":
|
|
9148
|
+
return await handleMessages(method, rest, body, ctx);
|
|
9149
|
+
case "submit":
|
|
9150
|
+
return await handleSubmit(method, rest, body, ctx);
|
|
9151
|
+
case "abort":
|
|
9152
|
+
return await handleAbort(method, rest, body, ctx);
|
|
9153
|
+
case "health":
|
|
9154
|
+
return await handleHealth(method, rest, body, ctx);
|
|
9155
|
+
case "sessions":
|
|
9156
|
+
return await handleSessions(method, rest, body, ctx);
|
|
9157
|
+
case "plans":
|
|
9158
|
+
return await handlePlans(method, rest, body, ctx);
|
|
9159
|
+
case "modal":
|
|
9160
|
+
return await handleModal(method, rest, body, ctx);
|
|
9161
|
+
case "edit-mode":
|
|
9162
|
+
return await handleEditMode(method, rest, body, ctx);
|
|
9163
|
+
case "settings":
|
|
9164
|
+
return await handleSettings(method, rest, body, ctx);
|
|
9165
|
+
case "hooks":
|
|
9166
|
+
return await handleHooks(method, rest, body, ctx);
|
|
9167
|
+
case "memory":
|
|
9168
|
+
return await handleMemory(method, rest, body, ctx);
|
|
9169
|
+
case "skills":
|
|
9170
|
+
return await handleSkills(method, rest, body, ctx);
|
|
9171
|
+
case "mcp":
|
|
9172
|
+
return await handleMcp(method, rest, body, ctx);
|
|
9173
|
+
case "files":
|
|
9174
|
+
return await handleFiles(method, rest, body, ctx);
|
|
9175
|
+
case "file":
|
|
9176
|
+
return await handleFile(method, rest, body, ctx);
|
|
9177
|
+
default:
|
|
9178
|
+
return { status: 404, body: { error: `no such endpoint: /${head}` } };
|
|
9179
|
+
}
|
|
9180
|
+
} catch (err) {
|
|
9181
|
+
return {
|
|
9182
|
+
status: 500,
|
|
9183
|
+
body: { error: `handler crashed: ${err.message}` }
|
|
9184
|
+
};
|
|
9185
|
+
}
|
|
9186
|
+
}
|
|
9187
|
+
|
|
9188
|
+
// src/server/index.ts
|
|
9189
|
+
function mintToken() {
|
|
9190
|
+
return randomBytes(32).toString("hex");
|
|
9191
|
+
}
|
|
9192
|
+
function constantTimeEquals(a, b) {
|
|
9193
|
+
if (a.length !== b.length) return false;
|
|
9194
|
+
let mismatch = 0;
|
|
9195
|
+
for (let i = 0; i < a.length; i++) {
|
|
9196
|
+
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
9197
|
+
}
|
|
9198
|
+
return mismatch === 0;
|
|
9199
|
+
}
|
|
9200
|
+
function checkAuth(req, expectedToken, isMutation) {
|
|
9201
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
9202
|
+
const queryToken = url.searchParams.get("token") ?? "";
|
|
9203
|
+
const headerToken = typeof req.headers["x-reasonix-token"] === "string" ? req.headers["x-reasonix-token"] : "";
|
|
9204
|
+
if (isMutation) {
|
|
9205
|
+
if (!headerToken || !constantTimeEquals(headerToken, expectedToken)) {
|
|
9206
|
+
return {
|
|
9207
|
+
status: 403,
|
|
9208
|
+
body: JSON.stringify({
|
|
9209
|
+
error: "mutation requires X-Reasonix-Token header (CSRF defence \u2014 query token alone is rejected for POST/DELETE)."
|
|
9210
|
+
})
|
|
9211
|
+
};
|
|
9212
|
+
}
|
|
9213
|
+
return null;
|
|
9214
|
+
}
|
|
9215
|
+
if (queryToken && constantTimeEquals(queryToken, expectedToken) || headerToken && constantTimeEquals(headerToken, expectedToken)) {
|
|
9216
|
+
return null;
|
|
9217
|
+
}
|
|
9218
|
+
return {
|
|
9219
|
+
status: 401,
|
|
9220
|
+
body: JSON.stringify({ error: "missing or invalid token" })
|
|
9221
|
+
};
|
|
9222
|
+
}
|
|
9223
|
+
var MAX_BODY_BYTES = 256 * 1024;
|
|
9224
|
+
async function readBody(req) {
|
|
9225
|
+
let total = 0;
|
|
9226
|
+
const chunks = [];
|
|
9227
|
+
return new Promise((resolve13, reject) => {
|
|
9228
|
+
req.on("data", (chunk) => {
|
|
9229
|
+
total += chunk.length;
|
|
9230
|
+
if (total > MAX_BODY_BYTES) {
|
|
9231
|
+
reject(new Error(`body exceeds ${MAX_BODY_BYTES} bytes`));
|
|
9232
|
+
req.destroy();
|
|
9233
|
+
return;
|
|
9234
|
+
}
|
|
9235
|
+
chunks.push(chunk);
|
|
9236
|
+
});
|
|
9237
|
+
req.on("end", () => resolve13(Buffer.concat(chunks).toString("utf8")));
|
|
9238
|
+
req.on("error", reject);
|
|
9239
|
+
});
|
|
9240
|
+
}
|
|
9241
|
+
async function dispatch(req, res, ctx, expectedToken) {
|
|
9242
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
9243
|
+
const path5 = url.pathname;
|
|
9244
|
+
const method = (req.method ?? "GET").toUpperCase();
|
|
9245
|
+
const isMutation = method === "POST" || method === "DELETE" || method === "PUT";
|
|
9246
|
+
if (path5 === "/" || path5 === "/index.html") {
|
|
9247
|
+
const fail = checkAuth(req, expectedToken, false);
|
|
9248
|
+
if (fail) {
|
|
9249
|
+
res.writeHead(fail.status, { "content-type": "text/plain" });
|
|
9250
|
+
res.end("unauthorized \u2014 open the URL printed by /dashboard, including ?token=\u2026");
|
|
9251
|
+
return;
|
|
9252
|
+
}
|
|
9253
|
+
const html = renderIndexHtml(expectedToken, ctx.mode);
|
|
9254
|
+
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
|
|
9255
|
+
res.end(html);
|
|
9256
|
+
return;
|
|
9257
|
+
}
|
|
9258
|
+
if (path5.startsWith("/assets/")) {
|
|
9259
|
+
const fail = checkAuth(req, expectedToken, false);
|
|
9260
|
+
if (fail) {
|
|
9261
|
+
res.writeHead(fail.status);
|
|
9262
|
+
res.end();
|
|
9263
|
+
return;
|
|
9264
|
+
}
|
|
9265
|
+
const asset = serveAsset(path5.slice("/assets/".length));
|
|
9266
|
+
if (!asset) {
|
|
9267
|
+
res.writeHead(404);
|
|
9268
|
+
res.end("not found");
|
|
9269
|
+
return;
|
|
9270
|
+
}
|
|
9271
|
+
res.writeHead(200, { "content-type": asset.contentType });
|
|
9272
|
+
res.end(asset.body);
|
|
9273
|
+
return;
|
|
9274
|
+
}
|
|
9275
|
+
if (path5 === "/api/events") {
|
|
9276
|
+
const fail = checkAuth(req, expectedToken, false);
|
|
9277
|
+
if (fail) {
|
|
9278
|
+
res.writeHead(fail.status, { "content-type": "application/json" });
|
|
9279
|
+
res.end(fail.body);
|
|
9280
|
+
return;
|
|
9281
|
+
}
|
|
9282
|
+
handleEvents(req, res, ctx);
|
|
9283
|
+
return;
|
|
9284
|
+
}
|
|
9285
|
+
if (path5.startsWith("/api/")) {
|
|
9286
|
+
const fail = checkAuth(req, expectedToken, isMutation);
|
|
9287
|
+
if (fail) {
|
|
9288
|
+
res.writeHead(fail.status, { "content-type": "application/json" });
|
|
9289
|
+
res.end(fail.body);
|
|
9290
|
+
return;
|
|
9291
|
+
}
|
|
9292
|
+
let body = "";
|
|
9293
|
+
if (isMutation) {
|
|
9294
|
+
try {
|
|
9295
|
+
body = await readBody(req);
|
|
9296
|
+
} catch (err) {
|
|
9297
|
+
res.writeHead(413, { "content-type": "application/json" });
|
|
9298
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
9299
|
+
return;
|
|
9300
|
+
}
|
|
9301
|
+
}
|
|
9302
|
+
const result = await handleApi(path5.slice("/api/".length), method, body, ctx);
|
|
9303
|
+
res.writeHead(result.status, { "content-type": "application/json" });
|
|
9304
|
+
res.end(JSON.stringify(result.body));
|
|
9305
|
+
return;
|
|
9306
|
+
}
|
|
9307
|
+
res.writeHead(404, { "content-type": "text/plain" });
|
|
9308
|
+
res.end("not found");
|
|
9309
|
+
}
|
|
9310
|
+
function startDashboardServer(ctx, opts = {}) {
|
|
9311
|
+
const token = opts.token ?? mintToken();
|
|
9312
|
+
const host = opts.host ?? "127.0.0.1";
|
|
9313
|
+
const port = opts.port ?? 0;
|
|
9314
|
+
return new Promise((resolve13, reject) => {
|
|
9315
|
+
const server = createServer((req, res) => {
|
|
9316
|
+
dispatch(req, res, ctx, token).catch((err) => {
|
|
9317
|
+
if (!res.headersSent) {
|
|
9318
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
9319
|
+
}
|
|
9320
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
9321
|
+
});
|
|
9322
|
+
});
|
|
9323
|
+
server.on("error", reject);
|
|
9324
|
+
server.listen(port, host, () => {
|
|
9325
|
+
const addr = server.address();
|
|
9326
|
+
const finalPort = addr.port;
|
|
9327
|
+
const url = `http://${host}:${finalPort}/?token=${token}`;
|
|
9328
|
+
let closed = false;
|
|
9329
|
+
const close = () => new Promise((doneResolve) => {
|
|
9330
|
+
if (closed) return doneResolve();
|
|
9331
|
+
closed = true;
|
|
9332
|
+
server.close(() => doneResolve());
|
|
9333
|
+
setTimeout(() => server.closeAllConnections?.(), 1e3).unref();
|
|
9334
|
+
});
|
|
9335
|
+
resolve13({ url, token, port: finalPort, close });
|
|
9336
|
+
});
|
|
9337
|
+
});
|
|
7476
9338
|
}
|
|
7477
9339
|
|
|
7478
9340
|
// src/tools/skills.ts
|
|
@@ -7554,7 +9416,7 @@ ${skill2.body}${argsBlock}`;
|
|
|
7554
9416
|
}
|
|
7555
9417
|
|
|
7556
9418
|
// src/tools/workspace.ts
|
|
7557
|
-
import { existsSync as
|
|
9419
|
+
import { existsSync as existsSync17, statSync as statSync10 } from "fs";
|
|
7558
9420
|
import * as pathMod4 from "path";
|
|
7559
9421
|
var WorkspaceConfirmationError = class extends Error {
|
|
7560
9422
|
path;
|
|
@@ -7588,11 +9450,11 @@ function registerWorkspaceTool(registry) {
|
|
|
7588
9450
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
7589
9451
|
const expanded = args.path.startsWith("~") && home ? pathMod4.join(home, args.path.slice(1)) : args.path;
|
|
7590
9452
|
const abs = pathMod4.resolve(expanded);
|
|
7591
|
-
if (!
|
|
9453
|
+
if (!existsSync17(abs)) {
|
|
7592
9454
|
throw new Error(`change_workspace: path does not exist \u2014 ${abs}`);
|
|
7593
9455
|
}
|
|
7594
9456
|
try {
|
|
7595
|
-
if (!
|
|
9457
|
+
if (!statSync10(abs).isDirectory()) {
|
|
7596
9458
|
throw new Error(`change_workspace: not a directory \u2014 ${abs}`);
|
|
7597
9459
|
}
|
|
7598
9460
|
} catch (err) {
|
|
@@ -8401,8 +10263,8 @@ function RiskLegend() {
|
|
|
8401
10263
|
var PlanStepList = React8.memo(PlanStepListInner);
|
|
8402
10264
|
|
|
8403
10265
|
// src/cli/ui/markdown.tsx
|
|
8404
|
-
import { readFileSync as
|
|
8405
|
-
import { isAbsolute as
|
|
10266
|
+
import { readFileSync as readFileSync19, statSync as statSync11 } from "fs";
|
|
10267
|
+
import { isAbsolute as isAbsolute5, join as join16 } from "path";
|
|
8406
10268
|
import { Box as Box8, Text as Text7 } from "ink";
|
|
8407
10269
|
import React9 from "react";
|
|
8408
10270
|
var SUPERSCRIPT = {
|
|
@@ -8643,7 +10505,7 @@ function validateCitation(url, projectRoot) {
|
|
|
8643
10505
|
const parts = parseCitationUrl(url);
|
|
8644
10506
|
if (!parts || !parts.path) return { ok: false, reason: "empty path" };
|
|
8645
10507
|
const normalized = parts.path.replace(/^[/\\]+/, "");
|
|
8646
|
-
const baseFullPath =
|
|
10508
|
+
const baseFullPath = isAbsolute5(normalized) ? normalized : join16(projectRoot, normalized);
|
|
8647
10509
|
const siblings = SIBLING_EXTENSIONS.get(extOf(baseFullPath)) ?? [];
|
|
8648
10510
|
const candidates = [
|
|
8649
10511
|
baseFullPath,
|
|
@@ -8653,7 +10515,7 @@ function validateCitation(url, projectRoot) {
|
|
|
8653
10515
|
let stat = null;
|
|
8654
10516
|
for (const candidate of candidates) {
|
|
8655
10517
|
try {
|
|
8656
|
-
stat =
|
|
10518
|
+
stat = statSync11(candidate);
|
|
8657
10519
|
fullPath = candidate;
|
|
8658
10520
|
break;
|
|
8659
10521
|
} catch {
|
|
@@ -8664,7 +10526,7 @@ function validateCitation(url, projectRoot) {
|
|
|
8664
10526
|
if (parts.startLine === void 0) return { ok: true };
|
|
8665
10527
|
let lineCount;
|
|
8666
10528
|
try {
|
|
8667
|
-
lineCount =
|
|
10529
|
+
lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
|
|
8668
10530
|
} catch {
|
|
8669
10531
|
return { ok: false, reason: "unreadable" };
|
|
8670
10532
|
}
|
|
@@ -11182,9 +13044,9 @@ function describeRepair(repair) {
|
|
|
11182
13044
|
}
|
|
11183
13045
|
|
|
11184
13046
|
// src/cli/ui/hash-memory.ts
|
|
11185
|
-
import { appendFileSync as appendFileSync3, existsSync as
|
|
11186
|
-
import { homedir as
|
|
11187
|
-
import { dirname as
|
|
13047
|
+
import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
|
|
13048
|
+
import { homedir as homedir9 } from "os";
|
|
13049
|
+
import { dirname as dirname15, join as join17 } from "path";
|
|
11188
13050
|
var PROJECT_HEADER = `# Reasonix project memory
|
|
11189
13051
|
|
|
11190
13052
|
Notes the user pinned via the \`#\` prompt prefix. The whole file is
|
|
@@ -11216,12 +13078,12 @@ function detectHashMemory(text) {
|
|
|
11216
13078
|
return { kind: "memory", note: body };
|
|
11217
13079
|
}
|
|
11218
13080
|
function appendProjectMemory(rootDir, note) {
|
|
11219
|
-
return appendBulletToFile(
|
|
13081
|
+
return appendBulletToFile(join17(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
|
|
11220
13082
|
}
|
|
11221
13083
|
var GLOBAL_MEMORY_DIR = ".reasonix";
|
|
11222
13084
|
var GLOBAL_MEMORY_FILE = "REASONIX.md";
|
|
11223
|
-
function globalMemoryPath(homeDir =
|
|
11224
|
-
return
|
|
13085
|
+
function globalMemoryPath(homeDir = homedir9()) {
|
|
13086
|
+
return join17(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
|
|
11225
13087
|
}
|
|
11226
13088
|
function appendGlobalMemory(note, homeDir) {
|
|
11227
13089
|
return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
|
|
@@ -11231,14 +13093,14 @@ function appendBulletToFile(path5, note, newFileHeader) {
|
|
|
11231
13093
|
if (!trimmed) throw new Error("note body cannot be empty");
|
|
11232
13094
|
const bullet = `- ${trimmed}
|
|
11233
13095
|
`;
|
|
11234
|
-
if (!
|
|
11235
|
-
|
|
11236
|
-
|
|
13096
|
+
if (!existsSync18(path5)) {
|
|
13097
|
+
mkdirSync12(dirname15(path5), { recursive: true });
|
|
13098
|
+
writeFileSync11(path5, `${newFileHeader}${bullet}`, "utf8");
|
|
11237
13099
|
return { path: path5, created: true };
|
|
11238
13100
|
}
|
|
11239
13101
|
let prefix = "";
|
|
11240
13102
|
try {
|
|
11241
|
-
const existing =
|
|
13103
|
+
const existing = readFileSync20(path5, "utf8");
|
|
11242
13104
|
if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
|
|
11243
13105
|
} catch {
|
|
11244
13106
|
}
|
|
@@ -11509,6 +13371,61 @@ function formatBytes2(n) {
|
|
|
11509
13371
|
return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
|
|
11510
13372
|
}
|
|
11511
13373
|
|
|
13374
|
+
// src/cli/ui/presets.ts
|
|
13375
|
+
var PRESETS = {
|
|
13376
|
+
// auto — flash baseline + auto-escalate to pro when the model emits
|
|
13377
|
+
// <<<NEEDS_PRO>>> OR after 3+ tool failure signals in one turn.
|
|
13378
|
+
// The default: cheap when easy, smart when hard.
|
|
13379
|
+
auto: {
|
|
13380
|
+
model: "deepseek-v4-flash",
|
|
13381
|
+
reasoningEffort: "max",
|
|
13382
|
+
autoEscalate: true,
|
|
13383
|
+
harvest: false,
|
|
13384
|
+
branch: 1
|
|
13385
|
+
},
|
|
13386
|
+
// flash — always flash, never escalate. `/pro` still arms a single
|
|
13387
|
+
// manual turn; auto-promotion is the thing this disables. Use when
|
|
13388
|
+
// you want predictable cost per turn.
|
|
13389
|
+
flash: {
|
|
13390
|
+
model: "deepseek-v4-flash",
|
|
13391
|
+
reasoningEffort: "max",
|
|
13392
|
+
autoEscalate: false,
|
|
13393
|
+
harvest: false,
|
|
13394
|
+
branch: 1
|
|
13395
|
+
},
|
|
13396
|
+
// pro — always pro. Hard pin; the model never downgrades. Use for
|
|
13397
|
+
// multi-turn architecture work where flash is just going to keep
|
|
13398
|
+
// escalating anyway and the back-and-forth wastes turns.
|
|
13399
|
+
pro: {
|
|
13400
|
+
model: "deepseek-v4-pro",
|
|
13401
|
+
reasoningEffort: "max",
|
|
13402
|
+
autoEscalate: false,
|
|
13403
|
+
harvest: false,
|
|
13404
|
+
branch: 1
|
|
13405
|
+
}
|
|
13406
|
+
};
|
|
13407
|
+
var PRESET_DESCRIPTIONS = {
|
|
13408
|
+
auto: {
|
|
13409
|
+
headline: "flash \u2192 pro on hard turns",
|
|
13410
|
+
cost: "default \xB7 ~96% turns stay on flash \xB7 pro kicks in only when needed"
|
|
13411
|
+
},
|
|
13412
|
+
flash: {
|
|
13413
|
+
headline: "v4-flash always",
|
|
13414
|
+
cost: "cheapest \xB7 predictable \xB7 /pro still works for a one-turn bump"
|
|
13415
|
+
},
|
|
13416
|
+
pro: {
|
|
13417
|
+
headline: "v4-pro always",
|
|
13418
|
+
cost: "~3\xD7 flash (5/31 discount) / ~12\xD7 full price \xB7 for hard multi-turn work"
|
|
13419
|
+
}
|
|
13420
|
+
};
|
|
13421
|
+
function resolvePreset(name) {
|
|
13422
|
+
if (name === "auto" || name === "flash" || name === "pro") return PRESETS[name];
|
|
13423
|
+
if (name === "fast") return { ...PRESETS.flash, reasoningEffort: "high" };
|
|
13424
|
+
if (name === "smart") return PRESETS.auto;
|
|
13425
|
+
if (name === "max") return PRESETS.pro;
|
|
13426
|
+
return PRESETS.auto;
|
|
13427
|
+
}
|
|
13428
|
+
|
|
11512
13429
|
// src/cli/ui/slash/commands.ts
|
|
11513
13430
|
var SLASH_COMMANDS = [
|
|
11514
13431
|
{ cmd: "help", summary: "show the full command reference" },
|
|
@@ -11579,6 +13496,18 @@ var SLASH_COMMANDS = [
|
|
|
11579
13496
|
argsHint: "[reload]",
|
|
11580
13497
|
summary: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk"
|
|
11581
13498
|
},
|
|
13499
|
+
{
|
|
13500
|
+
cmd: "permissions",
|
|
13501
|
+
argsHint: "[list|add <prefix>|remove <prefix|N>|clear confirm]",
|
|
13502
|
+
summary: "show / edit shell allowlist (builtin read-only \xB7 per-project: ~/.reasonix/config.json)",
|
|
13503
|
+
argCompleter: ["list", "add", "remove", "clear"]
|
|
13504
|
+
},
|
|
13505
|
+
{
|
|
13506
|
+
cmd: "dashboard",
|
|
13507
|
+
argsHint: "[stop]",
|
|
13508
|
+
summary: "launch the embedded web dashboard (127.0.0.1, token-gated)",
|
|
13509
|
+
argCompleter: ["stop"]
|
|
13510
|
+
},
|
|
11582
13511
|
{
|
|
11583
13512
|
cmd: "cwd",
|
|
11584
13513
|
argsHint: "<path>",
|
|
@@ -11738,11 +13667,11 @@ function parseSlash(text) {
|
|
|
11738
13667
|
}
|
|
11739
13668
|
|
|
11740
13669
|
// src/cli/ui/slash/handlers/admin.ts
|
|
11741
|
-
import { existsSync as
|
|
13670
|
+
import { existsSync as existsSync20, statSync as statSync12 } from "fs";
|
|
11742
13671
|
import * as pathMod5 from "path";
|
|
11743
13672
|
|
|
11744
13673
|
// src/cli/commands/stats.ts
|
|
11745
|
-
import { existsSync as
|
|
13674
|
+
import { existsSync as existsSync19, readFileSync as readFileSync21 } from "fs";
|
|
11746
13675
|
function statsCommand(opts) {
|
|
11747
13676
|
if (opts.transcript) {
|
|
11748
13677
|
transcriptSummary(opts.transcript);
|
|
@@ -11751,11 +13680,11 @@ function statsCommand(opts) {
|
|
|
11751
13680
|
dashboard(opts);
|
|
11752
13681
|
}
|
|
11753
13682
|
function transcriptSummary(path5) {
|
|
11754
|
-
if (!
|
|
13683
|
+
if (!existsSync19(path5)) {
|
|
11755
13684
|
console.error(`no such transcript: ${path5}`);
|
|
11756
13685
|
process.exit(1);
|
|
11757
13686
|
}
|
|
11758
|
-
const lines =
|
|
13687
|
+
const lines = readFileSync21(path5, "utf8").split(/\r?\n/).filter(Boolean);
|
|
11759
13688
|
let assistantTurns = 0;
|
|
11760
13689
|
let toolCalls = 0;
|
|
11761
13690
|
let lastTurn = 0;
|
|
@@ -11844,12 +13773,13 @@ function header() {
|
|
|
11844
13773
|
pad("turns", 8, "right"),
|
|
11845
13774
|
pad("cache hit", 10, "right"),
|
|
11846
13775
|
pad("cost (USD)", 14, "right"),
|
|
13776
|
+
pad("cache saved", 14, "right"),
|
|
11847
13777
|
pad("vs Claude", 14, "right"),
|
|
11848
13778
|
pad("saved", 10, "right")
|
|
11849
13779
|
].join(" ");
|
|
11850
13780
|
}
|
|
11851
13781
|
function divider() {
|
|
11852
|
-
return "-".repeat(
|
|
13782
|
+
return "-".repeat(86);
|
|
11853
13783
|
}
|
|
11854
13784
|
function bucketRow(b) {
|
|
11855
13785
|
const hit = bucketCacheHitRatio(b);
|
|
@@ -11859,6 +13789,11 @@ function bucketRow(b) {
|
|
|
11859
13789
|
pad(b.turns.toString(), 8, "right"),
|
|
11860
13790
|
pad(b.turns > 0 ? `${(hit * 100).toFixed(1)}%` : "\u2014", 10, "right"),
|
|
11861
13791
|
pad(b.turns > 0 ? `$${b.costUsd.toFixed(6)}` : "\u2014", 14, "right"),
|
|
13792
|
+
pad(
|
|
13793
|
+
b.turns > 0 && b.cacheSavingsUsd > 0 ? `$${b.cacheSavingsUsd.toFixed(4)}` : "\u2014",
|
|
13794
|
+
14,
|
|
13795
|
+
"right"
|
|
13796
|
+
),
|
|
11862
13797
|
pad(b.turns > 0 ? `$${b.claudeEquivUsd.toFixed(4)}` : "\u2014", 14, "right"),
|
|
11863
13798
|
pad(b.turns > 0 && savings > 0 ? `${(savings * 100).toFixed(1)}%` : "\u2014", 10, "right")
|
|
11864
13799
|
].join(" ");
|
|
@@ -11990,12 +13925,12 @@ var cwd = (args, _loop, ctx) => {
|
|
|
11990
13925
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
11991
13926
|
const expanded = raw.startsWith("~") && home ? pathMod5.join(home, raw.slice(1)) : raw;
|
|
11992
13927
|
const abs = pathMod5.resolve(expanded);
|
|
11993
|
-
if (!
|
|
13928
|
+
if (!existsSync20(abs)) {
|
|
11994
13929
|
return { info: `\u25B8 /cwd: path does not exist \u2014 ${abs}` };
|
|
11995
13930
|
}
|
|
11996
13931
|
let isDir = false;
|
|
11997
13932
|
try {
|
|
11998
|
-
isDir =
|
|
13933
|
+
isDir = statSync12(abs).isDirectory();
|
|
11999
13934
|
} catch {
|
|
12000
13935
|
}
|
|
12001
13936
|
if (!isDir) {
|
|
@@ -12225,6 +14160,50 @@ var handlers2 = {
|
|
|
12225
14160
|
loop
|
|
12226
14161
|
};
|
|
12227
14162
|
|
|
14163
|
+
// src/cli/ui/slash/handlers/dashboard.ts
|
|
14164
|
+
var dashboard2 = (args, _loop, ctx) => {
|
|
14165
|
+
if (!ctx.startDashboard || !ctx.getDashboardUrl) {
|
|
14166
|
+
return {
|
|
14167
|
+
info: "/dashboard is not available in this context (no startDashboard callback wired)."
|
|
14168
|
+
};
|
|
14169
|
+
}
|
|
14170
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
14171
|
+
if (sub === "stop" || sub === "off") {
|
|
14172
|
+
if (!ctx.stopDashboard) {
|
|
14173
|
+
return { info: "/dashboard stop: no stop callback wired." };
|
|
14174
|
+
}
|
|
14175
|
+
const url = ctx.getDashboardUrl();
|
|
14176
|
+
if (!url) return { info: "\u25B8 dashboard is not running." };
|
|
14177
|
+
ctx.stopDashboard();
|
|
14178
|
+
return { info: "\u25B8 dashboard stopping\u2026" };
|
|
14179
|
+
}
|
|
14180
|
+
const existing = ctx.getDashboardUrl();
|
|
14181
|
+
if (existing) {
|
|
14182
|
+
return {
|
|
14183
|
+
info: [
|
|
14184
|
+
"\u25B8 dashboard is already running:",
|
|
14185
|
+
` ${existing}`,
|
|
14186
|
+
"",
|
|
14187
|
+
"Open it in any browser. Type `/dashboard stop` to tear it down."
|
|
14188
|
+
].join("\n")
|
|
14189
|
+
};
|
|
14190
|
+
}
|
|
14191
|
+
ctx.startDashboard().then((url) => {
|
|
14192
|
+
ctx.postInfo?.(
|
|
14193
|
+
[
|
|
14194
|
+
"\u25B8 dashboard ready:",
|
|
14195
|
+
` ${url}`,
|
|
14196
|
+
"",
|
|
14197
|
+
"127.0.0.1 only \xB7 token-gated. Type `/dashboard stop` to shut down."
|
|
14198
|
+
].join("\n")
|
|
14199
|
+
);
|
|
14200
|
+
}).catch((err) => {
|
|
14201
|
+
ctx.postInfo?.(`\u25B8 dashboard failed to start: ${err.message}`);
|
|
14202
|
+
});
|
|
14203
|
+
return { info: "\u25B8 starting dashboard server\u2026" };
|
|
14204
|
+
};
|
|
14205
|
+
var handlers3 = { dashboard: dashboard2 };
|
|
14206
|
+
|
|
12228
14207
|
// src/cli/ui/slash/helpers.ts
|
|
12229
14208
|
import { spawnSync } from "child_process";
|
|
12230
14209
|
function resolveMemoryTarget(store, raw) {
|
|
@@ -12453,7 +14432,7 @@ var walk2 = (_args, _loop, ctx) => {
|
|
|
12453
14432
|
}
|
|
12454
14433
|
return { info: ctx.startWalkthrough() };
|
|
12455
14434
|
};
|
|
12456
|
-
var
|
|
14435
|
+
var handlers4 = {
|
|
12457
14436
|
undo,
|
|
12458
14437
|
history,
|
|
12459
14438
|
show,
|
|
@@ -12468,7 +14447,7 @@ var handlers3 = {
|
|
|
12468
14447
|
};
|
|
12469
14448
|
|
|
12470
14449
|
// src/cli/ui/slash/handlers/init.ts
|
|
12471
|
-
import { existsSync as
|
|
14450
|
+
import { existsSync as existsSync21 } from "fs";
|
|
12472
14451
|
import * as pathMod6 from "path";
|
|
12473
14452
|
var INIT_PROMPT = [
|
|
12474
14453
|
"# Task: Initialize REASONIX.md",
|
|
@@ -12540,7 +14519,7 @@ var init = (args, _loop, ctx) => {
|
|
|
12540
14519
|
}
|
|
12541
14520
|
const force = (args[0] ?? "").toLowerCase() === "force";
|
|
12542
14521
|
const target = pathMod6.join(ctx.codeRoot, "REASONIX.md");
|
|
12543
|
-
if (
|
|
14522
|
+
if (existsSync21(target) && !force) {
|
|
12544
14523
|
return {
|
|
12545
14524
|
info: [
|
|
12546
14525
|
`\u25B8 REASONIX.md already exists at ${target}`,
|
|
@@ -12560,7 +14539,7 @@ var init = (args, _loop, ctx) => {
|
|
|
12560
14539
|
resubmit: INIT_PROMPT
|
|
12561
14540
|
};
|
|
12562
14541
|
};
|
|
12563
|
-
var
|
|
14542
|
+
var handlers5 = {
|
|
12564
14543
|
init
|
|
12565
14544
|
};
|
|
12566
14545
|
|
|
@@ -12619,7 +14598,7 @@ $ ${out.command}`;
|
|
|
12619
14598
|
return { info: out.output ? `${header2}
|
|
12620
14599
|
${out.output}` : header2 };
|
|
12621
14600
|
};
|
|
12622
|
-
var
|
|
14601
|
+
var handlers6 = {
|
|
12623
14602
|
jobs,
|
|
12624
14603
|
kill,
|
|
12625
14604
|
logs
|
|
@@ -12680,7 +14659,7 @@ var mcp = (_args, loop2, ctx) => {
|
|
|
12680
14659
|
lines.push("To change this set, exit and run `reasonix setup`.");
|
|
12681
14660
|
return { info: lines.join("\n") };
|
|
12682
14661
|
};
|
|
12683
|
-
var
|
|
14662
|
+
var handlers7 = { mcp };
|
|
12684
14663
|
|
|
12685
14664
|
// src/cli/ui/slash/handlers/memory.ts
|
|
12686
14665
|
var memory = (args, _loop, ctx) => {
|
|
@@ -12815,7 +14794,7 @@ var memory = (args, _loop, ctx) => {
|
|
|
12815
14794
|
);
|
|
12816
14795
|
return { info: parts.join("\n") };
|
|
12817
14796
|
};
|
|
12818
|
-
var
|
|
14797
|
+
var handlers8 = { memory };
|
|
12819
14798
|
|
|
12820
14799
|
// src/cli/ui/slash/handlers/model.ts
|
|
12821
14800
|
var model = (args, loop2, ctx) => {
|
|
@@ -12968,7 +14947,7 @@ var pro = (args, loop2, ctx) => {
|
|
|
12968
14947
|
};
|
|
12969
14948
|
};
|
|
12970
14949
|
var ESCALATION_MODEL_ID = "deepseek-v4-pro";
|
|
12971
|
-
var
|
|
14950
|
+
var handlers9 = {
|
|
12972
14951
|
model,
|
|
12973
14952
|
models,
|
|
12974
14953
|
harvest: harvest2,
|
|
@@ -13121,7 +15100,7 @@ var compact = (args, loop2) => {
|
|
|
13121
15100
|
info: `\u25B8 compacted ${healedCount} payload(s) to ${cap.toLocaleString()} tokens each (tool results + tool-call args), saved ${tokensSaved.toLocaleString()} tokens (${charsSaved.toLocaleString()} chars). Session file rewritten.`
|
|
13122
15101
|
};
|
|
13123
15102
|
};
|
|
13124
|
-
var
|
|
15103
|
+
var handlers10 = {
|
|
13125
15104
|
think,
|
|
13126
15105
|
reasoning: think,
|
|
13127
15106
|
tool,
|
|
@@ -13130,6 +15109,150 @@ var handlers9 = {
|
|
|
13130
15109
|
compact
|
|
13131
15110
|
};
|
|
13132
15111
|
|
|
15112
|
+
// src/cli/ui/slash/handlers/permissions.ts
|
|
15113
|
+
var permissions = (args, _loop, ctx) => {
|
|
15114
|
+
const sub = (args[0] ?? "").toLowerCase();
|
|
15115
|
+
const root = ctx.codeRoot;
|
|
15116
|
+
const mode2 = ctx.editMode ?? null;
|
|
15117
|
+
if (sub === "" || sub === "list" || sub === "ls") {
|
|
15118
|
+
return { info: renderListing(root, mode2) };
|
|
15119
|
+
}
|
|
15120
|
+
if (!root) {
|
|
15121
|
+
return {
|
|
15122
|
+
info: "/permissions add / remove / clear are only available inside `reasonix code` \u2014 they edit the project-scoped allowlist (`~/.reasonix/config.json` projects[<root>].shellAllowed)."
|
|
15123
|
+
};
|
|
15124
|
+
}
|
|
15125
|
+
if (sub === "add") {
|
|
15126
|
+
const prefix = args.slice(1).join(" ").trim();
|
|
15127
|
+
if (!prefix) {
|
|
15128
|
+
return {
|
|
15129
|
+
info: 'usage: /permissions add <prefix> (multi-token OK: /permissions add "git push origin")'
|
|
15130
|
+
};
|
|
15131
|
+
}
|
|
15132
|
+
const before = loadProjectShellAllowed(root);
|
|
15133
|
+
if (before.includes(prefix)) {
|
|
15134
|
+
return { info: `\u25B8 already allowed: ${prefix}` };
|
|
15135
|
+
}
|
|
15136
|
+
if (BUILTIN_ALLOWLIST.includes(prefix)) {
|
|
15137
|
+
return {
|
|
15138
|
+
info: `\u25B8 \`${prefix}\` is already in the builtin allowlist \u2014 no per-project entry needed. (Builtin entries are always on.)`
|
|
15139
|
+
};
|
|
15140
|
+
}
|
|
15141
|
+
addProjectShellAllowed(root, prefix);
|
|
15142
|
+
return {
|
|
15143
|
+
info: `\u25B8 added: ${prefix}
|
|
15144
|
+
\u2192 next \`${prefix}\` invocation runs without prompting in this project.`
|
|
15145
|
+
};
|
|
15146
|
+
}
|
|
15147
|
+
if (sub === "remove" || sub === "rm" || sub === "delete") {
|
|
15148
|
+
const target = args.slice(1).join(" ").trim();
|
|
15149
|
+
if (!target) {
|
|
15150
|
+
return {
|
|
15151
|
+
info: "usage: /permissions remove <prefix-or-index> (e.g. /permissions remove 3, or /permissions remove npm)"
|
|
15152
|
+
};
|
|
15153
|
+
}
|
|
15154
|
+
const existing = loadProjectShellAllowed(root);
|
|
15155
|
+
let prefix = null;
|
|
15156
|
+
if (/^\d+$/.test(target)) {
|
|
15157
|
+
const idx = Number.parseInt(target, 10);
|
|
15158
|
+
if (idx < 1 || idx > existing.length) {
|
|
15159
|
+
return {
|
|
15160
|
+
info: existing.length === 0 ? "\u25B8 no project allowlist entries to remove." : `\u25B8 index out of range: ${idx} (project list has ${existing.length} entries)`
|
|
15161
|
+
};
|
|
15162
|
+
}
|
|
15163
|
+
prefix = existing[idx - 1] ?? null;
|
|
15164
|
+
} else {
|
|
15165
|
+
prefix = target;
|
|
15166
|
+
}
|
|
15167
|
+
if (prefix === null) return { info: "\u25B8 nothing to remove." };
|
|
15168
|
+
if (BUILTIN_ALLOWLIST.includes(prefix) && !existing.includes(prefix)) {
|
|
15169
|
+
return {
|
|
15170
|
+
info: `\u25B8 \`${prefix}\` is in the builtin allowlist (read-only). Builtin entries can't be removed at runtime \u2014 they're baked into the binary.`
|
|
15171
|
+
};
|
|
15172
|
+
}
|
|
15173
|
+
const ok = removeProjectShellAllowed(root, prefix);
|
|
15174
|
+
return {
|
|
15175
|
+
info: ok ? `\u25B8 removed: ${prefix}` : `\u25B8 no such project entry: ${prefix} (try /permissions list to see what's stored)`
|
|
15176
|
+
};
|
|
15177
|
+
}
|
|
15178
|
+
if (sub === "clear") {
|
|
15179
|
+
if ((args[1] ?? "").toLowerCase() !== "confirm") {
|
|
15180
|
+
const count = loadProjectShellAllowed(root).length;
|
|
15181
|
+
return {
|
|
15182
|
+
info: count === 0 ? "\u25B8 project allowlist is already empty." : `about to drop ${count} project allowlist entr${count === 1 ? "y" : "ies"} for ${root}. Re-run with the word 'confirm' to proceed: /permissions clear confirm`
|
|
15183
|
+
};
|
|
15184
|
+
}
|
|
15185
|
+
const dropped = clearProjectShellAllowed(root);
|
|
15186
|
+
return {
|
|
15187
|
+
info: dropped === 0 ? "\u25B8 project allowlist was already empty \u2014 nothing changed." : `\u25B8 cleared ${dropped} project allowlist entr${dropped === 1 ? "y" : "ies"}.`
|
|
15188
|
+
};
|
|
15189
|
+
}
|
|
15190
|
+
return {
|
|
15191
|
+
info: [
|
|
15192
|
+
"usage: /permissions [list] show current state",
|
|
15193
|
+
' /permissions add <prefix> persist (e.g. "npm run build")',
|
|
15194
|
+
" /permissions remove <prefix-or-N> drop one entry",
|
|
15195
|
+
" /permissions clear confirm wipe every project entry"
|
|
15196
|
+
].join("\n")
|
|
15197
|
+
};
|
|
15198
|
+
};
|
|
15199
|
+
function renderListing(root, mode2) {
|
|
15200
|
+
const lines = [];
|
|
15201
|
+
if (mode2 === "yolo") {
|
|
15202
|
+
lines.push(
|
|
15203
|
+
"\u25B8 edit mode: YOLO \u2014 every shell command auto-runs, allowlist is bypassed. /mode review to re-enable prompts."
|
|
15204
|
+
);
|
|
15205
|
+
} else if (mode2 === "auto") {
|
|
15206
|
+
lines.push(
|
|
15207
|
+
"\u25B8 edit mode: auto \u2014 edits auto-apply, shell still gated by allowlist (or ShellConfirm prompt for non-allowlisted)."
|
|
15208
|
+
);
|
|
15209
|
+
} else if (mode2 === "review") {
|
|
15210
|
+
lines.push(
|
|
15211
|
+
"\u25B8 edit mode: review \u2014 both edits and non-allowlisted shell commands ask before running."
|
|
15212
|
+
);
|
|
15213
|
+
}
|
|
15214
|
+
lines.push("");
|
|
15215
|
+
if (root) {
|
|
15216
|
+
const project = loadProjectShellAllowed(root);
|
|
15217
|
+
lines.push(`Project allowlist (${project.length}) \u2014 ${root}`);
|
|
15218
|
+
if (project.length === 0) {
|
|
15219
|
+
lines.push(' (none \u2014 pick "always allow" on a ShellConfirm prompt to add one,');
|
|
15220
|
+
lines.push(" or `/permissions add <prefix>` directly.)");
|
|
15221
|
+
} else {
|
|
15222
|
+
project.forEach((p, i) => {
|
|
15223
|
+
lines.push(` ${String(i + 1).padStart(2)}. ${p}`);
|
|
15224
|
+
});
|
|
15225
|
+
}
|
|
15226
|
+
} else {
|
|
15227
|
+
lines.push("Project allowlist \u2014 (no project root; chat mode shows builtin entries only)");
|
|
15228
|
+
}
|
|
15229
|
+
lines.push("");
|
|
15230
|
+
lines.push(`Builtin allowlist (${BUILTIN_ALLOWLIST.length}) \u2014 read-only, baked in`);
|
|
15231
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
15232
|
+
for (const entry of BUILTIN_ALLOWLIST) {
|
|
15233
|
+
const head = entry.split(" ")[0] ?? entry;
|
|
15234
|
+
if (!grouped.has(head)) grouped.set(head, []);
|
|
15235
|
+
grouped.get(head).push(entry);
|
|
15236
|
+
}
|
|
15237
|
+
for (const [head, items] of grouped) {
|
|
15238
|
+
if (items.length === 1 && items[0] === head) {
|
|
15239
|
+
lines.push(` \xB7 ${head}`);
|
|
15240
|
+
} else {
|
|
15241
|
+
const tail = items.map((i) => i.slice(head.length).trim() || "(bare)").join(", ");
|
|
15242
|
+
lines.push(` \xB7 ${head}: ${tail}`);
|
|
15243
|
+
}
|
|
15244
|
+
}
|
|
15245
|
+
lines.push("");
|
|
15246
|
+
lines.push(
|
|
15247
|
+
"Subcommands: /permissions add <prefix> \xB7 /permissions remove <prefix-or-N> \xB7 /permissions clear confirm"
|
|
15248
|
+
);
|
|
15249
|
+
return lines.join("\n");
|
|
15250
|
+
}
|
|
15251
|
+
var handlers11 = {
|
|
15252
|
+
permissions,
|
|
15253
|
+
perms: permissions
|
|
15254
|
+
};
|
|
15255
|
+
|
|
13133
15256
|
// src/cli/ui/slash/handlers/plans.ts
|
|
13134
15257
|
import { basename } from "path";
|
|
13135
15258
|
var plans = (_args, loop2) => {
|
|
@@ -13209,7 +15332,7 @@ var replay = (args, loop2) => {
|
|
|
13209
15332
|
}
|
|
13210
15333
|
};
|
|
13211
15334
|
};
|
|
13212
|
-
var
|
|
15335
|
+
var handlers12 = {
|
|
13213
15336
|
plans,
|
|
13214
15337
|
replay
|
|
13215
15338
|
};
|
|
@@ -13472,7 +15595,7 @@ async function startOllamaDaemon(opts = {}) {
|
|
|
13472
15595
|
return { ready: false, pid };
|
|
13473
15596
|
}
|
|
13474
15597
|
async function pullOllamaModel(modelName, opts = {}) {
|
|
13475
|
-
return new Promise((
|
|
15598
|
+
return new Promise((resolve13) => {
|
|
13476
15599
|
const child = spawn5("ollama", ["pull", modelName], {
|
|
13477
15600
|
stdio: ["ignore", "pipe", "pipe"],
|
|
13478
15601
|
windowsHide: true
|
|
@@ -13484,8 +15607,8 @@ async function pullOllamaModel(modelName, opts = {}) {
|
|
|
13484
15607
|
}
|
|
13485
15608
|
streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
|
|
13486
15609
|
streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
|
|
13487
|
-
child.once("exit", (code) =>
|
|
13488
|
-
child.once("error", () =>
|
|
15610
|
+
child.once("exit", (code) => resolve13(code ?? -1));
|
|
15611
|
+
child.once("error", () => resolve13(-1));
|
|
13489
15612
|
});
|
|
13490
15613
|
}
|
|
13491
15614
|
function streamLines(stream, cb) {
|
|
@@ -13582,7 +15705,7 @@ async function readIndexMeta(rootDir) {
|
|
|
13582
15705
|
return null;
|
|
13583
15706
|
}
|
|
13584
15707
|
}
|
|
13585
|
-
var
|
|
15708
|
+
var handlers13 = {
|
|
13586
15709
|
semantic
|
|
13587
15710
|
};
|
|
13588
15711
|
|
|
@@ -13617,7 +15740,7 @@ var forget = (_args, loop2) => {
|
|
|
13617
15740
|
info: ok ? `\u25B8 deleted session "${name}" \u2014 current screen still shows the conversation, but next launch starts fresh` : `could not delete session "${name}" (already gone?)`
|
|
13618
15741
|
};
|
|
13619
15742
|
};
|
|
13620
|
-
var
|
|
15743
|
+
var handlers14 = {
|
|
13621
15744
|
sessions,
|
|
13622
15745
|
forget
|
|
13623
15746
|
};
|
|
@@ -13693,7 +15816,7 @@ ${found.body}${argsLine}`;
|
|
|
13693
15816
|
resubmit: payload
|
|
13694
15817
|
};
|
|
13695
15818
|
};
|
|
13696
|
-
var
|
|
15819
|
+
var handlers15 = {
|
|
13697
15820
|
skill,
|
|
13698
15821
|
skills: skill
|
|
13699
15822
|
};
|
|
@@ -13712,7 +15835,9 @@ var HANDLERS = {
|
|
|
13712
15835
|
...handlers10,
|
|
13713
15836
|
...handlers11,
|
|
13714
15837
|
...handlers12,
|
|
13715
|
-
...handlers13
|
|
15838
|
+
...handlers13,
|
|
15839
|
+
...handlers14,
|
|
15840
|
+
...handlers15
|
|
13716
15841
|
};
|
|
13717
15842
|
function handleSlash(cmd, args, loop2, ctx = {}) {
|
|
13718
15843
|
const h = HANDLERS[cmd];
|
|
@@ -14296,6 +16421,9 @@ function App({
|
|
|
14296
16421
|
editModeRef.current = editMode;
|
|
14297
16422
|
if (codeMode) saveEditMode(editMode);
|
|
14298
16423
|
}, [editMode, codeMode]);
|
|
16424
|
+
const planModeRef = useRef6(false);
|
|
16425
|
+
const currentRootDirRef = useRef6("");
|
|
16426
|
+
const latestVersionRef = useRef6(null);
|
|
14299
16427
|
const [pendingEditReview, setPendingEditReview] = useState10(null);
|
|
14300
16428
|
const [walkthroughActive, setWalkthroughActive] = useState10(false);
|
|
14301
16429
|
const [pendingTick, setPendingTick] = useState10(0);
|
|
@@ -14341,6 +16469,9 @@ function App({
|
|
|
14341
16469
|
activeLoopRef.current = activeLoop;
|
|
14342
16470
|
}, [activeLoop]);
|
|
14343
16471
|
const toolHistoryRef = useRef6([]);
|
|
16472
|
+
const dashboardRef = useRef6(null);
|
|
16473
|
+
const eventSubscribersRef = useRef6(/* @__PURE__ */ new Set());
|
|
16474
|
+
const historicalRef = useRef6([]);
|
|
14344
16475
|
const planStepsRef = useRef6(null);
|
|
14345
16476
|
const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
|
|
14346
16477
|
const planBodyRef = useRef6(null);
|
|
@@ -14490,6 +16621,91 @@ function App({
|
|
|
14490
16621
|
refreshModels,
|
|
14491
16622
|
refreshLatestVersion
|
|
14492
16623
|
} = useSessionInfo(loop2);
|
|
16624
|
+
useEffect6(() => {
|
|
16625
|
+
planModeRef.current = planMode;
|
|
16626
|
+
}, [planMode]);
|
|
16627
|
+
useEffect6(() => {
|
|
16628
|
+
currentRootDirRef.current = currentRootDir;
|
|
16629
|
+
}, [currentRootDir]);
|
|
16630
|
+
useEffect6(() => {
|
|
16631
|
+
latestVersionRef.current = latestVersion ?? null;
|
|
16632
|
+
}, [latestVersion]);
|
|
16633
|
+
const balanceRef = useRef6(null);
|
|
16634
|
+
useEffect6(() => {
|
|
16635
|
+
balanceRef.current = balance;
|
|
16636
|
+
}, [balance]);
|
|
16637
|
+
useEffect6(() => {
|
|
16638
|
+
historicalRef.current = historical;
|
|
16639
|
+
}, [historical]);
|
|
16640
|
+
const broadcastDashboardEvent = useCallback4((ev) => {
|
|
16641
|
+
const subs = eventSubscribersRef.current;
|
|
16642
|
+
if (subs.size === 0) return;
|
|
16643
|
+
for (const h of subs) {
|
|
16644
|
+
try {
|
|
16645
|
+
h(ev);
|
|
16646
|
+
} catch {
|
|
16647
|
+
}
|
|
16648
|
+
}
|
|
16649
|
+
}, []);
|
|
16650
|
+
useEffect6(() => {
|
|
16651
|
+
broadcastDashboardEvent({ kind: "busy-change", busy });
|
|
16652
|
+
}, [busy, broadcastDashboardEvent]);
|
|
16653
|
+
useEffect6(() => {
|
|
16654
|
+
if (!pendingShell) return;
|
|
16655
|
+
const modal = {
|
|
16656
|
+
kind: "shell",
|
|
16657
|
+
command: pendingShell.command,
|
|
16658
|
+
allowPrefix: derivePrefix(pendingShell.command),
|
|
16659
|
+
shellKind: pendingShell.kind
|
|
16660
|
+
};
|
|
16661
|
+
broadcastDashboardEvent({ kind: "modal-up", modal });
|
|
16662
|
+
return () => {
|
|
16663
|
+
broadcastDashboardEvent({ kind: "modal-down", modalKind: "shell" });
|
|
16664
|
+
};
|
|
16665
|
+
}, [pendingShell, broadcastDashboardEvent]);
|
|
16666
|
+
useEffect6(() => {
|
|
16667
|
+
if (!pendingChoice) return;
|
|
16668
|
+
const modal = {
|
|
16669
|
+
kind: "choice",
|
|
16670
|
+
question: pendingChoice.question,
|
|
16671
|
+
options: pendingChoice.options,
|
|
16672
|
+
allowCustom: pendingChoice.allowCustom
|
|
16673
|
+
};
|
|
16674
|
+
broadcastDashboardEvent({ kind: "modal-up", modal });
|
|
16675
|
+
return () => {
|
|
16676
|
+
broadcastDashboardEvent({ kind: "modal-down", modalKind: "choice" });
|
|
16677
|
+
};
|
|
16678
|
+
}, [pendingChoice, broadcastDashboardEvent]);
|
|
16679
|
+
useEffect6(() => {
|
|
16680
|
+
if (!pendingPlan) return;
|
|
16681
|
+
broadcastDashboardEvent({
|
|
16682
|
+
kind: "modal-up",
|
|
16683
|
+
modal: { kind: "plan", body: pendingPlan }
|
|
16684
|
+
});
|
|
16685
|
+
return () => {
|
|
16686
|
+
broadcastDashboardEvent({ kind: "modal-down", modalKind: "plan" });
|
|
16687
|
+
};
|
|
16688
|
+
}, [pendingPlan, broadcastDashboardEvent]);
|
|
16689
|
+
useEffect6(() => {
|
|
16690
|
+
if (!pendingEditReview) return;
|
|
16691
|
+
const previewLines = (pendingEditReview.search || pendingEditReview.replace || "").split("\n").slice(0, 12);
|
|
16692
|
+
const preview = previewLines.join("\n");
|
|
16693
|
+
broadcastDashboardEvent({
|
|
16694
|
+
kind: "modal-up",
|
|
16695
|
+
modal: {
|
|
16696
|
+
kind: "edit-review",
|
|
16697
|
+
path: pendingEditReview.path,
|
|
16698
|
+
search: pendingEditReview.search ?? "",
|
|
16699
|
+
replace: pendingEditReview.replace ?? "",
|
|
16700
|
+
preview,
|
|
16701
|
+
total: pendingEdits.current.length,
|
|
16702
|
+
remaining: pendingEdits.current.length
|
|
16703
|
+
}
|
|
16704
|
+
});
|
|
16705
|
+
return () => {
|
|
16706
|
+
broadcastDashboardEvent({ kind: "modal-down", modalKind: "edit-review" });
|
|
16707
|
+
};
|
|
16708
|
+
}, [pendingEditReview, broadcastDashboardEvent]);
|
|
14493
16709
|
const {
|
|
14494
16710
|
slashMatches,
|
|
14495
16711
|
slashSelected,
|
|
@@ -14632,11 +16848,11 @@ function App({
|
|
|
14632
16848
|
if (key.escape && busy) {
|
|
14633
16849
|
if (abortedThisTurn.current) return;
|
|
14634
16850
|
abortedThisTurn.current = true;
|
|
14635
|
-
const
|
|
14636
|
-
if (
|
|
16851
|
+
const resolve13 = editReviewResolveRef.current;
|
|
16852
|
+
if (resolve13) {
|
|
14637
16853
|
editReviewResolveRef.current = null;
|
|
14638
16854
|
setPendingEditReview(null);
|
|
14639
|
-
|
|
16855
|
+
resolve13("reject");
|
|
14640
16856
|
}
|
|
14641
16857
|
if (activeLoopRef.current) stopLoop();
|
|
14642
16858
|
loop2.abort();
|
|
@@ -14924,6 +17140,211 @@ function App({
|
|
|
14924
17140
|
setWalkthroughActive(true);
|
|
14925
17141
|
return `\u25B8 walking ${pendingEdits.current.length} edit block(s) \u2014 y apply \xB7 n reject \xB7 a apply rest \xB7 A flip to AUTO \xB7 Esc cancels (keeps remaining queued).`;
|
|
14926
17142
|
}, [codeMode]);
|
|
17143
|
+
const startDashboard = useCallback4(async () => {
|
|
17144
|
+
if (dashboardRef.current) return dashboardRef.current.url;
|
|
17145
|
+
const handle = await startDashboardServer({
|
|
17146
|
+
mode: "attached",
|
|
17147
|
+
configPath: defaultConfigPath(),
|
|
17148
|
+
usageLogPath: defaultUsageLogPath(),
|
|
17149
|
+
loop: loop2,
|
|
17150
|
+
tools,
|
|
17151
|
+
mcpServers,
|
|
17152
|
+
getCurrentCwd: () => codeMode ? currentRootDirRef.current : void 0,
|
|
17153
|
+
getEditMode: () => codeMode ? editModeRef.current : void 0,
|
|
17154
|
+
getPlanMode: () => planModeRef.current,
|
|
17155
|
+
getPendingEditCount: () => pendingEdits.current.length,
|
|
17156
|
+
getLatestVersion: () => latestVersionRef.current,
|
|
17157
|
+
getSessionName: () => session ?? null,
|
|
17158
|
+
setEditMode: (m) => {
|
|
17159
|
+
setEditMode(m);
|
|
17160
|
+
editModeRef.current = m;
|
|
17161
|
+
saveEditMode(m);
|
|
17162
|
+
return m;
|
|
17163
|
+
},
|
|
17164
|
+
setPlanMode: (on) => {
|
|
17165
|
+
if (codeMode) togglePlanMode(on);
|
|
17166
|
+
},
|
|
17167
|
+
applyPresetLive: (name) => {
|
|
17168
|
+
const settings = resolvePreset(name);
|
|
17169
|
+
loop2.configure({
|
|
17170
|
+
model: settings.model,
|
|
17171
|
+
autoEscalate: settings.autoEscalate,
|
|
17172
|
+
reasoningEffort: settings.reasoningEffort
|
|
17173
|
+
});
|
|
17174
|
+
},
|
|
17175
|
+
applyEffortLive: (effort2) => {
|
|
17176
|
+
loop2.configure({ reasoningEffort: effort2 });
|
|
17177
|
+
},
|
|
17178
|
+
// ---------- Chat bridge ----------
|
|
17179
|
+
getMessages: () => {
|
|
17180
|
+
const out = [];
|
|
17181
|
+
for (const ev of historicalRef.current) {
|
|
17182
|
+
if (ev.role === "user" || ev.role === "assistant" || ev.role === "info" || ev.role === "warning") {
|
|
17183
|
+
const msg = { id: ev.id, role: ev.role, text: ev.text };
|
|
17184
|
+
if (ev.reasoning) msg.reasoning = ev.reasoning;
|
|
17185
|
+
out.push(msg);
|
|
17186
|
+
} else if (ev.role === "tool") {
|
|
17187
|
+
const msg = {
|
|
17188
|
+
id: ev.id,
|
|
17189
|
+
role: "tool",
|
|
17190
|
+
text: ev.text,
|
|
17191
|
+
toolName: ev.toolName
|
|
17192
|
+
};
|
|
17193
|
+
if (ev.toolArgs) msg.toolArgs = ev.toolArgs;
|
|
17194
|
+
out.push(msg);
|
|
17195
|
+
}
|
|
17196
|
+
}
|
|
17197
|
+
return out;
|
|
17198
|
+
},
|
|
17199
|
+
subscribeEvents: (handler) => {
|
|
17200
|
+
eventSubscribersRef.current.add(handler);
|
|
17201
|
+
return () => {
|
|
17202
|
+
eventSubscribersRef.current.delete(handler);
|
|
17203
|
+
};
|
|
17204
|
+
},
|
|
17205
|
+
submitPrompt: (text) => {
|
|
17206
|
+
if (busyRef.current) {
|
|
17207
|
+
return { accepted: false, reason: "loop is busy with a turn" };
|
|
17208
|
+
}
|
|
17209
|
+
const fn = handleSubmitRef.current;
|
|
17210
|
+
if (!fn) return { accepted: false, reason: "TUI not ready" };
|
|
17211
|
+
fn(text).catch(() => void 0);
|
|
17212
|
+
return { accepted: true };
|
|
17213
|
+
},
|
|
17214
|
+
abortTurn: () => {
|
|
17215
|
+
if (busyRef.current) loop2.abort();
|
|
17216
|
+
},
|
|
17217
|
+
isBusy: () => busyRef.current,
|
|
17218
|
+
getStats: () => {
|
|
17219
|
+
const s = loop2.stats.summary();
|
|
17220
|
+
const ctxCap = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
|
|
17221
|
+
return {
|
|
17222
|
+
turns: s.turns,
|
|
17223
|
+
totalCostUsd: s.totalCostUsd,
|
|
17224
|
+
lastTurnCostUsd: s.lastTurnCostUsd,
|
|
17225
|
+
totalInputCostUsd: s.totalInputCostUsd,
|
|
17226
|
+
totalOutputCostUsd: s.totalOutputCostUsd,
|
|
17227
|
+
cacheHitRatio: s.cacheHitRatio,
|
|
17228
|
+
lastPromptTokens: s.lastPromptTokens,
|
|
17229
|
+
contextCapTokens: ctxCap,
|
|
17230
|
+
// useSessionInfo's Balance is a flat { currency, total }; the
|
|
17231
|
+
// dashboard wire shape is the richer DeepSeek BalanceInfo
|
|
17232
|
+
// array (granted / topped_up split). Convert as a single-
|
|
17233
|
+
// entry array so the SPA always reads `balance[0]` shape.
|
|
17234
|
+
balance: balanceRef.current ? [
|
|
17235
|
+
{
|
|
17236
|
+
currency: balanceRef.current.currency,
|
|
17237
|
+
total_balance: String(balanceRef.current.total)
|
|
17238
|
+
}
|
|
17239
|
+
] : null
|
|
17240
|
+
};
|
|
17241
|
+
},
|
|
17242
|
+
// ---------- Modal mirroring ----------
|
|
17243
|
+
getActiveModal: () => {
|
|
17244
|
+
const ps = pendingShell;
|
|
17245
|
+
if (ps) {
|
|
17246
|
+
return {
|
|
17247
|
+
kind: "shell",
|
|
17248
|
+
command: ps.command,
|
|
17249
|
+
allowPrefix: derivePrefix(ps.command),
|
|
17250
|
+
shellKind: ps.kind
|
|
17251
|
+
};
|
|
17252
|
+
}
|
|
17253
|
+
const pc = pendingChoice;
|
|
17254
|
+
if (pc) {
|
|
17255
|
+
return {
|
|
17256
|
+
kind: "choice",
|
|
17257
|
+
question: pc.question,
|
|
17258
|
+
options: pc.options,
|
|
17259
|
+
allowCustom: pc.allowCustom
|
|
17260
|
+
};
|
|
17261
|
+
}
|
|
17262
|
+
if (pendingPlanRef.current) {
|
|
17263
|
+
return { kind: "plan", body: pendingPlanRef.current };
|
|
17264
|
+
}
|
|
17265
|
+
const er = pendingEditReview;
|
|
17266
|
+
if (er) {
|
|
17267
|
+
return {
|
|
17268
|
+
kind: "edit-review",
|
|
17269
|
+
path: er.path,
|
|
17270
|
+
search: er.search ?? "",
|
|
17271
|
+
replace: er.replace ?? "",
|
|
17272
|
+
preview: (er.search || er.replace || "").split("\n").slice(0, 12).join("\n"),
|
|
17273
|
+
total: pendingEdits.current.length,
|
|
17274
|
+
remaining: pendingEdits.current.length
|
|
17275
|
+
};
|
|
17276
|
+
}
|
|
17277
|
+
return null;
|
|
17278
|
+
},
|
|
17279
|
+
resolveShellConfirm: (choice) => {
|
|
17280
|
+
const fn = handleShellConfirmRef.current;
|
|
17281
|
+
if (fn) fn(choice).catch(() => void 0);
|
|
17282
|
+
},
|
|
17283
|
+
resolveChoiceConfirm: (choice) => {
|
|
17284
|
+
const fn = handleChoiceConfirmRef.current;
|
|
17285
|
+
if (fn) fn(choice).catch(() => void 0);
|
|
17286
|
+
},
|
|
17287
|
+
resolvePlanConfirm: (choice, text) => {
|
|
17288
|
+
if (choice === "cancel") {
|
|
17289
|
+
handlePlanConfirmRef.current("cancel").catch(() => void 0);
|
|
17290
|
+
return;
|
|
17291
|
+
}
|
|
17292
|
+
const plan2 = pendingPlanRef.current ?? "";
|
|
17293
|
+
handleStagedInputSubmitRef.current(text ?? "", { plan: plan2, mode: choice }).catch(() => void 0);
|
|
17294
|
+
},
|
|
17295
|
+
resolveEditReview: (choice) => {
|
|
17296
|
+
const resolve13 = editReviewResolveRef.current;
|
|
17297
|
+
if (resolve13) {
|
|
17298
|
+
editReviewResolveRef.current = null;
|
|
17299
|
+
setPendingEditReview(null);
|
|
17300
|
+
resolve13(choice);
|
|
17301
|
+
}
|
|
17302
|
+
},
|
|
17303
|
+
// ---------- v0.14 mutation surface ----------
|
|
17304
|
+
reloadHooks: () => {
|
|
17305
|
+
const fresh = loadHooks({ projectRoot: codeMode ? currentRootDirRef.current : void 0 });
|
|
17306
|
+
setHookList(fresh);
|
|
17307
|
+
return fresh.length;
|
|
17308
|
+
}
|
|
17309
|
+
});
|
|
17310
|
+
dashboardRef.current = handle;
|
|
17311
|
+
return handle.url;
|
|
17312
|
+
}, [
|
|
17313
|
+
loop2,
|
|
17314
|
+
tools,
|
|
17315
|
+
mcpServers,
|
|
17316
|
+
codeMode,
|
|
17317
|
+
session,
|
|
17318
|
+
togglePlanMode,
|
|
17319
|
+
pendingShell,
|
|
17320
|
+
pendingChoice,
|
|
17321
|
+
pendingEditReview
|
|
17322
|
+
]);
|
|
17323
|
+
const stopDashboard = useCallback4(async () => {
|
|
17324
|
+
const h = dashboardRef.current;
|
|
17325
|
+
if (!h) return;
|
|
17326
|
+
dashboardRef.current = null;
|
|
17327
|
+
try {
|
|
17328
|
+
await h.close();
|
|
17329
|
+
} catch {
|
|
17330
|
+
}
|
|
17331
|
+
setHistorical((prev) => [
|
|
17332
|
+
...prev,
|
|
17333
|
+
{ id: `dash-stop-${Date.now()}`, role: "info", text: "\u25B8 dashboard stopped." }
|
|
17334
|
+
]);
|
|
17335
|
+
}, []);
|
|
17336
|
+
const getDashboardUrl = useCallback4(() => {
|
|
17337
|
+
return dashboardRef.current?.url ?? null;
|
|
17338
|
+
}, []);
|
|
17339
|
+
useEffect6(() => {
|
|
17340
|
+
return () => {
|
|
17341
|
+
const h = dashboardRef.current;
|
|
17342
|
+
if (h) {
|
|
17343
|
+
dashboardRef.current = null;
|
|
17344
|
+
h.close().catch(() => void 0);
|
|
17345
|
+
}
|
|
17346
|
+
};
|
|
17347
|
+
}, []);
|
|
14927
17348
|
const handleWalkChoice = useCallback4(
|
|
14928
17349
|
(choice) => {
|
|
14929
17350
|
if (choice === "apply") {
|
|
@@ -14967,7 +17388,7 @@ function App({
|
|
|
14967
17388
|
nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
|
|
14968
17389
|
};
|
|
14969
17390
|
}, []);
|
|
14970
|
-
const
|
|
17391
|
+
const handleSubmit2 = useCallback4(
|
|
14971
17392
|
async (raw) => {
|
|
14972
17393
|
let text = raw.trim();
|
|
14973
17394
|
if (!text) return;
|
|
@@ -15125,6 +17546,9 @@ function App({
|
|
|
15125
17546
|
stopLoop,
|
|
15126
17547
|
getLoopStatus,
|
|
15127
17548
|
startWalkthrough: codeMode ? startWalkthrough : void 0,
|
|
17549
|
+
startDashboard,
|
|
17550
|
+
stopDashboard,
|
|
17551
|
+
getDashboardUrl,
|
|
15128
17552
|
jobs: codeMode?.jobs,
|
|
15129
17553
|
postInfo: (text2) => setHistorical((prev) => [
|
|
15130
17554
|
...prev,
|
|
@@ -15245,6 +17669,8 @@ function App({
|
|
|
15245
17669
|
leadSeparator: prev.length > 0
|
|
15246
17670
|
}
|
|
15247
17671
|
]);
|
|
17672
|
+
const userId = `u-${Date.now()}`;
|
|
17673
|
+
broadcastDashboardEvent({ kind: "user", id: userId, text });
|
|
15248
17674
|
const assistantId = `a-${Date.now()}`;
|
|
15249
17675
|
const streamRef = { id: assistantId, text: "", reasoning: "" };
|
|
15250
17676
|
const contentBuf = { current: "" };
|
|
@@ -15343,6 +17769,38 @@ function App({
|
|
|
15343
17769
|
try {
|
|
15344
17770
|
for await (const ev of loop2.step(modelInput)) {
|
|
15345
17771
|
writeTranscript(ev);
|
|
17772
|
+
if (eventSubscribersRef.current.size > 0) {
|
|
17773
|
+
const id = `${assistantId}-${ev.role}-${Date.now()}`;
|
|
17774
|
+
if (ev.role === "assistant_delta") {
|
|
17775
|
+
broadcastDashboardEvent({
|
|
17776
|
+
kind: "assistant_delta",
|
|
17777
|
+
id: assistantId,
|
|
17778
|
+
contentDelta: ev.content || void 0,
|
|
17779
|
+
reasoningDelta: ev.reasoningDelta
|
|
17780
|
+
});
|
|
17781
|
+
} else if (ev.role === "tool_start" && ev.toolName) {
|
|
17782
|
+
broadcastDashboardEvent({
|
|
17783
|
+
kind: "tool_start",
|
|
17784
|
+
id,
|
|
17785
|
+
toolName: ev.toolName,
|
|
17786
|
+
args: ev.toolArgs
|
|
17787
|
+
});
|
|
17788
|
+
} else if (ev.role === "tool" && ev.toolName) {
|
|
17789
|
+
broadcastDashboardEvent({
|
|
17790
|
+
kind: "tool",
|
|
17791
|
+
id,
|
|
17792
|
+
toolName: ev.toolName,
|
|
17793
|
+
content: ev.content,
|
|
17794
|
+
args: ev.toolArgs
|
|
17795
|
+
});
|
|
17796
|
+
} else if (ev.role === "warning") {
|
|
17797
|
+
broadcastDashboardEvent({ kind: "warning", id, text: ev.content });
|
|
17798
|
+
} else if (ev.role === "error") {
|
|
17799
|
+
broadcastDashboardEvent({ kind: "error", id, text: ev.content });
|
|
17800
|
+
} else if (ev.role === "status") {
|
|
17801
|
+
broadcastDashboardEvent({ kind: "status", text: ev.content });
|
|
17802
|
+
}
|
|
17803
|
+
}
|
|
15346
17804
|
if (ev.role !== "status") {
|
|
15347
17805
|
setStatusLine((cur) => cur ? null : cur);
|
|
15348
17806
|
}
|
|
@@ -15381,6 +17839,12 @@ function App({
|
|
|
15381
17839
|
flush();
|
|
15382
17840
|
const repairNote = ev.repair ? describeRepair(ev.repair) : "";
|
|
15383
17841
|
setStreaming(null);
|
|
17842
|
+
broadcastDashboardEvent({
|
|
17843
|
+
kind: "assistant_final",
|
|
17844
|
+
id: assistantId,
|
|
17845
|
+
text: ev.content || streamRef.text,
|
|
17846
|
+
reasoning: streamRef.reasoning || void 0
|
|
17847
|
+
});
|
|
15384
17848
|
setSummary(loop2.stats.summary());
|
|
15385
17849
|
if (ev.stats?.usage) {
|
|
15386
17850
|
appendUsage({
|
|
@@ -15486,6 +17950,7 @@ function App({
|
|
|
15486
17950
|
role: "tool",
|
|
15487
17951
|
text: ev.content,
|
|
15488
17952
|
toolName: ev.toolName,
|
|
17953
|
+
toolArgs: ev.toolArgs,
|
|
15489
17954
|
toolIndex,
|
|
15490
17955
|
durationMs
|
|
15491
17956
|
}
|
|
@@ -15712,12 +18177,16 @@ function App({
|
|
|
15712
18177
|
startLoop,
|
|
15713
18178
|
getLoopStatus,
|
|
15714
18179
|
startWalkthrough,
|
|
18180
|
+
startDashboard,
|
|
18181
|
+
stopDashboard,
|
|
18182
|
+
getDashboardUrl,
|
|
18183
|
+
broadcastDashboardEvent,
|
|
15715
18184
|
applyCwdChange
|
|
15716
18185
|
]
|
|
15717
18186
|
);
|
|
15718
18187
|
useEffect6(() => {
|
|
15719
|
-
handleSubmitRef.current =
|
|
15720
|
-
}, [
|
|
18188
|
+
handleSubmitRef.current = handleSubmit2;
|
|
18189
|
+
}, [handleSubmit2]);
|
|
15721
18190
|
useEffect6(() => {
|
|
15722
18191
|
if (!activeLoop) return;
|
|
15723
18192
|
const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
|
|
@@ -15845,18 +18314,18 @@ ${body}`;
|
|
|
15845
18314
|
loop2.abort();
|
|
15846
18315
|
setQueuedSubmit(synthetic);
|
|
15847
18316
|
} else {
|
|
15848
|
-
await
|
|
18317
|
+
await handleSubmit2(synthetic);
|
|
15849
18318
|
}
|
|
15850
18319
|
},
|
|
15851
|
-
[pendingShell, codeMode, currentRootDir,
|
|
18320
|
+
[pendingShell, codeMode, currentRootDir, handleSubmit2, busy, loop2]
|
|
15852
18321
|
);
|
|
15853
18322
|
useEffect6(() => {
|
|
15854
18323
|
if (!busy && queuedSubmit !== null) {
|
|
15855
18324
|
const text = queuedSubmit;
|
|
15856
18325
|
setQueuedSubmit(null);
|
|
15857
|
-
void
|
|
18326
|
+
void handleSubmit2(text);
|
|
15858
18327
|
}
|
|
15859
|
-
}, [busy, queuedSubmit,
|
|
18328
|
+
}, [busy, queuedSubmit, handleSubmit2]);
|
|
15860
18329
|
const handleWorkspaceConfirm = useCallback4(
|
|
15861
18330
|
async (choice) => {
|
|
15862
18331
|
const pending = pendingWorkspace;
|
|
@@ -15886,10 +18355,10 @@ ${body}`;
|
|
|
15886
18355
|
loop2.abort();
|
|
15887
18356
|
setQueuedSubmit(synthetic);
|
|
15888
18357
|
} else {
|
|
15889
|
-
await
|
|
18358
|
+
await handleSubmit2(synthetic);
|
|
15890
18359
|
}
|
|
15891
18360
|
},
|
|
15892
|
-
[pendingWorkspace, applyCwdChange, busy, loop2,
|
|
18361
|
+
[pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit2]
|
|
15893
18362
|
);
|
|
15894
18363
|
const handlePlanConfirm = useCallback4(
|
|
15895
18364
|
async (choice) => {
|
|
@@ -15923,10 +18392,10 @@ ${body}`;
|
|
|
15923
18392
|
loop2.abort();
|
|
15924
18393
|
setQueuedSubmit(synthetic);
|
|
15925
18394
|
} else {
|
|
15926
|
-
await
|
|
18395
|
+
await handleSubmit2(synthetic);
|
|
15927
18396
|
}
|
|
15928
18397
|
},
|
|
15929
|
-
[pendingPlan, togglePlanMode, busy, loop2,
|
|
18398
|
+
[pendingPlan, togglePlanMode, busy, loop2, handleSubmit2, persistPlanState]
|
|
15930
18399
|
);
|
|
15931
18400
|
const handlePlanConfirmRef = useRef6(handlePlanConfirm);
|
|
15932
18401
|
useEffect6(() => {
|
|
@@ -15937,9 +18406,13 @@ ${body}`;
|
|
|
15937
18406
|
[]
|
|
15938
18407
|
);
|
|
15939
18408
|
const handleStagedInputSubmit = useCallback4(
|
|
15940
|
-
async (feedback) => {
|
|
15941
|
-
const staged = stagedInput;
|
|
15942
|
-
|
|
18409
|
+
async (feedback, override) => {
|
|
18410
|
+
const staged = override ?? stagedInput;
|
|
18411
|
+
if (override) {
|
|
18412
|
+
setPendingPlan(null);
|
|
18413
|
+
} else {
|
|
18414
|
+
setStagedInput(null);
|
|
18415
|
+
}
|
|
15943
18416
|
if (!staged) return;
|
|
15944
18417
|
const trimmed = feedback.trim();
|
|
15945
18418
|
let synthetic;
|
|
@@ -15980,11 +18453,15 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
15980
18453
|
loop2.abort();
|
|
15981
18454
|
setQueuedSubmit(synthetic);
|
|
15982
18455
|
} else {
|
|
15983
|
-
await
|
|
18456
|
+
await handleSubmit2(synthetic);
|
|
15984
18457
|
}
|
|
15985
18458
|
},
|
|
15986
|
-
[stagedInput, togglePlanMode, busy, loop2,
|
|
18459
|
+
[stagedInput, togglePlanMode, busy, loop2, handleSubmit2]
|
|
15987
18460
|
);
|
|
18461
|
+
const handleStagedInputSubmitRef = useRef6(handleStagedInputSubmit);
|
|
18462
|
+
useEffect6(() => {
|
|
18463
|
+
handleStagedInputSubmitRef.current = handleStagedInputSubmit;
|
|
18464
|
+
}, [handleStagedInputSubmit]);
|
|
15988
18465
|
const handleStagedInputCancel = useCallback4(() => {
|
|
15989
18466
|
if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
|
|
15990
18467
|
setStagedInput(null);
|
|
@@ -16015,10 +18492,10 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
|
|
|
16015
18492
|
loop2.abort();
|
|
16016
18493
|
setQueuedSubmit(synthetic);
|
|
16017
18494
|
} else {
|
|
16018
|
-
await
|
|
18495
|
+
await handleSubmit2(synthetic);
|
|
16019
18496
|
}
|
|
16020
18497
|
},
|
|
16021
|
-
[pendingCheckpoint, busy, loop2,
|
|
18498
|
+
[pendingCheckpoint, busy, loop2, handleSubmit2]
|
|
16022
18499
|
);
|
|
16023
18500
|
const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
|
|
16024
18501
|
useEffect6(() => {
|
|
@@ -16049,10 +18526,10 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
16049
18526
|
loop2.abort();
|
|
16050
18527
|
setQueuedSubmit(synthetic);
|
|
16051
18528
|
} else {
|
|
16052
|
-
await
|
|
18529
|
+
await handleSubmit2(synthetic);
|
|
16053
18530
|
}
|
|
16054
18531
|
},
|
|
16055
|
-
[stagedCheckpointRevise, busy, loop2,
|
|
18532
|
+
[stagedCheckpointRevise, busy, loop2, handleSubmit2]
|
|
16056
18533
|
);
|
|
16057
18534
|
const handleCheckpointReviseCancel = useCallback4(() => {
|
|
16058
18535
|
const snap = stagedCheckpointRevise;
|
|
@@ -16078,7 +18555,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
16078
18555
|
loop2.abort();
|
|
16079
18556
|
setQueuedSubmit(synthetic2);
|
|
16080
18557
|
} else {
|
|
16081
|
-
await
|
|
18558
|
+
await handleSubmit2(synthetic2);
|
|
16082
18559
|
}
|
|
16083
18560
|
return;
|
|
16084
18561
|
}
|
|
@@ -16093,11 +18570,19 @@ If the feedback only tweaks how you execute (extra constraints, style preference
|
|
|
16093
18570
|
loop2.abort();
|
|
16094
18571
|
setQueuedSubmit(synthetic);
|
|
16095
18572
|
} else {
|
|
16096
|
-
await
|
|
18573
|
+
await handleSubmit2(synthetic);
|
|
16097
18574
|
}
|
|
16098
18575
|
},
|
|
16099
|
-
[pendingChoice, busy, loop2,
|
|
18576
|
+
[pendingChoice, busy, loop2, handleSubmit2]
|
|
16100
18577
|
);
|
|
18578
|
+
const handleShellConfirmRef = useRef6(handleShellConfirm);
|
|
18579
|
+
useEffect6(() => {
|
|
18580
|
+
handleShellConfirmRef.current = handleShellConfirm;
|
|
18581
|
+
}, [handleShellConfirm]);
|
|
18582
|
+
const pendingPlanRef = useRef6(null);
|
|
18583
|
+
useEffect6(() => {
|
|
18584
|
+
pendingPlanRef.current = pendingPlan;
|
|
18585
|
+
}, [pendingPlan]);
|
|
16101
18586
|
const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
|
|
16102
18587
|
useEffect6(() => {
|
|
16103
18588
|
handleChoiceConfirmRef.current = handleChoiceConfirm;
|
|
@@ -16124,10 +18609,10 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
|
|
|
16124
18609
|
loop2.abort();
|
|
16125
18610
|
setQueuedSubmit(synthetic);
|
|
16126
18611
|
} else {
|
|
16127
|
-
await
|
|
18612
|
+
await handleSubmit2(synthetic);
|
|
16128
18613
|
}
|
|
16129
18614
|
},
|
|
16130
|
-
[busy, loop2,
|
|
18615
|
+
[busy, loop2, handleSubmit2]
|
|
16131
18616
|
);
|
|
16132
18617
|
const handleChoiceCustomCancel = useCallback4(() => {
|
|
16133
18618
|
const snap = stagedChoiceCustom;
|
|
@@ -16149,7 +18634,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
|
|
|
16149
18634
|
loop2.abort();
|
|
16150
18635
|
setQueuedSubmit(synthetic2);
|
|
16151
18636
|
} else {
|
|
16152
|
-
await
|
|
18637
|
+
await handleSubmit2(synthetic2);
|
|
16153
18638
|
}
|
|
16154
18639
|
return;
|
|
16155
18640
|
}
|
|
@@ -16184,10 +18669,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
16184
18669
|
loop2.abort();
|
|
16185
18670
|
setQueuedSubmit(synthetic);
|
|
16186
18671
|
} else {
|
|
16187
|
-
await
|
|
18672
|
+
await handleSubmit2(synthetic);
|
|
16188
18673
|
}
|
|
16189
18674
|
},
|
|
16190
|
-
[pendingRevision, busy, loop2,
|
|
18675
|
+
[pendingRevision, busy, loop2, handleSubmit2, persistPlanState]
|
|
16191
18676
|
);
|
|
16192
18677
|
const handleReviseConfirmRef = useRef6(handleReviseConfirm);
|
|
16193
18678
|
useEffect6(() => {
|
|
@@ -16300,10 +18785,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
16300
18785
|
{
|
|
16301
18786
|
block: pendingEditReview,
|
|
16302
18787
|
onChoose: (choice) => {
|
|
16303
|
-
const
|
|
16304
|
-
if (
|
|
18788
|
+
const resolve13 = editReviewResolveRef.current;
|
|
18789
|
+
if (resolve13) {
|
|
16305
18790
|
editReviewResolveRef.current = null;
|
|
16306
|
-
|
|
18791
|
+
resolve13(choice);
|
|
16307
18792
|
}
|
|
16308
18793
|
}
|
|
16309
18794
|
}
|
|
@@ -16329,7 +18814,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
|
|
|
16329
18814
|
{
|
|
16330
18815
|
value: input,
|
|
16331
18816
|
onChange: setInput,
|
|
16332
|
-
onSubmit:
|
|
18817
|
+
onSubmit: handleSubmit2,
|
|
16333
18818
|
disabled: busy,
|
|
16334
18819
|
onHistoryPrev: recallPrev,
|
|
16335
18820
|
onHistoryNext: recallNext
|
|
@@ -16409,7 +18894,7 @@ function Setup({ onReady }) {
|
|
|
16409
18894
|
const [value, setValue] = useState11("");
|
|
16410
18895
|
const [error, setError] = useState11(null);
|
|
16411
18896
|
const { exit: exit2 } = useApp2();
|
|
16412
|
-
const
|
|
18897
|
+
const handleSubmit2 = (raw) => {
|
|
16413
18898
|
const trimmed = raw.trim();
|
|
16414
18899
|
if (trimmed === "/exit" || trimmed === "/quit") {
|
|
16415
18900
|
exit2();
|
|
@@ -16433,7 +18918,7 @@ function Setup({ onReady }) {
|
|
|
16433
18918
|
{
|
|
16434
18919
|
value,
|
|
16435
18920
|
onChange: setValue,
|
|
16436
|
-
onSubmit:
|
|
18921
|
+
onSubmit: handleSubmit2,
|
|
16437
18922
|
mask: "\u2022",
|
|
16438
18923
|
placeholder: "sk-..."
|
|
16439
18924
|
}
|
|
@@ -16513,7 +18998,7 @@ async function chatCommand(opts) {
|
|
|
16513
18998
|
try {
|
|
16514
18999
|
const spec = parseMcpSpec(raw);
|
|
16515
19000
|
const prefix = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
16516
|
-
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
19001
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
16517
19002
|
const mcp3 = new McpClient({ transport });
|
|
16518
19003
|
await mcp3.initialize();
|
|
16519
19004
|
const bridge = await bridgeMcpTools(mcp3, {
|
|
@@ -16535,7 +19020,7 @@ async function chatCommand(opts) {
|
|
|
16535
19020
|
};
|
|
16536
19021
|
}
|
|
16537
19022
|
const label = spec.name ?? "anon";
|
|
16538
|
-
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
19023
|
+
const source = spec.transport === "sse" || spec.transport === "streamable-http" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
16539
19024
|
process.stderr.write(
|
|
16540
19025
|
`\u25B8 MCP[${label}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
16541
19026
|
`
|
|
@@ -16578,7 +19063,7 @@ async function chatCommand(opts) {
|
|
|
16578
19063
|
const prior = loadSessionMessages(opts.session);
|
|
16579
19064
|
if (prior.length > 0) {
|
|
16580
19065
|
const p = sessionPath(opts.session);
|
|
16581
|
-
const mtime =
|
|
19066
|
+
const mtime = existsSync22(p) ? statSync13(p).mtime : /* @__PURE__ */ new Date();
|
|
16582
19067
|
sessionPreview = { messageCount: prior.length, lastActive: mtime };
|
|
16583
19068
|
}
|
|
16584
19069
|
} else if (opts.session && opts.forceNew) {
|
|
@@ -16609,7 +19094,7 @@ async function chatCommand(opts) {
|
|
|
16609
19094
|
}
|
|
16610
19095
|
|
|
16611
19096
|
// src/cli/commands/code.tsx
|
|
16612
|
-
import { basename as basename2, resolve as
|
|
19097
|
+
import { basename as basename2, resolve as resolve11 } from "path";
|
|
16613
19098
|
|
|
16614
19099
|
// src/index/semantic/builder.ts
|
|
16615
19100
|
import { promises as fs5 } from "fs";
|
|
@@ -17256,8 +19741,8 @@ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
|
|
|
17256
19741
|
|
|
17257
19742
|
// src/cli/commands/code.tsx
|
|
17258
19743
|
async function codeCommand(opts = {}) {
|
|
17259
|
-
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-
|
|
17260
|
-
const rootDir =
|
|
19744
|
+
const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-HNDDXDRH.js");
|
|
19745
|
+
const rootDir = resolve11(opts.dir ?? process.cwd());
|
|
17261
19746
|
const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
|
|
17262
19747
|
const tools = new ToolRegistry();
|
|
17263
19748
|
const jobs2 = new JobRegistry();
|
|
@@ -17304,7 +19789,7 @@ async function codeCommand(opts = {}) {
|
|
|
17304
19789
|
}
|
|
17305
19790
|
|
|
17306
19791
|
// src/cli/commands/diff.ts
|
|
17307
|
-
import { writeFileSync as
|
|
19792
|
+
import { writeFileSync as writeFileSync12 } from "fs";
|
|
17308
19793
|
import { basename as basename3 } from "path";
|
|
17309
19794
|
import { render as render2 } from "ink";
|
|
17310
19795
|
import React30 from "react";
|
|
@@ -17451,7 +19936,7 @@ async function diffCommand(opts) {
|
|
|
17451
19936
|
if (wantMarkdown) {
|
|
17452
19937
|
console.log(renderSummaryTable(report));
|
|
17453
19938
|
const md = renderMarkdown(report);
|
|
17454
|
-
|
|
19939
|
+
writeFileSync12(opts.mdPath, md, "utf8");
|
|
17455
19940
|
console.log(`
|
|
17456
19941
|
markdown report written to ${opts.mdPath}`);
|
|
17457
19942
|
return;
|
|
@@ -17468,7 +19953,7 @@ markdown report written to ${opts.mdPath}`);
|
|
|
17468
19953
|
}
|
|
17469
19954
|
|
|
17470
19955
|
// src/cli/commands/index.ts
|
|
17471
|
-
import { resolve as
|
|
19956
|
+
import { resolve as resolve12 } from "path";
|
|
17472
19957
|
|
|
17473
19958
|
// src/index/semantic/preflight.ts
|
|
17474
19959
|
import { stdin as stdin2, stdout } from "process";
|
|
@@ -17542,7 +20027,7 @@ async function confirm(question, defaultYes) {
|
|
|
17542
20027
|
|
|
17543
20028
|
// src/cli/commands/index.ts
|
|
17544
20029
|
async function indexCommand(opts = {}) {
|
|
17545
|
-
const root =
|
|
20030
|
+
const root = resolve12(opts.dir ?? process.cwd());
|
|
17546
20031
|
const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
|
|
17547
20032
|
const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
|
|
17548
20033
|
const preflightOk = await ollamaPreflight({
|
|
@@ -17664,7 +20149,7 @@ function makeTtyWriter() {
|
|
|
17664
20149
|
// src/cli/commands/mcp-inspect.ts
|
|
17665
20150
|
async function mcpInspectCommand(opts) {
|
|
17666
20151
|
const spec = parseMcpSpec(opts.spec);
|
|
17667
|
-
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
20152
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
17668
20153
|
const client = new McpClient({ transport });
|
|
17669
20154
|
try {
|
|
17670
20155
|
await client.initialize();
|
|
@@ -17996,11 +20481,11 @@ async function runCommand2(opts) {
|
|
|
17996
20481
|
try {
|
|
17997
20482
|
const spec = parseMcpSpec(raw);
|
|
17998
20483
|
const prefix2 = spec.name ? `${spec.name}_` : requestedSpecs.length === 1 && opts.mcpPrefix ? opts.mcpPrefix : "";
|
|
17999
|
-
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
20484
|
+
const transport = spec.transport === "sse" ? new SseTransport({ url: spec.url }) : spec.transport === "streamable-http" ? new StreamableHttpTransport({ url: spec.url }) : new StdioTransport({ command: spec.command, args: spec.args });
|
|
18000
20485
|
const mcp3 = new McpClient({ transport });
|
|
18001
20486
|
await mcp3.initialize();
|
|
18002
20487
|
const bridge = await bridgeMcpTools(mcp3, { registry: tools, namePrefix: prefix2 });
|
|
18003
|
-
const source = spec.transport === "sse" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
20488
|
+
const source = spec.transport === "sse" || spec.transport === "streamable-http" ? spec.url : `${spec.command} ${spec.args.join(" ")}`;
|
|
18004
20489
|
process.stderr.write(
|
|
18005
20490
|
`\u25B8 MCP[${spec.name ?? "anon"}]: ${bridge.registeredNames.length} tool(s) from ${source}
|
|
18006
20491
|
`
|
|
@@ -18166,38 +20651,6 @@ import React34 from "react";
|
|
|
18166
20651
|
import { Box as Box28, Text as Text26, useApp as useApp5, useInput as useInput3 } from "ink";
|
|
18167
20652
|
import TextInput2 from "ink-text-input";
|
|
18168
20653
|
import React33, { useState as useState15 } from "react";
|
|
18169
|
-
|
|
18170
|
-
// src/cli/ui/presets.ts
|
|
18171
|
-
var PRESETS = {
|
|
18172
|
-
// fast — flash + effort=high. Quick Q&A, one-line tweaks, anything
|
|
18173
|
-
// where shallow reasoning is enough. Cheapest turn possible.
|
|
18174
|
-
fast: { model: "deepseek-v4-flash", reasoningEffort: "high", harvest: false, branch: 1 },
|
|
18175
|
-
// smart — flash + effort=max. Full thinking budget on the cheap
|
|
18176
|
-
// model. The default: handles 90%+ of coding work at a fraction
|
|
18177
|
-
// of pro's cost.
|
|
18178
|
-
smart: { model: "deepseek-v4-flash", reasoningEffort: "max", harvest: false, branch: 1 },
|
|
18179
|
-
// max — pro + effort=max. Frontier model for hard tasks: cross-
|
|
18180
|
-
// file architecture, subtle bug hunts, anything where flash's
|
|
18181
|
-
// reasoning has measurably failed. ~12× per-token vs flash; save
|
|
18182
|
-
// for when you need it, or use `/pro` to escalate a single turn.
|
|
18183
|
-
max: { model: "deepseek-v4-pro", reasoningEffort: "max", harvest: false, branch: 1 }
|
|
18184
|
-
};
|
|
18185
|
-
var PRESET_DESCRIPTIONS = {
|
|
18186
|
-
fast: {
|
|
18187
|
-
headline: "v4-flash \xB7 effort=high",
|
|
18188
|
-
cost: "cheapest \xB7 quick Q&A, one-line edits"
|
|
18189
|
-
},
|
|
18190
|
-
smart: {
|
|
18191
|
-
headline: "v4-flash \xB7 effort=max",
|
|
18192
|
-
cost: "~1.5\xD7 fast \xB7 default \xB7 day-to-day coding"
|
|
18193
|
-
},
|
|
18194
|
-
max: {
|
|
18195
|
-
headline: "v4-pro \xB7 effort=max",
|
|
18196
|
-
cost: "~12\xD7 fast \xB7 hard single-shots \xB7 use /pro for a single-turn bump"
|
|
18197
|
-
}
|
|
18198
|
-
};
|
|
18199
|
-
|
|
18200
|
-
// src/cli/ui/Wizard.tsx
|
|
18201
20654
|
var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
|
|
18202
20655
|
function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
|
|
18203
20656
|
const { exit: exit2 } = useApp5();
|
|
@@ -18394,7 +20847,7 @@ function SummaryLine({ label, value }) {
|
|
|
18394
20847
|
return /* @__PURE__ */ React33.createElement(Box28, null, /* @__PURE__ */ React33.createElement(Text26, null, label.padEnd(12)), /* @__PURE__ */ React33.createElement(Text26, { bold: true }, value));
|
|
18395
20848
|
}
|
|
18396
20849
|
function presetItems() {
|
|
18397
|
-
return ["
|
|
20850
|
+
return ["auto", "flash", "pro"].map((name) => ({
|
|
18398
20851
|
value: name,
|
|
18399
20852
|
label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
|
|
18400
20853
|
hint: PRESET_DESCRIPTIONS[name].cost
|
|
@@ -18495,13 +20948,13 @@ function planUpdate(input) {
|
|
|
18495
20948
|
};
|
|
18496
20949
|
}
|
|
18497
20950
|
function defaultSpawn(argv) {
|
|
18498
|
-
return new Promise((
|
|
20951
|
+
return new Promise((resolve13, reject) => {
|
|
18499
20952
|
const child = spawn6(argv[0], argv.slice(1), {
|
|
18500
20953
|
stdio: "inherit",
|
|
18501
20954
|
shell: process.platform === "win32"
|
|
18502
20955
|
});
|
|
18503
20956
|
child.once("error", reject);
|
|
18504
|
-
child.once("exit", (code) =>
|
|
20957
|
+
child.once("exit", (code) => resolve13(code ?? 1));
|
|
18505
20958
|
});
|
|
18506
20959
|
}
|
|
18507
20960
|
async function updateCommand(opts = {}) {
|
|
@@ -18551,7 +21004,7 @@ function versionCommand() {
|
|
|
18551
21004
|
function resolveDefaults(flags) {
|
|
18552
21005
|
const cfg = flags.noConfig ? {} : readConfig();
|
|
18553
21006
|
const preset2 = pickPreset(flags.preset, cfg.preset);
|
|
18554
|
-
const presetSettings =
|
|
21007
|
+
const presetSettings = resolvePreset(preset2);
|
|
18555
21008
|
const model2 = flags.model ?? presetSettings.model;
|
|
18556
21009
|
const reasoningEffort = presetSettings.reasoningEffort;
|
|
18557
21010
|
const harvest3 = flags.harvest === true ? true : presetSettings.harvest;
|
|
@@ -18564,10 +21017,12 @@ function resolveDefaults(flags) {
|
|
|
18564
21017
|
function pickPreset(flagPreset, configPreset) {
|
|
18565
21018
|
if (flagPreset && isPresetName(flagPreset)) return flagPreset;
|
|
18566
21019
|
if (configPreset) return configPreset;
|
|
18567
|
-
return "
|
|
21020
|
+
return "auto";
|
|
18568
21021
|
}
|
|
18569
21022
|
function isPresetName(s) {
|
|
18570
|
-
return s === "
|
|
21023
|
+
return s === "auto" || s === "flash" || s === "pro" || // Legacy names — kept callable so old `--preset smart` invocations
|
|
21024
|
+
// and stale config.json entries don't error out.
|
|
21025
|
+
s === "fast" || s === "smart" || s === "max";
|
|
18571
21026
|
}
|
|
18572
21027
|
function normalizeBranch(raw) {
|
|
18573
21028
|
if (raw === void 0) return void 0;
|