reasonix 0.11.3 → 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";
@@ -179,8 +181,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
179
181
  }
180
182
  function sleep(ms, signal) {
181
183
  if (ms <= 0) return Promise.resolve();
182
- return new Promise((resolve12, reject) => {
183
- const timer = setTimeout(resolve12, ms);
184
+ return new Promise((resolve13, reject) => {
185
+ const timer = setTimeout(resolve13, ms);
184
186
  if (signal) {
185
187
  const onAbort = () => {
186
188
  clearTimeout(timer);
@@ -665,7 +667,7 @@ function matchesTool(hook, toolName) {
665
667
  }
666
668
  }
667
669
  function defaultSpawner(input) {
668
- return new Promise((resolve12) => {
670
+ return new Promise((resolve13) => {
669
671
  const child = spawn(input.command, {
670
672
  cwd: input.cwd,
671
673
  shell: true,
@@ -692,7 +694,7 @@ function defaultSpawner(input) {
692
694
  });
693
695
  child.once("error", (err) => {
694
696
  clearTimeout(timer);
695
- resolve12({
697
+ resolve13({
696
698
  exitCode: null,
697
699
  stdout: stdout3,
698
700
  stderr,
@@ -702,7 +704,7 @@ function defaultSpawner(input) {
702
704
  });
703
705
  child.once("close", (code) => {
704
706
  clearTimeout(timer);
705
- resolve12({
707
+ resolve13({
706
708
  exitCode: code,
707
709
  stdout: stdout3.trim(),
708
710
  stderr: stderr.trim(),
@@ -1490,25 +1492,32 @@ function coerceToToolCall(candidateJson, allowedNames) {
1490
1492
  var StormBreaker = class {
1491
1493
  windowSize;
1492
1494
  threshold;
1495
+ isMutating;
1493
1496
  recent = [];
1494
- constructor(windowSize = 6, threshold = 3) {
1497
+ constructor(windowSize = 6, threshold = 3, isMutating) {
1495
1498
  this.windowSize = windowSize;
1496
1499
  this.threshold = threshold;
1500
+ this.isMutating = isMutating;
1497
1501
  }
1498
1502
  inspect(call) {
1499
- const sig = signature(call);
1500
- if (!sig) return { suppress: false };
1501
- const count = this.recent.reduce(
1502
- (n, [name, args]) => name === sig[0] && args === sig[1] ? n + 1 : n,
1503
- 0
1504
- );
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);
1505
1514
  if (count >= this.threshold - 1) {
1506
1515
  return {
1507
1516
  suppress: true,
1508
- 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}`
1509
1518
  };
1510
1519
  }
1511
- this.recent.push(sig);
1520
+ this.recent.push({ name, args, readOnly });
1512
1521
  while (this.recent.length > this.windowSize) this.recent.shift();
1513
1522
  return { suppress: false };
1514
1523
  }
@@ -1516,11 +1525,6 @@ var StormBreaker = class {
1516
1525
  this.recent.length = 0;
1517
1526
  }
1518
1527
  };
1519
- function signature(call) {
1520
- const name = call.function?.name;
1521
- if (!name) return null;
1522
- return [name, call.function?.arguments ?? ""];
1523
- }
1524
1528
 
1525
1529
  // src/repair/truncation.ts
1526
1530
  function repairTruncatedJson(input) {
@@ -1598,7 +1602,7 @@ var ToolCallRepair = class {
1598
1602
  opts;
1599
1603
  constructor(opts) {
1600
1604
  this.opts = opts;
1601
- this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3);
1605
+ this.storm = new StormBreaker(opts.stormWindow ?? 6, opts.stormThreshold ?? 3, opts.isMutating);
1602
1606
  }
1603
1607
  /**
1604
1608
  * Drop the StormBreaker's sliding window of recent (name, args)
@@ -1622,13 +1626,13 @@ var ToolCallRepair = class {
1622
1626
  allowedNames: this.opts.allowedToolNames,
1623
1627
  maxCalls: this.opts.maxScavenge ?? 4
1624
1628
  });
1625
- const seenSignatures = new Set(declaredCalls.map(signature2));
1629
+ const seenSignatures = new Set(declaredCalls.map(signature));
1626
1630
  const merged = [...declaredCalls];
1627
1631
  for (const sc of scavenged.calls) {
1628
- if (!seenSignatures.has(signature2(sc))) {
1632
+ if (!seenSignatures.has(signature(sc))) {
1629
1633
  merged.push(sc);
1630
1634
  report.scavenged++;
1631
- seenSignatures.add(signature2(sc));
1635
+ seenSignatures.add(signature(sc));
1632
1636
  }
1633
1637
  }
1634
1638
  report.notes.push(...scavenged.notes);
@@ -1654,7 +1658,7 @@ var ToolCallRepair = class {
1654
1658
  return { calls: filtered, report };
1655
1659
  }
1656
1660
  };
1657
- function signature2(call) {
1661
+ function signature(call) {
1658
1662
  return `${call.function?.name ?? ""}::${call.function?.arguments ?? ""}`;
1659
1663
  }
1660
1664
 
@@ -1793,6 +1797,12 @@ function outputCostUsd(model2, usage) {
1793
1797
  if (!p) return 0;
1794
1798
  return usage.completionTokens * p.output / 1e6;
1795
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
+ }
1796
1806
  function claudeEquivalentCost(usage) {
1797
1807
  return (usage.promptTokens * CLAUDE_SONNET_PRICING.input + usage.completionTokens * CLAUDE_SONNET_PRICING.output) / 1e6;
1798
1808
  }
@@ -1883,6 +1893,13 @@ var CacheFirstLoop = class {
1883
1893
  branchOptions;
1884
1894
  /** See ReconfigurableOptions — mutable so `/effort` can flip mid-session. */
1885
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;
1886
1903
  sessionName;
1887
1904
  /**
1888
1905
  * Hook list, mutable so `/hooks reload` can swap it without
@@ -1947,6 +1964,7 @@ var CacheFirstLoop = class {
1947
1964
  this.tools = opts.tools ?? new ToolRegistry();
1948
1965
  this.model = opts.model ?? "deepseek-v4-flash";
1949
1966
  this.reasoningEffort = opts.reasoningEffort ?? "max";
1967
+ if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
1950
1968
  this.maxToolIters = opts.maxToolIters ?? 64;
1951
1969
  this.hooks = opts.hooks ?? [];
1952
1970
  this.hookCwd = opts.hookCwd ?? process.cwd();
@@ -1964,7 +1982,26 @@ var CacheFirstLoop = class {
1964
1982
  this._streamPreference = opts.stream ?? true;
1965
1983
  this.stream = this.branchEnabled ? false : this._streamPreference;
1966
1984
  const allowedNames = /* @__PURE__ */ new Set([...this.prefix.toolSpecs.map((s) => s.function.name)]);
1967
- 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 });
1968
2005
  this.sessionName = opts.session ?? null;
1969
2006
  if (this.sessionName) {
1970
2007
  const prior = loadSessionMessages(this.sessionName);
@@ -2145,6 +2182,7 @@ var CacheFirstLoop = class {
2145
2182
  if (opts.model !== void 0) this.model = opts.model;
2146
2183
  if (opts.stream !== void 0) this._streamPreference = opts.stream;
2147
2184
  if (opts.reasoningEffort !== void 0) this.reasoningEffort = opts.reasoningEffort;
2185
+ if (opts.autoEscalate !== void 0) this.autoEscalate = opts.autoEscalate;
2148
2186
  if (opts.branch !== void 0) {
2149
2187
  if (typeof opts.branch === "number") {
2150
2188
  this.branchOptions = { budget: opts.branch };
@@ -2260,7 +2298,7 @@ var CacheFirstLoop = class {
2260
2298
  if (repair.truncationsFixed > 0) bump("truncated", repair.truncationsFixed);
2261
2299
  if (repair.stormsBroken > 0) bump("storm-broken", repair.stormsBroken);
2262
2300
  }
2263
- if (bumped && !this._escalateThisTurn && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2301
+ if (bumped && !this._escalateThisTurn && this.autoEscalate && this._turnFailureCount >= FAILURE_ESCALATION_THRESHOLD) {
2264
2302
  this._escalateThisTurn = true;
2265
2303
  return true;
2266
2304
  }
@@ -2465,8 +2503,8 @@ var CacheFirstLoop = class {
2465
2503
  }
2466
2504
  );
2467
2505
  for (let k = 0; k < budget; k++) {
2468
- const sample = queue.shift() ?? await new Promise((resolve12) => {
2469
- waiter = resolve12;
2506
+ const sample = queue.shift() ?? await new Promise((resolve13) => {
2507
+ waiter = resolve13;
2470
2508
  });
2471
2509
  yield {
2472
2510
  turn: this._turn,
@@ -2505,7 +2543,7 @@ var CacheFirstLoop = class {
2505
2543
  const callBuf = /* @__PURE__ */ new Map();
2506
2544
  const readyIndices = /* @__PURE__ */ new Set();
2507
2545
  const callModel = this.modelForCurrentCall();
2508
- const bufferForEscalation = callModel !== ESCALATION_MODEL;
2546
+ const bufferForEscalation = this.autoEscalate && callModel !== ESCALATION_MODEL;
2509
2547
  let escalationBuf = "";
2510
2548
  let escalationBufFlushed = false;
2511
2549
  for await (const chunk of this.client.stream({
@@ -2617,7 +2655,7 @@ var CacheFirstLoop = class {
2617
2655
  };
2618
2656
  return;
2619
2657
  }
2620
- if (this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
2658
+ if (this.autoEscalate && this.modelForCurrentCall() !== ESCALATION_MODEL && this.isEscalationRequest(assistantContent)) {
2621
2659
  const { reason } = this.parseEscalationMarker(assistantContent);
2622
2660
  this._escalateThisTurn = true;
2623
2661
  const reasonSuffix = reason ? ` \u2014 ${reason}` : "";
@@ -3199,6 +3237,9 @@ var DEFAULT_PICKER_IGNORE_DIRS = [
3199
3237
  "venv",
3200
3238
  "__pycache__"
3201
3239
  ];
3240
+ function listFilesSync(root, opts = {}) {
3241
+ return listFilesWithStatsSync(root, opts).map((e) => e.path);
3242
+ }
3202
3243
  function listFilesWithStatsSync(root, opts = {}) {
3203
3244
  const maxResults = Math.max(1, opts.maxResults ?? 500);
3204
3245
  const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
@@ -5196,7 +5237,7 @@ async function runCommand(cmd, opts) {
5196
5237
  };
5197
5238
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5198
5239
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5199
- return await new Promise((resolve12, reject) => {
5240
+ return await new Promise((resolve13, reject) => {
5200
5241
  let child;
5201
5242
  try {
5202
5243
  child = spawn3(bin, args, effectiveSpawnOpts);
@@ -5241,7 +5282,7 @@ async function runCommand(cmd, opts) {
5241
5282
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5242
5283
 
5243
5284
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5244
- resolve12({ exitCode: code, output, timedOut });
5285
+ resolve13({ exitCode: code, output, timedOut });
5245
5286
  });
5246
5287
  });
5247
5288
  }
@@ -6445,7 +6486,7 @@ var McpClient = class {
6445
6486
  const id = this.nextId++;
6446
6487
  const frame = { jsonrpc: "2.0", id, method, params };
6447
6488
  let abortHandler = null;
6448
- const promise = new Promise((resolve12, reject) => {
6489
+ const promise = new Promise((resolve13, reject) => {
6449
6490
  const timeout = setTimeout(() => {
6450
6491
  this.pending.delete(id);
6451
6492
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -6454,7 +6495,7 @@ var McpClient = class {
6454
6495
  );
6455
6496
  }, this.requestTimeoutMs);
6456
6497
  this.pending.set(id, {
6457
- resolve: resolve12,
6498
+ resolve: resolve13,
6458
6499
  reject,
6459
6500
  timeout
6460
6501
  });
@@ -6577,12 +6618,12 @@ var StdioTransport = class {
6577
6618
  }
6578
6619
  async send(message) {
6579
6620
  if (this.closed) throw new Error("MCP transport is closed");
6580
- return new Promise((resolve12, reject) => {
6621
+ return new Promise((resolve13, reject) => {
6581
6622
  const line = `${JSON.stringify(message)}
6582
6623
  `;
6583
6624
  this.child.stdin.write(line, "utf8", (err) => {
6584
6625
  if (err) reject(err);
6585
- else resolve12();
6626
+ else resolve13();
6586
6627
  });
6587
6628
  });
6588
6629
  }
@@ -6593,8 +6634,8 @@ var StdioTransport = class {
6593
6634
  continue;
6594
6635
  }
6595
6636
  if (this.closed) return;
6596
- const next = await new Promise((resolve12) => {
6597
- this.waiters.push(resolve12);
6637
+ const next = await new Promise((resolve13) => {
6638
+ this.waiters.push(resolve13);
6598
6639
  });
6599
6640
  if (next === null) return;
6600
6641
  yield next;
@@ -6660,8 +6701,8 @@ var SseTransport = class {
6660
6701
  constructor(opts) {
6661
6702
  this.url = opts.url;
6662
6703
  this.headers = opts.headers ?? {};
6663
- this.endpointReady = new Promise((resolve12, reject) => {
6664
- this.resolveEndpoint = resolve12;
6704
+ this.endpointReady = new Promise((resolve13, reject) => {
6705
+ this.resolveEndpoint = resolve13;
6665
6706
  this.rejectEndpoint = reject;
6666
6707
  });
6667
6708
  this.endpointReady.catch(() => void 0);
@@ -6688,8 +6729,8 @@ var SseTransport = class {
6688
6729
  continue;
6689
6730
  }
6690
6731
  if (this.closed) return;
6691
- const next = await new Promise((resolve12) => {
6692
- this.waiters.push(resolve12);
6732
+ const next = await new Promise((resolve13) => {
6733
+ this.waiters.push(resolve13);
6693
6734
  });
6694
6735
  if (next === null) return;
6695
6736
  yield next;
@@ -6876,8 +6917,8 @@ var StreamableHttpTransport = class {
6876
6917
  continue;
6877
6918
  }
6878
6919
  if (this.closed) return;
6879
- const next = await new Promise((resolve12) => {
6880
- this.waiters.push(resolve12);
6920
+ const next = await new Promise((resolve13) => {
6921
+ this.waiters.push(resolve13);
6881
6922
  });
6882
6923
  if (next === null) return;
6883
6924
  yield next;
@@ -7354,7 +7395,8 @@ function emptyBucket(label, since) {
7354
7395
  cacheHitTokens: 0,
7355
7396
  cacheMissTokens: 0,
7356
7397
  costUsd: 0,
7357
- claudeEquivUsd: 0
7398
+ claudeEquivUsd: 0,
7399
+ cacheSavingsUsd: 0
7358
7400
  };
7359
7401
  }
7360
7402
  function addToBucket(b, r) {
@@ -7365,6 +7407,7 @@ function addToBucket(b, r) {
7365
7407
  b.cacheMissTokens += r.cacheMissTokens;
7366
7408
  b.costUsd += r.costUsd;
7367
7409
  b.claudeEquivUsd += r.claudeEquivUsd;
7410
+ b.cacheSavingsUsd += cacheSavingsUsd(r.model, r.cacheHitTokens);
7368
7411
  }
7369
7412
  function aggregateUsage(records, opts = {}) {
7370
7413
  const now = opts.now ?? Date.now();
@@ -7435,7 +7478,7 @@ function formatLogSize(path5 = defaultUsageLogPath()) {
7435
7478
  }
7436
7479
 
7437
7480
  // src/cli/commands/chat.tsx
7438
- import { existsSync as existsSync16, statSync as statSync9 } from "fs";
7481
+ import { existsSync as existsSync22, statSync as statSync13 } from "fs";
7439
7482
  import { render } from "ink";
7440
7483
  import React27, { useState as useState12 } from "react";
7441
7484
 
@@ -7598,62 +7641,1700 @@ function archivePlanState(sessionName) {
7598
7641
  return null;
7599
7642
  }
7600
7643
  }
7601
- function listPlanArchives(sessionName) {
7602
- const dir = sessionsDir();
7603
- if (!existsSync10(dir)) return [];
7604
- const prefix = `${sanitizeName(sessionName)}.plan.`;
7605
- const suffix = ".done.json";
7606
- let entries;
7607
- try {
7608
- entries = readdirSync3(dir);
7609
- } catch {
7610
- return [];
7644
+ function listPlanArchives(sessionName) {
7645
+ const dir = sessionsDir();
7646
+ if (!existsSync10(dir)) return [];
7647
+ const prefix = `${sanitizeName(sessionName)}.plan.`;
7648
+ const suffix = ".done.json";
7649
+ let entries;
7650
+ try {
7651
+ entries = readdirSync3(dir);
7652
+ } catch {
7653
+ return [];
7654
+ }
7655
+ const summaries = [];
7656
+ for (const name of entries) {
7657
+ if (!name.startsWith(prefix) || !name.endsWith(suffix)) continue;
7658
+ const full = join10(dir, name);
7659
+ try {
7660
+ const raw = readFileSync12(full, "utf8");
7661
+ const parsed = JSON.parse(raw);
7662
+ if (parsed.version !== 1) continue;
7663
+ if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) continue;
7664
+ const steps = parsed.steps.filter(
7665
+ (s) => !!s && typeof s === "object" && typeof s.id === "string" && typeof s.title === "string" && typeof s.action === "string"
7666
+ );
7667
+ if (steps.length === 0) continue;
7668
+ const completedStepIds = Array.isArray(parsed.completedStepIds) ? parsed.completedStepIds.filter((id) => typeof id === "string" && !!id) : [];
7669
+ let completedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
7670
+ if (!completedAt || Number.isNaN(Date.parse(completedAt))) {
7671
+ try {
7672
+ completedAt = statSync5(full).mtime.toISOString();
7673
+ } catch {
7674
+ completedAt = (/* @__PURE__ */ new Date(0)).toISOString();
7675
+ }
7676
+ }
7677
+ const entry = { path: full, completedAt, steps, completedStepIds };
7678
+ if (typeof parsed.body === "string" && parsed.body) entry.body = parsed.body;
7679
+ if (typeof parsed.summary === "string" && parsed.summary) entry.summary = parsed.summary;
7680
+ summaries.push(entry);
7681
+ } catch {
7682
+ }
7683
+ }
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;
7611
9257
  }
7612
- const summaries = [];
7613
- for (const name of entries) {
7614
- if (!name.startsWith(prefix) || !name.endsWith(suffix)) continue;
7615
- const full = join10(dir, name);
7616
- try {
7617
- const raw = readFileSync12(full, "utf8");
7618
- const parsed = JSON.parse(raw);
7619
- if (parsed.version !== 1) continue;
7620
- if (!Array.isArray(parsed.steps) || parsed.steps.length === 0) continue;
7621
- const steps = parsed.steps.filter(
7622
- (s) => !!s && typeof s === "object" && typeof s.id === "string" && typeof s.title === "string" && typeof s.action === "string"
7623
- );
7624
- if (steps.length === 0) continue;
7625
- const completedStepIds = Array.isArray(parsed.completedStepIds) ? parsed.completedStepIds.filter((id) => typeof id === "string" && !!id) : [];
7626
- let completedAt = typeof parsed.updatedAt === "string" ? parsed.updatedAt : "";
7627
- if (!completedAt || Number.isNaN(Date.parse(completedAt))) {
7628
- try {
7629
- completedAt = statSync5(full).mtime.toISOString();
7630
- } catch {
7631
- completedAt = (/* @__PURE__ */ new Date(0)).toISOString();
7632
- }
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;
7633
9300
  }
7634
- const entry = { path: full, completedAt, steps, completedStepIds };
7635
- if (typeof parsed.body === "string" && parsed.body) entry.body = parsed.body;
7636
- if (typeof parsed.summary === "string" && parsed.summary) entry.summary = parsed.summary;
7637
- summaries.push(entry);
7638
- } catch {
7639
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;
7640
9306
  }
7641
- summaries.sort((a, b) => b.completedAt.localeCompare(a.completedAt));
7642
- return summaries;
7643
- }
7644
- function relativeTime(updatedAt, now = Date.now()) {
7645
- const t2 = Date.parse(updatedAt);
7646
- if (Number.isNaN(t2)) return updatedAt;
7647
- const diffMs = Math.max(0, now - t2);
7648
- const sec = Math.floor(diffMs / 1e3);
7649
- if (sec < 60) return `${sec}s ago`;
7650
- const min = Math.floor(sec / 60);
7651
- if (min < 60) return `${min}m ago`;
7652
- const hr = Math.floor(min / 60);
7653
- if (hr < 24) return `${hr}h ago`;
7654
- const day = Math.floor(hr / 24);
7655
- if (day < 7) return `${day}d ago`;
7656
- return updatedAt.slice(0, 10);
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
+ });
7657
9338
  }
7658
9339
 
7659
9340
  // src/tools/skills.ts
@@ -7735,7 +9416,7 @@ ${skill2.body}${argsBlock}`;
7735
9416
  }
7736
9417
 
7737
9418
  // src/tools/workspace.ts
7738
- import { existsSync as existsSync11, statSync as statSync6 } from "fs";
9419
+ import { existsSync as existsSync17, statSync as statSync10 } from "fs";
7739
9420
  import * as pathMod4 from "path";
7740
9421
  var WorkspaceConfirmationError = class extends Error {
7741
9422
  path;
@@ -7769,11 +9450,11 @@ function registerWorkspaceTool(registry) {
7769
9450
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7770
9451
  const expanded = args.path.startsWith("~") && home ? pathMod4.join(home, args.path.slice(1)) : args.path;
7771
9452
  const abs = pathMod4.resolve(expanded);
7772
- if (!existsSync11(abs)) {
9453
+ if (!existsSync17(abs)) {
7773
9454
  throw new Error(`change_workspace: path does not exist \u2014 ${abs}`);
7774
9455
  }
7775
9456
  try {
7776
- if (!statSync6(abs).isDirectory()) {
9457
+ if (!statSync10(abs).isDirectory()) {
7777
9458
  throw new Error(`change_workspace: not a directory \u2014 ${abs}`);
7778
9459
  }
7779
9460
  } catch (err) {
@@ -8582,8 +10263,8 @@ function RiskLegend() {
8582
10263
  var PlanStepList = React8.memo(PlanStepListInner);
8583
10264
 
8584
10265
  // src/cli/ui/markdown.tsx
8585
- import { readFileSync as readFileSync13, statSync as statSync7 } from "fs";
8586
- 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";
8587
10268
  import { Box as Box8, Text as Text7 } from "ink";
8588
10269
  import React9 from "react";
8589
10270
  var SUPERSCRIPT = {
@@ -8824,7 +10505,7 @@ function validateCitation(url, projectRoot) {
8824
10505
  const parts = parseCitationUrl(url);
8825
10506
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
8826
10507
  const normalized = parts.path.replace(/^[/\\]+/, "");
8827
- const baseFullPath = isAbsolute4(normalized) ? normalized : join12(projectRoot, normalized);
10508
+ const baseFullPath = isAbsolute5(normalized) ? normalized : join16(projectRoot, normalized);
8828
10509
  const siblings = SIBLING_EXTENSIONS.get(extOf(baseFullPath)) ?? [];
8829
10510
  const candidates = [
8830
10511
  baseFullPath,
@@ -8834,7 +10515,7 @@ function validateCitation(url, projectRoot) {
8834
10515
  let stat = null;
8835
10516
  for (const candidate of candidates) {
8836
10517
  try {
8837
- stat = statSync7(candidate);
10518
+ stat = statSync11(candidate);
8838
10519
  fullPath = candidate;
8839
10520
  break;
8840
10521
  } catch {
@@ -8845,7 +10526,7 @@ function validateCitation(url, projectRoot) {
8845
10526
  if (parts.startLine === void 0) return { ok: true };
8846
10527
  let lineCount;
8847
10528
  try {
8848
- lineCount = readFileSync13(fullPath, "utf8").split("\n").length;
10529
+ lineCount = readFileSync19(fullPath, "utf8").split("\n").length;
8849
10530
  } catch {
8850
10531
  return { ok: false, reason: "unreadable" };
8851
10532
  }
@@ -11363,9 +13044,9 @@ function describeRepair(repair) {
11363
13044
  }
11364
13045
 
11365
13046
  // src/cli/ui/hash-memory.ts
11366
- import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11367
- import { homedir as homedir6 } from "os";
11368
- 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";
11369
13050
  var PROJECT_HEADER = `# Reasonix project memory
11370
13051
 
11371
13052
  Notes the user pinned via the \`#\` prompt prefix. The whole file is
@@ -11397,12 +13078,12 @@ function detectHashMemory(text) {
11397
13078
  return { kind: "memory", note: body };
11398
13079
  }
11399
13080
  function appendProjectMemory(rootDir, note) {
11400
- return appendBulletToFile(join13(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
13081
+ return appendBulletToFile(join17(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11401
13082
  }
11402
13083
  var GLOBAL_MEMORY_DIR = ".reasonix";
11403
13084
  var GLOBAL_MEMORY_FILE = "REASONIX.md";
11404
- function globalMemoryPath(homeDir = homedir6()) {
11405
- return join13(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
13085
+ function globalMemoryPath(homeDir = homedir9()) {
13086
+ return join17(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11406
13087
  }
11407
13088
  function appendGlobalMemory(note, homeDir) {
11408
13089
  return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
@@ -11412,14 +13093,14 @@ function appendBulletToFile(path5, note, newFileHeader) {
11412
13093
  if (!trimmed) throw new Error("note body cannot be empty");
11413
13094
  const bullet = `- ${trimmed}
11414
13095
  `;
11415
- if (!existsSync12(path5)) {
11416
- mkdirSync8(dirname10(path5), { recursive: true });
11417
- writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
13096
+ if (!existsSync18(path5)) {
13097
+ mkdirSync12(dirname15(path5), { recursive: true });
13098
+ writeFileSync11(path5, `${newFileHeader}${bullet}`, "utf8");
11418
13099
  return { path: path5, created: true };
11419
13100
  }
11420
13101
  let prefix = "";
11421
13102
  try {
11422
- const existing = readFileSync14(path5, "utf8");
13103
+ const existing = readFileSync20(path5, "utf8");
11423
13104
  if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
11424
13105
  } catch {
11425
13106
  }
@@ -11690,6 +13371,61 @@ function formatBytes2(n) {
11690
13371
  return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
11691
13372
  }
11692
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
+
11693
13429
  // src/cli/ui/slash/commands.ts
11694
13430
  var SLASH_COMMANDS = [
11695
13431
  { cmd: "help", summary: "show the full command reference" },
@@ -11766,6 +13502,12 @@ var SLASH_COMMANDS = [
11766
13502
  summary: "show / edit shell allowlist (builtin read-only \xB7 per-project: ~/.reasonix/config.json)",
11767
13503
  argCompleter: ["list", "add", "remove", "clear"]
11768
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
+ },
11769
13511
  {
11770
13512
  cmd: "cwd",
11771
13513
  argsHint: "<path>",
@@ -11925,11 +13667,11 @@ function parseSlash(text) {
11925
13667
  }
11926
13668
 
11927
13669
  // src/cli/ui/slash/handlers/admin.ts
11928
- import { existsSync as existsSync14, statSync as statSync8 } from "fs";
13670
+ import { existsSync as existsSync20, statSync as statSync12 } from "fs";
11929
13671
  import * as pathMod5 from "path";
11930
13672
 
11931
13673
  // src/cli/commands/stats.ts
11932
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
13674
+ import { existsSync as existsSync19, readFileSync as readFileSync21 } from "fs";
11933
13675
  function statsCommand(opts) {
11934
13676
  if (opts.transcript) {
11935
13677
  transcriptSummary(opts.transcript);
@@ -11938,11 +13680,11 @@ function statsCommand(opts) {
11938
13680
  dashboard(opts);
11939
13681
  }
11940
13682
  function transcriptSummary(path5) {
11941
- if (!existsSync13(path5)) {
13683
+ if (!existsSync19(path5)) {
11942
13684
  console.error(`no such transcript: ${path5}`);
11943
13685
  process.exit(1);
11944
13686
  }
11945
- const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
13687
+ const lines = readFileSync21(path5, "utf8").split(/\r?\n/).filter(Boolean);
11946
13688
  let assistantTurns = 0;
11947
13689
  let toolCalls = 0;
11948
13690
  let lastTurn = 0;
@@ -12031,12 +13773,13 @@ function header() {
12031
13773
  pad("turns", 8, "right"),
12032
13774
  pad("cache hit", 10, "right"),
12033
13775
  pad("cost (USD)", 14, "right"),
13776
+ pad("cache saved", 14, "right"),
12034
13777
  pad("vs Claude", 14, "right"),
12035
13778
  pad("saved", 10, "right")
12036
13779
  ].join(" ");
12037
13780
  }
12038
13781
  function divider() {
12039
- return "-".repeat(70);
13782
+ return "-".repeat(86);
12040
13783
  }
12041
13784
  function bucketRow(b) {
12042
13785
  const hit = bucketCacheHitRatio(b);
@@ -12046,6 +13789,11 @@ function bucketRow(b) {
12046
13789
  pad(b.turns.toString(), 8, "right"),
12047
13790
  pad(b.turns > 0 ? `${(hit * 100).toFixed(1)}%` : "\u2014", 10, "right"),
12048
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
+ ),
12049
13797
  pad(b.turns > 0 ? `$${b.claudeEquivUsd.toFixed(4)}` : "\u2014", 14, "right"),
12050
13798
  pad(b.turns > 0 && savings > 0 ? `${(savings * 100).toFixed(1)}%` : "\u2014", 10, "right")
12051
13799
  ].join(" ");
@@ -12177,12 +13925,12 @@ var cwd = (args, _loop, ctx) => {
12177
13925
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
12178
13926
  const expanded = raw.startsWith("~") && home ? pathMod5.join(home, raw.slice(1)) : raw;
12179
13927
  const abs = pathMod5.resolve(expanded);
12180
- if (!existsSync14(abs)) {
13928
+ if (!existsSync20(abs)) {
12181
13929
  return { info: `\u25B8 /cwd: path does not exist \u2014 ${abs}` };
12182
13930
  }
12183
13931
  let isDir = false;
12184
13932
  try {
12185
- isDir = statSync8(abs).isDirectory();
13933
+ isDir = statSync12(abs).isDirectory();
12186
13934
  } catch {
12187
13935
  }
12188
13936
  if (!isDir) {
@@ -12412,6 +14160,50 @@ var handlers2 = {
12412
14160
  loop
12413
14161
  };
12414
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
+
12415
14207
  // src/cli/ui/slash/helpers.ts
12416
14208
  import { spawnSync } from "child_process";
12417
14209
  function resolveMemoryTarget(store, raw) {
@@ -12640,7 +14432,7 @@ var walk2 = (_args, _loop, ctx) => {
12640
14432
  }
12641
14433
  return { info: ctx.startWalkthrough() };
12642
14434
  };
12643
- var handlers3 = {
14435
+ var handlers4 = {
12644
14436
  undo,
12645
14437
  history,
12646
14438
  show,
@@ -12655,7 +14447,7 @@ var handlers3 = {
12655
14447
  };
12656
14448
 
12657
14449
  // src/cli/ui/slash/handlers/init.ts
12658
- import { existsSync as existsSync15 } from "fs";
14450
+ import { existsSync as existsSync21 } from "fs";
12659
14451
  import * as pathMod6 from "path";
12660
14452
  var INIT_PROMPT = [
12661
14453
  "# Task: Initialize REASONIX.md",
@@ -12727,7 +14519,7 @@ var init = (args, _loop, ctx) => {
12727
14519
  }
12728
14520
  const force = (args[0] ?? "").toLowerCase() === "force";
12729
14521
  const target = pathMod6.join(ctx.codeRoot, "REASONIX.md");
12730
- if (existsSync15(target) && !force) {
14522
+ if (existsSync21(target) && !force) {
12731
14523
  return {
12732
14524
  info: [
12733
14525
  `\u25B8 REASONIX.md already exists at ${target}`,
@@ -12747,7 +14539,7 @@ var init = (args, _loop, ctx) => {
12747
14539
  resubmit: INIT_PROMPT
12748
14540
  };
12749
14541
  };
12750
- var handlers4 = {
14542
+ var handlers5 = {
12751
14543
  init
12752
14544
  };
12753
14545
 
@@ -12806,7 +14598,7 @@ $ ${out.command}`;
12806
14598
  return { info: out.output ? `${header2}
12807
14599
  ${out.output}` : header2 };
12808
14600
  };
12809
- var handlers5 = {
14601
+ var handlers6 = {
12810
14602
  jobs,
12811
14603
  kill,
12812
14604
  logs
@@ -12867,7 +14659,7 @@ var mcp = (_args, loop2, ctx) => {
12867
14659
  lines.push("To change this set, exit and run `reasonix setup`.");
12868
14660
  return { info: lines.join("\n") };
12869
14661
  };
12870
- var handlers6 = { mcp };
14662
+ var handlers7 = { mcp };
12871
14663
 
12872
14664
  // src/cli/ui/slash/handlers/memory.ts
12873
14665
  var memory = (args, _loop, ctx) => {
@@ -13002,7 +14794,7 @@ var memory = (args, _loop, ctx) => {
13002
14794
  );
13003
14795
  return { info: parts.join("\n") };
13004
14796
  };
13005
- var handlers7 = { memory };
14797
+ var handlers8 = { memory };
13006
14798
 
13007
14799
  // src/cli/ui/slash/handlers/model.ts
13008
14800
  var model = (args, loop2, ctx) => {
@@ -13155,7 +14947,7 @@ var pro = (args, loop2, ctx) => {
13155
14947
  };
13156
14948
  };
13157
14949
  var ESCALATION_MODEL_ID = "deepseek-v4-pro";
13158
- var handlers8 = {
14950
+ var handlers9 = {
13159
14951
  model,
13160
14952
  models,
13161
14953
  harvest: harvest2,
@@ -13308,7 +15100,7 @@ var compact = (args, loop2) => {
13308
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.`
13309
15101
  };
13310
15102
  };
13311
- var handlers9 = {
15103
+ var handlers10 = {
13312
15104
  think,
13313
15105
  reasoning: think,
13314
15106
  tool,
@@ -13456,7 +15248,7 @@ function renderListing(root, mode2) {
13456
15248
  );
13457
15249
  return lines.join("\n");
13458
15250
  }
13459
- var handlers10 = {
15251
+ var handlers11 = {
13460
15252
  permissions,
13461
15253
  perms: permissions
13462
15254
  };
@@ -13540,7 +15332,7 @@ var replay = (args, loop2) => {
13540
15332
  }
13541
15333
  };
13542
15334
  };
13543
- var handlers11 = {
15335
+ var handlers12 = {
13544
15336
  plans,
13545
15337
  replay
13546
15338
  };
@@ -13803,7 +15595,7 @@ async function startOllamaDaemon(opts = {}) {
13803
15595
  return { ready: false, pid };
13804
15596
  }
13805
15597
  async function pullOllamaModel(modelName, opts = {}) {
13806
- return new Promise((resolve12) => {
15598
+ return new Promise((resolve13) => {
13807
15599
  const child = spawn5("ollama", ["pull", modelName], {
13808
15600
  stdio: ["ignore", "pipe", "pipe"],
13809
15601
  windowsHide: true
@@ -13815,8 +15607,8 @@ async function pullOllamaModel(modelName, opts = {}) {
13815
15607
  }
13816
15608
  streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
13817
15609
  streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
13818
- child.once("exit", (code) => resolve12(code ?? -1));
13819
- child.once("error", () => resolve12(-1));
15610
+ child.once("exit", (code) => resolve13(code ?? -1));
15611
+ child.once("error", () => resolve13(-1));
13820
15612
  });
13821
15613
  }
13822
15614
  function streamLines(stream, cb) {
@@ -13913,7 +15705,7 @@ async function readIndexMeta(rootDir) {
13913
15705
  return null;
13914
15706
  }
13915
15707
  }
13916
- var handlers12 = {
15708
+ var handlers13 = {
13917
15709
  semantic
13918
15710
  };
13919
15711
 
@@ -13948,7 +15740,7 @@ var forget = (_args, loop2) => {
13948
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?)`
13949
15741
  };
13950
15742
  };
13951
- var handlers13 = {
15743
+ var handlers14 = {
13952
15744
  sessions,
13953
15745
  forget
13954
15746
  };
@@ -14024,7 +15816,7 @@ ${found.body}${argsLine}`;
14024
15816
  resubmit: payload
14025
15817
  };
14026
15818
  };
14027
- var handlers14 = {
15819
+ var handlers15 = {
14028
15820
  skill,
14029
15821
  skills: skill
14030
15822
  };
@@ -14044,7 +15836,8 @@ var HANDLERS = {
14044
15836
  ...handlers11,
14045
15837
  ...handlers12,
14046
15838
  ...handlers13,
14047
- ...handlers14
15839
+ ...handlers14,
15840
+ ...handlers15
14048
15841
  };
14049
15842
  function handleSlash(cmd, args, loop2, ctx = {}) {
14050
15843
  const h = HANDLERS[cmd];
@@ -14628,6 +16421,9 @@ function App({
14628
16421
  editModeRef.current = editMode;
14629
16422
  if (codeMode) saveEditMode(editMode);
14630
16423
  }, [editMode, codeMode]);
16424
+ const planModeRef = useRef6(false);
16425
+ const currentRootDirRef = useRef6("");
16426
+ const latestVersionRef = useRef6(null);
14631
16427
  const [pendingEditReview, setPendingEditReview] = useState10(null);
14632
16428
  const [walkthroughActive, setWalkthroughActive] = useState10(false);
14633
16429
  const [pendingTick, setPendingTick] = useState10(0);
@@ -14673,6 +16469,9 @@ function App({
14673
16469
  activeLoopRef.current = activeLoop;
14674
16470
  }, [activeLoop]);
14675
16471
  const toolHistoryRef = useRef6([]);
16472
+ const dashboardRef = useRef6(null);
16473
+ const eventSubscribersRef = useRef6(/* @__PURE__ */ new Set());
16474
+ const historicalRef = useRef6([]);
14676
16475
  const planStepsRef = useRef6(null);
14677
16476
  const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
14678
16477
  const planBodyRef = useRef6(null);
@@ -14822,6 +16621,91 @@ function App({
14822
16621
  refreshModels,
14823
16622
  refreshLatestVersion
14824
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]);
14825
16709
  const {
14826
16710
  slashMatches,
14827
16711
  slashSelected,
@@ -14964,11 +16848,11 @@ function App({
14964
16848
  if (key.escape && busy) {
14965
16849
  if (abortedThisTurn.current) return;
14966
16850
  abortedThisTurn.current = true;
14967
- const resolve12 = editReviewResolveRef.current;
14968
- if (resolve12) {
16851
+ const resolve13 = editReviewResolveRef.current;
16852
+ if (resolve13) {
14969
16853
  editReviewResolveRef.current = null;
14970
16854
  setPendingEditReview(null);
14971
- resolve12("reject");
16855
+ resolve13("reject");
14972
16856
  }
14973
16857
  if (activeLoopRef.current) stopLoop();
14974
16858
  loop2.abort();
@@ -15256,6 +17140,211 @@ function App({
15256
17140
  setWalkthroughActive(true);
15257
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).`;
15258
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
+ }, []);
15259
17348
  const handleWalkChoice = useCallback4(
15260
17349
  (choice) => {
15261
17350
  if (choice === "apply") {
@@ -15299,7 +17388,7 @@ function App({
15299
17388
  nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
15300
17389
  };
15301
17390
  }, []);
15302
- const handleSubmit = useCallback4(
17391
+ const handleSubmit2 = useCallback4(
15303
17392
  async (raw) => {
15304
17393
  let text = raw.trim();
15305
17394
  if (!text) return;
@@ -15457,6 +17546,9 @@ function App({
15457
17546
  stopLoop,
15458
17547
  getLoopStatus,
15459
17548
  startWalkthrough: codeMode ? startWalkthrough : void 0,
17549
+ startDashboard,
17550
+ stopDashboard,
17551
+ getDashboardUrl,
15460
17552
  jobs: codeMode?.jobs,
15461
17553
  postInfo: (text2) => setHistorical((prev) => [
15462
17554
  ...prev,
@@ -15577,6 +17669,8 @@ function App({
15577
17669
  leadSeparator: prev.length > 0
15578
17670
  }
15579
17671
  ]);
17672
+ const userId = `u-${Date.now()}`;
17673
+ broadcastDashboardEvent({ kind: "user", id: userId, text });
15580
17674
  const assistantId = `a-${Date.now()}`;
15581
17675
  const streamRef = { id: assistantId, text: "", reasoning: "" };
15582
17676
  const contentBuf = { current: "" };
@@ -15675,6 +17769,38 @@ function App({
15675
17769
  try {
15676
17770
  for await (const ev of loop2.step(modelInput)) {
15677
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
+ }
15678
17804
  if (ev.role !== "status") {
15679
17805
  setStatusLine((cur) => cur ? null : cur);
15680
17806
  }
@@ -15713,6 +17839,12 @@ function App({
15713
17839
  flush();
15714
17840
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
15715
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
+ });
15716
17848
  setSummary(loop2.stats.summary());
15717
17849
  if (ev.stats?.usage) {
15718
17850
  appendUsage({
@@ -15818,6 +17950,7 @@ function App({
15818
17950
  role: "tool",
15819
17951
  text: ev.content,
15820
17952
  toolName: ev.toolName,
17953
+ toolArgs: ev.toolArgs,
15821
17954
  toolIndex,
15822
17955
  durationMs
15823
17956
  }
@@ -16044,12 +18177,16 @@ function App({
16044
18177
  startLoop,
16045
18178
  getLoopStatus,
16046
18179
  startWalkthrough,
18180
+ startDashboard,
18181
+ stopDashboard,
18182
+ getDashboardUrl,
18183
+ broadcastDashboardEvent,
16047
18184
  applyCwdChange
16048
18185
  ]
16049
18186
  );
16050
18187
  useEffect6(() => {
16051
- handleSubmitRef.current = handleSubmit;
16052
- }, [handleSubmit]);
18188
+ handleSubmitRef.current = handleSubmit2;
18189
+ }, [handleSubmit2]);
16053
18190
  useEffect6(() => {
16054
18191
  if (!activeLoop) return;
16055
18192
  const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
@@ -16177,18 +18314,18 @@ ${body}`;
16177
18314
  loop2.abort();
16178
18315
  setQueuedSubmit(synthetic);
16179
18316
  } else {
16180
- await handleSubmit(synthetic);
18317
+ await handleSubmit2(synthetic);
16181
18318
  }
16182
18319
  },
16183
- [pendingShell, codeMode, currentRootDir, handleSubmit, busy, loop2]
18320
+ [pendingShell, codeMode, currentRootDir, handleSubmit2, busy, loop2]
16184
18321
  );
16185
18322
  useEffect6(() => {
16186
18323
  if (!busy && queuedSubmit !== null) {
16187
18324
  const text = queuedSubmit;
16188
18325
  setQueuedSubmit(null);
16189
- void handleSubmit(text);
18326
+ void handleSubmit2(text);
16190
18327
  }
16191
- }, [busy, queuedSubmit, handleSubmit]);
18328
+ }, [busy, queuedSubmit, handleSubmit2]);
16192
18329
  const handleWorkspaceConfirm = useCallback4(
16193
18330
  async (choice) => {
16194
18331
  const pending = pendingWorkspace;
@@ -16218,10 +18355,10 @@ ${body}`;
16218
18355
  loop2.abort();
16219
18356
  setQueuedSubmit(synthetic);
16220
18357
  } else {
16221
- await handleSubmit(synthetic);
18358
+ await handleSubmit2(synthetic);
16222
18359
  }
16223
18360
  },
16224
- [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit]
18361
+ [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit2]
16225
18362
  );
16226
18363
  const handlePlanConfirm = useCallback4(
16227
18364
  async (choice) => {
@@ -16255,10 +18392,10 @@ ${body}`;
16255
18392
  loop2.abort();
16256
18393
  setQueuedSubmit(synthetic);
16257
18394
  } else {
16258
- await handleSubmit(synthetic);
18395
+ await handleSubmit2(synthetic);
16259
18396
  }
16260
18397
  },
16261
- [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
18398
+ [pendingPlan, togglePlanMode, busy, loop2, handleSubmit2, persistPlanState]
16262
18399
  );
16263
18400
  const handlePlanConfirmRef = useRef6(handlePlanConfirm);
16264
18401
  useEffect6(() => {
@@ -16269,9 +18406,13 @@ ${body}`;
16269
18406
  []
16270
18407
  );
16271
18408
  const handleStagedInputSubmit = useCallback4(
16272
- async (feedback) => {
16273
- const staged = stagedInput;
16274
- setStagedInput(null);
18409
+ async (feedback, override) => {
18410
+ const staged = override ?? stagedInput;
18411
+ if (override) {
18412
+ setPendingPlan(null);
18413
+ } else {
18414
+ setStagedInput(null);
18415
+ }
16275
18416
  if (!staged) return;
16276
18417
  const trimmed = feedback.trim();
16277
18418
  let synthetic;
@@ -16312,11 +18453,15 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
16312
18453
  loop2.abort();
16313
18454
  setQueuedSubmit(synthetic);
16314
18455
  } else {
16315
- await handleSubmit(synthetic);
18456
+ await handleSubmit2(synthetic);
16316
18457
  }
16317
18458
  },
16318
- [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
18459
+ [stagedInput, togglePlanMode, busy, loop2, handleSubmit2]
16319
18460
  );
18461
+ const handleStagedInputSubmitRef = useRef6(handleStagedInputSubmit);
18462
+ useEffect6(() => {
18463
+ handleStagedInputSubmitRef.current = handleStagedInputSubmit;
18464
+ }, [handleStagedInputSubmit]);
16320
18465
  const handleStagedInputCancel = useCallback4(() => {
16321
18466
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
16322
18467
  setStagedInput(null);
@@ -16347,10 +18492,10 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
16347
18492
  loop2.abort();
16348
18493
  setQueuedSubmit(synthetic);
16349
18494
  } else {
16350
- await handleSubmit(synthetic);
18495
+ await handleSubmit2(synthetic);
16351
18496
  }
16352
18497
  },
16353
- [pendingCheckpoint, busy, loop2, handleSubmit]
18498
+ [pendingCheckpoint, busy, loop2, handleSubmit2]
16354
18499
  );
16355
18500
  const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
16356
18501
  useEffect6(() => {
@@ -16381,10 +18526,10 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16381
18526
  loop2.abort();
16382
18527
  setQueuedSubmit(synthetic);
16383
18528
  } else {
16384
- await handleSubmit(synthetic);
18529
+ await handleSubmit2(synthetic);
16385
18530
  }
16386
18531
  },
16387
- [stagedCheckpointRevise, busy, loop2, handleSubmit]
18532
+ [stagedCheckpointRevise, busy, loop2, handleSubmit2]
16388
18533
  );
16389
18534
  const handleCheckpointReviseCancel = useCallback4(() => {
16390
18535
  const snap = stagedCheckpointRevise;
@@ -16410,7 +18555,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16410
18555
  loop2.abort();
16411
18556
  setQueuedSubmit(synthetic2);
16412
18557
  } else {
16413
- await handleSubmit(synthetic2);
18558
+ await handleSubmit2(synthetic2);
16414
18559
  }
16415
18560
  return;
16416
18561
  }
@@ -16425,11 +18570,19 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16425
18570
  loop2.abort();
16426
18571
  setQueuedSubmit(synthetic);
16427
18572
  } else {
16428
- await handleSubmit(synthetic);
18573
+ await handleSubmit2(synthetic);
16429
18574
  }
16430
18575
  },
16431
- [pendingChoice, busy, loop2, handleSubmit]
18576
+ [pendingChoice, busy, loop2, handleSubmit2]
16432
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]);
16433
18586
  const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
16434
18587
  useEffect6(() => {
16435
18588
  handleChoiceConfirmRef.current = handleChoiceConfirm;
@@ -16456,10 +18609,10 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
16456
18609
  loop2.abort();
16457
18610
  setQueuedSubmit(synthetic);
16458
18611
  } else {
16459
- await handleSubmit(synthetic);
18612
+ await handleSubmit2(synthetic);
16460
18613
  }
16461
18614
  },
16462
- [busy, loop2, handleSubmit]
18615
+ [busy, loop2, handleSubmit2]
16463
18616
  );
16464
18617
  const handleChoiceCustomCancel = useCallback4(() => {
16465
18618
  const snap = stagedChoiceCustom;
@@ -16481,7 +18634,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
16481
18634
  loop2.abort();
16482
18635
  setQueuedSubmit(synthetic2);
16483
18636
  } else {
16484
- await handleSubmit(synthetic2);
18637
+ await handleSubmit2(synthetic2);
16485
18638
  }
16486
18639
  return;
16487
18640
  }
@@ -16516,10 +18669,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16516
18669
  loop2.abort();
16517
18670
  setQueuedSubmit(synthetic);
16518
18671
  } else {
16519
- await handleSubmit(synthetic);
18672
+ await handleSubmit2(synthetic);
16520
18673
  }
16521
18674
  },
16522
- [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
18675
+ [pendingRevision, busy, loop2, handleSubmit2, persistPlanState]
16523
18676
  );
16524
18677
  const handleReviseConfirmRef = useRef6(handleReviseConfirm);
16525
18678
  useEffect6(() => {
@@ -16632,10 +18785,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16632
18785
  {
16633
18786
  block: pendingEditReview,
16634
18787
  onChoose: (choice) => {
16635
- const resolve12 = editReviewResolveRef.current;
16636
- if (resolve12) {
18788
+ const resolve13 = editReviewResolveRef.current;
18789
+ if (resolve13) {
16637
18790
  editReviewResolveRef.current = null;
16638
- resolve12(choice);
18791
+ resolve13(choice);
16639
18792
  }
16640
18793
  }
16641
18794
  }
@@ -16661,7 +18814,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16661
18814
  {
16662
18815
  value: input,
16663
18816
  onChange: setInput,
16664
- onSubmit: handleSubmit,
18817
+ onSubmit: handleSubmit2,
16665
18818
  disabled: busy,
16666
18819
  onHistoryPrev: recallPrev,
16667
18820
  onHistoryNext: recallNext
@@ -16741,7 +18894,7 @@ function Setup({ onReady }) {
16741
18894
  const [value, setValue] = useState11("");
16742
18895
  const [error, setError] = useState11(null);
16743
18896
  const { exit: exit2 } = useApp2();
16744
- const handleSubmit = (raw) => {
18897
+ const handleSubmit2 = (raw) => {
16745
18898
  const trimmed = raw.trim();
16746
18899
  if (trimmed === "/exit" || trimmed === "/quit") {
16747
18900
  exit2();
@@ -16765,7 +18918,7 @@ function Setup({ onReady }) {
16765
18918
  {
16766
18919
  value,
16767
18920
  onChange: setValue,
16768
- onSubmit: handleSubmit,
18921
+ onSubmit: handleSubmit2,
16769
18922
  mask: "\u2022",
16770
18923
  placeholder: "sk-..."
16771
18924
  }
@@ -16910,7 +19063,7 @@ async function chatCommand(opts) {
16910
19063
  const prior = loadSessionMessages(opts.session);
16911
19064
  if (prior.length > 0) {
16912
19065
  const p = sessionPath(opts.session);
16913
- const mtime = existsSync16(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
19066
+ const mtime = existsSync22(p) ? statSync13(p).mtime : /* @__PURE__ */ new Date();
16914
19067
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
16915
19068
  }
16916
19069
  } else if (opts.session && opts.forceNew) {
@@ -16941,7 +19094,7 @@ async function chatCommand(opts) {
16941
19094
  }
16942
19095
 
16943
19096
  // src/cli/commands/code.tsx
16944
- import { basename as basename2, resolve as resolve10 } from "path";
19097
+ import { basename as basename2, resolve as resolve11 } from "path";
16945
19098
 
16946
19099
  // src/index/semantic/builder.ts
16947
19100
  import { promises as fs5 } from "fs";
@@ -17588,8 +19741,8 @@ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
17588
19741
 
17589
19742
  // src/cli/commands/code.tsx
17590
19743
  async function codeCommand(opts = {}) {
17591
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-YRY4HPMZ.js");
17592
- const rootDir = resolve10(opts.dir ?? process.cwd());
19744
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-HNDDXDRH.js");
19745
+ const rootDir = resolve11(opts.dir ?? process.cwd());
17593
19746
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
17594
19747
  const tools = new ToolRegistry();
17595
19748
  const jobs2 = new JobRegistry();
@@ -17636,7 +19789,7 @@ async function codeCommand(opts = {}) {
17636
19789
  }
17637
19790
 
17638
19791
  // src/cli/commands/diff.ts
17639
- import { writeFileSync as writeFileSync8 } from "fs";
19792
+ import { writeFileSync as writeFileSync12 } from "fs";
17640
19793
  import { basename as basename3 } from "path";
17641
19794
  import { render as render2 } from "ink";
17642
19795
  import React30 from "react";
@@ -17783,7 +19936,7 @@ async function diffCommand(opts) {
17783
19936
  if (wantMarkdown) {
17784
19937
  console.log(renderSummaryTable(report));
17785
19938
  const md = renderMarkdown(report);
17786
- writeFileSync8(opts.mdPath, md, "utf8");
19939
+ writeFileSync12(opts.mdPath, md, "utf8");
17787
19940
  console.log(`
17788
19941
  markdown report written to ${opts.mdPath}`);
17789
19942
  return;
@@ -17800,7 +19953,7 @@ markdown report written to ${opts.mdPath}`);
17800
19953
  }
17801
19954
 
17802
19955
  // src/cli/commands/index.ts
17803
- import { resolve as resolve11 } from "path";
19956
+ import { resolve as resolve12 } from "path";
17804
19957
 
17805
19958
  // src/index/semantic/preflight.ts
17806
19959
  import { stdin as stdin2, stdout } from "process";
@@ -17874,7 +20027,7 @@ async function confirm(question, defaultYes) {
17874
20027
 
17875
20028
  // src/cli/commands/index.ts
17876
20029
  async function indexCommand(opts = {}) {
17877
- const root = resolve11(opts.dir ?? process.cwd());
20030
+ const root = resolve12(opts.dir ?? process.cwd());
17878
20031
  const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
17879
20032
  const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17880
20033
  const preflightOk = await ollamaPreflight({
@@ -18498,38 +20651,6 @@ import React34 from "react";
18498
20651
  import { Box as Box28, Text as Text26, useApp as useApp5, useInput as useInput3 } from "ink";
18499
20652
  import TextInput2 from "ink-text-input";
18500
20653
  import React33, { useState as useState15 } from "react";
18501
-
18502
- // src/cli/ui/presets.ts
18503
- var PRESETS = {
18504
- // fast — flash + effort=high. Quick Q&A, one-line tweaks, anything
18505
- // where shallow reasoning is enough. Cheapest turn possible.
18506
- fast: { model: "deepseek-v4-flash", reasoningEffort: "high", harvest: false, branch: 1 },
18507
- // smart — flash + effort=max. Full thinking budget on the cheap
18508
- // model. The default: handles 90%+ of coding work at a fraction
18509
- // of pro's cost.
18510
- smart: { model: "deepseek-v4-flash", reasoningEffort: "max", harvest: false, branch: 1 },
18511
- // max — pro + effort=max. Frontier model for hard tasks: cross-
18512
- // file architecture, subtle bug hunts, anything where flash's
18513
- // reasoning has measurably failed. ~12× per-token vs flash; save
18514
- // for when you need it, or use `/pro` to escalate a single turn.
18515
- max: { model: "deepseek-v4-pro", reasoningEffort: "max", harvest: false, branch: 1 }
18516
- };
18517
- var PRESET_DESCRIPTIONS = {
18518
- fast: {
18519
- headline: "v4-flash \xB7 effort=high",
18520
- cost: "cheapest \xB7 quick Q&A, one-line edits"
18521
- },
18522
- smart: {
18523
- headline: "v4-flash \xB7 effort=max",
18524
- cost: "~1.5\xD7 fast \xB7 default \xB7 day-to-day coding"
18525
- },
18526
- max: {
18527
- headline: "v4-pro \xB7 effort=max",
18528
- cost: "~12\xD7 fast \xB7 hard single-shots \xB7 use /pro for a single-turn bump"
18529
- }
18530
- };
18531
-
18532
- // src/cli/ui/Wizard.tsx
18533
20654
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
18534
20655
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
18535
20656
  const { exit: exit2 } = useApp5();
@@ -18726,7 +20847,7 @@ function SummaryLine({ label, value }) {
18726
20847
  return /* @__PURE__ */ React33.createElement(Box28, null, /* @__PURE__ */ React33.createElement(Text26, null, label.padEnd(12)), /* @__PURE__ */ React33.createElement(Text26, { bold: true }, value));
18727
20848
  }
18728
20849
  function presetItems() {
18729
- return ["fast", "smart", "max"].map((name) => ({
20850
+ return ["auto", "flash", "pro"].map((name) => ({
18730
20851
  value: name,
18731
20852
  label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
18732
20853
  hint: PRESET_DESCRIPTIONS[name].cost
@@ -18827,13 +20948,13 @@ function planUpdate(input) {
18827
20948
  };
18828
20949
  }
18829
20950
  function defaultSpawn(argv) {
18830
- return new Promise((resolve12, reject) => {
20951
+ return new Promise((resolve13, reject) => {
18831
20952
  const child = spawn6(argv[0], argv.slice(1), {
18832
20953
  stdio: "inherit",
18833
20954
  shell: process.platform === "win32"
18834
20955
  });
18835
20956
  child.once("error", reject);
18836
- child.once("exit", (code) => resolve12(code ?? 1));
20957
+ child.once("exit", (code) => resolve13(code ?? 1));
18837
20958
  });
18838
20959
  }
18839
20960
  async function updateCommand(opts = {}) {
@@ -18883,7 +21004,7 @@ function versionCommand() {
18883
21004
  function resolveDefaults(flags) {
18884
21005
  const cfg = flags.noConfig ? {} : readConfig();
18885
21006
  const preset2 = pickPreset(flags.preset, cfg.preset);
18886
- const presetSettings = PRESETS[preset2];
21007
+ const presetSettings = resolvePreset(preset2);
18887
21008
  const model2 = flags.model ?? presetSettings.model;
18888
21009
  const reasoningEffort = presetSettings.reasoningEffort;
18889
21010
  const harvest3 = flags.harvest === true ? true : presetSettings.harvest;
@@ -18896,10 +21017,12 @@ function resolveDefaults(flags) {
18896
21017
  function pickPreset(flagPreset, configPreset) {
18897
21018
  if (flagPreset && isPresetName(flagPreset)) return flagPreset;
18898
21019
  if (configPreset) return configPreset;
18899
- return "smart";
21020
+ return "auto";
18900
21021
  }
18901
21022
  function isPresetName(s) {
18902
- 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";
18903
21026
  }
18904
21027
  function normalizeBranch(raw) {
18905
21028
  if (raw === void 0) return void 0;