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/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-JDVY4JDU.js";
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((resolve12, reject) => {
160
- const timer = setTimeout(resolve12, ms);
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((resolve12) => {
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
- resolve12({
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
- resolve12({
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 sig = signature(call);
1477
- if (!sig) return { suppress: false };
1478
- const count = this.recent.reduce(
1479
- (n, [name, args]) => name === sig[0] && args === sig[1] ? n + 1 : n,
1480
- 0
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: ${sig[0]} called with identical args ${count + 1} times within window=${this.windowSize}`
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(sig);
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(signature2));
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(signature2(sc))) {
1632
+ if (!seenSignatures.has(signature(sc))) {
1606
1633
  merged.push(sc);
1607
1634
  report.scavenged++;
1608
- seenSignatures.add(signature2(sc));
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 signature2(call) {
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
- this.repair = new ToolCallRepair({ allowedToolNames: allowedNames });
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((resolve12) => {
2446
- waiter = resolve12;
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((resolve12, reject) => {
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
- resolve12({ exitCode: code, output, timedOut });
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((resolve12, reject) => {
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: resolve12,
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((resolve12, reject) => {
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 resolve12();
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((resolve12) => {
6574
- this.waiters.push(resolve12);
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((resolve12, reject) => {
6641
- this.resolveEndpoint = resolve12;
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((resolve12) => {
6669
- this.waiters.push(resolve12);
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 existsSync16, statSync as statSync9 } from "fs";
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 existsSync11, statSync as statSync6 } from "fs";
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 (!existsSync11(abs)) {
9453
+ if (!existsSync17(abs)) {
7592
9454
  throw new Error(`change_workspace: path does not exist \u2014 ${abs}`);
7593
9455
  }
7594
9456
  try {
7595
- if (!statSync6(abs).isDirectory()) {
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 readFileSync13, statSync as statSync7 } from "fs";
8405
- import { isAbsolute as isAbsolute4, join as join12 } from "path";
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 = isAbsolute4(normalized) ? normalized : join12(projectRoot, normalized);
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 = statSync7(candidate);
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 = readFileSync13(fullPath, "utf8").split("\n").length;
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 existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11186
- import { homedir as homedir6 } from "os";
11187
- import { dirname as dirname10, join as join13 } from "path";
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(join13(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
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 = homedir6()) {
11224
- return join13(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
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 (!existsSync12(path5)) {
11235
- mkdirSync8(dirname10(path5), { recursive: true });
11236
- writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
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 = readFileSync14(path5, "utf8");
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 existsSync14, statSync as statSync8 } from "fs";
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 existsSync13, readFileSync as readFileSync15 } from "fs";
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 (!existsSync13(path5)) {
13683
+ if (!existsSync19(path5)) {
11755
13684
  console.error(`no such transcript: ${path5}`);
11756
13685
  process.exit(1);
11757
13686
  }
11758
- const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
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(70);
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 (!existsSync14(abs)) {
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 = statSync8(abs).isDirectory();
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 handlers3 = {
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 existsSync15 } from "fs";
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 (existsSync15(target) && !force) {
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 handlers4 = {
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 handlers5 = {
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 handlers6 = { mcp };
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 handlers7 = { memory };
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 handlers8 = {
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 handlers9 = {
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 handlers10 = {
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((resolve12) => {
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) => resolve12(code ?? -1));
13488
- child.once("error", () => resolve12(-1));
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 handlers11 = {
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 handlers12 = {
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 handlers13 = {
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 resolve12 = editReviewResolveRef.current;
14636
- if (resolve12) {
16851
+ const resolve13 = editReviewResolveRef.current;
16852
+ if (resolve13) {
14637
16853
  editReviewResolveRef.current = null;
14638
16854
  setPendingEditReview(null);
14639
- resolve12("reject");
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 handleSubmit = useCallback4(
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 = handleSubmit;
15720
- }, [handleSubmit]);
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 handleSubmit(synthetic);
18317
+ await handleSubmit2(synthetic);
15849
18318
  }
15850
18319
  },
15851
- [pendingShell, codeMode, currentRootDir, handleSubmit, busy, loop2]
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 handleSubmit(text);
18326
+ void handleSubmit2(text);
15858
18327
  }
15859
- }, [busy, queuedSubmit, handleSubmit]);
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 handleSubmit(synthetic);
18358
+ await handleSubmit2(synthetic);
15890
18359
  }
15891
18360
  },
15892
- [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit]
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 handleSubmit(synthetic);
18395
+ await handleSubmit2(synthetic);
15927
18396
  }
15928
18397
  },
15929
- [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
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
- setStagedInput(null);
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 handleSubmit(synthetic);
18456
+ await handleSubmit2(synthetic);
15984
18457
  }
15985
18458
  },
15986
- [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
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 handleSubmit(synthetic);
18495
+ await handleSubmit2(synthetic);
16019
18496
  }
16020
18497
  },
16021
- [pendingCheckpoint, busy, loop2, handleSubmit]
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 handleSubmit(synthetic);
18529
+ await handleSubmit2(synthetic);
16053
18530
  }
16054
18531
  },
16055
- [stagedCheckpointRevise, busy, loop2, handleSubmit]
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 handleSubmit(synthetic2);
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 handleSubmit(synthetic);
18573
+ await handleSubmit2(synthetic);
16097
18574
  }
16098
18575
  },
16099
- [pendingChoice, busy, loop2, handleSubmit]
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 handleSubmit(synthetic);
18612
+ await handleSubmit2(synthetic);
16128
18613
  }
16129
18614
  },
16130
- [busy, loop2, handleSubmit]
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 handleSubmit(synthetic2);
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 handleSubmit(synthetic);
18672
+ await handleSubmit2(synthetic);
16188
18673
  }
16189
18674
  },
16190
- [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
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 resolve12 = editReviewResolveRef.current;
16304
- if (resolve12) {
18788
+ const resolve13 = editReviewResolveRef.current;
18789
+ if (resolve13) {
16305
18790
  editReviewResolveRef.current = null;
16306
- resolve12(choice);
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: handleSubmit,
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 handleSubmit = (raw) => {
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: handleSubmit,
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 = existsSync16(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
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 resolve10 } from "path";
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-YRY4HPMZ.js");
17260
- const rootDir = resolve10(opts.dir ?? process.cwd());
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 writeFileSync8 } from "fs";
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
- writeFileSync8(opts.mdPath, md, "utf8");
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 resolve11 } from "path";
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 = resolve11(opts.dir ?? process.cwd());
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 ["fast", "smart", "max"].map((name) => ({
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((resolve12, reject) => {
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) => resolve12(code ?? 1));
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 = PRESETS[preset2];
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 "smart";
21020
+ return "auto";
18568
21021
  }
18569
21022
  function isPresetName(s) {
18570
- return s === "fast" || s === "smart" || s === "max";
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;