reasonix 0.11.3 → 0.12.8

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
  }
@@ -11046,7 +12727,8 @@ function StatsPanel({
11046
12727
  updateAvailable,
11047
12728
  busy,
11048
12729
  proArmed,
11049
- escalated
12730
+ escalated,
12731
+ dashboardUrl
11050
12732
  }) {
11051
12733
  const branchOn = (branchBudget ?? 1) > 1;
11052
12734
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
@@ -11074,7 +12756,7 @@ function StatsPanel({
11074
12756
  escalated: escalated ?? false,
11075
12757
  animate: false
11076
12758
  }
11077
- ), narrow ? /* @__PURE__ */ React21.createElement(
12759
+ ), dashboardUrl ? /* @__PURE__ */ React21.createElement(DashboardRow, { url: dashboardUrl, narrow }) : null, narrow ? /* @__PURE__ */ React21.createElement(
11078
12760
  StackedMetrics,
11079
12761
  {
11080
12762
  summary,
@@ -11116,6 +12798,14 @@ function Header({
11116
12798
  const showSecondary = animate && !narrow;
11117
12799
  return /* @__PURE__ */ React21.createElement(Box19, { justifyContent: "space-between" }, /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Wordmark, { busy, animate }), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, ` ${VERSION}`), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "yellow", bold: true }, model2.replace(/^deepseek-/, "")), modePill ? /* @__PURE__ */ React21.createElement(React21.Fragment, null, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Pill, { label: modePill.label, bg: modePill.bg })) : null, proPill ? /* @__PURE__ */ React21.createElement(React21.Fragment, null, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Pill, { label: proPill.label, bg: proPill.bg })) : null, showSecondary && harvestOn ? /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "magenta" }, "harvest")) : null, showSecondary && branchOn ? /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "blue" }, `branch\xD7${branchBudget}`)) : null, showSecondary && reasoningEffort === "max" ? /* @__PURE__ */ React21.createElement(React21.Fragment, null, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "green", dimColor: true }, "max")) : null, showSecondary && reasoningEffort === "high" ? /* @__PURE__ */ React21.createElement(React21.Fragment, null, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "yellow", dimColor: true }, "high")) : null), /* @__PURE__ */ React21.createElement(Text17, null, updateAvailable ? /* @__PURE__ */ React21.createElement(Text17, { color: "yellow", bold: true }, `\u2191 ${updateAvailable} `) : null, /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, narrow ? `t${turns}` : `turn ${turns} \xB7 /help`)));
11118
12800
  }
12801
+ function DashboardRow({ url, narrow }) {
12802
+ return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", marginTop: narrow ? 0 : 1 }, /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Text17, { color: COLOR.info }, "\u25C7 web "), /* @__PURE__ */ React21.createElement(Text17, { dimColor: true }, "open the dashboard in a browser (chat \xB7 files \xB7 stats \xB7 settings)")), /* @__PURE__ */ React21.createElement(Box19, null, /* @__PURE__ */ React21.createElement(Text17, null, " "), /* @__PURE__ */ React21.createElement(Text17, { color: "cyan", bold: true }, hyperlink(url, url))));
12803
+ }
12804
+ function hyperlink(url, label) {
12805
+ const ESC = "\x1B";
12806
+ const ST = `${ESC}\\`;
12807
+ return `${ESC}]8;;${url}${ST}${label}${ESC}]8;;${ST}`;
12808
+ }
11119
12809
  function Pill({ label, bg }) {
11120
12810
  return /* @__PURE__ */ React21.createElement(Text17, { backgroundColor: bg, color: "white", bold: true }, ` ${label} `);
11121
12811
  }
@@ -11363,9 +13053,9 @@ function describeRepair(repair) {
11363
13053
  }
11364
13054
 
11365
13055
  // 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";
13056
+ import { appendFileSync as appendFileSync3, existsSync as existsSync18, mkdirSync as mkdirSync12, readFileSync as readFileSync20, writeFileSync as writeFileSync11 } from "fs";
13057
+ import { homedir as homedir9 } from "os";
13058
+ import { dirname as dirname15, join as join17 } from "path";
11369
13059
  var PROJECT_HEADER = `# Reasonix project memory
11370
13060
 
11371
13061
  Notes the user pinned via the \`#\` prompt prefix. The whole file is
@@ -11397,12 +13087,12 @@ function detectHashMemory(text) {
11397
13087
  return { kind: "memory", note: body };
11398
13088
  }
11399
13089
  function appendProjectMemory(rootDir, note) {
11400
- return appendBulletToFile(join13(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
13090
+ return appendBulletToFile(join17(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11401
13091
  }
11402
13092
  var GLOBAL_MEMORY_DIR = ".reasonix";
11403
13093
  var GLOBAL_MEMORY_FILE = "REASONIX.md";
11404
- function globalMemoryPath(homeDir = homedir6()) {
11405
- return join13(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
13094
+ function globalMemoryPath(homeDir = homedir9()) {
13095
+ return join17(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11406
13096
  }
11407
13097
  function appendGlobalMemory(note, homeDir) {
11408
13098
  return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
@@ -11412,14 +13102,14 @@ function appendBulletToFile(path5, note, newFileHeader) {
11412
13102
  if (!trimmed) throw new Error("note body cannot be empty");
11413
13103
  const bullet = `- ${trimmed}
11414
13104
  `;
11415
- if (!existsSync12(path5)) {
11416
- mkdirSync8(dirname10(path5), { recursive: true });
11417
- writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
13105
+ if (!existsSync18(path5)) {
13106
+ mkdirSync12(dirname15(path5), { recursive: true });
13107
+ writeFileSync11(path5, `${newFileHeader}${bullet}`, "utf8");
11418
13108
  return { path: path5, created: true };
11419
13109
  }
11420
13110
  let prefix = "";
11421
13111
  try {
11422
- const existing = readFileSync14(path5, "utf8");
13112
+ const existing = readFileSync20(path5, "utf8");
11423
13113
  if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
11424
13114
  } catch {
11425
13115
  }
@@ -11690,6 +13380,61 @@ function formatBytes2(n) {
11690
13380
  return `${mb.toFixed(mb >= 10 ? 0 : 1)} MB`;
11691
13381
  }
11692
13382
 
13383
+ // src/cli/ui/presets.ts
13384
+ var PRESETS = {
13385
+ // auto — flash baseline + auto-escalate to pro when the model emits
13386
+ // <<<NEEDS_PRO>>> OR after 3+ tool failure signals in one turn.
13387
+ // The default: cheap when easy, smart when hard.
13388
+ auto: {
13389
+ model: "deepseek-v4-flash",
13390
+ reasoningEffort: "max",
13391
+ autoEscalate: true,
13392
+ harvest: false,
13393
+ branch: 1
13394
+ },
13395
+ // flash — always flash, never escalate. `/pro` still arms a single
13396
+ // manual turn; auto-promotion is the thing this disables. Use when
13397
+ // you want predictable cost per turn.
13398
+ flash: {
13399
+ model: "deepseek-v4-flash",
13400
+ reasoningEffort: "max",
13401
+ autoEscalate: false,
13402
+ harvest: false,
13403
+ branch: 1
13404
+ },
13405
+ // pro — always pro. Hard pin; the model never downgrades. Use for
13406
+ // multi-turn architecture work where flash is just going to keep
13407
+ // escalating anyway and the back-and-forth wastes turns.
13408
+ pro: {
13409
+ model: "deepseek-v4-pro",
13410
+ reasoningEffort: "max",
13411
+ autoEscalate: false,
13412
+ harvest: false,
13413
+ branch: 1
13414
+ }
13415
+ };
13416
+ var PRESET_DESCRIPTIONS = {
13417
+ auto: {
13418
+ headline: "flash \u2192 pro on hard turns",
13419
+ cost: "default \xB7 ~96% turns stay on flash \xB7 pro kicks in only when needed"
13420
+ },
13421
+ flash: {
13422
+ headline: "v4-flash always",
13423
+ cost: "cheapest \xB7 predictable \xB7 /pro still works for a one-turn bump"
13424
+ },
13425
+ pro: {
13426
+ headline: "v4-pro always",
13427
+ cost: "~3\xD7 flash (5/31 discount) / ~12\xD7 full price \xB7 for hard multi-turn work"
13428
+ }
13429
+ };
13430
+ function resolvePreset(name) {
13431
+ if (name === "auto" || name === "flash" || name === "pro") return PRESETS[name];
13432
+ if (name === "fast") return { ...PRESETS.flash, reasoningEffort: "high" };
13433
+ if (name === "smart") return PRESETS.auto;
13434
+ if (name === "max") return PRESETS.pro;
13435
+ return PRESETS.auto;
13436
+ }
13437
+
11693
13438
  // src/cli/ui/slash/commands.ts
11694
13439
  var SLASH_COMMANDS = [
11695
13440
  { cmd: "help", summary: "show the full command reference" },
@@ -11766,6 +13511,12 @@ var SLASH_COMMANDS = [
11766
13511
  summary: "show / edit shell allowlist (builtin read-only \xB7 per-project: ~/.reasonix/config.json)",
11767
13512
  argCompleter: ["list", "add", "remove", "clear"]
11768
13513
  },
13514
+ {
13515
+ cmd: "dashboard",
13516
+ argsHint: "[stop]",
13517
+ summary: "launch the embedded web dashboard (127.0.0.1, token-gated)",
13518
+ argCompleter: ["stop"]
13519
+ },
11769
13520
  {
11770
13521
  cmd: "cwd",
11771
13522
  argsHint: "<path>",
@@ -11925,11 +13676,11 @@ function parseSlash(text) {
11925
13676
  }
11926
13677
 
11927
13678
  // src/cli/ui/slash/handlers/admin.ts
11928
- import { existsSync as existsSync14, statSync as statSync8 } from "fs";
13679
+ import { existsSync as existsSync20, statSync as statSync12 } from "fs";
11929
13680
  import * as pathMod5 from "path";
11930
13681
 
11931
13682
  // src/cli/commands/stats.ts
11932
- import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
13683
+ import { existsSync as existsSync19, readFileSync as readFileSync21 } from "fs";
11933
13684
  function statsCommand(opts) {
11934
13685
  if (opts.transcript) {
11935
13686
  transcriptSummary(opts.transcript);
@@ -11938,11 +13689,11 @@ function statsCommand(opts) {
11938
13689
  dashboard(opts);
11939
13690
  }
11940
13691
  function transcriptSummary(path5) {
11941
- if (!existsSync13(path5)) {
13692
+ if (!existsSync19(path5)) {
11942
13693
  console.error(`no such transcript: ${path5}`);
11943
13694
  process.exit(1);
11944
13695
  }
11945
- const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
13696
+ const lines = readFileSync21(path5, "utf8").split(/\r?\n/).filter(Boolean);
11946
13697
  let assistantTurns = 0;
11947
13698
  let toolCalls = 0;
11948
13699
  let lastTurn = 0;
@@ -12031,12 +13782,13 @@ function header() {
12031
13782
  pad("turns", 8, "right"),
12032
13783
  pad("cache hit", 10, "right"),
12033
13784
  pad("cost (USD)", 14, "right"),
13785
+ pad("cache saved", 14, "right"),
12034
13786
  pad("vs Claude", 14, "right"),
12035
13787
  pad("saved", 10, "right")
12036
13788
  ].join(" ");
12037
13789
  }
12038
13790
  function divider() {
12039
- return "-".repeat(70);
13791
+ return "-".repeat(86);
12040
13792
  }
12041
13793
  function bucketRow(b) {
12042
13794
  const hit = bucketCacheHitRatio(b);
@@ -12046,6 +13798,11 @@ function bucketRow(b) {
12046
13798
  pad(b.turns.toString(), 8, "right"),
12047
13799
  pad(b.turns > 0 ? `${(hit * 100).toFixed(1)}%` : "\u2014", 10, "right"),
12048
13800
  pad(b.turns > 0 ? `$${b.costUsd.toFixed(6)}` : "\u2014", 14, "right"),
13801
+ pad(
13802
+ b.turns > 0 && b.cacheSavingsUsd > 0 ? `$${b.cacheSavingsUsd.toFixed(4)}` : "\u2014",
13803
+ 14,
13804
+ "right"
13805
+ ),
12049
13806
  pad(b.turns > 0 ? `$${b.claudeEquivUsd.toFixed(4)}` : "\u2014", 14, "right"),
12050
13807
  pad(b.turns > 0 && savings > 0 ? `${(savings * 100).toFixed(1)}%` : "\u2014", 10, "right")
12051
13808
  ].join(" ");
@@ -12177,12 +13934,12 @@ var cwd = (args, _loop, ctx) => {
12177
13934
  const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
12178
13935
  const expanded = raw.startsWith("~") && home ? pathMod5.join(home, raw.slice(1)) : raw;
12179
13936
  const abs = pathMod5.resolve(expanded);
12180
- if (!existsSync14(abs)) {
13937
+ if (!existsSync20(abs)) {
12181
13938
  return { info: `\u25B8 /cwd: path does not exist \u2014 ${abs}` };
12182
13939
  }
12183
13940
  let isDir = false;
12184
13941
  try {
12185
- isDir = statSync8(abs).isDirectory();
13942
+ isDir = statSync12(abs).isDirectory();
12186
13943
  } catch {
12187
13944
  }
12188
13945
  if (!isDir) {
@@ -12412,6 +14169,50 @@ var handlers2 = {
12412
14169
  loop
12413
14170
  };
12414
14171
 
14172
+ // src/cli/ui/slash/handlers/dashboard.ts
14173
+ var dashboard2 = (args, _loop, ctx) => {
14174
+ if (!ctx.startDashboard || !ctx.getDashboardUrl) {
14175
+ return {
14176
+ info: "/dashboard is not available in this context (no startDashboard callback wired)."
14177
+ };
14178
+ }
14179
+ const sub = (args[0] ?? "").toLowerCase();
14180
+ if (sub === "stop" || sub === "off") {
14181
+ if (!ctx.stopDashboard) {
14182
+ return { info: "/dashboard stop: no stop callback wired." };
14183
+ }
14184
+ const url = ctx.getDashboardUrl();
14185
+ if (!url) return { info: "\u25B8 dashboard is not running." };
14186
+ ctx.stopDashboard();
14187
+ return { info: "\u25B8 dashboard stopping\u2026" };
14188
+ }
14189
+ const existing = ctx.getDashboardUrl();
14190
+ if (existing) {
14191
+ return {
14192
+ info: [
14193
+ "\u25B8 dashboard is already running:",
14194
+ ` ${existing}`,
14195
+ "",
14196
+ "Open it in any browser. Type `/dashboard stop` to tear it down."
14197
+ ].join("\n")
14198
+ };
14199
+ }
14200
+ ctx.startDashboard().then((url) => {
14201
+ ctx.postInfo?.(
14202
+ [
14203
+ "\u25B8 dashboard ready:",
14204
+ ` ${url}`,
14205
+ "",
14206
+ "127.0.0.1 only \xB7 token-gated. Type `/dashboard stop` to shut down."
14207
+ ].join("\n")
14208
+ );
14209
+ }).catch((err) => {
14210
+ ctx.postInfo?.(`\u25B8 dashboard failed to start: ${err.message}`);
14211
+ });
14212
+ return { info: "\u25B8 starting dashboard server\u2026" };
14213
+ };
14214
+ var handlers3 = { dashboard: dashboard2 };
14215
+
12415
14216
  // src/cli/ui/slash/helpers.ts
12416
14217
  import { spawnSync } from "child_process";
12417
14218
  function resolveMemoryTarget(store, raw) {
@@ -12640,7 +14441,7 @@ var walk2 = (_args, _loop, ctx) => {
12640
14441
  }
12641
14442
  return { info: ctx.startWalkthrough() };
12642
14443
  };
12643
- var handlers3 = {
14444
+ var handlers4 = {
12644
14445
  undo,
12645
14446
  history,
12646
14447
  show,
@@ -12655,7 +14456,7 @@ var handlers3 = {
12655
14456
  };
12656
14457
 
12657
14458
  // src/cli/ui/slash/handlers/init.ts
12658
- import { existsSync as existsSync15 } from "fs";
14459
+ import { existsSync as existsSync21 } from "fs";
12659
14460
  import * as pathMod6 from "path";
12660
14461
  var INIT_PROMPT = [
12661
14462
  "# Task: Initialize REASONIX.md",
@@ -12727,7 +14528,7 @@ var init = (args, _loop, ctx) => {
12727
14528
  }
12728
14529
  const force = (args[0] ?? "").toLowerCase() === "force";
12729
14530
  const target = pathMod6.join(ctx.codeRoot, "REASONIX.md");
12730
- if (existsSync15(target) && !force) {
14531
+ if (existsSync21(target) && !force) {
12731
14532
  return {
12732
14533
  info: [
12733
14534
  `\u25B8 REASONIX.md already exists at ${target}`,
@@ -12747,7 +14548,7 @@ var init = (args, _loop, ctx) => {
12747
14548
  resubmit: INIT_PROMPT
12748
14549
  };
12749
14550
  };
12750
- var handlers4 = {
14551
+ var handlers5 = {
12751
14552
  init
12752
14553
  };
12753
14554
 
@@ -12806,7 +14607,7 @@ $ ${out.command}`;
12806
14607
  return { info: out.output ? `${header2}
12807
14608
  ${out.output}` : header2 };
12808
14609
  };
12809
- var handlers5 = {
14610
+ var handlers6 = {
12810
14611
  jobs,
12811
14612
  kill,
12812
14613
  logs
@@ -12867,7 +14668,7 @@ var mcp = (_args, loop2, ctx) => {
12867
14668
  lines.push("To change this set, exit and run `reasonix setup`.");
12868
14669
  return { info: lines.join("\n") };
12869
14670
  };
12870
- var handlers6 = { mcp };
14671
+ var handlers7 = { mcp };
12871
14672
 
12872
14673
  // src/cli/ui/slash/handlers/memory.ts
12873
14674
  var memory = (args, _loop, ctx) => {
@@ -13002,7 +14803,7 @@ var memory = (args, _loop, ctx) => {
13002
14803
  );
13003
14804
  return { info: parts.join("\n") };
13004
14805
  };
13005
- var handlers7 = { memory };
14806
+ var handlers8 = { memory };
13006
14807
 
13007
14808
  // src/cli/ui/slash/handlers/model.ts
13008
14809
  var model = (args, loop2, ctx) => {
@@ -13155,7 +14956,7 @@ var pro = (args, loop2, ctx) => {
13155
14956
  };
13156
14957
  };
13157
14958
  var ESCALATION_MODEL_ID = "deepseek-v4-pro";
13158
- var handlers8 = {
14959
+ var handlers9 = {
13159
14960
  model,
13160
14961
  models,
13161
14962
  harvest: harvest2,
@@ -13308,7 +15109,7 @@ var compact = (args, loop2) => {
13308
15109
  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
15110
  };
13310
15111
  };
13311
- var handlers9 = {
15112
+ var handlers10 = {
13312
15113
  think,
13313
15114
  reasoning: think,
13314
15115
  tool,
@@ -13456,7 +15257,7 @@ function renderListing(root, mode2) {
13456
15257
  );
13457
15258
  return lines.join("\n");
13458
15259
  }
13459
- var handlers10 = {
15260
+ var handlers11 = {
13460
15261
  permissions,
13461
15262
  perms: permissions
13462
15263
  };
@@ -13540,7 +15341,7 @@ var replay = (args, loop2) => {
13540
15341
  }
13541
15342
  };
13542
15343
  };
13543
- var handlers11 = {
15344
+ var handlers12 = {
13544
15345
  plans,
13545
15346
  replay
13546
15347
  };
@@ -13803,7 +15604,7 @@ async function startOllamaDaemon(opts = {}) {
13803
15604
  return { ready: false, pid };
13804
15605
  }
13805
15606
  async function pullOllamaModel(modelName, opts = {}) {
13806
- return new Promise((resolve12) => {
15607
+ return new Promise((resolve13) => {
13807
15608
  const child = spawn5("ollama", ["pull", modelName], {
13808
15609
  stdio: ["ignore", "pipe", "pipe"],
13809
15610
  windowsHide: true
@@ -13815,8 +15616,8 @@ async function pullOllamaModel(modelName, opts = {}) {
13815
15616
  }
13816
15617
  streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
13817
15618
  streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
13818
- child.once("exit", (code) => resolve12(code ?? -1));
13819
- child.once("error", () => resolve12(-1));
15619
+ child.once("exit", (code) => resolve13(code ?? -1));
15620
+ child.once("error", () => resolve13(-1));
13820
15621
  });
13821
15622
  }
13822
15623
  function streamLines(stream, cb) {
@@ -13913,7 +15714,7 @@ async function readIndexMeta(rootDir) {
13913
15714
  return null;
13914
15715
  }
13915
15716
  }
13916
- var handlers12 = {
15717
+ var handlers13 = {
13917
15718
  semantic
13918
15719
  };
13919
15720
 
@@ -13948,7 +15749,7 @@ var forget = (_args, loop2) => {
13948
15749
  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
15750
  };
13950
15751
  };
13951
- var handlers13 = {
15752
+ var handlers14 = {
13952
15753
  sessions,
13953
15754
  forget
13954
15755
  };
@@ -14024,7 +15825,7 @@ ${found.body}${argsLine}`;
14024
15825
  resubmit: payload
14025
15826
  };
14026
15827
  };
14027
- var handlers14 = {
15828
+ var handlers15 = {
14028
15829
  skill,
14029
15830
  skills: skill
14030
15831
  };
@@ -14044,7 +15845,8 @@ var HANDLERS = {
14044
15845
  ...handlers11,
14045
15846
  ...handlers12,
14046
15847
  ...handlers13,
14047
- ...handlers14
15848
+ ...handlers14,
15849
+ ...handlers15
14048
15850
  };
14049
15851
  function handleSlash(cmd, args, loop2, ctx = {}) {
14050
15852
  const h = HANDLERS[cmd];
@@ -14554,7 +16356,8 @@ function App({
14554
16356
  mcpSpecs,
14555
16357
  mcpServers,
14556
16358
  progressSink,
14557
- codeMode
16359
+ codeMode,
16360
+ noDashboard
14558
16361
  }) {
14559
16362
  const { exit: exit2 } = useApp();
14560
16363
  const [historical, setHistorical] = useState10([]);
@@ -14628,6 +16431,9 @@ function App({
14628
16431
  editModeRef.current = editMode;
14629
16432
  if (codeMode) saveEditMode(editMode);
14630
16433
  }, [editMode, codeMode]);
16434
+ const planModeRef = useRef6(false);
16435
+ const currentRootDirRef = useRef6("");
16436
+ const latestVersionRef = useRef6(null);
14631
16437
  const [pendingEditReview, setPendingEditReview] = useState10(null);
14632
16438
  const [walkthroughActive, setWalkthroughActive] = useState10(false);
14633
16439
  const [pendingTick, setPendingTick] = useState10(0);
@@ -14673,6 +16479,9 @@ function App({
14673
16479
  activeLoopRef.current = activeLoop;
14674
16480
  }, [activeLoop]);
14675
16481
  const toolHistoryRef = useRef6([]);
16482
+ const dashboardRef = useRef6(null);
16483
+ const eventSubscribersRef = useRef6(/* @__PURE__ */ new Set());
16484
+ const historicalRef = useRef6([]);
14676
16485
  const planStepsRef = useRef6(null);
14677
16486
  const completedStepIdsRef = useRef6(/* @__PURE__ */ new Set());
14678
16487
  const planBodyRef = useRef6(null);
@@ -14822,6 +16631,91 @@ function App({
14822
16631
  refreshModels,
14823
16632
  refreshLatestVersion
14824
16633
  } = useSessionInfo(loop2);
16634
+ useEffect6(() => {
16635
+ planModeRef.current = planMode;
16636
+ }, [planMode]);
16637
+ useEffect6(() => {
16638
+ currentRootDirRef.current = currentRootDir;
16639
+ }, [currentRootDir]);
16640
+ useEffect6(() => {
16641
+ latestVersionRef.current = latestVersion ?? null;
16642
+ }, [latestVersion]);
16643
+ const balanceRef = useRef6(null);
16644
+ useEffect6(() => {
16645
+ balanceRef.current = balance;
16646
+ }, [balance]);
16647
+ useEffect6(() => {
16648
+ historicalRef.current = historical;
16649
+ }, [historical]);
16650
+ const broadcastDashboardEvent = useCallback4((ev) => {
16651
+ const subs = eventSubscribersRef.current;
16652
+ if (subs.size === 0) return;
16653
+ for (const h of subs) {
16654
+ try {
16655
+ h(ev);
16656
+ } catch {
16657
+ }
16658
+ }
16659
+ }, []);
16660
+ useEffect6(() => {
16661
+ broadcastDashboardEvent({ kind: "busy-change", busy });
16662
+ }, [busy, broadcastDashboardEvent]);
16663
+ useEffect6(() => {
16664
+ if (!pendingShell) return;
16665
+ const modal = {
16666
+ kind: "shell",
16667
+ command: pendingShell.command,
16668
+ allowPrefix: derivePrefix(pendingShell.command),
16669
+ shellKind: pendingShell.kind
16670
+ };
16671
+ broadcastDashboardEvent({ kind: "modal-up", modal });
16672
+ return () => {
16673
+ broadcastDashboardEvent({ kind: "modal-down", modalKind: "shell" });
16674
+ };
16675
+ }, [pendingShell, broadcastDashboardEvent]);
16676
+ useEffect6(() => {
16677
+ if (!pendingChoice) return;
16678
+ const modal = {
16679
+ kind: "choice",
16680
+ question: pendingChoice.question,
16681
+ options: pendingChoice.options,
16682
+ allowCustom: pendingChoice.allowCustom
16683
+ };
16684
+ broadcastDashboardEvent({ kind: "modal-up", modal });
16685
+ return () => {
16686
+ broadcastDashboardEvent({ kind: "modal-down", modalKind: "choice" });
16687
+ };
16688
+ }, [pendingChoice, broadcastDashboardEvent]);
16689
+ useEffect6(() => {
16690
+ if (!pendingPlan) return;
16691
+ broadcastDashboardEvent({
16692
+ kind: "modal-up",
16693
+ modal: { kind: "plan", body: pendingPlan }
16694
+ });
16695
+ return () => {
16696
+ broadcastDashboardEvent({ kind: "modal-down", modalKind: "plan" });
16697
+ };
16698
+ }, [pendingPlan, broadcastDashboardEvent]);
16699
+ useEffect6(() => {
16700
+ if (!pendingEditReview) return;
16701
+ const previewLines = (pendingEditReview.search || pendingEditReview.replace || "").split("\n").slice(0, 12);
16702
+ const preview = previewLines.join("\n");
16703
+ broadcastDashboardEvent({
16704
+ kind: "modal-up",
16705
+ modal: {
16706
+ kind: "edit-review",
16707
+ path: pendingEditReview.path,
16708
+ search: pendingEditReview.search ?? "",
16709
+ replace: pendingEditReview.replace ?? "",
16710
+ preview,
16711
+ total: pendingEdits.current.length,
16712
+ remaining: pendingEdits.current.length
16713
+ }
16714
+ });
16715
+ return () => {
16716
+ broadcastDashboardEvent({ kind: "modal-down", modalKind: "edit-review" });
16717
+ };
16718
+ }, [pendingEditReview, broadcastDashboardEvent]);
14825
16719
  const {
14826
16720
  slashMatches,
14827
16721
  slashSelected,
@@ -14964,11 +16858,11 @@ function App({
14964
16858
  if (key.escape && busy) {
14965
16859
  if (abortedThisTurn.current) return;
14966
16860
  abortedThisTurn.current = true;
14967
- const resolve12 = editReviewResolveRef.current;
14968
- if (resolve12) {
16861
+ const resolve13 = editReviewResolveRef.current;
16862
+ if (resolve13) {
14969
16863
  editReviewResolveRef.current = null;
14970
16864
  setPendingEditReview(null);
14971
- resolve12("reject");
16865
+ resolve13("reject");
14972
16866
  }
14973
16867
  if (activeLoopRef.current) stopLoop();
14974
16868
  loop2.abort();
@@ -15256,6 +17150,220 @@ function App({
15256
17150
  setWalkthroughActive(true);
15257
17151
  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
17152
  }, [codeMode]);
17153
+ const startDashboard = useCallback4(async () => {
17154
+ if (dashboardRef.current) return dashboardRef.current.url;
17155
+ const handle = await startDashboardServer({
17156
+ mode: "attached",
17157
+ configPath: defaultConfigPath(),
17158
+ usageLogPath: defaultUsageLogPath(),
17159
+ loop: loop2,
17160
+ tools,
17161
+ mcpServers,
17162
+ getCurrentCwd: () => codeMode ? currentRootDirRef.current : void 0,
17163
+ getEditMode: () => codeMode ? editModeRef.current : void 0,
17164
+ getPlanMode: () => planModeRef.current,
17165
+ getPendingEditCount: () => pendingEdits.current.length,
17166
+ getLatestVersion: () => latestVersionRef.current,
17167
+ getSessionName: () => session ?? null,
17168
+ setEditMode: (m) => {
17169
+ setEditMode(m);
17170
+ editModeRef.current = m;
17171
+ saveEditMode(m);
17172
+ return m;
17173
+ },
17174
+ setPlanMode: (on) => {
17175
+ if (codeMode) togglePlanMode(on);
17176
+ },
17177
+ applyPresetLive: (name) => {
17178
+ const settings = resolvePreset(name);
17179
+ loop2.configure({
17180
+ model: settings.model,
17181
+ autoEscalate: settings.autoEscalate,
17182
+ reasoningEffort: settings.reasoningEffort
17183
+ });
17184
+ },
17185
+ applyEffortLive: (effort2) => {
17186
+ loop2.configure({ reasoningEffort: effort2 });
17187
+ },
17188
+ // ---------- Chat bridge ----------
17189
+ getMessages: () => {
17190
+ const out = [];
17191
+ for (const ev of historicalRef.current) {
17192
+ if (ev.role === "user" || ev.role === "assistant" || ev.role === "info" || ev.role === "warning") {
17193
+ const msg = { id: ev.id, role: ev.role, text: ev.text };
17194
+ if (ev.reasoning) msg.reasoning = ev.reasoning;
17195
+ out.push(msg);
17196
+ } else if (ev.role === "tool") {
17197
+ const msg = {
17198
+ id: ev.id,
17199
+ role: "tool",
17200
+ text: ev.text,
17201
+ toolName: ev.toolName
17202
+ };
17203
+ if (ev.toolArgs) msg.toolArgs = ev.toolArgs;
17204
+ out.push(msg);
17205
+ }
17206
+ }
17207
+ return out;
17208
+ },
17209
+ subscribeEvents: (handler) => {
17210
+ eventSubscribersRef.current.add(handler);
17211
+ return () => {
17212
+ eventSubscribersRef.current.delete(handler);
17213
+ };
17214
+ },
17215
+ submitPrompt: (text) => {
17216
+ if (busyRef.current) {
17217
+ return { accepted: false, reason: "loop is busy with a turn" };
17218
+ }
17219
+ const fn = handleSubmitRef.current;
17220
+ if (!fn) return { accepted: false, reason: "TUI not ready" };
17221
+ fn(text).catch(() => void 0);
17222
+ return { accepted: true };
17223
+ },
17224
+ abortTurn: () => {
17225
+ if (busyRef.current) loop2.abort();
17226
+ },
17227
+ isBusy: () => busyRef.current,
17228
+ getStats: () => {
17229
+ const s = loop2.stats.summary();
17230
+ const ctxCap = DEEPSEEK_CONTEXT_TOKENS[loop2.model] ?? DEFAULT_CONTEXT_TOKENS;
17231
+ return {
17232
+ turns: s.turns,
17233
+ totalCostUsd: s.totalCostUsd,
17234
+ lastTurnCostUsd: s.lastTurnCostUsd,
17235
+ totalInputCostUsd: s.totalInputCostUsd,
17236
+ totalOutputCostUsd: s.totalOutputCostUsd,
17237
+ cacheHitRatio: s.cacheHitRatio,
17238
+ lastPromptTokens: s.lastPromptTokens,
17239
+ contextCapTokens: ctxCap,
17240
+ // useSessionInfo's Balance is a flat { currency, total }; the
17241
+ // dashboard wire shape is the richer DeepSeek BalanceInfo
17242
+ // array (granted / topped_up split). Convert as a single-
17243
+ // entry array so the SPA always reads `balance[0]` shape.
17244
+ balance: balanceRef.current ? [
17245
+ {
17246
+ currency: balanceRef.current.currency,
17247
+ total_balance: String(balanceRef.current.total)
17248
+ }
17249
+ ] : null
17250
+ };
17251
+ },
17252
+ // ---------- Modal mirroring ----------
17253
+ getActiveModal: () => {
17254
+ const ps = pendingShell;
17255
+ if (ps) {
17256
+ return {
17257
+ kind: "shell",
17258
+ command: ps.command,
17259
+ allowPrefix: derivePrefix(ps.command),
17260
+ shellKind: ps.kind
17261
+ };
17262
+ }
17263
+ const pc = pendingChoice;
17264
+ if (pc) {
17265
+ return {
17266
+ kind: "choice",
17267
+ question: pc.question,
17268
+ options: pc.options,
17269
+ allowCustom: pc.allowCustom
17270
+ };
17271
+ }
17272
+ if (pendingPlanRef.current) {
17273
+ return { kind: "plan", body: pendingPlanRef.current };
17274
+ }
17275
+ const er = pendingEditReview;
17276
+ if (er) {
17277
+ return {
17278
+ kind: "edit-review",
17279
+ path: er.path,
17280
+ search: er.search ?? "",
17281
+ replace: er.replace ?? "",
17282
+ preview: (er.search || er.replace || "").split("\n").slice(0, 12).join("\n"),
17283
+ total: pendingEdits.current.length,
17284
+ remaining: pendingEdits.current.length
17285
+ };
17286
+ }
17287
+ return null;
17288
+ },
17289
+ resolveShellConfirm: (choice) => {
17290
+ const fn = handleShellConfirmRef.current;
17291
+ if (fn) fn(choice).catch(() => void 0);
17292
+ },
17293
+ resolveChoiceConfirm: (choice) => {
17294
+ const fn = handleChoiceConfirmRef.current;
17295
+ if (fn) fn(choice).catch(() => void 0);
17296
+ },
17297
+ resolvePlanConfirm: (choice, text) => {
17298
+ if (choice === "cancel") {
17299
+ handlePlanConfirmRef.current("cancel").catch(() => void 0);
17300
+ return;
17301
+ }
17302
+ const plan2 = pendingPlanRef.current ?? "";
17303
+ handleStagedInputSubmitRef.current(text ?? "", { plan: plan2, mode: choice }).catch(() => void 0);
17304
+ },
17305
+ resolveEditReview: (choice) => {
17306
+ const resolve13 = editReviewResolveRef.current;
17307
+ if (resolve13) {
17308
+ editReviewResolveRef.current = null;
17309
+ setPendingEditReview(null);
17310
+ resolve13(choice);
17311
+ }
17312
+ },
17313
+ // ---------- v0.14 mutation surface ----------
17314
+ reloadHooks: () => {
17315
+ const fresh = loadHooks({ projectRoot: codeMode ? currentRootDirRef.current : void 0 });
17316
+ setHookList(fresh);
17317
+ return fresh.length;
17318
+ }
17319
+ });
17320
+ dashboardRef.current = handle;
17321
+ setDashboardUrlState(handle.url);
17322
+ return handle.url;
17323
+ }, [
17324
+ loop2,
17325
+ tools,
17326
+ mcpServers,
17327
+ codeMode,
17328
+ session,
17329
+ togglePlanMode,
17330
+ pendingShell,
17331
+ pendingChoice,
17332
+ pendingEditReview
17333
+ ]);
17334
+ const stopDashboard = useCallback4(async () => {
17335
+ const h = dashboardRef.current;
17336
+ if (!h) return;
17337
+ dashboardRef.current = null;
17338
+ setDashboardUrlState(null);
17339
+ try {
17340
+ await h.close();
17341
+ } catch {
17342
+ }
17343
+ setHistorical((prev) => [
17344
+ ...prev,
17345
+ { id: `dash-stop-${Date.now()}`, role: "info", text: "\u25B8 dashboard stopped." }
17346
+ ]);
17347
+ }, []);
17348
+ const getDashboardUrl = useCallback4(() => {
17349
+ return dashboardRef.current?.url ?? null;
17350
+ }, []);
17351
+ const [dashboardUrl, setDashboardUrlState] = useState10(null);
17352
+ useEffect6(() => {
17353
+ if (noDashboard) return;
17354
+ if (dashboardRef.current) return;
17355
+ startDashboard().catch(() => {
17356
+ });
17357
+ }, [noDashboard, startDashboard]);
17358
+ useEffect6(() => {
17359
+ return () => {
17360
+ const h = dashboardRef.current;
17361
+ if (h) {
17362
+ dashboardRef.current = null;
17363
+ h.close().catch(() => void 0);
17364
+ }
17365
+ };
17366
+ }, []);
15259
17367
  const handleWalkChoice = useCallback4(
15260
17368
  (choice) => {
15261
17369
  if (choice === "apply") {
@@ -15299,7 +17407,7 @@ function App({
15299
17407
  nextFireMs: Math.max(0, cur.nextFireAt - Date.now())
15300
17408
  };
15301
17409
  }, []);
15302
- const handleSubmit = useCallback4(
17410
+ const handleSubmit2 = useCallback4(
15303
17411
  async (raw) => {
15304
17412
  let text = raw.trim();
15305
17413
  if (!text) return;
@@ -15457,6 +17565,9 @@ function App({
15457
17565
  stopLoop,
15458
17566
  getLoopStatus,
15459
17567
  startWalkthrough: codeMode ? startWalkthrough : void 0,
17568
+ startDashboard,
17569
+ stopDashboard,
17570
+ getDashboardUrl,
15460
17571
  jobs: codeMode?.jobs,
15461
17572
  postInfo: (text2) => setHistorical((prev) => [
15462
17573
  ...prev,
@@ -15577,6 +17688,8 @@ function App({
15577
17688
  leadSeparator: prev.length > 0
15578
17689
  }
15579
17690
  ]);
17691
+ const userId = `u-${Date.now()}`;
17692
+ broadcastDashboardEvent({ kind: "user", id: userId, text });
15580
17693
  const assistantId = `a-${Date.now()}`;
15581
17694
  const streamRef = { id: assistantId, text: "", reasoning: "" };
15582
17695
  const contentBuf = { current: "" };
@@ -15675,6 +17788,38 @@ function App({
15675
17788
  try {
15676
17789
  for await (const ev of loop2.step(modelInput)) {
15677
17790
  writeTranscript(ev);
17791
+ if (eventSubscribersRef.current.size > 0) {
17792
+ const id = `${assistantId}-${ev.role}-${Date.now()}`;
17793
+ if (ev.role === "assistant_delta") {
17794
+ broadcastDashboardEvent({
17795
+ kind: "assistant_delta",
17796
+ id: assistantId,
17797
+ contentDelta: ev.content || void 0,
17798
+ reasoningDelta: ev.reasoningDelta
17799
+ });
17800
+ } else if (ev.role === "tool_start" && ev.toolName) {
17801
+ broadcastDashboardEvent({
17802
+ kind: "tool_start",
17803
+ id,
17804
+ toolName: ev.toolName,
17805
+ args: ev.toolArgs
17806
+ });
17807
+ } else if (ev.role === "tool" && ev.toolName) {
17808
+ broadcastDashboardEvent({
17809
+ kind: "tool",
17810
+ id,
17811
+ toolName: ev.toolName,
17812
+ content: ev.content,
17813
+ args: ev.toolArgs
17814
+ });
17815
+ } else if (ev.role === "warning") {
17816
+ broadcastDashboardEvent({ kind: "warning", id, text: ev.content });
17817
+ } else if (ev.role === "error") {
17818
+ broadcastDashboardEvent({ kind: "error", id, text: ev.content });
17819
+ } else if (ev.role === "status") {
17820
+ broadcastDashboardEvent({ kind: "status", text: ev.content });
17821
+ }
17822
+ }
15678
17823
  if (ev.role !== "status") {
15679
17824
  setStatusLine((cur) => cur ? null : cur);
15680
17825
  }
@@ -15713,6 +17858,12 @@ function App({
15713
17858
  flush();
15714
17859
  const repairNote = ev.repair ? describeRepair(ev.repair) : "";
15715
17860
  setStreaming(null);
17861
+ broadcastDashboardEvent({
17862
+ kind: "assistant_final",
17863
+ id: assistantId,
17864
+ text: ev.content || streamRef.text,
17865
+ reasoning: streamRef.reasoning || void 0
17866
+ });
15716
17867
  setSummary(loop2.stats.summary());
15717
17868
  if (ev.stats?.usage) {
15718
17869
  appendUsage({
@@ -15818,6 +17969,7 @@ function App({
15818
17969
  role: "tool",
15819
17970
  text: ev.content,
15820
17971
  toolName: ev.toolName,
17972
+ toolArgs: ev.toolArgs,
15821
17973
  toolIndex,
15822
17974
  durationMs
15823
17975
  }
@@ -16044,12 +18196,16 @@ function App({
16044
18196
  startLoop,
16045
18197
  getLoopStatus,
16046
18198
  startWalkthrough,
18199
+ startDashboard,
18200
+ stopDashboard,
18201
+ getDashboardUrl,
18202
+ broadcastDashboardEvent,
16047
18203
  applyCwdChange
16048
18204
  ]
16049
18205
  );
16050
18206
  useEffect6(() => {
16051
- handleSubmitRef.current = handleSubmit;
16052
- }, [handleSubmit]);
18207
+ handleSubmitRef.current = handleSubmit2;
18208
+ }, [handleSubmit2]);
16053
18209
  useEffect6(() => {
16054
18210
  if (!activeLoop) return;
16055
18211
  const delay = Math.max(0, activeLoop.nextFireAt - Date.now());
@@ -16177,18 +18333,18 @@ ${body}`;
16177
18333
  loop2.abort();
16178
18334
  setQueuedSubmit(synthetic);
16179
18335
  } else {
16180
- await handleSubmit(synthetic);
18336
+ await handleSubmit2(synthetic);
16181
18337
  }
16182
18338
  },
16183
- [pendingShell, codeMode, currentRootDir, handleSubmit, busy, loop2]
18339
+ [pendingShell, codeMode, currentRootDir, handleSubmit2, busy, loop2]
16184
18340
  );
16185
18341
  useEffect6(() => {
16186
18342
  if (!busy && queuedSubmit !== null) {
16187
18343
  const text = queuedSubmit;
16188
18344
  setQueuedSubmit(null);
16189
- void handleSubmit(text);
18345
+ void handleSubmit2(text);
16190
18346
  }
16191
- }, [busy, queuedSubmit, handleSubmit]);
18347
+ }, [busy, queuedSubmit, handleSubmit2]);
16192
18348
  const handleWorkspaceConfirm = useCallback4(
16193
18349
  async (choice) => {
16194
18350
  const pending = pendingWorkspace;
@@ -16218,10 +18374,10 @@ ${body}`;
16218
18374
  loop2.abort();
16219
18375
  setQueuedSubmit(synthetic);
16220
18376
  } else {
16221
- await handleSubmit(synthetic);
18377
+ await handleSubmit2(synthetic);
16222
18378
  }
16223
18379
  },
16224
- [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit]
18380
+ [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit2]
16225
18381
  );
16226
18382
  const handlePlanConfirm = useCallback4(
16227
18383
  async (choice) => {
@@ -16255,10 +18411,10 @@ ${body}`;
16255
18411
  loop2.abort();
16256
18412
  setQueuedSubmit(synthetic);
16257
18413
  } else {
16258
- await handleSubmit(synthetic);
18414
+ await handleSubmit2(synthetic);
16259
18415
  }
16260
18416
  },
16261
- [pendingPlan, togglePlanMode, busy, loop2, handleSubmit, persistPlanState]
18417
+ [pendingPlan, togglePlanMode, busy, loop2, handleSubmit2, persistPlanState]
16262
18418
  );
16263
18419
  const handlePlanConfirmRef = useRef6(handlePlanConfirm);
16264
18420
  useEffect6(() => {
@@ -16269,9 +18425,13 @@ ${body}`;
16269
18425
  []
16270
18426
  );
16271
18427
  const handleStagedInputSubmit = useCallback4(
16272
- async (feedback) => {
16273
- const staged = stagedInput;
16274
- setStagedInput(null);
18428
+ async (feedback, override) => {
18429
+ const staged = override ?? stagedInput;
18430
+ if (override) {
18431
+ setPendingPlan(null);
18432
+ } else {
18433
+ setStagedInput(null);
18434
+ }
16275
18435
  if (!staged) return;
16276
18436
  const trimmed = feedback.trim();
16277
18437
  let synthetic;
@@ -16312,11 +18472,15 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
16312
18472
  loop2.abort();
16313
18473
  setQueuedSubmit(synthetic);
16314
18474
  } else {
16315
- await handleSubmit(synthetic);
18475
+ await handleSubmit2(synthetic);
16316
18476
  }
16317
18477
  },
16318
- [stagedInput, togglePlanMode, busy, loop2, handleSubmit]
18478
+ [stagedInput, togglePlanMode, busy, loop2, handleSubmit2]
16319
18479
  );
18480
+ const handleStagedInputSubmitRef = useRef6(handleStagedInputSubmit);
18481
+ useEffect6(() => {
18482
+ handleStagedInputSubmitRef.current = handleStagedInputSubmit;
18483
+ }, [handleStagedInputSubmit]);
16320
18484
  const handleStagedInputCancel = useCallback4(() => {
16321
18485
  if (stagedInput?.plan) setPendingPlan(stagedInput.plan);
16322
18486
  setStagedInput(null);
@@ -16347,10 +18511,10 @@ Stay in plan mode \u2014 address the feedback (explore more if needed), then sub
16347
18511
  loop2.abort();
16348
18512
  setQueuedSubmit(synthetic);
16349
18513
  } else {
16350
- await handleSubmit(synthetic);
18514
+ await handleSubmit2(synthetic);
16351
18515
  }
16352
18516
  },
16353
- [pendingCheckpoint, busy, loop2, handleSubmit]
18517
+ [pendingCheckpoint, busy, loop2, handleSubmit2]
16354
18518
  );
16355
18519
  const handleCheckpointConfirmRef = useRef6(handleCheckpointConfirm);
16356
18520
  useEffect6(() => {
@@ -16381,10 +18545,10 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16381
18545
  loop2.abort();
16382
18546
  setQueuedSubmit(synthetic);
16383
18547
  } else {
16384
- await handleSubmit(synthetic);
18548
+ await handleSubmit2(synthetic);
16385
18549
  }
16386
18550
  },
16387
- [stagedCheckpointRevise, busy, loop2, handleSubmit]
18551
+ [stagedCheckpointRevise, busy, loop2, handleSubmit2]
16388
18552
  );
16389
18553
  const handleCheckpointReviseCancel = useCallback4(() => {
16390
18554
  const snap = stagedCheckpointRevise;
@@ -16410,7 +18574,7 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16410
18574
  loop2.abort();
16411
18575
  setQueuedSubmit(synthetic2);
16412
18576
  } else {
16413
- await handleSubmit(synthetic2);
18577
+ await handleSubmit2(synthetic2);
16414
18578
  }
16415
18579
  return;
16416
18580
  }
@@ -16425,11 +18589,19 @@ If the feedback only tweaks how you execute (extra constraints, style preference
16425
18589
  loop2.abort();
16426
18590
  setQueuedSubmit(synthetic);
16427
18591
  } else {
16428
- await handleSubmit(synthetic);
18592
+ await handleSubmit2(synthetic);
16429
18593
  }
16430
18594
  },
16431
- [pendingChoice, busy, loop2, handleSubmit]
18595
+ [pendingChoice, busy, loop2, handleSubmit2]
16432
18596
  );
18597
+ const handleShellConfirmRef = useRef6(handleShellConfirm);
18598
+ useEffect6(() => {
18599
+ handleShellConfirmRef.current = handleShellConfirm;
18600
+ }, [handleShellConfirm]);
18601
+ const pendingPlanRef = useRef6(null);
18602
+ useEffect6(() => {
18603
+ pendingPlanRef.current = pendingPlan;
18604
+ }, [pendingPlan]);
16433
18605
  const handleChoiceConfirmRef = useRef6(handleChoiceConfirm);
16434
18606
  useEffect6(() => {
16435
18607
  handleChoiceConfirmRef.current = handleChoiceConfirm;
@@ -16456,10 +18628,10 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
16456
18628
  loop2.abort();
16457
18629
  setQueuedSubmit(synthetic);
16458
18630
  } else {
16459
- await handleSubmit(synthetic);
18631
+ await handleSubmit2(synthetic);
16460
18632
  }
16461
18633
  },
16462
- [busy, loop2, handleSubmit]
18634
+ [busy, loop2, handleSubmit2]
16463
18635
  );
16464
18636
  const handleChoiceCustomCancel = useCallback4(() => {
16465
18637
  const snap = stagedChoiceCustom;
@@ -16481,7 +18653,7 @@ Read it carefully and proceed \u2014 don't snap back to the options you listed u
16481
18653
  loop2.abort();
16482
18654
  setQueuedSubmit(synthetic2);
16483
18655
  } else {
16484
- await handleSubmit(synthetic2);
18656
+ await handleSubmit2(synthetic2);
16485
18657
  }
16486
18658
  return;
16487
18659
  }
@@ -16516,10 +18688,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16516
18688
  loop2.abort();
16517
18689
  setQueuedSubmit(synthetic);
16518
18690
  } else {
16519
- await handleSubmit(synthetic);
18691
+ await handleSubmit2(synthetic);
16520
18692
  }
16521
18693
  },
16522
- [pendingRevision, busy, loop2, handleSubmit, persistPlanState]
18694
+ [pendingRevision, busy, loop2, handleSubmit2, persistPlanState]
16523
18695
  );
16524
18696
  const handleReviseConfirmRef = useRef6(handleReviseConfirm);
16525
18697
  useEffect6(() => {
@@ -16549,7 +18721,8 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16549
18721
  busy,
16550
18722
  updateAvailable,
16551
18723
  proArmed,
16552
- escalated: turnOnPro
18724
+ escalated: turnOnPro,
18725
+ dashboardUrl
16553
18726
  }
16554
18727
  ), /* @__PURE__ */ React24.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React24.createElement(EventRow, { key: item.id, event: item, projectRoot: currentRootDir })), !historical.some((e) => e.role === "user" || e.role === "assistant") && !busy && !streaming ? /* @__PURE__ */ React24.createElement(WelcomeBanner, { inCodeMode: !!codeMode }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React24.createElement(Box22, { marginY: 1 }, /* @__PURE__ */ React24.createElement(EventRow, { event: streaming, projectRoot: currentRootDir })) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React24.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React24.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React24.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React24.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React24.createElement(
16555
18728
  PlanRefineInput,
@@ -16632,10 +18805,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16632
18805
  {
16633
18806
  block: pendingEditReview,
16634
18807
  onChoose: (choice) => {
16635
- const resolve12 = editReviewResolveRef.current;
16636
- if (resolve12) {
18808
+ const resolve13 = editReviewResolveRef.current;
18809
+ if (resolve13) {
16637
18810
  editReviewResolveRef.current = null;
16638
- resolve12(choice);
18811
+ resolve13(choice);
16639
18812
  }
16640
18813
  }
16641
18814
  }
@@ -16661,7 +18834,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
16661
18834
  {
16662
18835
  value: input,
16663
18836
  onChange: setInput,
16664
- onSubmit: handleSubmit,
18837
+ onSubmit: handleSubmit2,
16665
18838
  disabled: busy,
16666
18839
  onHistoryPrev: recallPrev,
16667
18840
  onHistoryNext: recallNext
@@ -16741,7 +18914,7 @@ function Setup({ onReady }) {
16741
18914
  const [value, setValue] = useState11("");
16742
18915
  const [error, setError] = useState11(null);
16743
18916
  const { exit: exit2 } = useApp2();
16744
- const handleSubmit = (raw) => {
18917
+ const handleSubmit2 = (raw) => {
16745
18918
  const trimmed = raw.trim();
16746
18919
  if (trimmed === "/exit" || trimmed === "/quit") {
16747
18920
  exit2();
@@ -16765,7 +18938,7 @@ function Setup({ onReady }) {
16765
18938
  {
16766
18939
  value,
16767
18940
  onChange: setValue,
16768
- onSubmit: handleSubmit,
18941
+ onSubmit: handleSubmit2,
16769
18942
  mask: "\u2022",
16770
18943
  placeholder: "sk-..."
16771
18944
  }
@@ -16825,7 +18998,8 @@ function Root({
16825
18998
  mcpSpecs,
16826
18999
  mcpServers,
16827
19000
  progressSink,
16828
- codeMode: appProps.codeMode
19001
+ codeMode: appProps.codeMode,
19002
+ noDashboard: appProps.noDashboard
16829
19003
  }
16830
19004
  ));
16831
19005
  }
@@ -16910,7 +19084,7 @@ async function chatCommand(opts) {
16910
19084
  const prior = loadSessionMessages(opts.session);
16911
19085
  if (prior.length > 0) {
16912
19086
  const p = sessionPath(opts.session);
16913
- const mtime = existsSync16(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
19087
+ const mtime = existsSync22(p) ? statSync13(p).mtime : /* @__PURE__ */ new Date();
16914
19088
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
16915
19089
  }
16916
19090
  } else if (opts.session && opts.forceNew) {
@@ -16941,7 +19115,7 @@ async function chatCommand(opts) {
16941
19115
  }
16942
19116
 
16943
19117
  // src/cli/commands/code.tsx
16944
- import { basename as basename2, resolve as resolve10 } from "path";
19118
+ import { basename as basename2, resolve as resolve11 } from "path";
16945
19119
 
16946
19120
  // src/index/semantic/builder.ts
16947
19121
  import { promises as fs5 } from "fs";
@@ -17588,8 +19762,8 @@ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
17588
19762
 
17589
19763
  // src/cli/commands/code.tsx
17590
19764
  async function codeCommand(opts = {}) {
17591
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-YRY4HPMZ.js");
17592
- const rootDir = resolve10(opts.dir ?? process.cwd());
19765
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-HNDDXDRH.js");
19766
+ const rootDir = resolve11(opts.dir ?? process.cwd());
17593
19767
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
17594
19768
  const tools = new ToolRegistry();
17595
19769
  const jobs2 = new JobRegistry();
@@ -17631,12 +19805,13 @@ async function codeCommand(opts = {}) {
17631
19805
  seedTools: tools,
17632
19806
  codeMode: { rootDir, jobs: jobs2, reregisterTools: registerRootedTools },
17633
19807
  forceResume: opts.forceResume,
17634
- forceNew: opts.forceNew
19808
+ forceNew: opts.forceNew,
19809
+ noDashboard: opts.noDashboard
17635
19810
  });
17636
19811
  }
17637
19812
 
17638
19813
  // src/cli/commands/diff.ts
17639
- import { writeFileSync as writeFileSync8 } from "fs";
19814
+ import { writeFileSync as writeFileSync12 } from "fs";
17640
19815
  import { basename as basename3 } from "path";
17641
19816
  import { render as render2 } from "ink";
17642
19817
  import React30 from "react";
@@ -17783,7 +19958,7 @@ async function diffCommand(opts) {
17783
19958
  if (wantMarkdown) {
17784
19959
  console.log(renderSummaryTable(report));
17785
19960
  const md = renderMarkdown(report);
17786
- writeFileSync8(opts.mdPath, md, "utf8");
19961
+ writeFileSync12(opts.mdPath, md, "utf8");
17787
19962
  console.log(`
17788
19963
  markdown report written to ${opts.mdPath}`);
17789
19964
  return;
@@ -17800,7 +19975,7 @@ markdown report written to ${opts.mdPath}`);
17800
19975
  }
17801
19976
 
17802
19977
  // src/cli/commands/index.ts
17803
- import { resolve as resolve11 } from "path";
19978
+ import { resolve as resolve12 } from "path";
17804
19979
 
17805
19980
  // src/index/semantic/preflight.ts
17806
19981
  import { stdin as stdin2, stdout } from "process";
@@ -17874,7 +20049,7 @@ async function confirm(question, defaultYes) {
17874
20049
 
17875
20050
  // src/cli/commands/index.ts
17876
20051
  async function indexCommand(opts = {}) {
17877
- const root = resolve11(opts.dir ?? process.cwd());
20052
+ const root = resolve12(opts.dir ?? process.cwd());
17878
20053
  const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
17879
20054
  const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17880
20055
  const preflightOk = await ollamaPreflight({
@@ -18498,38 +20673,6 @@ import React34 from "react";
18498
20673
  import { Box as Box28, Text as Text26, useApp as useApp5, useInput as useInput3 } from "ink";
18499
20674
  import TextInput2 from "ink-text-input";
18500
20675
  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
20676
  var CATALOG_BY_NAME = new Map(MCP_CATALOG.map((e) => [e.name, e]));
18534
20677
  function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
18535
20678
  const { exit: exit2 } = useApp5();
@@ -18726,7 +20869,7 @@ function SummaryLine({ label, value }) {
18726
20869
  return /* @__PURE__ */ React33.createElement(Box28, null, /* @__PURE__ */ React33.createElement(Text26, null, label.padEnd(12)), /* @__PURE__ */ React33.createElement(Text26, { bold: true }, value));
18727
20870
  }
18728
20871
  function presetItems() {
18729
- return ["fast", "smart", "max"].map((name) => ({
20872
+ return ["auto", "flash", "pro"].map((name) => ({
18730
20873
  value: name,
18731
20874
  label: `${name} \u2014 ${PRESET_DESCRIPTIONS[name].headline}`,
18732
20875
  hint: PRESET_DESCRIPTIONS[name].cost
@@ -18827,13 +20970,13 @@ function planUpdate(input) {
18827
20970
  };
18828
20971
  }
18829
20972
  function defaultSpawn(argv) {
18830
- return new Promise((resolve12, reject) => {
20973
+ return new Promise((resolve13, reject) => {
18831
20974
  const child = spawn6(argv[0], argv.slice(1), {
18832
20975
  stdio: "inherit",
18833
20976
  shell: process.platform === "win32"
18834
20977
  });
18835
20978
  child.once("error", reject);
18836
- child.once("exit", (code) => resolve12(code ?? 1));
20979
+ child.once("exit", (code) => resolve13(code ?? 1));
18837
20980
  });
18838
20981
  }
18839
20982
  async function updateCommand(opts = {}) {
@@ -18883,7 +21026,7 @@ function versionCommand() {
18883
21026
  function resolveDefaults(flags) {
18884
21027
  const cfg = flags.noConfig ? {} : readConfig();
18885
21028
  const preset2 = pickPreset(flags.preset, cfg.preset);
18886
- const presetSettings = PRESETS[preset2];
21029
+ const presetSettings = resolvePreset(preset2);
18887
21030
  const model2 = flags.model ?? presetSettings.model;
18888
21031
  const reasoningEffort = presetSettings.reasoningEffort;
18889
21032
  const harvest3 = flags.harvest === true ? true : presetSettings.harvest;
@@ -18896,10 +21039,12 @@ function resolveDefaults(flags) {
18896
21039
  function pickPreset(flagPreset, configPreset) {
18897
21040
  if (flagPreset && isPresetName(flagPreset)) return flagPreset;
18898
21041
  if (configPreset) return configPreset;
18899
- return "smart";
21042
+ return "auto";
18900
21043
  }
18901
21044
  function isPresetName(s) {
18902
- return s === "fast" || s === "smart" || s === "max";
21045
+ return s === "auto" || s === "flash" || s === "pro" || // Legacy names — kept callable so old `--preset smart` invocations
21046
+ // and stale config.json entries don't error out.
21047
+ s === "fast" || s === "smart" || s === "max";
18903
21048
  }
18904
21049
  function normalizeBranch(raw) {
18905
21050
  if (raw === void 0) return void 0;
@@ -18983,6 +21128,9 @@ program.command("code [dir]").description(
18983
21128
  ).option("-m, --model <id>", "Override default model (v4-flash)").option("--no-session", "Disable session persistence for this run").option("-r, --resume", "Skip the session picker \u2014 always continue prior messages").option("-n, --new", "Skip the session picker \u2014 always wipe prior messages and start fresh").option("--transcript <path>", "Write a JSONL transcript to this path").option(
18984
21129
  "--harvest",
18985
21130
  "Opt-in Pillar-2 plan-state extraction. Adds one flash call per turn; off by default (no preset enables it)."
21131
+ ).option(
21132
+ "--no-dashboard",
21133
+ "Suppress the auto-launched embedded web dashboard. Default behavior boots it on TUI mount and shows the URL in the status bar (clickable in OSC-8-aware terminals)."
18986
21134
  ).action(async (dir, opts) => {
18987
21135
  await codeCommand({
18988
21136
  dir,
@@ -18991,7 +21139,8 @@ program.command("code [dir]").description(
18991
21139
  transcript: opts.transcript,
18992
21140
  forceResume: !!opts.resume,
18993
21141
  forceNew: !!opts.new,
18994
- harvest: !!opts.harvest
21142
+ harvest: !!opts.harvest,
21143
+ noDashboard: opts.dashboard === false
18995
21144
  });
18996
21145
  });
18997
21146
  program.command("chat").description("Interactive Ink TUI with live cache/cost panel.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt (pinned in the immutable prefix)", DEFAULT_SYSTEM).option("--transcript <path>", "Write a JSONL transcript to this path").option(
@@ -19015,7 +21164,10 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
19015
21164
  ).option(
19016
21165
  "--mcp-prefix <str>",
19017
21166
  "Global prefix applied to every MCP tool (only honored when no per-spec name is set; avoids collisions with a single anonymous server)"
19018
- ).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").action(async (opts) => {
21167
+ ).option("--no-config", "Ignore `~/.reasonix/config.json` \u2014 useful for CI or reproducing issues").option(
21168
+ "--no-dashboard",
21169
+ "Suppress the auto-launched embedded web dashboard. Default behavior boots it on TUI mount and shows the URL in the status bar (clickable in OSC-8-aware terminals)."
21170
+ ).action(async (opts) => {
19019
21171
  const defaults = resolveDefaults({
19020
21172
  model: opts.model,
19021
21173
  harvest: opts.harvest,
@@ -19042,7 +21194,8 @@ program.command("chat").description("Interactive Ink TUI with live cache/cost pa
19042
21194
  mcp: defaults.mcp,
19043
21195
  mcpPrefix: opts.mcpPrefix,
19044
21196
  forceResume: continueOpts.forceResume,
19045
- forceNew: !!opts.new
21197
+ forceNew: !!opts.new,
21198
+ noDashboard: opts.dashboard === false
19046
21199
  });
19047
21200
  });
19048
21201
  program.command("run <task>").description("Run a single task non-interactively, streaming output.").option("-m, --model <id>", "DeepSeek model id (overrides preset)").option("-s, --system <prompt>", "System prompt", DEFAULT_SYSTEM).option("--preset <name>", "Bundle of model + harvest + branch: fast | smart | max").option("--harvest", "Extract typed plan state from R1 reasoning (Pillar 2)").option(