reasonix 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  memoryEnabled,
11
11
  readProjectMemory,
12
12
  sanitizeMemoryName
13
- } from "./chunk-WRG56OKI.js";
13
+ } from "./chunk-JDVY4JDU.js";
14
14
 
15
15
  // src/cli/index.ts
16
16
  import { Command } from "commander";
@@ -22,80 +22,80 @@ import { dirname, join } from "path";
22
22
  function defaultConfigPath() {
23
23
  return join(homedir(), ".reasonix", "config.json");
24
24
  }
25
- function readConfig(path = defaultConfigPath()) {
25
+ function readConfig(path5 = defaultConfigPath()) {
26
26
  try {
27
- const raw = readFileSync(path, "utf8");
27
+ const raw = readFileSync(path5, "utf8");
28
28
  const parsed = JSON.parse(raw);
29
29
  if (parsed && typeof parsed === "object") return parsed;
30
30
  } catch {
31
31
  }
32
32
  return {};
33
33
  }
34
- function writeConfig(cfg, path = defaultConfigPath()) {
35
- mkdirSync(dirname(path), { recursive: true });
36
- writeFileSync(path, JSON.stringify(cfg, null, 2), "utf8");
34
+ function writeConfig(cfg, path5 = defaultConfigPath()) {
35
+ mkdirSync(dirname(path5), { recursive: true });
36
+ writeFileSync(path5, JSON.stringify(cfg, null, 2), "utf8");
37
37
  try {
38
- chmodSync(path, 384);
38
+ chmodSync(path5, 384);
39
39
  } catch {
40
40
  }
41
41
  }
42
- function loadApiKey(path = defaultConfigPath()) {
42
+ function loadApiKey(path5 = defaultConfigPath()) {
43
43
  if (process.env.DEEPSEEK_API_KEY) return process.env.DEEPSEEK_API_KEY;
44
- return readConfig(path).apiKey;
44
+ return readConfig(path5).apiKey;
45
45
  }
46
- function searchEnabled(path = defaultConfigPath()) {
46
+ function searchEnabled(path5 = defaultConfigPath()) {
47
47
  const env = process.env.REASONIX_SEARCH;
48
48
  if (env === "off" || env === "false" || env === "0") return false;
49
- const cfg = readConfig(path).search;
49
+ const cfg = readConfig(path5).search;
50
50
  if (cfg === false) return false;
51
51
  return true;
52
52
  }
53
- function saveApiKey(key, path = defaultConfigPath()) {
54
- const cfg = readConfig(path);
53
+ function saveApiKey(key, path5 = defaultConfigPath()) {
54
+ const cfg = readConfig(path5);
55
55
  cfg.apiKey = key.trim();
56
- writeConfig(cfg, path);
56
+ writeConfig(cfg, path5);
57
57
  }
58
- function loadProjectShellAllowed(rootDir, path = defaultConfigPath()) {
59
- const cfg = readConfig(path);
58
+ function loadProjectShellAllowed(rootDir, path5 = defaultConfigPath()) {
59
+ const cfg = readConfig(path5);
60
60
  return cfg.projects?.[rootDir]?.shellAllowed ?? [];
61
61
  }
62
- function addProjectShellAllowed(rootDir, prefix, path = defaultConfigPath()) {
62
+ function addProjectShellAllowed(rootDir, prefix, path5 = defaultConfigPath()) {
63
63
  const trimmed = prefix.trim();
64
64
  if (!trimmed) return;
65
- const cfg = readConfig(path);
65
+ const cfg = readConfig(path5);
66
66
  if (!cfg.projects) cfg.projects = {};
67
67
  if (!cfg.projects[rootDir]) cfg.projects[rootDir] = {};
68
68
  const existing = cfg.projects[rootDir].shellAllowed ?? [];
69
69
  if (existing.includes(trimmed)) return;
70
70
  cfg.projects[rootDir].shellAllowed = [...existing, trimmed];
71
- writeConfig(cfg, path);
71
+ writeConfig(cfg, path5);
72
72
  }
73
- function loadEditMode(path = defaultConfigPath()) {
74
- const v = readConfig(path).editMode;
73
+ function loadEditMode(path5 = defaultConfigPath()) {
74
+ const v = readConfig(path5).editMode;
75
75
  return v === "auto" ? "auto" : "review";
76
76
  }
77
- function saveEditMode(mode2, path = defaultConfigPath()) {
78
- const cfg = readConfig(path);
77
+ function saveEditMode(mode2, path5 = defaultConfigPath()) {
78
+ const cfg = readConfig(path5);
79
79
  cfg.editMode = mode2;
80
- writeConfig(cfg, path);
80
+ writeConfig(cfg, path5);
81
81
  }
82
- function editModeHintShown(path = defaultConfigPath()) {
83
- return readConfig(path).editModeHintShown === true;
82
+ function editModeHintShown(path5 = defaultConfigPath()) {
83
+ return readConfig(path5).editModeHintShown === true;
84
84
  }
85
- function loadReasoningEffort(path = defaultConfigPath()) {
86
- const v = readConfig(path).reasoningEffort;
85
+ function loadReasoningEffort(path5 = defaultConfigPath()) {
86
+ const v = readConfig(path5).reasoningEffort;
87
87
  return v === "high" ? "high" : "max";
88
88
  }
89
- function saveReasoningEffort(effort2, path = defaultConfigPath()) {
90
- const cfg = readConfig(path);
89
+ function saveReasoningEffort(effort2, path5 = defaultConfigPath()) {
90
+ const cfg = readConfig(path5);
91
91
  cfg.reasoningEffort = effort2;
92
- writeConfig(cfg, path);
92
+ writeConfig(cfg, path5);
93
93
  }
94
- function markEditModeHintShown(path = defaultConfigPath()) {
95
- const cfg = readConfig(path);
94
+ function markEditModeHintShown(path5 = defaultConfigPath()) {
95
+ const cfg = readConfig(path5);
96
96
  if (cfg.editModeHintShown === true) return;
97
97
  cfg.editModeHintShown = true;
98
- writeConfig(cfg, path);
98
+ writeConfig(cfg, path5);
99
99
  }
100
100
  function isPlausibleKey(key) {
101
101
  const trimmed = key.trim();
@@ -156,8 +156,8 @@ function computeWait(attempt, initial, cap, retryAfter) {
156
156
  }
157
157
  function sleep(ms, signal) {
158
158
  if (ms <= 0) return Promise.resolve();
159
- return new Promise((resolve8, reject) => {
160
- const timer = setTimeout(resolve8, ms);
159
+ return new Promise((resolve12, reject) => {
160
+ const timer = setTimeout(resolve12, ms);
161
161
  if (signal) {
162
162
  const onAbort = () => {
163
163
  clearTimeout(timer);
@@ -597,10 +597,10 @@ function globalSettingsPath(homeDirOverride) {
597
597
  function projectSettingsPath(projectRoot) {
598
598
  return join2(projectRoot, HOOK_SETTINGS_DIRNAME, HOOK_SETTINGS_FILENAME);
599
599
  }
600
- function readSettingsFile(path) {
601
- if (!existsSync(path)) return null;
600
+ function readSettingsFile(path5) {
601
+ if (!existsSync(path5)) return null;
602
602
  try {
603
- const raw = readFileSync2(path, "utf8");
603
+ const raw = readFileSync2(path5, "utf8");
604
604
  const parsed = JSON.parse(raw);
605
605
  if (parsed && typeof parsed === "object") return parsed;
606
606
  } catch {
@@ -642,13 +642,13 @@ function matchesTool(hook, toolName) {
642
642
  }
643
643
  }
644
644
  function defaultSpawner(input) {
645
- return new Promise((resolve8) => {
645
+ return new Promise((resolve12) => {
646
646
  const child = spawn(input.command, {
647
647
  cwd: input.cwd,
648
648
  shell: true,
649
649
  stdio: ["pipe", "pipe", "pipe"]
650
650
  });
651
- let stdout2 = "";
651
+ let stdout3 = "";
652
652
  let stderr = "";
653
653
  let timedOut = false;
654
654
  const timer = setTimeout(() => {
@@ -662,16 +662,16 @@ function defaultSpawner(input) {
662
662
  }, 500);
663
663
  }, input.timeoutMs);
664
664
  child.stdout.on("data", (chunk) => {
665
- stdout2 += chunk.toString("utf8");
665
+ stdout3 += chunk.toString("utf8");
666
666
  });
667
667
  child.stderr.on("data", (chunk) => {
668
668
  stderr += chunk.toString("utf8");
669
669
  });
670
670
  child.once("error", (err) => {
671
671
  clearTimeout(timer);
672
- resolve8({
672
+ resolve12({
673
673
  exitCode: null,
674
- stdout: stdout2,
674
+ stdout: stdout3,
675
675
  stderr,
676
676
  timedOut: false,
677
677
  spawnError: err
@@ -679,9 +679,9 @@ function defaultSpawner(input) {
679
679
  });
680
680
  child.once("close", (code) => {
681
681
  clearTimeout(timer);
682
- resolve8({
682
+ resolve12({
683
683
  exitCode: code,
684
- stdout: stdout2.trim(),
684
+ stdout: stdout3.trim(),
685
685
  stderr: stderr.trim(),
686
686
  timedOut
687
687
  });
@@ -715,13 +715,13 @@ async function runHooks(opts) {
715
715
  const matching = opts.hooks.filter((h) => h.event === event && matchesTool(h, toolName));
716
716
  const outcomes = [];
717
717
  let blocked = false;
718
- const stdin3 = `${JSON.stringify(opts.payload)}
718
+ const stdin4 = `${JSON.stringify(opts.payload)}
719
719
  `;
720
720
  for (const hook of matching) {
721
721
  const start = Date.now();
722
722
  const timeoutMs = hook.timeout ?? DEFAULT_TIMEOUTS_MS[event];
723
- const cwd = hook.cwd ?? opts.payload.cwd;
724
- const raw = await spawner({ command: hook.command, cwd, stdin: stdin3, timeoutMs });
723
+ const cwd2 = hook.cwd ?? opts.payload.cwd;
724
+ const raw = await spawner({ command: hook.command, cwd: cwd2, stdin: stdin4, timeoutMs });
725
725
  const decision = decideOutcome(event, raw);
726
726
  outcomes.push({
727
727
  hook,
@@ -804,10 +804,10 @@ function loadTokenizer() {
804
804
  }
805
805
  const addedMap = /* @__PURE__ */ new Map();
806
806
  const addedContents = [];
807
- for (const t of data.added_tokens) {
808
- if (!t.special) {
809
- addedMap.set(t.content, t.id);
810
- addedContents.push(t.content);
807
+ for (const t2 of data.added_tokens) {
808
+ if (!t2.special) {
809
+ addedMap.set(t2.content, t2.id);
810
+ addedContents.push(t2.content);
811
811
  }
812
812
  }
813
813
  addedContents.sort((a, b) => b.length - a.length);
@@ -874,29 +874,29 @@ function bpeEncode(piece, mergeRank) {
874
874
  }
875
875
  function encode(text) {
876
876
  if (!text) return [];
877
- const t = loadTokenizer();
877
+ const t2 = loadTokenizer();
878
878
  const ids = [];
879
879
  const process2 = (segment) => {
880
880
  if (!segment) return;
881
881
  let chunks = [segment];
882
- for (const re of t.splitRegexes) chunks = applySplit(chunks, re);
882
+ for (const re of t2.splitRegexes) chunks = applySplit(chunks, re);
883
883
  for (const chunk of chunks) {
884
884
  if (!chunk) continue;
885
- const byteLevel = byteLevelEncode(chunk, t.byteToChar);
886
- const pieces = bpeEncode(byteLevel, t.mergeRank);
885
+ const byteLevel = byteLevelEncode(chunk, t2.byteToChar);
886
+ const pieces = bpeEncode(byteLevel, t2.mergeRank);
887
887
  for (const p of pieces) {
888
- const id = t.vocab[p];
888
+ const id = t2.vocab[p];
889
889
  if (id !== void 0) ids.push(id);
890
890
  }
891
891
  }
892
892
  };
893
- if (t.addedPattern) {
894
- t.addedPattern.lastIndex = 0;
893
+ if (t2.addedPattern) {
894
+ t2.addedPattern.lastIndex = 0;
895
895
  let last = 0;
896
- for (const m of text.matchAll(t.addedPattern)) {
896
+ for (const m of text.matchAll(t2.addedPattern)) {
897
897
  const idx = m.index ?? 0;
898
898
  if (idx > last) process2(text.slice(last, idx));
899
- const id = t.addedMap.get(m[0]);
899
+ const id = t2.addedMap.get(m[0]);
900
900
  if (id !== void 0) ids.push(id);
901
901
  last = idx + m[0].length;
902
902
  }
@@ -987,14 +987,14 @@ function collect(prefix, schema, out, required, isRootRequired) {
987
987
  out[prefix] = schema;
988
988
  if (isRootRequired) required.push(prefix);
989
989
  }
990
- function setByPath(target, path, value) {
990
+ function setByPath(target, path5, value) {
991
991
  let cur = target;
992
- for (let i = 0; i < path.length - 1; i++) {
993
- const key = path[i];
992
+ for (let i = 0; i < path5.length - 1; i++) {
993
+ const key = path5[i];
994
994
  if (typeof cur[key] !== "object" || cur[key] === null) cur[key] = {};
995
995
  cur = cur[key];
996
996
  }
997
- cur[path[path.length - 1]] = value;
997
+ cur[path5[path5.length - 1]] = value;
998
998
  }
999
999
 
1000
1000
  // src/tools.ts
@@ -1060,12 +1060,12 @@ var ToolRegistry = class {
1060
1060
  return Boolean(this._tools.get(name)?.flatSchema);
1061
1061
  }
1062
1062
  specs() {
1063
- return [...this._tools.values()].map((t) => ({
1063
+ return [...this._tools.values()].map((t2) => ({
1064
1064
  type: "function",
1065
1065
  function: {
1066
- name: t.name,
1067
- description: t.description ?? "",
1068
- parameters: t.flatSchema ?? t.parameters ?? { type: "object", properties: {} }
1066
+ name: t2.name,
1067
+ description: t2.description ?? "",
1068
+ parameters: t2.flatSchema ?? t2.parameters ?? { type: "object", properties: {} }
1069
1069
  }
1070
1070
  }));
1071
1071
  }
@@ -1277,7 +1277,7 @@ var ImmutablePrefix = class {
1277
1277
  return [{ role: "system", content: this.system }, ...this.fewShots.map((m) => ({ ...m }))];
1278
1278
  }
1279
1279
  tools() {
1280
- return this.toolSpecs.map((t) => structuredClone(t));
1280
+ return this.toolSpecs.map((t2) => structuredClone(t2));
1281
1281
  }
1282
1282
  get fingerprint() {
1283
1283
  const blob = JSON.stringify({
@@ -1660,10 +1660,10 @@ function sanitizeName(name) {
1660
1660
  return cleaned || "default";
1661
1661
  }
1662
1662
  function loadSessionMessages(name) {
1663
- const path = sessionPath(name);
1664
- if (!existsSync3(path)) return [];
1663
+ const path5 = sessionPath(name);
1664
+ if (!existsSync3(path5)) return [];
1665
1665
  try {
1666
- const raw = readFileSync4(path, "utf8");
1666
+ const raw = readFileSync4(path5, "utf8");
1667
1667
  const out = [];
1668
1668
  for (const line of raw.split(/\r?\n/)) {
1669
1669
  const trimmed = line.trim();
@@ -1680,12 +1680,12 @@ function loadSessionMessages(name) {
1680
1680
  }
1681
1681
  }
1682
1682
  function appendSessionMessage(name, message) {
1683
- const path = sessionPath(name);
1684
- mkdirSync2(dirname3(path), { recursive: true });
1685
- appendFileSync(path, `${JSON.stringify(message)}
1683
+ const path5 = sessionPath(name);
1684
+ mkdirSync2(dirname3(path5), { recursive: true });
1685
+ appendFileSync(path5, `${JSON.stringify(message)}
1686
1686
  `, "utf8");
1687
1687
  try {
1688
- chmodSync2(path, 384);
1688
+ chmodSync2(path5, 384);
1689
1689
  } catch {
1690
1690
  }
1691
1691
  }
@@ -1695,21 +1695,21 @@ function listSessions() {
1695
1695
  try {
1696
1696
  const files = readdirSync(dir).filter((f) => f.endsWith(".jsonl"));
1697
1697
  return files.map((file) => {
1698
- const path = join4(dir, file);
1699
- const stat = statSync(path);
1698
+ const path5 = join4(dir, file);
1699
+ const stat = statSync(path5);
1700
1700
  const name = file.replace(/\.jsonl$/, "");
1701
- const messageCount = countLines(path);
1702
- return { name, path, size: stat.size, messageCount, mtime: stat.mtime };
1701
+ const messageCount = countLines(path5);
1702
+ return { name, path: path5, size: stat.size, messageCount, mtime: stat.mtime };
1703
1703
  }).sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
1704
1704
  } catch {
1705
1705
  return [];
1706
1706
  }
1707
1707
  }
1708
1708
  function deleteSession(name) {
1709
- const path = sessionPath(name);
1709
+ const path5 = sessionPath(name);
1710
1710
  try {
1711
- unlinkSync(path);
1712
- const sidecar = path.replace(/\.jsonl$/, ".pending.json");
1711
+ unlinkSync(path5);
1712
+ const sidecar = path5.replace(/\.jsonl$/, ".pending.json");
1713
1713
  try {
1714
1714
  unlinkSync(sidecar);
1715
1715
  } catch {
@@ -1720,19 +1720,19 @@ function deleteSession(name) {
1720
1720
  }
1721
1721
  }
1722
1722
  function rewriteSession(name, messages) {
1723
- const path = sessionPath(name);
1724
- mkdirSync2(dirname3(path), { recursive: true });
1723
+ const path5 = sessionPath(name);
1724
+ mkdirSync2(dirname3(path5), { recursive: true });
1725
1725
  const body = messages.map((m) => JSON.stringify(m)).join("\n");
1726
- writeFileSync2(path, body ? `${body}
1726
+ writeFileSync2(path5, body ? `${body}
1727
1727
  ` : "", "utf8");
1728
1728
  try {
1729
- chmodSync2(path, 384);
1729
+ chmodSync2(path5, 384);
1730
1730
  } catch {
1731
1731
  }
1732
1732
  }
1733
- function countLines(path) {
1733
+ function countLines(path5) {
1734
1734
  try {
1735
- const raw = readFileSync4(path, "utf8");
1735
+ const raw = readFileSync4(path5, "utf8");
1736
1736
  return raw.split(/\r?\n/).filter((l) => l.trim()).length;
1737
1737
  } catch {
1738
1738
  return 0;
@@ -1788,27 +1788,27 @@ var SessionStats = class {
1788
1788
  return stats2;
1789
1789
  }
1790
1790
  get totalCost() {
1791
- return this.turns.reduce((sum, t) => sum + t.cost, 0);
1791
+ return this.turns.reduce((sum, t2) => sum + t2.cost, 0);
1792
1792
  }
1793
1793
  get totalClaudeEquivalent() {
1794
- return this.turns.reduce((sum, t) => sum + claudeEquivalentCost(t.usage), 0);
1794
+ return this.turns.reduce((sum, t2) => sum + claudeEquivalentCost(t2.usage), 0);
1795
1795
  }
1796
1796
  get savingsVsClaude() {
1797
1797
  const c = this.totalClaudeEquivalent;
1798
1798
  return c > 0 ? 1 - this.totalCost / c : 0;
1799
1799
  }
1800
1800
  get totalInputCost() {
1801
- return this.turns.reduce((sum, t) => sum + inputCostUsd(t.model, t.usage), 0);
1801
+ return this.turns.reduce((sum, t2) => sum + inputCostUsd(t2.model, t2.usage), 0);
1802
1802
  }
1803
1803
  get totalOutputCost() {
1804
- return this.turns.reduce((sum, t) => sum + outputCostUsd(t.model, t.usage), 0);
1804
+ return this.turns.reduce((sum, t2) => sum + outputCostUsd(t2.model, t2.usage), 0);
1805
1805
  }
1806
1806
  get aggregateCacheHitRatio() {
1807
1807
  let hit = 0;
1808
1808
  let miss = 0;
1809
- for (const t of this.turns) {
1810
- hit += t.usage.promptCacheHitTokens;
1811
- miss += t.usage.promptCacheMissTokens;
1809
+ for (const t2 of this.turns) {
1810
+ hit += t2.usage.promptCacheHitTokens;
1811
+ miss += t2.usage.promptCacheMissTokens;
1812
1812
  }
1813
1813
  const denom = hit + miss;
1814
1814
  return denom > 0 ? hit / denom : 0;
@@ -1867,7 +1867,11 @@ var CacheFirstLoop = class {
1867
1867
  * tool call is one array length check.
1868
1868
  */
1869
1869
  hooks;
1870
- /** `cwd` reported to hook stdin. Resolved once at construction. */
1870
+ /**
1871
+ * `cwd` reported to hook stdin. Mutable so `/cwd` can switch the
1872
+ * working directory mid-session — the App keeps it in sync with
1873
+ * the same currentRootDir that drives tool re-registration.
1874
+ */
1871
1875
  hookCwd;
1872
1876
  /** Number of messages that were pre-loaded from the session file. */
1873
1877
  resumedMessageCount;
@@ -2198,13 +2202,13 @@ var CacheFirstLoop = class {
2198
2202
  * with `<`.
2199
2203
  */
2200
2204
  looksLikePartialEscalationMarker(buf) {
2201
- const t = buf.trimStart();
2202
- if (t.length === 0) return true;
2203
- if (t.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2204
- return NEEDS_PRO_MARKER_PREFIX.startsWith(t);
2205
+ const t2 = buf.trimStart();
2206
+ if (t2.length === 0) return true;
2207
+ if (t2.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2208
+ return NEEDS_PRO_MARKER_PREFIX.startsWith(t2);
2205
2209
  }
2206
- if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
- const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
2210
+ if (!t2.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2211
+ const rest = t2.slice(NEEDS_PRO_MARKER_PREFIX.length);
2208
2212
  if (rest[0] !== ">" && rest[0] !== ":") return false;
2209
2213
  return true;
2210
2214
  }
@@ -2312,7 +2316,9 @@ var CacheFirstLoop = class {
2312
2316
  this._proArmedForNextTurn = false;
2313
2317
  armedConsumed = true;
2314
2318
  }
2319
+ const carryAbort = this._turnAbort.signal.aborted;
2315
2320
  this._turnAbort = new AbortController();
2321
+ if (carryAbort) this._turnAbort.abort();
2316
2322
  const signal = this._turnAbort.signal;
2317
2323
  if (armedConsumed) {
2318
2324
  yield {
@@ -2342,6 +2348,7 @@ var CacheFirstLoop = class {
2342
2348
  };
2343
2349
  this.autoCompactToolResultsOnTurnEnd();
2344
2350
  yield { turn: this._turn, role: "done", content: stoppedMsg };
2351
+ this._turnAbort = new AbortController();
2345
2352
  return;
2346
2353
  }
2347
2354
  if (iter > 0) {
@@ -2435,8 +2442,8 @@ var CacheFirstLoop = class {
2435
2442
  }
2436
2443
  );
2437
2444
  for (let k = 0; k < budget; k++) {
2438
- const sample = queue.shift() ?? await new Promise((resolve8) => {
2439
- waiter = resolve8;
2445
+ const sample = queue.shift() ?? await new Promise((resolve12) => {
2446
+ waiter = resolve12;
2440
2447
  });
2441
2448
  yield {
2442
2449
  turn: this._turn,
@@ -3259,7 +3266,7 @@ function rankPickerCandidates(files, query, limitOrOpts) {
3259
3266
  var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
3260
3267
  function expandAtMentions(text, rootDir, opts = {}) {
3261
3268
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
3262
- const fs2 = opts.fs ?? defaultFs;
3269
+ const fs6 = opts.fs ?? defaultFs;
3263
3270
  const root = resolve(rootDir);
3264
3271
  const seen = /* @__PURE__ */ new Map();
3265
3272
  const expansions = [];
@@ -3269,7 +3276,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3269
3276
  if (!cleaned) continue;
3270
3277
  const token = `@${cleaned}`;
3271
3278
  if (seen.has(token)) continue;
3272
- const expansion = resolveMention(cleaned, root, maxBytes, fs2);
3279
+ const expansion = resolveMention(cleaned, root, maxBytes, fs6);
3273
3280
  seen.set(token, expansion);
3274
3281
  expansions.push(expansion);
3275
3282
  }
@@ -3277,7 +3284,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3277
3284
  const blocks = [];
3278
3285
  for (const ex of expansions) {
3279
3286
  if (ex.ok) {
3280
- const content = readSafe(root, ex.path, fs2);
3287
+ const content = readSafe(root, ex.path, fs6);
3281
3288
  blocks.push(`<file path="${ex.path}">
3282
3289
  ${content}
3283
3290
  </file>`);
@@ -3291,7 +3298,7 @@ ${content}
3291
3298
  ${blocks.join("\n\n")}`;
3292
3299
  return { text: augmented, expansions };
3293
3300
  }
3294
- function resolveMention(rawPath, root, maxBytes, fs2) {
3301
+ function resolveMention(rawPath, root, maxBytes, fs6) {
3295
3302
  if (isAbsolute(rawPath)) {
3296
3303
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3297
3304
  }
@@ -3300,22 +3307,22 @@ function resolveMention(rawPath, root, maxBytes, fs2) {
3300
3307
  if (rel.startsWith("..") || isAbsolute(rel)) {
3301
3308
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3302
3309
  }
3303
- if (!fs2.exists(resolved)) {
3310
+ if (!fs6.exists(resolved)) {
3304
3311
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
3305
3312
  }
3306
- if (!fs2.isFile(resolved)) {
3313
+ if (!fs6.isFile(resolved)) {
3307
3314
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
3308
3315
  }
3309
- const size = fs2.size(resolved);
3316
+ const size = fs6.size(resolved);
3310
3317
  if (size > maxBytes) {
3311
3318
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
3312
3319
  }
3313
3320
  return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
3314
3321
  }
3315
- function readSafe(root, rawPath, fs2) {
3322
+ function readSafe(root, rawPath, fs6) {
3316
3323
  const resolved = resolve(root, rawPath);
3317
3324
  try {
3318
- return fs2.read(resolved);
3325
+ return fs6.read(resolved);
3319
3326
  } catch {
3320
3327
  return "(read failed)";
3321
3328
  }
@@ -3513,9 +3520,9 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3513
3520
  ".pyo"
3514
3521
  ]);
3515
3522
  function isLikelyBinaryByName(name) {
3516
- const dot = name.lastIndexOf(".");
3517
- if (dot < 0) return false;
3518
- return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
3523
+ const dot2 = name.lastIndexOf(".");
3524
+ if (dot2 < 0) return false;
3525
+ return BINARY_EXTENSIONS.has(name.slice(dot2).toLowerCase());
3519
3526
  }
3520
3527
  function registerFilesystemTools(registry, opts) {
3521
3528
  const rootDir = pathMod.resolve(opts.rootDir);
@@ -4077,7 +4084,7 @@ function registerMemoryTools(registry, opts = {}) {
4077
4084
  });
4078
4085
  }
4079
4086
  try {
4080
- const path = store.write({
4087
+ const path5 = store.write({
4081
4088
  name: args.name,
4082
4089
  type: args.type,
4083
4090
  scope: args.scope,
@@ -4090,7 +4097,7 @@ function registerMemoryTools(registry, opts = {}) {
4090
4097
  "",
4091
4098
  "TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
4092
4099
  "The user just told you \u2014 don't re-explore the filesystem to re-derive it.",
4093
- `(Saved to ${path}; pins into the system prompt on next /new or launch.)`
4100
+ `(Saved to ${path5}; pins into the system prompt on next /new or launch.)`
4094
4101
  ].join("\n");
4095
4102
  } catch (err) {
4096
4103
  return JSON.stringify({ error: `remember failed: ${err.message}` });
@@ -4568,7 +4575,11 @@ async function spawnSubagent(opts) {
4568
4575
  stream: false
4569
4576
  });
4570
4577
  const onParentAbort = () => childLoop.abort();
4571
- opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4578
+ if (opts.parentSignal?.aborted) {
4579
+ childLoop.abort();
4580
+ } else {
4581
+ opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4582
+ }
4572
4583
  let final = "";
4573
4584
  let errorMessage;
4574
4585
  let toolIter = 0;
@@ -4586,7 +4597,11 @@ async function spawnSubagent(opts) {
4586
4597
  });
4587
4598
  }
4588
4599
  if (ev.role === "assistant_final") {
4589
- final = ev.content ?? "";
4600
+ if (ev.forcedSummary) {
4601
+ errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
4602
+ } else {
4603
+ final = ev.content ?? "";
4604
+ }
4590
4605
  }
4591
4606
  if (ev.role === "error") {
4592
4607
  errorMessage = ev.error ?? "subagent error";
@@ -4635,12 +4650,12 @@ async function spawnSubagent(opts) {
4635
4650
  }
4636
4651
  function aggregateChildUsage(loop2) {
4637
4652
  const agg = new Usage();
4638
- for (const t of loop2.stats.turns) {
4639
- agg.promptTokens += t.usage.promptTokens;
4640
- agg.completionTokens += t.usage.completionTokens;
4641
- agg.totalTokens += t.usage.totalTokens;
4642
- agg.promptCacheHitTokens += t.usage.promptCacheHitTokens;
4643
- agg.promptCacheMissTokens += t.usage.promptCacheMissTokens;
4653
+ for (const t2 of loop2.stats.turns) {
4654
+ agg.promptTokens += t2.usage.promptTokens;
4655
+ agg.completionTokens += t2.usage.completionTokens;
4656
+ agg.totalTokens += t2.usage.totalTokens;
4657
+ agg.promptCacheHitTokens += t2.usage.promptCacheHitTokens;
4658
+ agg.promptCacheMissTokens += t2.usage.promptCacheMissTokens;
4644
4659
  }
4645
4660
  return agg;
4646
4661
  }
@@ -5158,7 +5173,7 @@ async function runCommand(cmd, opts) {
5158
5173
  };
5159
5174
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5160
5175
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5161
- return await new Promise((resolve8, reject) => {
5176
+ return await new Promise((resolve12, reject) => {
5162
5177
  let child;
5163
5178
  try {
5164
5179
  child = spawn3(bin, args, effectiveSpawnOpts);
@@ -5166,7 +5181,9 @@ async function runCommand(cmd, opts) {
5166
5181
  reject(err);
5167
5182
  return;
5168
5183
  }
5169
- let buf = "";
5184
+ const chunks = [];
5185
+ let totalBytes = 0;
5186
+ const byteCap = maxChars * 2 * 4;
5170
5187
  let timedOut = false;
5171
5188
  const killTimer = setTimeout(() => {
5172
5189
  timedOut = true;
@@ -5175,8 +5192,16 @@ async function runCommand(cmd, opts) {
5175
5192
  const onAbort = () => child.kill("SIGKILL");
5176
5193
  opts.signal?.addEventListener("abort", onAbort, { once: true });
5177
5194
  const onData = (chunk) => {
5178
- buf += chunk.toString();
5179
- if (buf.length > maxChars * 2) buf = `${buf.slice(0, maxChars * 2)}`;
5195
+ const b = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
5196
+ if (totalBytes >= byteCap) return;
5197
+ const remaining = byteCap - totalBytes;
5198
+ if (b.length > remaining) {
5199
+ chunks.push(b.subarray(0, remaining));
5200
+ totalBytes = byteCap;
5201
+ } else {
5202
+ chunks.push(b);
5203
+ totalBytes += b.length;
5204
+ }
5180
5205
  };
5181
5206
  child.stdout?.on("data", onData);
5182
5207
  child.stderr?.on("data", onData);
@@ -5188,13 +5213,29 @@ async function runCommand(cmd, opts) {
5188
5213
  child.on("close", (code) => {
5189
5214
  clearTimeout(killTimer);
5190
5215
  opts.signal?.removeEventListener("abort", onAbort);
5216
+ const merged = Buffer.concat(chunks);
5217
+ const buf = smartDecodeOutput(merged);
5191
5218
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5192
5219
 
5193
5220
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5194
- resolve8({ exitCode: code, output, timedOut });
5221
+ resolve12({ exitCode: code, output, timedOut });
5195
5222
  });
5196
5223
  });
5197
5224
  }
5225
+ function smartDecodeOutput(buf) {
5226
+ if (buf.length === 0) return "";
5227
+ try {
5228
+ return new TextDecoder("utf-8", { fatal: true }).decode(buf);
5229
+ } catch {
5230
+ }
5231
+ if (process.platform === "win32") {
5232
+ try {
5233
+ return new TextDecoder("gb18030").decode(buf);
5234
+ } catch {
5235
+ }
5236
+ }
5237
+ return buf.toString("utf8");
5238
+ }
5198
5239
  function resolveExecutable(cmd, opts = {}) {
5199
5240
  const platform = opts.platform ?? process.platform;
5200
5241
  if (platform !== "win32") return cmd;
@@ -5664,10 +5705,10 @@ ${i + 1}. ${r.title}`);
5664
5705
  // src/env.ts
5665
5706
  import { readFileSync as readFileSync6 } from "fs";
5666
5707
  import { resolve as resolve5 } from "path";
5667
- function loadDotenv(path = ".env") {
5708
+ function loadDotenv(path5 = ".env") {
5668
5709
  let raw;
5669
5710
  try {
5670
- raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
5711
+ raw = readFileSync6(resolve5(process.cwd(), path5), "utf8");
5671
5712
  } catch {
5672
5713
  return;
5673
5714
  }
@@ -5731,13 +5772,13 @@ function writeMeta(stream, meta) {
5731
5772
  stream.write(`${JSON.stringify(line)}
5732
5773
  `);
5733
5774
  }
5734
- function openTranscriptFile(path, meta) {
5735
- const stream = createWriteStream(path, { flags: "a" });
5775
+ function openTranscriptFile(path5, meta) {
5776
+ const stream = createWriteStream(path5, { flags: "a" });
5736
5777
  writeMeta(stream, meta);
5737
5778
  return stream;
5738
5779
  }
5739
- function readTranscript(path) {
5740
- const raw = readFileSync7(path, "utf8");
5780
+ function readTranscript(path5) {
5781
+ const raw = readFileSync7(path5, "utf8");
5741
5782
  return parseTranscript(raw);
5742
5783
  }
5743
5784
  function isPlanStateEmptyShape(s) {
@@ -5786,8 +5827,8 @@ function computeCumulativeStats(pages, upToIdx) {
5786
5827
  }
5787
5828
  return computeReplayStats(flat);
5788
5829
  }
5789
- function replayFromFile(path) {
5790
- const parsed = readTranscript(path);
5830
+ function replayFromFile(path5) {
5831
+ const parsed = readTranscript(path5);
5791
5832
  return { parsed, stats: computeReplayStats(parsed.records) };
5792
5833
  }
5793
5834
  function computeReplayStats(records) {
@@ -5844,15 +5885,15 @@ function computeReplayStats(records) {
5844
5885
  };
5845
5886
  }
5846
5887
  function summarizeTurns(turns) {
5847
- const totalCost = turns.reduce((s, t) => s + t.cost, 0);
5848
- const totalInput = turns.reduce((s, t) => s + inputCostUsd(t.model, t.usage), 0);
5849
- const totalOutput = turns.reduce((s, t) => s + outputCostUsd(t.model, t.usage), 0);
5850
- const totalClaude = turns.reduce((s, t) => s + claudeEquivalentCost(t.usage), 0);
5888
+ const totalCost = turns.reduce((s, t2) => s + t2.cost, 0);
5889
+ const totalInput = turns.reduce((s, t2) => s + inputCostUsd(t2.model, t2.usage), 0);
5890
+ const totalOutput = turns.reduce((s, t2) => s + outputCostUsd(t2.model, t2.usage), 0);
5891
+ const totalClaude = turns.reduce((s, t2) => s + claudeEquivalentCost(t2.usage), 0);
5851
5892
  let hit = 0;
5852
5893
  let miss = 0;
5853
- for (const t of turns) {
5854
- hit += t.usage.promptCacheHitTokens;
5855
- miss += t.usage.promptCacheMissTokens;
5894
+ for (const t2 of turns) {
5895
+ hit += t2.usage.promptCacheHitTokens;
5896
+ miss += t2.usage.promptCacheMissTokens;
5856
5897
  }
5857
5898
  const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
5858
5899
  const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
@@ -5929,8 +5970,8 @@ function diffTranscripts(a, b) {
5929
5970
  return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
5930
5971
  }
5931
5972
  function classifyDivergence(a, b, aTools, bTools) {
5932
- const aNames = aTools.map((t) => t.tool ?? "").sort();
5933
- const bNames = bTools.map((t) => t.tool ?? "").sort();
5973
+ const aNames = aTools.map((t2) => t2.tool ?? "").sort();
5974
+ const bNames = bTools.map((t2) => t2.tool ?? "").sort();
5934
5975
  if (aNames.join(",") !== bNames.join(",")) {
5935
5976
  return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
5936
5977
  }
@@ -5960,7 +6001,7 @@ function tokenOverlap(a, b) {
5960
6001
  const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
5961
6002
  if (ta.size === 0 && tb.size === 0) return 1;
5962
6003
  let shared = 0;
5963
- for (const t of ta) if (tb.has(t)) shared++;
6004
+ for (const t2 of ta) if (tb.has(t2)) shared++;
5964
6005
  return 2 * shared / (ta.size + tb.size);
5965
6006
  }
5966
6007
  function levenshtein(a, b) {
@@ -6153,8 +6194,8 @@ function renderMarkdown(report) {
6153
6194
  out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
6154
6195
  out.push("|---:|:---:|---|---|---|");
6155
6196
  for (const p of report.pairs) {
6156
- const aTools = p.aTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
6157
- const bTools = p.bTools.map((t) => t.tool).filter(Boolean).join(", ") || "\u2014";
6197
+ const aTools = p.aTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6198
+ const bTools = p.bTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6158
6199
  out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
6159
6200
  }
6160
6201
  out.push("");
@@ -6381,7 +6422,7 @@ var McpClient = class {
6381
6422
  const id = this.nextId++;
6382
6423
  const frame = { jsonrpc: "2.0", id, method, params };
6383
6424
  let abortHandler = null;
6384
- const promise = new Promise((resolve8, reject) => {
6425
+ const promise = new Promise((resolve12, reject) => {
6385
6426
  const timeout = setTimeout(() => {
6386
6427
  this.pending.delete(id);
6387
6428
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -6390,7 +6431,7 @@ var McpClient = class {
6390
6431
  );
6391
6432
  }, this.requestTimeoutMs);
6392
6433
  this.pending.set(id, {
6393
- resolve: resolve8,
6434
+ resolve: resolve12,
6394
6435
  reject,
6395
6436
  timeout
6396
6437
  });
@@ -6513,12 +6554,12 @@ var StdioTransport = class {
6513
6554
  }
6514
6555
  async send(message) {
6515
6556
  if (this.closed) throw new Error("MCP transport is closed");
6516
- return new Promise((resolve8, reject) => {
6557
+ return new Promise((resolve12, reject) => {
6517
6558
  const line = `${JSON.stringify(message)}
6518
6559
  `;
6519
6560
  this.child.stdin.write(line, "utf8", (err) => {
6520
6561
  if (err) reject(err);
6521
- else resolve8();
6562
+ else resolve12();
6522
6563
  });
6523
6564
  });
6524
6565
  }
@@ -6529,8 +6570,8 @@ var StdioTransport = class {
6529
6570
  continue;
6530
6571
  }
6531
6572
  if (this.closed) return;
6532
- const next = await new Promise((resolve8) => {
6533
- this.waiters.push(resolve8);
6573
+ const next = await new Promise((resolve12) => {
6574
+ this.waiters.push(resolve12);
6534
6575
  });
6535
6576
  if (next === null) return;
6536
6577
  yield next;
@@ -6596,8 +6637,8 @@ var SseTransport = class {
6596
6637
  constructor(opts) {
6597
6638
  this.url = opts.url;
6598
6639
  this.headers = opts.headers ?? {};
6599
- this.endpointReady = new Promise((resolve8, reject) => {
6600
- this.resolveEndpoint = resolve8;
6640
+ this.endpointReady = new Promise((resolve12, reject) => {
6641
+ this.resolveEndpoint = resolve12;
6601
6642
  this.rejectEndpoint = reject;
6602
6643
  });
6603
6644
  this.endpointReady.catch(() => void 0);
@@ -6624,8 +6665,8 @@ var SseTransport = class {
6624
6665
  continue;
6625
6666
  }
6626
6667
  if (this.closed) return;
6627
- const next = await new Promise((resolve8) => {
6628
- this.waiters.push(resolve8);
6668
+ const next = await new Promise((resolve12) => {
6669
+ this.waiters.push(resolve12);
6629
6670
  });
6630
6671
  if (next === null) return;
6631
6672
  yield next;
@@ -6893,8 +6934,8 @@ function applyEditBlock(block, rootDir) {
6893
6934
  function applyEditBlocks(blocks, rootDir) {
6894
6935
  return blocks.map((b) => applyEditBlock(b, rootDir));
6895
6936
  }
6896
- function toWholeFileEditBlock(path, content, rootDir) {
6897
- const abs = resolve6(rootDir, path);
6937
+ function toWholeFileEditBlock(path5, content, rootDir) {
6938
+ const abs = resolve6(rootDir, path5);
6898
6939
  let search = "";
6899
6940
  if (existsSync6(abs)) {
6900
6941
  try {
@@ -6903,7 +6944,7 @@ function toWholeFileEditBlock(path, content, rootDir) {
6903
6944
  search = "";
6904
6945
  }
6905
6946
  }
6906
- return { path, search, replace: content, offset: 0 };
6947
+ return { path: path5, search, replace: content, offset: 0 };
6907
6948
  }
6908
6949
  function snapshotBeforeEdits(blocks, rootDir) {
6909
6950
  const absRoot = resolve6(rootDir);
@@ -7082,20 +7123,20 @@ function appendUsage(input) {
7082
7123
  };
7083
7124
  if (input.kind === "subagent") record.kind = "subagent";
7084
7125
  if (input.subagent) record.subagent = input.subagent;
7085
- const path = input.path ?? defaultUsageLogPath();
7126
+ const path5 = input.path ?? defaultUsageLogPath();
7086
7127
  try {
7087
- mkdirSync5(dirname7(path), { recursive: true });
7088
- appendFileSync2(path, `${JSON.stringify(record)}
7128
+ mkdirSync5(dirname7(path5), { recursive: true });
7129
+ appendFileSync2(path5, `${JSON.stringify(record)}
7089
7130
  `, "utf8");
7090
7131
  } catch {
7091
7132
  }
7092
7133
  return record;
7093
7134
  }
7094
- function readUsageLog(path = defaultUsageLogPath()) {
7095
- if (!existsSync8(path)) return [];
7135
+ function readUsageLog(path5 = defaultUsageLogPath()) {
7136
+ if (!existsSync8(path5)) return [];
7096
7137
  let raw;
7097
7138
  try {
7098
- raw = readFileSync10(path, "utf8");
7139
+ raw = readFileSync10(path5, "utf8");
7099
7140
  } catch {
7100
7141
  return [];
7101
7142
  }
@@ -7199,10 +7240,10 @@ function aggregateUsage(records, opts = {}) {
7199
7240
  subagents
7200
7241
  };
7201
7242
  }
7202
- function formatLogSize(path = defaultUsageLogPath()) {
7203
- if (!existsSync8(path)) return "";
7243
+ function formatLogSize(path5 = defaultUsageLogPath()) {
7244
+ if (!existsSync8(path5)) return "";
7204
7245
  try {
7205
- const s = statSync4(path);
7246
+ const s = statSync4(path5);
7206
7247
  const bytes = s.size;
7207
7248
  if (bytes < 1024) return `${bytes} B`;
7208
7249
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -7213,13 +7254,14 @@ function formatLogSize(path = defaultUsageLogPath()) {
7213
7254
  }
7214
7255
 
7215
7256
  // src/cli/commands/chat.tsx
7216
- import { existsSync as existsSync13, statSync as statSync7 } from "fs";
7257
+ import { existsSync as existsSync15, statSync as statSync9 } from "fs";
7217
7258
  import { render } from "ink";
7218
- import React26, { useState as useState12 } from "react";
7259
+ import React27, { useState as useState12 } from "react";
7219
7260
 
7220
7261
  // src/cli/ui/App.tsx
7221
- import { Box as Box21, Static, Text as Text19, useApp, useStdout as useStdout8 } from "ink";
7222
- import React23, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7262
+ import * as pathMod6 from "path";
7263
+ import { Box as Box22, Static, Text as Text20, useApp, useStdout as useStdout8 } from "ink";
7264
+ import React24, { useCallback as useCallback4, useEffect as useEffect6, useMemo as useMemo3, useRef as useRef6, useState as useState10 } from "react";
7223
7265
 
7224
7266
  // src/code/pending-edits.ts
7225
7267
  import { existsSync as existsSync9, mkdirSync as mkdirSync6, readFileSync as readFileSync11, unlinkSync as unlinkSync3, writeFileSync as writeFileSync5 } from "fs";
@@ -7229,24 +7271,24 @@ function pendingEditsPath(sessionName) {
7229
7271
  }
7230
7272
  function savePendingEdits(sessionName, blocks) {
7231
7273
  if (!sessionName) return;
7232
- const path = pendingEditsPath(sessionName);
7274
+ const path5 = pendingEditsPath(sessionName);
7233
7275
  try {
7234
7276
  if (blocks.length === 0) {
7235
- if (existsSync9(path)) unlinkSync3(path);
7277
+ if (existsSync9(path5)) unlinkSync3(path5);
7236
7278
  return;
7237
7279
  }
7238
- mkdirSync6(dirname8(path), { recursive: true });
7239
- writeFileSync5(path, JSON.stringify(blocks, null, 2), "utf8");
7280
+ mkdirSync6(dirname8(path5), { recursive: true });
7281
+ writeFileSync5(path5, JSON.stringify(blocks, null, 2), "utf8");
7240
7282
  } catch {
7241
7283
  }
7242
7284
  }
7243
7285
  function loadPendingEdits(sessionName) {
7244
7286
  if (!sessionName) return null;
7245
- const path = pendingEditsPath(sessionName);
7246
- if (!existsSync9(path)) return null;
7287
+ const path5 = pendingEditsPath(sessionName);
7288
+ if (!existsSync9(path5)) return null;
7247
7289
  let raw;
7248
7290
  try {
7249
- raw = readFileSync11(path, "utf8");
7291
+ raw = readFileSync11(path5, "utf8");
7250
7292
  } catch {
7251
7293
  return null;
7252
7294
  }
@@ -7266,9 +7308,9 @@ function loadPendingEdits(sessionName) {
7266
7308
  }
7267
7309
  function clearPendingEdits(sessionName) {
7268
7310
  if (!sessionName) return;
7269
- const path = pendingEditsPath(sessionName);
7311
+ const path5 = pendingEditsPath(sessionName);
7270
7312
  try {
7271
- if (existsSync9(path)) unlinkSync3(path);
7313
+ if (existsSync9(path5)) unlinkSync3(path5);
7272
7314
  } catch {
7273
7315
  }
7274
7316
  }
@@ -7289,10 +7331,10 @@ function planStatePath(sessionName) {
7289
7331
  return join10(sessionsDir(), `${sanitizeName(sessionName)}.plan.json`);
7290
7332
  }
7291
7333
  function loadPlanState(sessionName) {
7292
- const path = planStatePath(sessionName);
7293
- if (!existsSync10(path)) return null;
7334
+ const path5 = planStatePath(sessionName);
7335
+ if (!existsSync10(path5)) return null;
7294
7336
  try {
7295
- const raw = readFileSync12(path, "utf8");
7337
+ const raw = readFileSync12(path5, "utf8");
7296
7338
  const parsed = JSON.parse(raw);
7297
7339
  if (!parsed || typeof parsed !== "object") return null;
7298
7340
  if (parsed.version !== 1) return null;
@@ -7328,9 +7370,9 @@ function loadPlanState(sessionName) {
7328
7370
  }
7329
7371
  }
7330
7372
  function savePlanState(sessionName, steps, completedStepIds, extras) {
7331
- const path = planStatePath(sessionName);
7373
+ const path5 = planStatePath(sessionName);
7332
7374
  try {
7333
- mkdirSync7(dirname9(path), { recursive: true });
7375
+ mkdirSync7(dirname9(path5), { recursive: true });
7334
7376
  const state = {
7335
7377
  version: 1,
7336
7378
  steps,
@@ -7339,7 +7381,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7339
7381
  };
7340
7382
  if (extras?.body) state.body = extras.body;
7341
7383
  if (extras?.summary) state.summary = extras.summary;
7342
- writeFileSync6(path, `${JSON.stringify(state, null, 2)}
7384
+ writeFileSync6(path5, `${JSON.stringify(state, null, 2)}
7343
7385
  `, "utf8");
7344
7386
  } catch (err) {
7345
7387
  process.stderr.write(
@@ -7349,9 +7391,9 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7349
7391
  }
7350
7392
  }
7351
7393
  function clearPlanState(sessionName) {
7352
- const path = planStatePath(sessionName);
7394
+ const path5 = planStatePath(sessionName);
7353
7395
  try {
7354
- if (existsSync10(path)) unlinkSync4(path);
7396
+ if (existsSync10(path5)) unlinkSync4(path5);
7355
7397
  } catch {
7356
7398
  }
7357
7399
  }
@@ -7419,9 +7461,9 @@ function listPlanArchives(sessionName) {
7419
7461
  return summaries;
7420
7462
  }
7421
7463
  function relativeTime(updatedAt, now = Date.now()) {
7422
- const t = Date.parse(updatedAt);
7423
- if (Number.isNaN(t)) return updatedAt;
7424
- const diffMs = Math.max(0, now - t);
7464
+ const t2 = Date.parse(updatedAt);
7465
+ if (Number.isNaN(t2)) return updatedAt;
7466
+ const diffMs = Math.max(0, now - t2);
7425
7467
  const sec = Math.floor(diffMs / 1e3);
7426
7468
  if (sec < 60) return `${sec}s ago`;
7427
7469
  const min = Math.floor(sec / 60);
@@ -7466,7 +7508,7 @@ function registerSkillTools(registry, opts = {}) {
7466
7508
  }
7467
7509
  const stripped = raw.replace(/\[[^\]]*\]/g, " ").trim();
7468
7510
  const tokens = stripped.split(/\s+/).filter(Boolean);
7469
- const name = tokens.find((t) => /^[a-zA-Z0-9]/.test(t)) ?? "";
7511
+ const name = tokens.find((t2) => /^[a-zA-Z0-9]/.test(t2)) ?? "";
7470
7512
  if (!name) {
7471
7513
  return JSON.stringify({
7472
7514
  error: "run_skill requires a 'name' argument",
@@ -7511,6 +7553,60 @@ ${skill2.body}${argsBlock}`;
7511
7553
  return registry;
7512
7554
  }
7513
7555
 
7556
+ // src/tools/workspace.ts
7557
+ import { existsSync as existsSync11, statSync as statSync6 } from "fs";
7558
+ import * as pathMod4 from "path";
7559
+ var WorkspaceConfirmationError = class extends Error {
7560
+ path;
7561
+ constructor(path5) {
7562
+ super(
7563
+ `change_workspace: switching to "${path5}" needs the user's approval before it takes effect. STOP calling tools now \u2014 the TUI has already prompted the user to press Enter (switch) or Esc (deny). Wait for their next message; it will either confirm the switch (and your subsequent file/shell tools will resolve against the new root) or tell you to continue without changing directories.`
7564
+ );
7565
+ this.name = "WorkspaceConfirmationError";
7566
+ this.path = path5;
7567
+ }
7568
+ };
7569
+ function registerWorkspaceTool(registry) {
7570
+ registry.register({
7571
+ name: "change_workspace",
7572
+ description: "Switch the session's working directory to a different project root. Re-registers filesystem / shell / memory tools against the new path so subsequent file reads, edits, and run_command calls all land there. EVERY switch requires explicit user approval via a modal \u2014 do NOT batch switches or chain a switch with subsequent tool calls before the user has confirmed. Use ONLY when the user explicitly asked to change directory or open a different project; never use to 'preview' a sibling repo. MCP servers stay anchored to the original launch root (their child processes can't be reconnected mid-session); the modal warns the user about this.",
7573
+ parameters: {
7574
+ type: "object",
7575
+ required: ["path"],
7576
+ properties: {
7577
+ path: {
7578
+ type: "string",
7579
+ description: "Target directory. Absolute paths land verbatim. Leading `~` expands to the user's home. Relative paths resolve against the user's launch cwd (not the current session root, so paths the user typed in chat resolve where they expect)."
7580
+ }
7581
+ }
7582
+ },
7583
+ fn: (rawArgs) => {
7584
+ const args = rawArgs ?? {};
7585
+ if (typeof args.path !== "string" || args.path.trim() === "") {
7586
+ throw new Error("change_workspace: `path` must be a non-empty string");
7587
+ }
7588
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
7589
+ const expanded = args.path.startsWith("~") && home ? pathMod4.join(home, args.path.slice(1)) : args.path;
7590
+ const abs = pathMod4.resolve(expanded);
7591
+ if (!existsSync11(abs)) {
7592
+ throw new Error(`change_workspace: path does not exist \u2014 ${abs}`);
7593
+ }
7594
+ try {
7595
+ if (!statSync6(abs).isDirectory()) {
7596
+ throw new Error(`change_workspace: not a directory \u2014 ${abs}`);
7597
+ }
7598
+ } catch (err) {
7599
+ if (err.code === "ENOENT") {
7600
+ throw new Error(`change_workspace: path does not exist \u2014 ${abs}`);
7601
+ }
7602
+ throw err;
7603
+ }
7604
+ throw new WorkspaceConfirmationError(abs);
7605
+ }
7606
+ });
7607
+ return registry;
7608
+ }
7609
+
7514
7610
  // src/cli/ui/AtMentionSuggestions.tsx
7515
7611
  import { Box, Text } from "ink";
7516
7612
  import React from "react";
@@ -7529,12 +7625,12 @@ function AtMentionSuggestions({
7529
7625
  const shown = matches.slice(windowStart, windowStart + MAX);
7530
7626
  const hiddenAbove = windowStart;
7531
7627
  const hiddenBelow = total - windowStart - shown.length;
7532
- return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((path, i) => /* @__PURE__ */ React.createElement(FileRow, { key: path, path, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick \xB7 file content inlined on send"));
7628
+ return /* @__PURE__ */ React.createElement(Box, { flexDirection: "column", paddingX: 1, marginTop: 1 }, hiddenAbove > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2191 ", hiddenAbove, " more above") : null, shown.map((path5, i) => /* @__PURE__ */ React.createElement(FileRow, { key: path5, path: path5, isSelected: windowStart + i === selectedIndex })), hiddenBelow > 0 ? /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " \u2193 ", hiddenBelow, " more below") : null, /* @__PURE__ */ React.createElement(Text, { dimColor: true }, " [\u2191\u2193] navigate \xB7 [Tab]/[Enter] pick \xB7 file content inlined on send"));
7533
7629
  }
7534
- function FileRow({ path, isSelected }) {
7535
- const slash = path.lastIndexOf("/");
7536
- const dir = slash >= 0 ? `${path.slice(0, slash)}/` : "";
7537
- const base = slash >= 0 ? path.slice(slash + 1) : path;
7630
+ function FileRow({ path: path5, isSelected }) {
7631
+ const slash = path5.lastIndexOf("/");
7632
+ const dir = slash >= 0 ? `${path5.slice(0, slash)}/` : "";
7633
+ const base = slash >= 0 ? path5.slice(slash + 1) : path5;
7538
7634
  if (isSelected) {
7539
7635
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { backgroundColor: "#67e8f9", color: "black", bold: true }, ` \u25B8 ${base}${dir ? ` ${dir}` : ""} `));
7540
7636
  }
@@ -7555,8 +7651,8 @@ function ModalCard({
7555
7651
  icon,
7556
7652
  children
7557
7653
  }) {
7558
- const { stdout: stdout2 } = useStdout();
7559
- const cols = stdout2?.columns ?? 80;
7654
+ const { stdout: stdout3 } = useStdout();
7655
+ const cols = stdout3?.columns ?? 80;
7560
7656
  const ruleWidth = Math.min(80, Math.max(28, cols - 4));
7561
7657
  const titleText = icon ? ` ${icon} ${title} ` : ` ${title} `;
7562
7658
  return /* @__PURE__ */ React2.createElement(Box2, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React2.createElement(Box2, null, /* @__PURE__ */ React2.createElement(Text2, { color: accent }, "\u2594".repeat(ruleWidth))), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { backgroundColor: accent, color: "black", bold: true }, titleText), subtitle ? /* @__PURE__ */ React2.createElement(Text2, { dimColor: true }, ` ${subtitle}`) : null), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1, flexDirection: "column" }, children), /* @__PURE__ */ React2.createElement(Box2, { marginTop: 1 }, /* @__PURE__ */ React2.createElement(Text2, { color: accent, dimColor: true }, "\u2581".repeat(ruleWidth))));
@@ -8119,8 +8215,8 @@ function capLines(lines, maxLines, indent) {
8119
8215
  var MODAL_OVERHEAD_ROWS = 18;
8120
8216
  var MIN_DIFF_ROWS = 8;
8121
8217
  function EditConfirm({ block, onChoose }) {
8122
- const { stdout: stdout2 } = useStdout2();
8123
- const rows = stdout2?.rows ?? 40;
8218
+ const { stdout: stdout3 } = useStdout2();
8219
+ const rows = stdout3?.rows ?? 40;
8124
8220
  const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
8125
8221
  const allLines = useMemo(
8126
8222
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
@@ -8305,8 +8401,8 @@ function RiskLegend() {
8305
8401
  var PlanStepList = React8.memo(PlanStepListInner);
8306
8402
 
8307
8403
  // src/cli/ui/markdown.tsx
8308
- import { readFileSync as readFileSync13, statSync as statSync6 } from "fs";
8309
- import { isAbsolute as isAbsolute4, join as join11 } from "path";
8404
+ import { readFileSync as readFileSync13, statSync as statSync7 } from "fs";
8405
+ import { isAbsolute as isAbsolute4, join as join12 } from "path";
8310
8406
  import { Box as Box8, Text as Text7 } from "ink";
8311
8407
  import React9 from "react";
8312
8408
  var SUPERSCRIPT = {
@@ -8348,7 +8444,32 @@ function toSubscript(s) {
8348
8444
  for (const c of s) out += SUBSCRIPT[c] ?? c;
8349
8445
  return out;
8350
8446
  }
8447
+ var HAS_MATH_RE = new RegExp(
8448
+ [
8449
+ "\\$",
8450
+ // dollar-delimited (block or inline)
8451
+ "\\\\[([]",
8452
+ // \( or \[
8453
+ "\\\\[a-zA-Z]+\\s*\\{",
8454
+ // \anyCommand{...} — covers catch-all braced transforms
8455
+ // Bare (no-brace) LaTeX commands the pipeline knows how to handle.
8456
+ // Listed explicitly because a generic `\\[a-zA-Z]+` would also match
8457
+ // Windows paths (`F:\TEST1`) and re-introduce the bug we're fixing.
8458
+ "\\\\(?:cdot|times|div|pm|mp|leq|geq|neq|approx|in|notin|infty|sum|prod|int|alpha|beta|gamma|delta|theta|lambda|mu|pi|sigma|phi|omega|implies|iff|to|rightarrow|leftarrow|Rightarrow|Leftarrow|ldots|cdots|quad|qquad)(?![a-zA-Z])",
8459
+ "[\\^_]\\{",
8460
+ // LaTeX braced super/subscript: ^{2}, _{ij}
8461
+ "\\^[0-9+\\-n](?![A-Za-z])",
8462
+ // LaTeX single-char super: ^2, ^-, ^n
8463
+ "_[0-9+\\-](?![A-Za-z])",
8464
+ // LaTeX single-char sub: _1, _+, _-
8465
+ "\\^[A-Za-z0-9+\\-]+\\^",
8466
+ // Pandoc super: ^2^, ^abc^
8467
+ "(?<!~)~[A-Za-z0-9+\\-]+~(?!~)"
8468
+ // Pandoc sub: ~2~ (lookarounds avoid ~~strike~~)
8469
+ ].join("|")
8470
+ );
8351
8471
  function stripMath(s) {
8472
+ if (!HAS_MATH_RE.test(s)) return s;
8352
8473
  return s.replace(/\$\$([\s\S]+?)\$\$/g, (_m, c) => `
8353
8474
 
8354
8475
  ${c.trim()}
@@ -8522,7 +8643,7 @@ function validateCitation(url, projectRoot) {
8522
8643
  const parts = parseCitationUrl(url);
8523
8644
  if (!parts || !parts.path) return { ok: false, reason: "empty path" };
8524
8645
  const normalized = parts.path.replace(/^[/\\]+/, "");
8525
- const baseFullPath = isAbsolute4(normalized) ? normalized : join11(projectRoot, normalized);
8646
+ const baseFullPath = isAbsolute4(normalized) ? normalized : join12(projectRoot, normalized);
8526
8647
  const siblings = SIBLING_EXTENSIONS.get(extOf(baseFullPath)) ?? [];
8527
8648
  const candidates = [
8528
8649
  baseFullPath,
@@ -8532,7 +8653,7 @@ function validateCitation(url, projectRoot) {
8532
8653
  let stat = null;
8533
8654
  for (const candidate of candidates) {
8534
8655
  try {
8535
- stat = statSync6(candidate);
8656
+ stat = statSync7(candidate);
8536
8657
  fullPath = candidate;
8537
8658
  break;
8538
8659
  } catch {
@@ -9110,10 +9231,10 @@ function gradientCells(width, glyph = GLYPH.block) {
9110
9231
  if (width <= 0) return cells;
9111
9232
  const last = GRADIENT.length - 1;
9112
9233
  for (let i = 0; i < width; i++) {
9113
- const t = width === 1 ? 0 : i * last / (width - 1);
9114
- const lo = Math.floor(t);
9234
+ const t2 = width === 1 ? 0 : i * last / (width - 1);
9235
+ const lo = Math.floor(t2);
9115
9236
  const hi = Math.min(last, lo + 1);
9116
- const color = t - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9237
+ const color = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9117
9238
  cells.push({ ch: glyph, color });
9118
9239
  }
9119
9240
  return cells;
@@ -9127,7 +9248,7 @@ function TickerProvider({ children, disabled }) {
9127
9248
  const [tick, setTick] = useState3(0);
9128
9249
  useEffect2(() => {
9129
9250
  if (disabled) return;
9130
- const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
9251
+ const id = setInterval(() => setTick((t2) => t2 + 1), TICK_MS);
9131
9252
  return () => clearInterval(id);
9132
9253
  }, [disabled]);
9133
9254
  return /* @__PURE__ */ React10.createElement(TickContext.Provider, { value: tick }, children);
@@ -9416,8 +9537,8 @@ var EventRow = React11.memo(function EventRow2({
9416
9537
  return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, null, event.text));
9417
9538
  });
9418
9539
  function TurnSeparator() {
9419
- const { stdout: stdout2 } = useStdout3();
9420
- const cols = stdout2?.columns ?? 80;
9540
+ const { stdout: stdout3 } = useStdout3();
9541
+ const cols = stdout3?.columns ?? 80;
9421
9542
  const width = Math.max(16, cols - 2);
9422
9543
  const sideWidth = Math.max(2, Math.floor((width - 5) / 2));
9423
9544
  const leftCells = gradientCells(sideWidth, "\u2500");
@@ -9448,10 +9569,10 @@ function EditFileDiff({ text }) {
9448
9569
  function BranchBlock({ branch: branch2 }) {
9449
9570
  return /* @__PURE__ */ React11.createElement(Box9, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, { backgroundColor: "#93c5fd", color: "black", bold: true }, ` \u2387 BRANCH \xD7${branch2.budget} `), /* @__PURE__ */ React11.createElement(Text8, null, " "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd" }, "picked "), /* @__PURE__ */ React11.createElement(Text8, { color: "#93c5fd", bold: true }, "#", branch2.chosenIndex)), /* @__PURE__ */ React11.createElement(Box9, { paddingLeft: 2, marginTop: 1 }, branch2.uncertainties.map((u, i) => {
9450
9571
  const chosen = i === branch2.chosenIndex;
9451
- const t = (branch2.temperatures[i] ?? 0).toFixed(1);
9572
+ const t2 = (branch2.temperatures[i] ?? 0).toFixed(1);
9452
9573
  return (
9453
9574
  // biome-ignore lint/suspicious/noArrayIndexKey: branch index is positional and stable
9454
- /* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${t} u=${u} `))
9575
+ /* @__PURE__ */ React11.createElement(Text8, { key: `b-${i}` }, /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#475569", bold: chosen }, chosen ? "\u25B8 " : " "), /* @__PURE__ */ React11.createElement(Text8, { color: chosen ? "#93c5fd" : "#94a3b8", bold: chosen }, `#${i}`), /* @__PURE__ */ React11.createElement(Text8, { dimColor: true }, ` T=${t2} u=${u} `))
9455
9576
  );
9456
9577
  })));
9457
9578
  }
@@ -9575,8 +9696,8 @@ function ModeStatusBar({
9575
9696
  return /* @__PURE__ */ React12.createElement(ModeBarFrame, null, /* @__PURE__ */ React12.createElement(ModePill, { label, bg, flash }), /* @__PURE__ */ React12.createElement(Text9, { dimColor: true }, ` ${mid} \xB7 Shift+Tab to flip`), jobsTag);
9576
9697
  }
9577
9698
  function ModeBarFrame({ children }) {
9578
- const { stdout: stdout2 } = useStdout4();
9579
- const cols = stdout2?.columns ?? 80;
9699
+ const { stdout: stdout3 } = useStdout4();
9700
+ const cols = stdout3?.columns ?? 80;
9580
9701
  const ruleWidth = Math.max(20, cols - 2);
9581
9702
  return /* @__PURE__ */ React12.createElement(Box10, { flexDirection: "column" }, /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, /* @__PURE__ */ React12.createElement(Text9, { color: "#475569", dimColor: true }, "\u254C".repeat(ruleWidth))), /* @__PURE__ */ React12.createElement(Box10, { paddingX: 1 }, children));
9582
9703
  }
@@ -9635,26 +9756,26 @@ function summarizeToolArgs(name, args) {
9635
9756
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9636
9757
  }
9637
9758
  const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
9638
- const path = typeof parsed.path === "string" ? parsed.path : void 0;
9759
+ const path5 = typeof parsed.path === "string" ? parsed.path : void 0;
9639
9760
  if (hasSuffix("read_file")) {
9640
9761
  const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
9641
9762
  const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
9642
- return `path: ${path ?? "?"}${head}${tail}`;
9763
+ return `path: ${path5 ?? "?"}${head}${tail}`;
9643
9764
  }
9644
9765
  if (hasSuffix("write_file")) {
9645
9766
  const content = typeof parsed.content === "string" ? parsed.content : "";
9646
- return `path: ${path ?? "?"} (${content.length} chars)`;
9767
+ return `path: ${path5 ?? "?"} (${content.length} chars)`;
9647
9768
  }
9648
9769
  if (hasSuffix("edit_file")) {
9649
9770
  const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
9650
- return `path: ${path ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9771
+ return `path: ${path5 ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9651
9772
  }
9652
9773
  if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
9653
- return `path: ${path ?? "?"}`;
9774
+ return `path: ${path5 ?? "?"}`;
9654
9775
  }
9655
9776
  if (hasSuffix("search_files")) {
9656
9777
  const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
9657
- return `path: ${path ?? "?"} \xB7 pattern: ${pattern}`;
9778
+ return `path: ${path5 ?? "?"} \xB7 pattern: ${pattern}`;
9658
9779
  }
9659
9780
  if (hasSuffix("move_file")) {
9660
9781
  const src = typeof parsed.source === "string" ? parsed.source : "?";
@@ -9662,7 +9783,7 @@ function summarizeToolArgs(name, args) {
9662
9783
  return `${src} \u2192 ${dst}`;
9663
9784
  }
9664
9785
  if (hasSuffix("get_file_info")) {
9665
- return `path: ${path ?? "?"}`;
9786
+ return `path: ${path5 ?? "?"}`;
9666
9787
  }
9667
9788
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9668
9789
  }
@@ -10377,8 +10498,8 @@ function PromptInput({
10377
10498
  if (action.historyHandoff === "prev") onHistoryPrev?.();
10378
10499
  if (action.historyHandoff === "next") onHistoryNext?.();
10379
10500
  }, !disabled);
10380
- const { stdout: stdout2 } = useStdout5();
10381
- const cols = stdout2?.columns ?? 80;
10501
+ const { stdout: stdout3 } = useStdout5();
10502
+ const cols = stdout3?.columns ?? 80;
10382
10503
  const narrow = cols <= 90;
10383
10504
  const promptBody = narrow ? "\u203A " : "you \u203A ";
10384
10505
  const promptPrefix = BAR + promptBody;
@@ -10749,8 +10870,8 @@ function StatsPanel({
10749
10870
  const branchOn = (branchBudget ?? 1) > 1;
10750
10871
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
10751
10872
  const ctxRatio = summary.lastPromptTokens / ctxMax;
10752
- const { stdout: stdout2 } = useStdout6();
10753
- const columns = stdout2?.columns ?? 80;
10873
+ const { stdout: stdout3 } = useStdout6();
10874
+ const columns = stdout3?.columns ?? 80;
10754
10875
  const narrow = columns < NARROW_BREAKPOINT;
10755
10876
  const coldStart = summary.turns <= COLD_START_TURNS;
10756
10877
  return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
@@ -10916,8 +11037,8 @@ function formatTokens(n) {
10916
11037
  import { Box as Box20, Text as Text18, useStdout as useStdout7 } from "ink";
10917
11038
  import React22 from "react";
10918
11039
  function WelcomeBanner({ inCodeMode }) {
10919
- const { stdout: stdout2 } = useStdout7();
10920
- const cols = stdout2?.columns ?? 80;
11040
+ const { stdout: stdout3 } = useStdout7();
11041
+ const cols = stdout3?.columns ?? 80;
10921
11042
  const ruleWidth = Math.min(60, Math.max(28, cols - 4));
10922
11043
  return /* @__PURE__ */ React22.createElement(Box20, { flexDirection: "column", paddingX: 1, marginY: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth }), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.brand }, "\u25C8 welcome"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " \xB7 type a message to start")), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.primary }, "quick start")), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/help", desc: "every command + keyboard shortcut" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "/skill", desc: "invoke a stored playbook" }), inCodeMode ? /* @__PURE__ */ React22.createElement(React22.Fragment, null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "@path", desc: "inline a file in your message" }), /* @__PURE__ */ React22.createElement(Hint, { cmd: "!cmd", desc: "run a shell command, output goes to context" })) : null, /* @__PURE__ */ React22.createElement(Hint, { cmd: "/exit", desc: "quit (Ctrl+C also works)" }), /* @__PURE__ */ React22.createElement(BarRow, null), /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { dimColor: true, italic: true }, "tip:"), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, " Ctrl+J inserts a newline \xB7 trailing \\ also continues")), /* @__PURE__ */ React22.createElement(Box20, { marginTop: 1 }, /* @__PURE__ */ React22.createElement(GradientRule, { width: ruleWidth, thin: true })));
10923
11044
  }
@@ -10935,6 +11056,39 @@ function Hint({ cmd, desc }) {
10935
11056
  return /* @__PURE__ */ React22.createElement(BarRow, null, /* @__PURE__ */ React22.createElement(Text18, { bold: true, color: COLOR.accent }, cmd.padEnd(8)), /* @__PURE__ */ React22.createElement(Text18, { dimColor: true }, ` ${desc}`));
10936
11057
  }
10937
11058
 
11059
+ // src/cli/ui/WorkspaceConfirm.tsx
11060
+ import { Box as Box21, Text as Text19 } from "ink";
11061
+ import React23 from "react";
11062
+ function WorkspaceConfirm({
11063
+ path: path5,
11064
+ currentRoot,
11065
+ mcpServerCount,
11066
+ onChoose
11067
+ }) {
11068
+ const subtitle = mcpServerCount > 0 ? `MCP servers (${mcpServerCount}) stay anchored to the original launch root.` : "Re-registers filesystem / shell / memory tools at the new path.";
11069
+ return /* @__PURE__ */ React23.createElement(ModalCard, { accent: "#f59e0b", icon: "\u21C4", title: "switch workspace", subtitle }, /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "from "), /* @__PURE__ */ React23.createElement(Text19, { color: "#a3a3a3" }, currentRoot)), /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { dimColor: true }, "to "), /* @__PURE__ */ React23.createElement(Text19, { color: "#67e8f9", bold: true }, path5))), /* @__PURE__ */ React23.createElement(
11070
+ SingleSelect,
11071
+ {
11072
+ initialValue: "switch",
11073
+ items: [
11074
+ {
11075
+ value: "switch",
11076
+ label: "Switch",
11077
+ hint: "Re-register filesystem / shell / memory tools against the new root."
11078
+ },
11079
+ {
11080
+ value: "deny",
11081
+ label: "Deny",
11082
+ hint: "Tell the model the user refused; it will continue without changing directories."
11083
+ }
11084
+ ],
11085
+ onSubmit: (v) => onChoose(v),
11086
+ onCancel: () => onChoose("deny"),
11087
+ footer: "[\u2191\u2193] navigate \xB7 [Enter] select \xB7 [Esc] deny"
11088
+ }
11089
+ ));
11090
+ }
11091
+
10938
11092
  // src/cli/ui/bang.ts
10939
11093
  function detectBangCommand(text) {
10940
11094
  if (!text.startsWith("!")) return null;
@@ -10978,7 +11132,7 @@ function parseEditIndices(raw, max) {
10978
11132
  if (!trimmed) return { ok: [] };
10979
11133
  if (max <= 0) return { error: "no pending edits to address" };
10980
11134
  const seen = /* @__PURE__ */ new Set();
10981
- const tokens = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
11135
+ const tokens = trimmed.split(",").map((t2) => t2.trim()).filter((t2) => t2.length > 0);
10982
11136
  if (tokens.length === 0) return { ok: [] };
10983
11137
  for (const tok of tokens) {
10984
11138
  const range = tok.match(/^(\d+)-(\d+)$/);
@@ -11028,9 +11182,9 @@ function describeRepair(repair) {
11028
11182
  }
11029
11183
 
11030
11184
  // src/cli/ui/hash-memory.ts
11031
- import { appendFileSync as appendFileSync3, existsSync as existsSync11, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11185
+ import { appendFileSync as appendFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync8, readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "fs";
11032
11186
  import { homedir as homedir6 } from "os";
11033
- import { dirname as dirname10, join as join12 } from "path";
11187
+ import { dirname as dirname10, join as join13 } from "path";
11034
11188
  var PROJECT_HEADER = `# Reasonix project memory
11035
11189
 
11036
11190
  Notes the user pinned via the \`#\` prompt prefix. The whole file is
@@ -11062,34 +11216,34 @@ function detectHashMemory(text) {
11062
11216
  return { kind: "memory", note: body };
11063
11217
  }
11064
11218
  function appendProjectMemory(rootDir, note) {
11065
- return appendBulletToFile(join12(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11219
+ return appendBulletToFile(join13(rootDir, PROJECT_MEMORY_FILE), note, PROJECT_HEADER);
11066
11220
  }
11067
11221
  var GLOBAL_MEMORY_DIR = ".reasonix";
11068
11222
  var GLOBAL_MEMORY_FILE = "REASONIX.md";
11069
11223
  function globalMemoryPath(homeDir = homedir6()) {
11070
- return join12(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11224
+ return join13(homeDir, GLOBAL_MEMORY_DIR, GLOBAL_MEMORY_FILE);
11071
11225
  }
11072
11226
  function appendGlobalMemory(note, homeDir) {
11073
11227
  return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
11074
11228
  }
11075
- function appendBulletToFile(path, note, newFileHeader) {
11229
+ function appendBulletToFile(path5, note, newFileHeader) {
11076
11230
  const trimmed = note.trim();
11077
11231
  if (!trimmed) throw new Error("note body cannot be empty");
11078
11232
  const bullet = `- ${trimmed}
11079
11233
  `;
11080
- if (!existsSync11(path)) {
11081
- mkdirSync8(dirname10(path), { recursive: true });
11082
- writeFileSync7(path, `${newFileHeader}${bullet}`, "utf8");
11083
- return { path, created: true };
11234
+ if (!existsSync12(path5)) {
11235
+ mkdirSync8(dirname10(path5), { recursive: true });
11236
+ writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
11237
+ return { path: path5, created: true };
11084
11238
  }
11085
11239
  let prefix = "";
11086
11240
  try {
11087
- const existing = readFileSync14(path, "utf8");
11241
+ const existing = readFileSync14(path5, "utf8");
11088
11242
  if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
11089
11243
  } catch {
11090
11244
  }
11091
- appendFileSync3(path, `${prefix}${bullet}`, "utf8");
11092
- return { path, created: false };
11245
+ appendFileSync3(path5, `${prefix}${bullet}`, "utf8");
11246
+ return { path: path5, created: false };
11093
11247
  }
11094
11248
 
11095
11249
  // src/cli/ui/loop.ts
@@ -11425,6 +11579,11 @@ var SLASH_COMMANDS = [
11425
11579
  argsHint: "[reload]",
11426
11580
  summary: "list active hooks (settings.json under .reasonix/) \xB7 reload re-reads from disk"
11427
11581
  },
11582
+ {
11583
+ cmd: "cwd",
11584
+ argsHint: "<path>",
11585
+ summary: "switch session working directory (re-registers code tools, reloads hooks; MCP servers stay)"
11586
+ },
11428
11587
  {
11429
11588
  cmd: "update",
11430
11589
  summary: "show current vs latest version + the shell command to upgrade"
@@ -11454,6 +11613,10 @@ var SLASH_COMMANDS = [
11454
11613
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
11455
11614
  { cmd: "forget", summary: "delete the current session from disk" },
11456
11615
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
11616
+ {
11617
+ cmd: "semantic",
11618
+ summary: "show semantic_search status \u2014 built? Ollama installed? how to enable"
11619
+ },
11457
11620
  { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
11458
11621
  { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
11459
11622
  {
@@ -11567,8 +11730,12 @@ function parseSlash(text) {
11567
11730
  return { cmd, args: parts.slice(1) };
11568
11731
  }
11569
11732
 
11733
+ // src/cli/ui/slash/handlers/admin.ts
11734
+ import { existsSync as existsSync14, statSync as statSync8 } from "fs";
11735
+ import * as pathMod5 from "path";
11736
+
11570
11737
  // src/cli/commands/stats.ts
11571
- import { existsSync as existsSync12, readFileSync as readFileSync15 } from "fs";
11738
+ import { existsSync as existsSync13, readFileSync as readFileSync15 } from "fs";
11572
11739
  function statsCommand(opts) {
11573
11740
  if (opts.transcript) {
11574
11741
  transcriptSummary(opts.transcript);
@@ -11576,12 +11743,12 @@ function statsCommand(opts) {
11576
11743
  }
11577
11744
  dashboard(opts);
11578
11745
  }
11579
- function transcriptSummary(path) {
11580
- if (!existsSync12(path)) {
11581
- console.error(`no such transcript: ${path}`);
11746
+ function transcriptSummary(path5) {
11747
+ if (!existsSync13(path5)) {
11748
+ console.error(`no such transcript: ${path5}`);
11582
11749
  process.exit(1);
11583
11750
  }
11584
- const lines = readFileSync15(path, "utf8").split(/\r?\n/).filter(Boolean);
11751
+ const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
11585
11752
  let assistantTurns = 0;
11586
11753
  let toolCalls = 0;
11587
11754
  let lastTurn = 0;
@@ -11594,25 +11761,25 @@ function transcriptSummary(path) {
11594
11761
  } catch {
11595
11762
  }
11596
11763
  }
11597
- console.log(`transcript: ${path}`);
11764
+ console.log(`transcript: ${path5}`);
11598
11765
  console.log(`assistant turns: ${assistantTurns}`);
11599
11766
  console.log(`tool invocations: ${toolCalls}`);
11600
11767
  console.log(`last turn index: ${lastTurn}`);
11601
11768
  }
11602
11769
  function dashboard(opts) {
11603
- const path = opts.logPath ?? defaultUsageLogPath();
11604
- const records = readUsageLog(path);
11770
+ const path5 = opts.logPath ?? defaultUsageLogPath();
11771
+ const records = readUsageLog(path5);
11605
11772
  if (records.length === 0) {
11606
11773
  console.log("no usage data yet.");
11607
11774
  console.log("");
11608
- console.log(` ${path}`);
11775
+ console.log(` ${path5}`);
11609
11776
  console.log("");
11610
11777
  console.log("run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn");
11611
11778
  console.log("appends one line to the log and `reasonix stats` will roll it up.");
11612
11779
  return;
11613
11780
  }
11614
11781
  const agg = aggregateUsage(records, { now: opts.now });
11615
- console.log(renderDashboard(agg, path));
11782
+ console.log(renderDashboard(agg, path5));
11616
11783
  }
11617
11784
  function renderDashboard(agg, logPath) {
11618
11785
  const lines = [];
@@ -11784,14 +11951,14 @@ var update = (_args, _loop, ctx) => {
11784
11951
  return { info: lines.join("\n") };
11785
11952
  };
11786
11953
  var stats = () => {
11787
- const path = defaultUsageLogPath();
11788
- const records = readUsageLog(path);
11954
+ const path5 = defaultUsageLogPath();
11955
+ const records = readUsageLog(path5);
11789
11956
  if (records.length === 0) {
11790
11957
  return {
11791
11958
  info: [
11792
11959
  "no usage data yet.",
11793
11960
  "",
11794
- ` ${path}`,
11961
+ ` ${path5}`,
11795
11962
  "",
11796
11963
  "every turn you run here appends one record \u2014 this session's turns",
11797
11964
  "will show up in the dashboard once you send a message."
@@ -11799,11 +11966,53 @@ var stats = () => {
11799
11966
  };
11800
11967
  }
11801
11968
  const agg = aggregateUsage(records);
11802
- return { info: renderDashboard(agg, path) };
11969
+ return { info: renderDashboard(agg, path5) };
11970
+ };
11971
+ var cwd = (args, _loop, ctx) => {
11972
+ if (!ctx.setCwd) {
11973
+ return {
11974
+ info: "/cwd is not available in this context (no setCwd callback wired)."
11975
+ };
11976
+ }
11977
+ const raw = (args[0] ?? "").trim();
11978
+ if (!raw) {
11979
+ return {
11980
+ info: "usage: /cwd <path> (absolute or relative, ~ expands to home)"
11981
+ };
11982
+ }
11983
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
11984
+ const expanded = raw.startsWith("~") && home ? pathMod5.join(home, raw.slice(1)) : raw;
11985
+ const abs = pathMod5.resolve(expanded);
11986
+ if (!existsSync14(abs)) {
11987
+ return { info: `\u25B8 /cwd: path does not exist \u2014 ${abs}` };
11988
+ }
11989
+ let isDir = false;
11990
+ try {
11991
+ isDir = statSync8(abs).isDirectory();
11992
+ } catch {
11993
+ }
11994
+ if (!isDir) {
11995
+ return { info: `\u25B8 /cwd: not a directory \u2014 ${abs}` };
11996
+ }
11997
+ let info;
11998
+ try {
11999
+ info = ctx.setCwd(abs);
12000
+ } catch (err) {
12001
+ return { info: `\u25B8 /cwd failed: ${err.message}` };
12002
+ }
12003
+ const lines = [info];
12004
+ if (ctx.mcpServers && ctx.mcpServers.length > 0) {
12005
+ lines.push(
12006
+ ` note: ${ctx.mcpServers.length} MCP server(s) still anchored to the original cwd \u2014`,
12007
+ " their tools won't follow this switch. Restart the session for full reset."
12008
+ );
12009
+ }
12010
+ return { info: lines.join("\n") };
11803
12011
  };
11804
12012
  var handlers = {
11805
12013
  hook: hooks,
11806
12014
  hooks,
12015
+ cwd,
11807
12016
  update,
11808
12017
  stats
11809
12018
  };
@@ -12101,8 +12310,8 @@ ${gitTail(commit2)}` };
12101
12310
  }
12102
12311
  function gitTail(res) {
12103
12312
  const stderr = res.stderr ?? "";
12104
- const stdout2 = res.stdout ?? "";
12105
- const body = stderr.trim() || stdout2.trim();
12313
+ const stdout3 = res.stdout ?? "";
12314
+ const body = stderr.trim() || stdout3.trim();
12106
12315
  if (body) return body;
12107
12316
  if (res.error) return res.error.message;
12108
12317
  return "(no output from git)";
@@ -12361,7 +12570,7 @@ var mcp = (_args, loop2, ctx) => {
12361
12570
  }
12362
12571
  if (toolSpecs.length > 0) {
12363
12572
  lines.push(`Tools in registry (${toolSpecs.length}):`);
12364
- for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
12573
+ for (const t2 of toolSpecs) lines.push(` \xB7 ${t2.function.name}`);
12365
12574
  }
12366
12575
  lines.push("");
12367
12576
  lines.push("To change this set, exit and run `reasonix setup`.");
@@ -12757,9 +12966,9 @@ var context = (_args, loop2) => {
12757
12966
  const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
12758
12967
  lines.push("");
12759
12968
  lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
12760
- for (const t of top) {
12969
+ for (const t2 of top) {
12761
12970
  lines.push(
12762
- ` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
12971
+ ` turn ${String(t2.turn).padStart(3)} ${t2.name.padEnd(22)} ${compactNum(t2.tokens).padStart(8)} tokens`
12763
12972
  );
12764
12973
  }
12765
12974
  }
@@ -12901,6 +13110,378 @@ var handlers9 = {
12901
13110
  replay
12902
13111
  };
12903
13112
 
13113
+ // src/cli/ui/slash/handlers/semantic.ts
13114
+ import { promises as fs2 } from "fs";
13115
+ import path from "path";
13116
+
13117
+ // src/index/semantic/embedding.ts
13118
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
13119
+ var DEFAULT_EMBED_MODEL = "nomic-embed-text";
13120
+ var DEFAULT_TIMEOUT_MS = 3e4;
13121
+ var EmbeddingError = class extends Error {
13122
+ constructor(message, cause) {
13123
+ super(message);
13124
+ this.cause = cause;
13125
+ this.name = "EmbeddingError";
13126
+ }
13127
+ cause;
13128
+ };
13129
+ async function embed(text, opts = {}) {
13130
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
13131
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
13132
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
13133
+ const controller = new AbortController();
13134
+ const onCallerAbort = () => controller.abort(opts.signal?.reason);
13135
+ if (opts.signal) {
13136
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
13137
+ else opts.signal.addEventListener("abort", onCallerAbort, { once: true });
13138
+ }
13139
+ const timer = setTimeout(() => controller.abort(new Error("embedding timeout")), timeoutMs);
13140
+ let res;
13141
+ try {
13142
+ res = await fetch(`${baseUrl}/api/embeddings`, {
13143
+ method: "POST",
13144
+ headers: { "content-type": "application/json" },
13145
+ body: JSON.stringify({ model: model2, prompt: text }),
13146
+ signal: controller.signal
13147
+ });
13148
+ } catch (err) {
13149
+ clearTimeout(timer);
13150
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
13151
+ const msg = err instanceof Error ? err.message : String(err);
13152
+ if (/ECONNREFUSED|connect ECONNREFUSED|fetch failed/i.test(msg)) {
13153
+ throw new EmbeddingError(
13154
+ `Cannot reach Ollama at ${baseUrl}. Install from https://ollama.com, then run \`ollama pull ${model2}\` and \`ollama serve\`. Override the URL via OLLAMA_URL.`,
13155
+ err
13156
+ );
13157
+ }
13158
+ throw new EmbeddingError(`embedding request failed: ${msg}`, err);
13159
+ } finally {
13160
+ clearTimeout(timer);
13161
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
13162
+ }
13163
+ if (!res.ok) {
13164
+ const body = await res.text().catch(() => "");
13165
+ if (res.status === 404 && /model.*not found/i.test(body)) {
13166
+ throw new EmbeddingError(
13167
+ `Embedding model "${model2}" not pulled. Run \`ollama pull ${model2}\` once, then retry.`
13168
+ );
13169
+ }
13170
+ throw new EmbeddingError(`Ollama returned ${res.status}: ${body.slice(0, 200)}`);
13171
+ }
13172
+ const json = await res.json();
13173
+ if (!json.embedding || !Array.isArray(json.embedding)) {
13174
+ throw new EmbeddingError(`Ollama response missing 'embedding' array`);
13175
+ }
13176
+ const out = new Float32Array(json.embedding.length);
13177
+ for (let i = 0; i < json.embedding.length; i++) {
13178
+ const v = json.embedding[i];
13179
+ if (typeof v !== "number" || !Number.isFinite(v)) {
13180
+ throw new EmbeddingError(`embedding[${i}] is not a finite number`);
13181
+ }
13182
+ out[i] = v;
13183
+ }
13184
+ return out;
13185
+ }
13186
+ async function embedAll(texts, opts = {}) {
13187
+ const out = new Array(texts.length).fill(null);
13188
+ for (let i = 0; i < texts.length; i++) {
13189
+ if (opts.signal?.aborted) {
13190
+ throw new EmbeddingError("embedding aborted");
13191
+ }
13192
+ const text = texts[i];
13193
+ if (text === void 0) continue;
13194
+ try {
13195
+ out[i] = await embed(text, opts);
13196
+ } catch (err) {
13197
+ opts.onError?.(i, err);
13198
+ }
13199
+ opts.onProgress?.(i + 1, texts.length);
13200
+ }
13201
+ return out;
13202
+ }
13203
+ async function probeOllama(opts = {}) {
13204
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
13205
+ try {
13206
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: opts.signal });
13207
+ if (!res.ok) return { ok: false, error: `Ollama returned ${res.status}` };
13208
+ const json = await res.json();
13209
+ const models2 = (json.models ?? []).map((m) => m.name).filter((n) => typeof n === "string");
13210
+ return { ok: true, models: models2 };
13211
+ } catch (err) {
13212
+ const msg = err instanceof Error ? err.message : String(err);
13213
+ return { ok: false, error: msg };
13214
+ }
13215
+ }
13216
+
13217
+ // src/index/semantic/i18n.ts
13218
+ var cachedLocale = null;
13219
+ function detectLocale() {
13220
+ if (cachedLocale) return cachedLocale;
13221
+ const override = (process.env.REASONIX_LANG ?? "").toLowerCase();
13222
+ if (override === "zh" || override === "en") {
13223
+ cachedLocale = override;
13224
+ return cachedLocale;
13225
+ }
13226
+ const env = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
13227
+ if (/^zh[-_]/i.test(env)) {
13228
+ cachedLocale = "zh";
13229
+ return "zh";
13230
+ }
13231
+ try {
13232
+ const sys = new Intl.DateTimeFormat().resolvedOptions().locale ?? "";
13233
+ if (/^zh[-_]/i.test(sys)) {
13234
+ cachedLocale = "zh";
13235
+ return "zh";
13236
+ }
13237
+ } catch {
13238
+ }
13239
+ cachedLocale = "en";
13240
+ return "en";
13241
+ }
13242
+ function t(key, vars = {}) {
13243
+ const loc = detectLocale();
13244
+ const dict = loc === "zh" ? ZH : EN;
13245
+ const tpl = dict[key] ?? EN[key];
13246
+ return tpl.replace(/\{(\w+)\}/g, (_m, name) => {
13247
+ const v = vars[name];
13248
+ return v === void 0 ? `{${name}}` : String(v);
13249
+ });
13250
+ }
13251
+ var EN = {
13252
+ // ── preflight ─────────────────────────────────────────────────────
13253
+ ollamaNotFound: "\u2717 `ollama` not found on PATH.\n Install from https://ollama.com (one-time, ~150 MB), then retry.\n",
13254
+ daemonNotReachableHint: "\u2717 Ollama daemon not reachable. Run `ollama serve` and retry, or pass --yes to start it automatically.\n",
13255
+ daemonStartConfirm: "Ollama daemon isn't running. Start `ollama serve` now?",
13256
+ daemonAbortStart: "\u2717 aborted \u2014 start `ollama serve` yourself and retry.\n",
13257
+ daemonStarting: "\u25B8 starting `ollama serve`\u2026\n",
13258
+ daemonStartTimeout: "\u2717 daemon didn't come up within 15s. Try `ollama serve` in a separate terminal and retry.\n",
13259
+ daemonReady: "\u2713 daemon up{pid}\n",
13260
+ modelNotPulledHint: '\u2717 embedding model "{model}" not pulled. Run `ollama pull {model}` and retry, or pass --yes to pull it automatically.\n',
13261
+ modelPullConfirm: `Embedding model "{model}" isn't pulled yet. Pull it now? (~274 MB for nomic-embed-text)`,
13262
+ modelAbortPull: "\u2717 aborted \u2014 pull the model yourself and retry.\n",
13263
+ modelPulling: "\u25B8 pulling {model}\u2026\n",
13264
+ modelPullFailed: "\u2717 `ollama pull {model}` failed (exit {code}).\n",
13265
+ modelPulled: "\u2713 {model} pulled\n",
13266
+ // ── progress ─────────────────────────────────────────────────────
13267
+ // The TTY-mode progress writer paints `<spinner> <status> <elapsed>s`
13268
+ // every 120ms. The status itself comes from one of these keys based
13269
+ // on the current phase. {files}, {done}, {total}, {pct} are
13270
+ // substituted by the writer.
13271
+ progressStarting: "starting\u2026",
13272
+ progressScan: "scanning project \xB7 {files} files",
13273
+ progressEmbed: "embedding {done}/{total} chunks \xB7 {pct}%",
13274
+ progressEmbedHeartbeat: " {done}/{total}\n",
13275
+ progressScanLine: "scanning files\u2026\n",
13276
+ progressEmbedLine: "embedding {total} chunks across {files} files\u2026\n",
13277
+ // Final result line after a successful build.
13278
+ indexSuccess: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed) in {seconds}s\n",
13279
+ indexSuccessWithSkips: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed, {skipped} skipped due to embed errors) in {seconds}s\n",
13280
+ indexNothingToDo: " (nothing to do \u2014 re-run with --rebuild to force a full rebuild)\n",
13281
+ indexFailed: "\u2717 index failed: {msg}\n",
13282
+ // ── /semantic slash ──────────────────────────────────────────────
13283
+ slashHeader: "semantic_search status",
13284
+ slashEnabled: "\u2713 enabled \u2014 index built, tool registered.",
13285
+ slashEnabledDetail: " index size: {chunks} chunks across {files} files",
13286
+ slashEnabledHowto: " the model will call semantic_search automatically when it fits.",
13287
+ slashIndexMissing: "\u2717 no index built yet for this project.",
13288
+ slashHowToBuild: " to enable, exit Reasonix and run in your shell:\n reasonix index",
13289
+ slashOllamaMissing: " prerequisite: install Ollama from https://ollama.com",
13290
+ slashDaemonDown: " Ollama is installed but the daemon isn't running. start it with: ollama serve",
13291
+ slashIndexInfo: " what semantic_search does: cross-language code understanding via local embeddings.\n better than grep when you describe WHAT something does, not WHICH token to find."
13292
+ };
13293
+ var ZH = {
13294
+ ollamaNotFound: "\u2717 \u672A\u627E\u5230 `ollama`\u3002\n \u8BF7\u8BBF\u95EE https://ollama.com \u5B89\u88C5\uFF08\u4E00\u6B21\u6027\uFF0C\u7EA6 150 MB\uFF09\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\n",
13295
+ daemonNotReachableHint: "\u2717 Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\u3002\u8BF7\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u542F\u52A8\u3002\n",
13296
+ daemonStartConfirm: "Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C\u3002\u73B0\u5728\u542F\u52A8 `ollama serve` \u5417\uFF1F",
13297
+ daemonAbortStart: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
13298
+ daemonStarting: "\u25B8 \u6B63\u5728\u542F\u52A8 `ollama serve`\u2026\n",
13299
+ daemonStartTimeout: "\u2717 15 \u79D2\u5185\u5B88\u62A4\u8FDB\u7A0B\u672A\u5C31\u7EEA\u3002\u8BF7\u5728\u53E6\u4E00\u4E2A\u7EC8\u7AEF\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
13300
+ daemonReady: "\u2713 \u5B88\u62A4\u8FDB\u7A0B\u5DF2\u542F\u52A8{pid}\n",
13301
+ modelNotPulledHint: '\u2717 \u5D4C\u5165\u6A21\u578B "{model}" \u672A\u4E0B\u8F7D\u3002\u8BF7\u8FD0\u884C `ollama pull {model}` \u540E\u91CD\u8BD5\uFF0C\u6216\u52A0 --yes \u8BA9\u6211\u81EA\u52A8\u4E0B\u8F7D\u3002\n',
13302
+ modelPullConfirm: '\u5D4C\u5165\u6A21\u578B "{model}" \u8FD8\u672A\u4E0B\u8F7D\u3002\u73B0\u5728\u4E0B\u8F7D\u5417\uFF1F\uFF08nomic-embed-text \u7EA6 274 MB\uFF09',
13303
+ modelAbortPull: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u4E0B\u8F7D\u6A21\u578B\u540E\u91CD\u8BD5\u3002\n",
13304
+ modelPulling: "\u25B8 \u6B63\u5728\u4E0B\u8F7D {model}\u2026\n",
13305
+ modelPullFailed: "\u2717 `ollama pull {model}` \u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\n",
13306
+ modelPulled: "\u2713 {model} \u4E0B\u8F7D\u5B8C\u6210\n",
13307
+ progressStarting: "\u6B63\u5728\u542F\u52A8\u2026",
13308
+ progressScan: "\u626B\u63CF\u9879\u76EE \xB7 \u5DF2\u626B\u63CF {files} \u4E2A\u6587\u4EF6",
13309
+ progressEmbed: "\u6B63\u5728\u5411\u91CF\u5316 {done}/{total} \u4E2A\u7247\u6BB5 \xB7 {pct}%",
13310
+ progressEmbedHeartbeat: " {done}/{total}\n",
13311
+ progressScanLine: "\u6B63\u5728\u626B\u63CF\u6587\u4EF6\u2026\n",
13312
+ progressEmbedLine: "\u6B63\u5728\u5411\u91CF\u5316 {total} \u4E2A\u7247\u6BB5\uFF08\u6D89\u53CA {files} \u4E2A\u6587\u4EF6\uFF09\u2026\n",
13313
+ indexSuccess: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
13314
+ indexSuccessWithSkips: "\u2713 \u5DF2\u5EFA\u7ACB\u7D22\u5F15\uFF1A\u626B\u63CF {scanned} \u4E2A\u6587\u4EF6\uFF08{changed} \u4E2A\u6709\u53D8\u5316\uFF0C\u65B0\u589E {added} \u4E2A\u7247\u6BB5\uFF0C\u79FB\u9664 {removed} \u4E2A\u8FC7\u671F\uFF0C\u8DF3\u8FC7 {skipped} \u4E2A\u5D4C\u5165\u5931\u8D25\u7684\u7247\u6BB5\uFF09\uFF1B\u8017\u65F6 {seconds}s\n",
13315
+ indexNothingToDo: " \uFF08\u6CA1\u6709\u53D8\u5316\u2014\u2014\u52A0 --rebuild \u5F3A\u5236\u91CD\u5EFA\uFF09\n",
13316
+ indexFailed: "\u2717 \u5EFA\u7ACB\u7D22\u5F15\u5931\u8D25\uFF1A{msg}\n",
13317
+ slashHeader: "semantic_search \u72B6\u6001",
13318
+ slashEnabled: "\u2713 \u5DF2\u542F\u7528\u2014\u2014\u7D22\u5F15\u5DF2\u5EFA\u597D\uFF0C\u5DE5\u5177\u5DF2\u6CE8\u518C\u3002",
13319
+ slashEnabledDetail: " \u7D22\u5F15\u89C4\u6A21\uFF1A{chunks} \u4E2A\u7247\u6BB5\uFF0C{files} \u4E2A\u6587\u4EF6",
13320
+ slashEnabledHowto: " \u6A21\u578B\u5728\u5408\u9002\u7684\u65F6\u5019\u4F1A\u81EA\u52A8\u8C03\u7528 semantic_search\u3002",
13321
+ slashIndexMissing: "\u2717 \u5F53\u524D\u9879\u76EE\u8FD8\u6CA1\u6709\u7D22\u5F15\u3002",
13322
+ slashHowToBuild: " \u542F\u7528\u65B9\u5F0F\uFF1A\u9000\u51FA Reasonix\uFF0C\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF1A\n reasonix index",
13323
+ slashOllamaMissing: " \u524D\u7F6E\u4F9D\u8D56\uFF1A\u4ECE https://ollama.com \u5B89\u88C5 Ollama",
13324
+ slashDaemonDown: " \u5DF2\u88C5 Ollama \u4F46\u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C\uFF1Aollama serve",
13325
+ slashIndexInfo: ' semantic_search \u7528\u672C\u5730 embedding \u505A\u8DE8\u8BED\u8A00\u4EE3\u7801\u7406\u89E3\u3002\n \u5F53\u4F60\u63CF\u8FF0"\u505A\u4EC0\u4E48"\u800C\u4E0D\u662F\u5177\u4F53 token \u65F6\uFF0C\u6BD4 grep \u66F4\u597D\u3002'
13326
+ };
13327
+
13328
+ // src/index/semantic/ollama-launcher.ts
13329
+ import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
13330
+ import { setTimeout as sleep2 } from "timers/promises";
13331
+ function findOllamaBinary() {
13332
+ const cmd = process.platform === "win32" ? "where" : "which";
13333
+ const out = spawnSync2(cmd, ["ollama"], { encoding: "utf8" });
13334
+ if (out.status !== 0) return null;
13335
+ const first = out.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
13336
+ return first ? first.trim() : null;
13337
+ }
13338
+ async function checkOllamaStatus(modelName, baseUrl) {
13339
+ const binary = findOllamaBinary();
13340
+ const probe = await probeOllama({ baseUrl });
13341
+ const installedModels = probe.ok ? probe.models : [];
13342
+ const wanted = modelName.includes(":") ? modelName : `${modelName}:latest`;
13343
+ const modelPulled = installedModels.some((m) => m === modelName || m === wanted);
13344
+ return {
13345
+ binaryFound: binary !== null,
13346
+ daemonRunning: probe.ok,
13347
+ modelPulled,
13348
+ modelName,
13349
+ installedModels
13350
+ };
13351
+ }
13352
+ async function startOllamaDaemon(opts = {}) {
13353
+ const timeoutMs = opts.timeoutMs ?? 15e3;
13354
+ const child = spawn5("ollama", ["serve"], {
13355
+ detached: true,
13356
+ stdio: "ignore",
13357
+ windowsHide: true
13358
+ });
13359
+ child.unref();
13360
+ const pid = child.pid ?? null;
13361
+ const start = Date.now();
13362
+ while (Date.now() - start < timeoutMs) {
13363
+ if (opts.signal?.aborted) return { ready: false, pid };
13364
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
13365
+ if (probe.ok) return { ready: true, pid };
13366
+ await sleep2(500);
13367
+ }
13368
+ return { ready: false, pid };
13369
+ }
13370
+ async function pullOllamaModel(modelName, opts = {}) {
13371
+ return new Promise((resolve12) => {
13372
+ const child = spawn5("ollama", ["pull", modelName], {
13373
+ stdio: ["ignore", "pipe", "pipe"],
13374
+ windowsHide: true
13375
+ });
13376
+ if (opts.signal) {
13377
+ const onAbort = () => child.kill();
13378
+ opts.signal.addEventListener("abort", onAbort, { once: true });
13379
+ child.once("exit", () => opts.signal?.removeEventListener("abort", onAbort));
13380
+ }
13381
+ streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
13382
+ streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
13383
+ child.once("exit", (code) => resolve12(code ?? -1));
13384
+ child.once("error", () => resolve12(-1));
13385
+ });
13386
+ }
13387
+ function streamLines(stream, cb) {
13388
+ if (!stream) return;
13389
+ let buf = "";
13390
+ stream.setEncoding("utf8");
13391
+ stream.on("data", (chunk) => {
13392
+ buf += chunk;
13393
+ let nl = buf.indexOf("\n");
13394
+ while (nl !== -1) {
13395
+ const line = buf.slice(0, nl).replace(/\r$/, "");
13396
+ buf = buf.slice(nl + 1);
13397
+ if (line.length > 0) cb(line);
13398
+ nl = buf.indexOf("\n");
13399
+ }
13400
+ });
13401
+ stream.on("end", () => {
13402
+ if (buf.length > 0) cb(buf.replace(/\r$/, ""));
13403
+ });
13404
+ }
13405
+
13406
+ // src/cli/ui/slash/handlers/semantic.ts
13407
+ var semantic = (_args, _loop, ctx) => {
13408
+ const root = ctx.codeRoot;
13409
+ if (!root) {
13410
+ return {
13411
+ info: "/semantic is only available inside `reasonix code` (needs a project root)."
13412
+ };
13413
+ }
13414
+ void (async () => {
13415
+ const status2 = await renderSemanticStatus(root);
13416
+ ctx.postInfo?.(status2);
13417
+ })();
13418
+ return { info: "\u25B8 checking semantic_search status\u2026" };
13419
+ };
13420
+ async function renderSemanticStatus(rootDir) {
13421
+ const lines = [t("slashHeader"), ""];
13422
+ const indexExists2 = await indexFileExists(rootDir);
13423
+ if (indexExists2) {
13424
+ const meta = await readIndexMeta(rootDir);
13425
+ lines.push(t("slashEnabled"));
13426
+ if (meta) {
13427
+ lines.push(
13428
+ t("slashEnabledDetail", {
13429
+ chunks: meta.chunks,
13430
+ files: meta.files
13431
+ })
13432
+ );
13433
+ }
13434
+ lines.push(t("slashEnabledHowto"));
13435
+ return lines.join("\n");
13436
+ }
13437
+ lines.push(t("slashIndexMissing"));
13438
+ lines.push(t("slashIndexInfo"));
13439
+ lines.push("");
13440
+ if (findOllamaBinary() === null) {
13441
+ lines.push(t("slashOllamaMissing"));
13442
+ } else {
13443
+ const probe = await probeOllama();
13444
+ if (!probe.ok) lines.push(t("slashDaemonDown"));
13445
+ }
13446
+ lines.push(t("slashHowToBuild"));
13447
+ return lines.join("\n");
13448
+ }
13449
+ async function indexFileExists(rootDir) {
13450
+ try {
13451
+ await fs2.access(path.join(rootDir, ".reasonix", "semantic", "index.meta.json"));
13452
+ return true;
13453
+ } catch {
13454
+ return false;
13455
+ }
13456
+ }
13457
+ async function readIndexMeta(rootDir) {
13458
+ const dataPath = path.join(rootDir, ".reasonix", "semantic", "index.jsonl");
13459
+ try {
13460
+ const stat = await fs2.stat(dataPath);
13461
+ if (stat.size > 10 * 1024 * 1024) {
13462
+ return { chunks: Math.round(stat.size / 500), files: 0 };
13463
+ }
13464
+ const raw = await fs2.readFile(dataPath, "utf8");
13465
+ const seenPaths = /* @__PURE__ */ new Set();
13466
+ let chunks = 0;
13467
+ for (const line of raw.split("\n")) {
13468
+ if (line.length === 0) continue;
13469
+ chunks++;
13470
+ try {
13471
+ const parsed = JSON.parse(line);
13472
+ if (parsed.p) seenPaths.add(parsed.p);
13473
+ } catch {
13474
+ }
13475
+ }
13476
+ return { chunks, files: seenPaths.size };
13477
+ } catch {
13478
+ return null;
13479
+ }
13480
+ }
13481
+ var handlers10 = {
13482
+ semantic
13483
+ };
13484
+
12904
13485
  // src/cli/ui/slash/handlers/sessions.ts
12905
13486
  var sessions = (_args, loop2) => {
12906
13487
  const items = listSessions();
@@ -12932,7 +13513,7 @@ var forget = (_args, loop2) => {
12932
13513
  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?)`
12933
13514
  };
12934
13515
  };
12935
- var handlers10 = {
13516
+ var handlers11 = {
12936
13517
  sessions,
12937
13518
  forget
12938
13519
  };
@@ -13008,7 +13589,7 @@ ${found.body}${argsLine}`;
13008
13589
  resubmit: payload
13009
13590
  };
13010
13591
  };
13011
- var handlers11 = {
13592
+ var handlers12 = {
13012
13593
  skill,
13013
13594
  skills: skill
13014
13595
  };
@@ -13025,7 +13606,8 @@ var HANDLERS = {
13025
13606
  ...handlers8,
13026
13607
  ...handlers9,
13027
13608
  ...handlers10,
13028
- ...handlers11
13609
+ ...handlers11,
13610
+ ...handlers12
13029
13611
  };
13030
13612
  function handleSlash(cmd, args, loop2, ctx = {}) {
13031
13613
  const h = HANDLERS[cmd];
@@ -13039,6 +13621,7 @@ function useCompletionPickers({
13039
13621
  input,
13040
13622
  setInput,
13041
13623
  codeMode,
13624
+ rootDir,
13042
13625
  models: models2,
13043
13626
  mcpServers
13044
13627
  }) {
@@ -13056,13 +13639,13 @@ function useCompletionPickers({
13056
13639
  }, [slashMatches]);
13057
13640
  const [atSelected, setAtSelected] = useState6(0);
13058
13641
  const atFiles = useMemo2(() => {
13059
- if (!codeMode?.rootDir) return [];
13642
+ if (!codeMode) return [];
13060
13643
  try {
13061
- return listFilesWithStatsSync(codeMode.rootDir, { maxResults: 500 });
13644
+ return listFilesWithStatsSync(rootDir, { maxResults: 500 });
13062
13645
  } catch {
13063
13646
  return [];
13064
13647
  }
13065
- }, [codeMode?.rootDir]);
13648
+ }, [codeMode, rootDir]);
13066
13649
  const recentFilesRef = useRef3([]);
13067
13650
  const recordRecentFile = useCallback((p) => {
13068
13651
  const list = recentFilesRef.current;
@@ -13072,10 +13655,10 @@ function useCompletionPickers({
13072
13655
  if (list.length > 20) list.length = 20;
13073
13656
  }, []);
13074
13657
  const atPicker = useMemo2(() => {
13075
- if (!codeMode?.rootDir) return null;
13658
+ if (!codeMode) return null;
13076
13659
  if (slashMatches !== null) return null;
13077
13660
  return detectAtPicker(input);
13078
- }, [codeMode?.rootDir, input, slashMatches]);
13661
+ }, [codeMode, input, slashMatches]);
13079
13662
  const atMatches = useMemo2(() => {
13080
13663
  if (!atPicker) return null;
13081
13664
  return rankPickerCandidates(atFiles, atPicker.query, {
@@ -13339,16 +13922,16 @@ function useEditHistory(codeMode) {
13339
13922
  const status2 = entryStatus(entry);
13340
13923
  const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status2} \xB7 ${files.length} file(s)`;
13341
13924
  const countLines3 = (s) => s.length === 0 ? 0 : (s.match(/\n/g)?.length ?? 0) + 1;
13342
- const fileLines = files.map((path) => {
13343
- const fileBlocks = entry.blocks.filter((b) => b.path === path);
13925
+ const fileLines = files.map((path5) => {
13926
+ const fileBlocks = entry.blocks.filter((b) => b.path === path5);
13344
13927
  let removed = 0;
13345
13928
  let added = 0;
13346
13929
  for (const b of fileBlocks) {
13347
13930
  removed += countLines3(b.search);
13348
13931
  added += countLines3(b.replace);
13349
13932
  }
13350
- const state = entry.undoneFiles.has(path) ? "UNDONE" : "applied";
13351
- return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
13933
+ const state = entry.undoneFiles.has(path5) ? "UNDONE" : "applied";
13934
+ return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path5} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
13352
13935
  });
13353
13936
  return [
13354
13937
  header2,
@@ -13515,13 +14098,13 @@ var PLAIN_UI = process.env.REASONIX_UI === "plain";
13515
14098
  function LoopStatusRow({
13516
14099
  loop: loop2
13517
14100
  }) {
13518
- const [, setTick] = React23.useState(0);
13519
- React23.useEffect(() => {
13520
- const id = setInterval(() => setTick((t) => t + 1), 1e3);
14101
+ const [, setTick] = React24.useState(0);
14102
+ React24.useEffect(() => {
14103
+ const id = setInterval(() => setTick((t2) => t2 + 1), 1e3);
13521
14104
  return () => clearInterval(id);
13522
14105
  }, []);
13523
14106
  const nextFireMs = Math.max(0, loop2.nextFireAt - Date.now());
13524
- return /* @__PURE__ */ React23.createElement(Box21, null, /* @__PURE__ */ React23.createElement(Text19, { color: "cyan" }, `\u25B8 ${formatLoopStatus(loop2.prompt, nextFireMs, loop2.iter)} \xB7 /loop stop or type to cancel`));
14107
+ return /* @__PURE__ */ React24.createElement(Box22, null, /* @__PURE__ */ React24.createElement(Text20, { color: "cyan" }, `\u25B8 ${formatLoopStatus(loop2.prompt, nextFireMs, loop2.iter)} \xB7 /loop stop or type to cancel`));
13525
14108
  }
13526
14109
  function App({
13527
14110
  model: model2,
@@ -13547,19 +14130,19 @@ function App({
13547
14130
  }, [busy]);
13548
14131
  const [ongoingTool, setOngoingTool] = useState10(null);
13549
14132
  const [toolProgress, setToolProgress] = useState10(null);
13550
- const { stdout: stdout2 } = useStdout8();
14133
+ const { stdout: stdout3 } = useStdout8();
13551
14134
  useEffect6(() => {
13552
- if (!stdout2 || !stdout2.isTTY) return;
13553
- stdout2.write("\x1B[?2004h");
13554
- stdout2.write("\x1B[>4;2m");
14135
+ if (!stdout3 || !stdout3.isTTY) return;
14136
+ stdout3.write("\x1B[?2004h");
14137
+ stdout3.write("\x1B[>4;2m");
13555
14138
  return () => {
13556
- stdout2.write("\x1B[?2004l");
13557
- stdout2.write("\x1B[>4m");
14139
+ stdout3.write("\x1B[?2004l");
14140
+ stdout3.write("\x1B[>4m");
13558
14141
  };
13559
- }, [stdout2]);
14142
+ }, [stdout3]);
13560
14143
  const [isResizing, setIsResizing] = useState10(false);
13561
14144
  useEffect6(() => {
13562
- if (!stdout2 || !stdout2.isTTY) return;
14145
+ if (!stdout3 || !stdout3.isTTY) return;
13563
14146
  let timer = null;
13564
14147
  const onResize = () => {
13565
14148
  setIsResizing(true);
@@ -13569,21 +14152,23 @@ function App({
13569
14152
  timer = null;
13570
14153
  }, 400);
13571
14154
  };
13572
- stdout2.on("resize", onResize);
14155
+ stdout3.on("resize", onResize);
13573
14156
  return () => {
13574
- stdout2.off("resize", onResize);
14157
+ stdout3.off("resize", onResize);
13575
14158
  if (timer) clearTimeout(timer);
13576
14159
  };
13577
- }, [stdout2]);
14160
+ }, [stdout3]);
13578
14161
  const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
13579
14162
  session,
13580
14163
  setHistorical
13581
14164
  });
13582
14165
  const [statusLine, setStatusLine] = useState10(null);
14166
+ const [currentRootDir, setCurrentRootDir] = useState10(
14167
+ () => codeMode?.rootDir ?? process.cwd()
14168
+ );
13583
14169
  const [hookList, setHookList] = useState10(
13584
14170
  () => loadHooks({ projectRoot: codeMode?.rootDir })
13585
14171
  );
13586
- const hookCwd = codeMode?.rootDir ?? process.cwd();
13587
14172
  const {
13588
14173
  undoBanner,
13589
14174
  recordEdit,
@@ -13598,7 +14183,7 @@ function App({
13598
14183
  const [pendingCount, setPendingCount] = useState10(0);
13599
14184
  const syncPendingCount = useCallback4(() => {
13600
14185
  setPendingCount(pendingEdits.current.length);
13601
- setPendingTick((t) => t + 1);
14186
+ setPendingTick((t2) => t2 + 1);
13602
14187
  }, []);
13603
14188
  const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
13604
14189
  const editModeRef = useRef6(editMode);
@@ -13625,6 +14210,7 @@ function App({
13625
14210
  }, 1200);
13626
14211
  }, [editMode]);
13627
14212
  const [pendingShell, setPendingShell] = useState10(null);
14213
+ const [pendingWorkspace, setPendingWorkspace] = useState10(null);
13628
14214
  const [pendingPlan, setPendingPlan] = useState10(null);
13629
14215
  const [stagedInput, setStagedInput] = useState10(null);
13630
14216
  const [pendingCheckpoint, setPendingCheckpoint] = useState10(null);
@@ -13719,6 +14305,9 @@ function App({
13719
14305
  }
13720
14306
  });
13721
14307
  }
14308
+ if (tools && !tools.has("change_workspace")) {
14309
+ registerWorkspaceTool(tools);
14310
+ }
13722
14311
  const prefix = new ImmutablePrefix({
13723
14312
  system,
13724
14313
  toolSpecs: tools?.specs()
@@ -13732,7 +14321,7 @@ function App({
13732
14321
  branch: branch2,
13733
14322
  session,
13734
14323
  hooks: hookList,
13735
- hookCwd,
14324
+ hookCwd: currentRootDir,
13736
14325
  // Restore the user's last-chosen effort cap. Without this a
13737
14326
  // `/effort high` silently reverted to `max` on relaunch — the
13738
14327
  // loop's constructor default wins over persisted state.
@@ -13744,6 +14333,49 @@ function App({
13744
14333
  useEffect6(() => {
13745
14334
  loop2.hooks = hookList;
13746
14335
  }, [loop2, hookList]);
14336
+ const applyCwdChange = useCallback4(
14337
+ (newRoot) => {
14338
+ setCurrentRootDir(newRoot);
14339
+ const fresh = loadHooks({ projectRoot: codeMode ? newRoot : void 0 });
14340
+ setHookList(fresh);
14341
+ const codeRebound = codeMode?.reregisterTools !== void 0;
14342
+ if (codeMode?.reregisterTools) {
14343
+ codeMode.reregisterTools(newRoot);
14344
+ }
14345
+ if (tools) {
14346
+ registerSkillTools(tools, {
14347
+ projectRoot: codeMode ? newRoot : void 0,
14348
+ subagentRunner: async (skill2, task) => {
14349
+ const result = await spawnSubagent({
14350
+ client: loop2.client,
14351
+ parentRegistry: tools,
14352
+ system: skill2.body,
14353
+ task,
14354
+ model: skill2.model,
14355
+ sink: subagentSinkRef.current,
14356
+ skillName: skill2.name
14357
+ });
14358
+ return formatSubagentResult(result);
14359
+ }
14360
+ });
14361
+ }
14362
+ const lines = [`\u25B8 cwd \u2192 ${newRoot}`, ` hooks reloaded (${fresh.length} active)`];
14363
+ if (codeMode) {
14364
+ lines.push(
14365
+ codeRebound ? " filesystem / shell / memory tools rebound to new root" : " warning: reregisterTools callback missing \u2014 tool sandbox unchanged"
14366
+ );
14367
+ lines.push(
14368
+ " note: system prompt context (gitignore, REASONIX.md stack) was",
14369
+ " baked at session start and still references the original root."
14370
+ );
14371
+ }
14372
+ return lines.join("\n");
14373
+ },
14374
+ [codeMode, loop2, tools, subagentSinkRef]
14375
+ );
14376
+ useEffect6(() => {
14377
+ loop2.hookCwd = currentRootDir;
14378
+ }, [loop2, currentRootDir]);
13747
14379
  const {
13748
14380
  balance,
13749
14381
  models: models2,
@@ -13768,7 +14400,14 @@ function App({
13768
14400
  slashArgSelected,
13769
14401
  setSlashArgSelected,
13770
14402
  pickSlashArg
13771
- } = useCompletionPickers({ input, setInput, codeMode, models: models2, mcpServers });
14403
+ } = useCompletionPickers({
14404
+ input,
14405
+ setInput,
14406
+ codeMode,
14407
+ rootDir: currentRootDir,
14408
+ models: models2,
14409
+ mcpServers
14410
+ });
13772
14411
  useEffect6(() => {
13773
14412
  if (!progressSink) return;
13774
14413
  progressSink.current = (info) => {
@@ -13888,11 +14527,11 @@ function App({
13888
14527
  if (key.escape && busy) {
13889
14528
  if (abortedThisTurn.current) return;
13890
14529
  abortedThisTurn.current = true;
13891
- const resolve8 = editReviewResolveRef.current;
13892
- if (resolve8) {
14530
+ const resolve12 = editReviewResolveRef.current;
14531
+ if (resolve12) {
13893
14532
  editReviewResolveRef.current = null;
13894
14533
  setPendingEditReview(null);
13895
- resolve8("reject");
14534
+ resolve12("reject");
13896
14535
  }
13897
14536
  if (activeLoopRef.current) stopLoop();
13898
14537
  loop2.abort();
@@ -13915,7 +14554,7 @@ function App({
13915
14554
  ]);
13916
14555
  return;
13917
14556
  }
13918
- if (codeMode && key.shift && key.tab && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
14557
+ if (codeMode && key.shift && key.tab && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision) {
13919
14558
  setEditMode((m) => {
13920
14559
  const next = m === "review" ? "auto" : m === "auto" ? "yolo" : "review";
13921
14560
  const message = next === "yolo" ? "\u25B8 edit mode: YOLO \u2014 edits AND shell commands auto-run. /undo still rolls back edits. Use carefully." : next === "auto" ? "\u25B8 edit mode: AUTO \u2014 edits apply immediately; press u within 5s to undo. Shell commands still ask." : "\u25B8 edit mode: review \u2014 edits queue for /apply (or y) / /discard (or n)";
@@ -13927,7 +14566,7 @@ function App({
13927
14566
  });
13928
14567
  return;
13929
14568
  }
13930
- if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
14569
+ if (codeMode && input.length === 0 && (chKey === "u" || chKey === "U") && !pendingShell && !pendingWorkspace && !pendingPlan && !stagedInput && !pendingEditReview && !walkthroughActive && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision && // Fire when EITHER the banner is up OR there's any non-undone
13931
14570
  // history entry — the keybind is useful long after the 5-second
13932
14571
  // banner expires, which users rightly want.
13933
14572
  (undoBanner || hasUndoable())) {
@@ -14016,11 +14655,11 @@ function App({
14016
14655
  block = { path: relPath, search, replace, offset: 0 };
14017
14656
  } else {
14018
14657
  const content = typeof args.content === "string" ? args.content : "";
14019
- block = toWholeFileEditBlock(relPath, content, codeMode.rootDir);
14658
+ block = toWholeFileEditBlock(relPath, content, currentRootDir);
14020
14659
  }
14021
14660
  const applyNow = () => {
14022
- const snaps = snapshotBeforeEdits([block], codeMode.rootDir);
14023
- const results = applyEditBlocks([block], codeMode.rootDir);
14661
+ const snaps = snapshotBeforeEdits([block], currentRootDir);
14662
+ const results = applyEditBlocks([block], currentRootDir);
14024
14663
  const good = results.some((r) => r.status === "applied" || r.status === "created");
14025
14664
  if (good) {
14026
14665
  recordEdit("auto", [block], results, snaps);
@@ -14089,8 +14728,8 @@ function App({
14089
14728
  if (selected.length === 0) {
14090
14729
  return "\u25B8 no edits matched those indices \u2014 nothing applied. Use /apply with no args to commit them all.";
14091
14730
  }
14092
- const snaps = snapshotBeforeEdits(selected, codeMode.rootDir);
14093
- const results = applyEditBlocks(selected, codeMode.rootDir);
14731
+ const snaps = snapshotBeforeEdits(selected, currentRootDir);
14732
+ const results = applyEditBlocks(selected, currentRootDir);
14094
14733
  const anyApplied = results.some((r) => r.status === "applied" || r.status === "created");
14095
14734
  if (anyApplied) recordEdit("review-apply", selected, results, snaps);
14096
14735
  pendingEdits.current = remaining;
@@ -14101,7 +14740,7 @@ function App({
14101
14740
  \u25B8 ${remaining.length} edit block(s) still pending \u2014 /apply or /discard to clear them.` : "";
14102
14741
  return formatEditResults(results) + tail;
14103
14742
  },
14104
- [codeMode, session, syncPendingCount, recordEdit]
14743
+ [codeMode, currentRootDir, session, syncPendingCount, recordEdit]
14105
14744
  );
14106
14745
  const codeDiscard = useCallback4(
14107
14746
  (indices) => {
@@ -14266,7 +14905,7 @@ function App({
14266
14905
  const hashParse = detectHashMemory(text);
14267
14906
  if (hashParse?.kind === "memory" || hashParse?.kind === "memory-global") {
14268
14907
  const isGlobal = hashParse.kind === "memory-global";
14269
- const memRoot = codeMode?.rootDir ?? process.cwd();
14908
+ const memRoot = currentRootDir;
14270
14909
  promptHistory.current.push(text);
14271
14910
  try {
14272
14911
  const result = isGlobal ? appendGlobalMemory(hashParse.note) : appendProjectMemory(memRoot, hashParse.note);
@@ -14297,7 +14936,7 @@ function App({
14297
14936
  }
14298
14937
  const bangCmd = detectBangCommand(text);
14299
14938
  if (bangCmd !== null) {
14300
- const bangRoot = codeMode?.rootDir ?? process.cwd();
14939
+ const bangRoot = currentRootDir;
14301
14940
  promptHistory.current.push(text);
14302
14941
  setHistorical((prev) => [
14303
14942
  ...prev,
@@ -14360,10 +14999,10 @@ function App({
14360
14999
  codeDiscard: codeMode ? codeDiscard : void 0,
14361
15000
  codeHistory: codeMode ? codeHistory : void 0,
14362
15001
  codeShowEdit: codeMode ? codeShowEdit : void 0,
14363
- codeRoot: codeMode?.rootDir,
15002
+ codeRoot: codeMode ? currentRootDir : void 0,
14364
15003
  pendingEditCount: codeMode ? pendingEdits.current.length : void 0,
14365
15004
  toolHistory: () => toolHistoryRef.current,
14366
- memoryRoot: codeMode?.rootDir ?? process.cwd(),
15005
+ memoryRoot: currentRootDir,
14367
15006
  planMode,
14368
15007
  setPlanMode: codeMode ? togglePlanMode : void 0,
14369
15008
  clearPendingPlan: codeMode ? clearPendingPlan : void 0,
@@ -14387,10 +15026,11 @@ function App({
14387
15026
  { id: `sys-late-${Date.now()}-${Math.random()}`, role: "info", text: text2 }
14388
15027
  ]),
14389
15028
  reloadHooks: () => {
14390
- const fresh = loadHooks({ projectRoot: codeMode?.rootDir });
15029
+ const fresh = loadHooks({ projectRoot: codeMode ? currentRootDir : void 0 });
14391
15030
  setHookList(fresh);
14392
15031
  return fresh.length;
14393
15032
  },
15033
+ setCwd: (newRoot) => applyCwdChange(newRoot),
14394
15034
  latestVersion,
14395
15035
  refreshLatestVersion,
14396
15036
  models: models2,
@@ -14403,7 +15043,7 @@ function App({
14403
15043
  return;
14404
15044
  }
14405
15045
  if (result.clear && result.info) {
14406
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
15046
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14407
15047
  setHistorical([
14408
15048
  {
14409
15049
  id: `sys-${Date.now()}`,
@@ -14420,7 +15060,7 @@ function App({
14420
15060
  return;
14421
15061
  }
14422
15062
  if (result.clear) {
14423
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
15063
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14424
15064
  setHistorical([]);
14425
15065
  if (codeMode) {
14426
15066
  pendingEdits.current = [];
@@ -14471,7 +15111,7 @@ function App({
14471
15111
  if (hookList.some((h) => h.event === "UserPromptSubmit")) {
14472
15112
  const promptReport = await runHooks({
14473
15113
  hooks: hookList,
14474
- payload: { event: "UserPromptSubmit", cwd: hookCwd, prompt: text }
15114
+ payload: { event: "UserPromptSubmit", cwd: currentRootDir, prompt: text }
14475
15115
  });
14476
15116
  if (promptReport.outcomes.length > 0) {
14477
15117
  setHistorical((prev) => [
@@ -14539,8 +15179,8 @@ function App({
14539
15179
  };
14540
15180
  const timer = PLAIN_UI ? null : setInterval(flush, FLUSH_INTERVAL_MS);
14541
15181
  let modelInput = text;
14542
- if (codeMode?.rootDir) {
14543
- const expanded = expandAtMentions(text, codeMode.rootDir);
15182
+ if (codeMode) {
15183
+ const expanded = expandAtMentions(text, currentRootDir);
14544
15184
  if (expanded.expansions.length > 0) {
14545
15185
  modelInput = expanded.text;
14546
15186
  const inlined = expanded.expansions.filter((ex) => ex.ok).map((ex) => `${ex.path} (${(ex.bytes ?? 0).toLocaleString()} bytes)`);
@@ -14671,8 +15311,8 @@ function App({
14671
15311
  const blocks = parseEditBlocks(finalText);
14672
15312
  if (blocks.length > 0) {
14673
15313
  if (editModeRef.current === "auto" || editModeRef.current === "yolo") {
14674
- const snaps = snapshotBeforeEdits(blocks, codeMode.rootDir);
14675
- const results = applyEditBlocks(blocks, codeMode.rootDir);
15314
+ const snaps = snapshotBeforeEdits(blocks, currentRootDir);
15315
+ const results = applyEditBlocks(blocks, currentRootDir);
14676
15316
  const good = results.some(
14677
15317
  (r) => r.status === "applied" || r.status === "created"
14678
15318
  );
@@ -14758,6 +15398,18 @@ function App({
14758
15398
  } catch {
14759
15399
  }
14760
15400
  }
15401
+ if (ev.toolName === "change_workspace" && ev.content.includes('"WorkspaceConfirmationError:') && ev.toolArgs) {
15402
+ try {
15403
+ const parsed = JSON.parse(ev.toolArgs);
15404
+ if (typeof parsed.path === "string" && parsed.path.trim()) {
15405
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
15406
+ const expanded = parsed.path.startsWith("~") && home ? pathMod6.join(home, parsed.path.slice(1)) : parsed.path;
15407
+ const abs = pathMod6.resolve(expanded);
15408
+ setPendingWorkspace({ path: abs });
15409
+ }
15410
+ } catch {
15411
+ }
15412
+ }
14761
15413
  if (codeMode && ev.toolName === "submit_plan" && ev.content.includes('"PlanProposedError:')) {
14762
15414
  try {
14763
15415
  const parsed = JSON.parse(ev.content);
@@ -14880,7 +15532,7 @@ function App({
14880
15532
  hooks: hookList,
14881
15533
  payload: {
14882
15534
  event: "Stop",
14883
- cwd: hookCwd,
15535
+ cwd: currentRootDir,
14884
15536
  lastAssistantText: streamRef.text,
14885
15537
  turn: loop2.stats.summary().turns
14886
15538
  }
@@ -14918,8 +15570,8 @@ function App({
14918
15570
  codeMode,
14919
15571
  codeShowEdit,
14920
15572
  codeUndo,
15573
+ currentRootDir,
14921
15574
  exit2,
14922
- hookCwd,
14923
15575
  hookList,
14924
15576
  loop2,
14925
15577
  latestVersion,
@@ -14950,11 +15602,12 @@ function App({
14950
15602
  refreshModels,
14951
15603
  proArmed,
14952
15604
  persistPlanState,
14953
- stdout2,
15605
+ stdout3,
14954
15606
  stopLoop,
14955
15607
  startLoop,
14956
15608
  getLoopStatus,
14957
- startWalkthrough
15609
+ startWalkthrough,
15610
+ applyCwdChange
14958
15611
  ]
14959
15612
  );
14960
15613
  useEffect6(() => {
@@ -15011,13 +15664,13 @@ function App({
15011
15664
  } else {
15012
15665
  if (choice === "always_allow") {
15013
15666
  const prefix = derivePrefix(cmd);
15014
- addProjectShellAllowed(codeMode.rootDir, prefix);
15667
+ addProjectShellAllowed(currentRootDir, prefix);
15015
15668
  setHistorical((prev) => [
15016
15669
  ...prev,
15017
15670
  {
15018
15671
  id: `sh-allow-${Date.now()}`,
15019
15672
  role: "info",
15020
- text: `\u25B8 always allowed "${prefix}" for ${codeMode.rootDir}`
15673
+ text: `\u25B8 always allowed "${prefix}" for ${currentRootDir}`
15021
15674
  }
15022
15675
  ]);
15023
15676
  }
@@ -15034,7 +15687,7 @@ function App({
15034
15687
  let jobId = null;
15035
15688
  let preview = "";
15036
15689
  try {
15037
- const res = await codeMode.jobs.start(cmd, { cwd: codeMode.rootDir });
15690
+ const res = await codeMode.jobs.start(cmd, { cwd: currentRootDir });
15038
15691
  startedOk = true;
15039
15692
  jobId = res.jobId;
15040
15693
  preview = res.preview;
@@ -15068,7 +15721,7 @@ ${msg}`;
15068
15721
  } else {
15069
15722
  let body;
15070
15723
  try {
15071
- const res = await runCommand(cmd, { cwd: codeMode.rootDir });
15724
+ const res = await runCommand(cmd, { cwd: currentRootDir });
15072
15725
  body = formatCommandResult(cmd, res);
15073
15726
  } catch (err) {
15074
15727
  body = `$ ${cmd}
@@ -15090,7 +15743,7 @@ ${body}`;
15090
15743
  await handleSubmit(synthetic);
15091
15744
  }
15092
15745
  },
15093
- [pendingShell, codeMode, handleSubmit, busy, loop2]
15746
+ [pendingShell, codeMode, currentRootDir, handleSubmit, busy, loop2]
15094
15747
  );
15095
15748
  useEffect6(() => {
15096
15749
  if (!busy && queuedSubmit !== null) {
@@ -15099,6 +15752,40 @@ ${body}`;
15099
15752
  void handleSubmit(text);
15100
15753
  }
15101
15754
  }, [busy, queuedSubmit, handleSubmit]);
15755
+ const handleWorkspaceConfirm = useCallback4(
15756
+ async (choice) => {
15757
+ const pending = pendingWorkspace;
15758
+ if (!pending) return;
15759
+ const target = pending.path;
15760
+ setPendingWorkspace(null);
15761
+ let synthetic;
15762
+ if (choice === "deny") {
15763
+ setHistorical((prev) => [
15764
+ ...prev,
15765
+ {
15766
+ id: `ws-deny-${Date.now()}`,
15767
+ role: "info",
15768
+ text: `\u25B8 denied workspace switch: ${target}`
15769
+ }
15770
+ ]);
15771
+ synthetic = `I denied switching the workspace to \`${target}\`. Please continue without changing directories.`;
15772
+ } else {
15773
+ const info = applyCwdChange(target);
15774
+ setHistorical((prev) => [
15775
+ ...prev,
15776
+ { id: `ws-switch-${Date.now()}`, role: "info", text: info }
15777
+ ]);
15778
+ synthetic = `I approved the workspace switch. The session is now rooted at \`${target}\` \u2014 your filesystem / shell / memory tools resolve against that path on every subsequent call. Continue with my original request from this new root.`;
15779
+ }
15780
+ if (busy) {
15781
+ loop2.abort();
15782
+ setQueuedSubmit(synthetic);
15783
+ } else {
15784
+ await handleSubmit(synthetic);
15785
+ }
15786
+ },
15787
+ [pendingWorkspace, applyCwdChange, busy, loop2, handleSubmit]
15788
+ );
15102
15789
  const handlePlanConfirm = useCallback4(
15103
15790
  async (choice) => {
15104
15791
  const hadPendingPlan = pendingPlan !== null;
@@ -15405,12 +16092,12 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15405
16092
  async (choice) => handleReviseConfirmRef.current(choice),
15406
16093
  []
15407
16094
  );
15408
- return /* @__PURE__ */ React23.createElement(React23.Fragment, null, /* @__PURE__ */ React23.createElement(
16095
+ return /* @__PURE__ */ React24.createElement(React24.Fragment, null, /* @__PURE__ */ React24.createElement(
15409
16096
  TickerProvider,
15410
16097
  {
15411
- disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingEditReview || walkthroughActive || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
16098
+ disabled: PLAIN_UI || isResizing || !!pendingPlan || !!pendingShell || !!pendingWorkspace || !!pendingEditReview || walkthroughActive || !!pendingCheckpoint || !!stagedCheckpointRevise || !!pendingChoice || !!stagedChoiceCustom || !!pendingRevision
15412
16099
  },
15413
- /* @__PURE__ */ React23.createElement(Box21, { flexDirection: "column" }, /* @__PURE__ */ React23.createElement(
16100
+ /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column" }, /* @__PURE__ */ React24.createElement(
15414
16101
  StatsPanel,
15415
16102
  {
15416
16103
  summary,
@@ -15427,28 +16114,28 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15427
16114
  proArmed,
15428
16115
  escalated: turnOnPro
15429
16116
  }
15430
- ), /* @__PURE__ */ React23.createElement(Static, { items: historical }, (item) => /* @__PURE__ */ React23.createElement(EventRow, { key: item.id, event: item, projectRoot: hookCwd })), !historical.some((e) => e.role === "user" || e.role === "assistant") && !busy && !streaming ? /* @__PURE__ */ React23.createElement(WelcomeBanner, { inCodeMode: !!codeMode }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && streaming ? /* @__PURE__ */ React23.createElement(Box21, { marginY: 1 }, /* @__PURE__ */ React23.createElement(EventRow, { event: streaming, projectRoot: hookCwd })) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && ongoingTool ? /* @__PURE__ */ React23.createElement(OngoingToolRow, { tool: ongoingTool, progress: toolProgress }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && subagentActivity ? /* @__PURE__ */ React23.createElement(SubagentRow, { activity: subagentActivity }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !ongoingTool && statusLine ? /* @__PURE__ */ React23.createElement(StatusRow, { text: statusLine }) : null, !PLAIN_UI && undoBanner && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && !pendingChoice && !stagedChoiceCustom && !pendingRevision ? /* @__PURE__ */ React23.createElement(UndoBanner, { banner: undoBanner }) : null, !PLAIN_UI && !pendingShell && !pendingPlan && !stagedInput && !pendingEditReview && !pendingCheckpoint && !stagedCheckpointRevise && busy && !streaming && !ongoingTool && !statusLine ? /* @__PURE__ */ React23.createElement(StatusRow, { text: "processing\u2026" }) : null, stagedInput ? /* @__PURE__ */ React23.createElement(
16117
+ ), /* @__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(
15431
16118
  PlanRefineInput,
15432
16119
  {
15433
16120
  mode: stagedInput.mode,
15434
16121
  onSubmit: handleStagedInputSubmit,
15435
16122
  onCancel: handleStagedInputCancel
15436
16123
  }
15437
- ) : stagedCheckpointRevise ? /* @__PURE__ */ React23.createElement(
16124
+ ) : stagedCheckpointRevise ? /* @__PURE__ */ React24.createElement(
15438
16125
  PlanRefineInput,
15439
16126
  {
15440
16127
  mode: "checkpoint-revise",
15441
16128
  onSubmit: handleCheckpointReviseSubmit,
15442
16129
  onCancel: handleCheckpointReviseCancel
15443
16130
  }
15444
- ) : stagedChoiceCustom ? /* @__PURE__ */ React23.createElement(
16131
+ ) : stagedChoiceCustom ? /* @__PURE__ */ React24.createElement(
15445
16132
  PlanRefineInput,
15446
16133
  {
15447
16134
  mode: "choice-custom",
15448
16135
  onSubmit: handleChoiceCustomSubmit,
15449
16136
  onCancel: handleChoiceCustomCancel
15450
16137
  }
15451
- ) : pendingChoice ? /* @__PURE__ */ React23.createElement(
16138
+ ) : pendingChoice ? /* @__PURE__ */ React24.createElement(
15452
16139
  ChoiceConfirm,
15453
16140
  {
15454
16141
  question: pendingChoice.question,
@@ -15456,7 +16143,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15456
16143
  allowCustom: pendingChoice.allowCustom,
15457
16144
  onChoose: stableHandleChoiceConfirm
15458
16145
  }
15459
- ) : pendingRevision ? /* @__PURE__ */ React23.createElement(
16146
+ ) : pendingRevision ? /* @__PURE__ */ React24.createElement(
15460
16147
  PlanReviseConfirm,
15461
16148
  {
15462
16149
  reason: pendingRevision.reason,
@@ -15467,7 +16154,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15467
16154
  summary: pendingRevision.summary,
15468
16155
  onChoose: stableHandleReviseConfirm
15469
16156
  }
15470
- ) : pendingCheckpoint ? /* @__PURE__ */ React23.createElement(
16157
+ ) : pendingCheckpoint ? /* @__PURE__ */ React24.createElement(
15471
16158
  PlanCheckpointConfirm,
15472
16159
  {
15473
16160
  stepId: pendingCheckpoint.stepId,
@@ -15478,16 +16165,16 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15478
16165
  completedStepIds: completedStepIdsRef.current,
15479
16166
  onChoose: stableHandleCheckpointConfirm
15480
16167
  }
15481
- ) : pendingPlan ? /* @__PURE__ */ React23.createElement(
16168
+ ) : pendingPlan ? /* @__PURE__ */ React24.createElement(
15482
16169
  PlanConfirm,
15483
16170
  {
15484
16171
  plan: pendingPlan,
15485
16172
  steps: planStepsRef.current ?? void 0,
15486
16173
  summary: planSummaryRef.current ?? void 0,
15487
16174
  onChoose: stableHandlePlanConfirm,
15488
- projectRoot: hookCwd
16175
+ projectRoot: currentRootDir
15489
16176
  }
15490
- ) : pendingShell ? /* @__PURE__ */ React23.createElement(
16177
+ ) : pendingShell ? /* @__PURE__ */ React24.createElement(
15491
16178
  ShellConfirm,
15492
16179
  {
15493
16180
  command: pendingShell.command,
@@ -15495,26 +16182,34 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15495
16182
  kind: pendingShell.kind,
15496
16183
  onChoose: handleShellConfirm
15497
16184
  }
15498
- ) : pendingEditReview ? /* @__PURE__ */ React23.createElement(
16185
+ ) : pendingWorkspace ? /* @__PURE__ */ React24.createElement(
16186
+ WorkspaceConfirm,
16187
+ {
16188
+ path: pendingWorkspace.path,
16189
+ currentRoot: currentRootDir,
16190
+ mcpServerCount: mcpServers?.length ?? 0,
16191
+ onChoose: handleWorkspaceConfirm
16192
+ }
16193
+ ) : pendingEditReview ? /* @__PURE__ */ React24.createElement(
15499
16194
  EditConfirm,
15500
16195
  {
15501
16196
  block: pendingEditReview,
15502
16197
  onChoose: (choice) => {
15503
- const resolve8 = editReviewResolveRef.current;
15504
- if (resolve8) {
16198
+ const resolve12 = editReviewResolveRef.current;
16199
+ if (resolve12) {
15505
16200
  editReviewResolveRef.current = null;
15506
- resolve8(choice);
16201
+ resolve12(choice);
15507
16202
  }
15508
16203
  }
15509
16204
  }
15510
- ) : walkthroughActive && pendingEdits.current.length > 0 ? /* @__PURE__ */ React23.createElement(
16205
+ ) : walkthroughActive && pendingEdits.current.length > 0 ? /* @__PURE__ */ React24.createElement(
15511
16206
  EditConfirm,
15512
16207
  {
15513
16208
  key: `walk-${pendingTick}`,
15514
16209
  block: pendingEdits.current[0],
15515
16210
  onChoose: handleWalkChoice
15516
16211
  }
15517
- ) : /* @__PURE__ */ React23.createElement(React23.Fragment, null, codeMode ? /* @__PURE__ */ React23.createElement(
16212
+ ) : /* @__PURE__ */ React24.createElement(React24.Fragment, null, codeMode ? /* @__PURE__ */ React24.createElement(
15518
16213
  ModeStatusBar,
15519
16214
  {
15520
16215
  editMode,
@@ -15524,7 +16219,7 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15524
16219
  undoArmed: !!undoBanner || hasUndoable(),
15525
16220
  jobs: codeMode.jobs
15526
16221
  }
15527
- ) : null, activeLoop ? /* @__PURE__ */ React23.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React23.createElement(
16222
+ ) : null, activeLoop ? /* @__PURE__ */ React24.createElement(LoopStatusRow, { loop: activeLoop }) : null, /* @__PURE__ */ React24.createElement(
15528
16223
  PromptInput,
15529
16224
  {
15530
16225
  value: input,
@@ -15534,14 +16229,14 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15534
16229
  onHistoryPrev: recallPrev,
15535
16230
  onHistoryNext: recallNext
15536
16231
  }
15537
- ), /* @__PURE__ */ React23.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React23.createElement(
16232
+ ), /* @__PURE__ */ React24.createElement(SlashSuggestions, { matches: slashMatches, selectedIndex: slashSelected }), /* @__PURE__ */ React24.createElement(
15538
16233
  AtMentionSuggestions,
15539
16234
  {
15540
16235
  matches: atMatches,
15541
16236
  selectedIndex: atSelected,
15542
16237
  query: atPicker?.query ?? ""
15543
16238
  }
15544
- ), slashArgContext ? /* @__PURE__ */ React23.createElement(
16239
+ ), slashArgContext ? /* @__PURE__ */ React24.createElement(
15545
16240
  SlashArgPicker,
15546
16241
  {
15547
16242
  matches: slashArgMatches,
@@ -15555,15 +16250,15 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15555
16250
  }
15556
16251
 
15557
16252
  // src/cli/ui/SessionPicker.tsx
15558
- import { Box as Box22, Text as Text20 } from "ink";
15559
- import React24 from "react";
16253
+ import { Box as Box23, Text as Text21 } from "ink";
16254
+ import React25 from "react";
15560
16255
  function SessionPicker({
15561
16256
  sessionName,
15562
16257
  messageCount,
15563
16258
  lastActive,
15564
16259
  onChoose
15565
16260
  }) {
15566
- return /* @__PURE__ */ React24.createElement(Box22, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React24.createElement(Box22, { marginBottom: 1 }, /* @__PURE__ */ React24.createElement(Text20, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React24.createElement(
16261
+ return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", marginY: 1 }, /* @__PURE__ */ React25.createElement(Box23, { marginBottom: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, `Session "${sessionName}" has ${messageCount} prior message${messageCount === 1 ? "" : "s"}`), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, ` \xB7 last active ${relativeTime2(lastActive)}`)), /* @__PURE__ */ React25.createElement(
15567
16262
  SingleSelect,
15568
16263
  {
15569
16264
  initialValue: "new",
@@ -15586,7 +16281,7 @@ function SessionPicker({
15586
16281
  ],
15587
16282
  onSubmit: (v) => onChoose(v)
15588
16283
  }
15589
- ), /* @__PURE__ */ React24.createElement(Box22, { marginTop: 1 }, /* @__PURE__ */ React24.createElement(Text20, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
16284
+ ), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] select")));
15590
16285
  }
15591
16286
  function relativeTime2(date) {
15592
16287
  const ms = Date.now() - date.getTime();
@@ -15602,9 +16297,9 @@ function relativeTime2(date) {
15602
16297
  }
15603
16298
 
15604
16299
  // src/cli/ui/Setup.tsx
15605
- import { Box as Box23, Text as Text21, useApp as useApp2 } from "ink";
16300
+ import { Box as Box24, Text as Text22, useApp as useApp2 } from "ink";
15606
16301
  import TextInput from "ink-text-input";
15607
- import React25, { useState as useState11 } from "react";
16302
+ import React26, { useState as useState11 } from "react";
15608
16303
  function Setup({ onReady }) {
15609
16304
  const [value, setValue] = useState11("");
15610
16305
  const [error, setError] = useState11(null);
@@ -15628,7 +16323,7 @@ function Setup({ onReady }) {
15628
16323
  }
15629
16324
  onReady(trimmed);
15630
16325
  };
15631
- return /* @__PURE__ */ React25.createElement(Box23, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React25.createElement(
16326
+ return /* @__PURE__ */ React26.createElement(Box24, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React26.createElement(Text22, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React26.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React26.createElement(Text22, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React26.createElement(Text22, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React26.createElement(Text22, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React26.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React26.createElement(Text22, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React26.createElement(
15632
16327
  TextInput,
15633
16328
  {
15634
16329
  value,
@@ -15637,7 +16332,7 @@ function Setup({ onReady }) {
15637
16332
  mask: "\u2022",
15638
16333
  placeholder: "sk-..."
15639
16334
  }
15640
- )), error ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { color: "red" }, error)) : value ? /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React25.createElement(Box23, { marginTop: 1 }, /* @__PURE__ */ React25.createElement(Text21, { dimColor: true }, "(Type /exit to abort.)")));
16335
+ )), error ? /* @__PURE__ */ React26.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React26.createElement(Text22, { color: "red" }, error)) : value ? /* @__PURE__ */ React26.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React26.createElement(Text22, { dimColor: true }, "preview: ", redactKey(value))) : null, /* @__PURE__ */ React26.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React26.createElement(Text22, { dimColor: true }, "(Type /exit to abort.)")));
15641
16336
  }
15642
16337
 
15643
16338
  // src/cli/commands/chat.tsx
@@ -15653,7 +16348,7 @@ function Root({
15653
16348
  const [key, setKey] = useState12(initialKey);
15654
16349
  const [pending, setPending] = useState12(sessionPreview);
15655
16350
  if (!key) {
15656
- return /* @__PURE__ */ React26.createElement(
16351
+ return /* @__PURE__ */ React27.createElement(
15657
16352
  Setup,
15658
16353
  {
15659
16354
  onReady: (k) => {
@@ -15665,7 +16360,7 @@ function Root({
15665
16360
  }
15666
16361
  process.env.DEEPSEEK_API_KEY = key;
15667
16362
  if (pending && appProps.session) {
15668
- return /* @__PURE__ */ React26.createElement(KeystrokeProvider, null, /* @__PURE__ */ React26.createElement(
16363
+ return /* @__PURE__ */ React27.createElement(KeystrokeProvider, null, /* @__PURE__ */ React27.createElement(
15669
16364
  SessionPicker,
15670
16365
  {
15671
16366
  sessionName: appProps.session,
@@ -15680,7 +16375,7 @@ function Root({
15680
16375
  }
15681
16376
  ));
15682
16377
  }
15683
- return /* @__PURE__ */ React26.createElement(KeystrokeProvider, null, /* @__PURE__ */ React26.createElement(
16378
+ return /* @__PURE__ */ React27.createElement(KeystrokeProvider, null, /* @__PURE__ */ React27.createElement(
15684
16379
  App,
15685
16380
  {
15686
16381
  model: appProps.model,
@@ -15778,14 +16473,14 @@ async function chatCommand(opts) {
15778
16473
  const prior = loadSessionMessages(opts.session);
15779
16474
  if (prior.length > 0) {
15780
16475
  const p = sessionPath(opts.session);
15781
- const mtime = existsSync13(p) ? statSync7(p).mtime : /* @__PURE__ */ new Date();
16476
+ const mtime = existsSync15(p) ? statSync9(p).mtime : /* @__PURE__ */ new Date();
15782
16477
  sessionPreview = { messageCount: prior.length, lastActive: mtime };
15783
16478
  }
15784
16479
  } else if (opts.session && opts.forceNew) {
15785
16480
  rewriteSession(opts.session, []);
15786
16481
  }
15787
16482
  const { waitUntilExit } = render(
15788
- /* @__PURE__ */ React26.createElement(
16483
+ /* @__PURE__ */ React27.createElement(
15789
16484
  Root,
15790
16485
  {
15791
16486
  initialKey,
@@ -15809,84 +16504,733 @@ async function chatCommand(opts) {
15809
16504
  }
15810
16505
 
15811
16506
  // src/cli/commands/code.tsx
15812
- import { basename as basename2, resolve as resolve7 } from "path";
15813
- async function codeCommand(opts = {}) {
15814
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-LJ44NWSU.js");
15815
- const rootDir = resolve7(opts.dir ?? process.cwd());
15816
- const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15817
- const tools = new ToolRegistry();
15818
- registerFilesystemTools(tools, { rootDir });
15819
- const jobs2 = new JobRegistry();
15820
- registerShellTools(tools, {
15821
- rootDir,
15822
- // Per-project "always allow" list persisted from prior ShellConfirm
15823
- // choices; merged on top of the built-in allowlist in shell.ts.
15824
- // GETTER form — re-read every dispatch so a prefix the user adds
15825
- // via ShellConfirm mid-session takes effect on the next shell call
15826
- // instead of waiting for `/new` or a relaunch.
15827
- extraAllowed: () => loadProjectShellAllowed(rootDir),
15828
- // `yolo` edit-mode disables shell confirmations entirely. Re-read
15829
- // from config on each dispatch so /mode yolo (or Shift+Tab cycling
15830
- // through to it) flips the gate live without forcing a relaunch.
15831
- allowAll: () => loadEditMode() === "yolo",
15832
- jobs: jobs2
15833
- });
15834
- registerPlanTool(tools);
15835
- registerChoiceTool(tools);
15836
- registerMemoryTools(tools, { projectRoot: rootDir });
15837
- process.stderr.write(
15838
- `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
15839
- `
15840
- );
15841
- process.once("exit", () => {
15842
- void jobs2.shutdown();
15843
- });
15844
- await chatCommand({
15845
- model: opts.model ?? "deepseek-v4-flash",
15846
- harvest: opts.harvest ?? false,
15847
- system: codeSystemPrompt2(rootDir),
15848
- transcript: opts.transcript,
15849
- session,
15850
- seedTools: tools,
15851
- codeMode: { rootDir, jobs: jobs2 },
15852
- forceResume: opts.forceResume,
15853
- forceNew: opts.forceNew
15854
- });
15855
- }
15856
-
15857
- // src/cli/commands/diff.ts
15858
- import { writeFileSync as writeFileSync8 } from "fs";
15859
- import { basename as basename3 } from "path";
15860
- import { render as render2 } from "ink";
15861
- import React29 from "react";
16507
+ import { basename as basename2, resolve as resolve10 } from "path";
15862
16508
 
15863
- // src/cli/ui/DiffApp.tsx
15864
- import { Box as Box25, Static as Static2, Text as Text23, useApp as useApp3, useInput } from "ink";
15865
- import React28, { useState as useState13 } from "react";
16509
+ // src/index/semantic/builder.ts
16510
+ import { promises as fs5 } from "fs";
16511
+ import path4 from "path";
15866
16512
 
15867
- // src/cli/ui/RecordView.tsx
15868
- import { Box as Box24, Text as Text22 } from "ink";
15869
- import React27 from "react";
15870
- function RecordView({ rec, compact: compact2 = false }) {
15871
- const toolArgsMax = compact2 ? 120 : 200;
15872
- const toolContentMax = compact2 ? 200 : 400;
15873
- if (rec.role === "user") {
16513
+ // src/index/semantic/chunker.ts
16514
+ import { promises as fs3 } from "fs";
16515
+ import path2 from "path";
16516
+ var DEFAULT_MAX_CHUNK_CHARS = 4e3;
16517
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
16518
+ "node_modules",
16519
+ ".git",
16520
+ "dist",
16521
+ "build",
16522
+ "out",
16523
+ ".next",
16524
+ ".nuxt",
16525
+ "target",
16526
+ ".venv",
16527
+ "venv",
16528
+ "__pycache__",
16529
+ ".pytest_cache",
16530
+ ".mypy_cache",
16531
+ ".cache",
16532
+ "coverage",
16533
+ ".turbo",
16534
+ ".vercel",
16535
+ ".reasonix"
16536
+ ]);
16537
+ var SKIP_FILES = /* @__PURE__ */ new Set([
16538
+ "package-lock.json",
16539
+ "yarn.lock",
16540
+ "pnpm-lock.yaml",
16541
+ "Cargo.lock",
16542
+ "poetry.lock",
16543
+ "Pipfile.lock",
16544
+ "go.sum",
16545
+ ".DS_Store"
16546
+ ]);
16547
+ var BINARY_EXTS = /* @__PURE__ */ new Set([
16548
+ // Images
16549
+ ".png",
16550
+ ".jpg",
16551
+ ".jpeg",
16552
+ ".gif",
16553
+ ".webp",
16554
+ ".bmp",
16555
+ ".ico",
16556
+ ".tiff",
16557
+ // Fonts
16558
+ ".woff",
16559
+ ".woff2",
16560
+ ".ttf",
16561
+ ".otf",
16562
+ ".eot",
16563
+ // Archives / binaries
16564
+ ".zip",
16565
+ ".tar",
16566
+ ".gz",
16567
+ ".rar",
16568
+ ".7z",
16569
+ ".exe",
16570
+ ".dll",
16571
+ ".so",
16572
+ ".dylib",
16573
+ ".class",
16574
+ ".jar",
16575
+ ".wasm",
16576
+ ".o",
16577
+ ".a",
16578
+ // Media
16579
+ ".mp3",
16580
+ ".mp4",
16581
+ ".wav",
16582
+ ".ogg",
16583
+ ".webm",
16584
+ ".mov",
16585
+ // Other
16586
+ ".pdf",
16587
+ ".sqlite",
16588
+ ".db"
16589
+ ]);
16590
+ function chunkText(text, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
16591
+ const lines = text.split(/\r?\n/);
16592
+ if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
16593
+ const stride = Math.max(1, windowLines - overlap);
16594
+ const chunks = [];
16595
+ for (let start = 0; start < lines.length; start += stride) {
16596
+ const end = Math.min(lines.length, start + windowLines);
16597
+ const slice = lines.slice(start, end).join("\n").trim();
16598
+ if (slice.length === 0) {
16599
+ if (end >= lines.length) break;
16600
+ continue;
16601
+ }
16602
+ const window = {
16603
+ path: filePath,
16604
+ startLine: start + 1,
16605
+ endLine: end,
16606
+ text: slice
16607
+ };
16608
+ for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
16609
+ if (end >= lines.length) break;
16610
+ }
16611
+ return chunks;
16612
+ }
16613
+ function safeSplit(chunk, maxChars) {
16614
+ if (chunk.text.length <= maxChars) return [chunk];
16615
+ const lines = chunk.text.split("\n");
16616
+ const out = [];
16617
+ let bufLines = [];
16618
+ let bufStart = chunk.startLine;
16619
+ let bufLen = 0;
16620
+ const flush = (untilLineNo) => {
16621
+ if (bufLines.length === 0) return;
16622
+ out.push({
16623
+ path: chunk.path,
16624
+ startLine: bufStart,
16625
+ endLine: untilLineNo,
16626
+ text: bufLines.join("\n")
16627
+ });
16628
+ bufLines = [];
16629
+ bufLen = 0;
16630
+ };
16631
+ for (let i = 0; i < lines.length; i++) {
16632
+ const line = lines[i] ?? "";
16633
+ const lineLen = line.length + 1;
16634
+ if (lineLen > maxChars) {
16635
+ flush(chunk.startLine + i - 1);
16636
+ out.push({
16637
+ path: chunk.path,
16638
+ startLine: chunk.startLine + i,
16639
+ endLine: chunk.startLine + i,
16640
+ text: line.slice(0, maxChars)
16641
+ });
16642
+ bufStart = chunk.startLine + i + 1;
16643
+ continue;
16644
+ }
16645
+ if (bufLen + lineLen > maxChars && bufLines.length > 0) {
16646
+ flush(chunk.startLine + i - 1);
16647
+ bufStart = chunk.startLine + i;
16648
+ }
16649
+ bufLines.push(line);
16650
+ bufLen += lineLen;
16651
+ }
16652
+ flush(chunk.endLine);
16653
+ return out;
16654
+ }
16655
+ async function* walkChunks(root, opts = {}) {
16656
+ const windowLines = opts.windowLines ?? 60;
16657
+ const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
16658
+ const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
16659
+ const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
16660
+ const stack = [root];
16661
+ while (stack.length > 0) {
16662
+ const dir = stack.pop();
16663
+ if (!dir) break;
16664
+ let entries;
16665
+ try {
16666
+ entries = await fs3.readdir(dir, { withFileTypes: true });
16667
+ } catch {
16668
+ continue;
16669
+ }
16670
+ for (const entry of entries) {
16671
+ const name = entry.name;
16672
+ if (entry.isDirectory()) {
16673
+ if (SKIP_DIRS.has(name) || name.startsWith(".")) {
16674
+ if (SKIP_DIRS.has(name) || name === ".git") continue;
16675
+ }
16676
+ stack.push(path2.join(dir, name));
16677
+ continue;
16678
+ }
16679
+ if (!entry.isFile()) continue;
16680
+ if (SKIP_FILES.has(name)) continue;
16681
+ const ext = path2.extname(name).toLowerCase();
16682
+ if (BINARY_EXTS.has(ext)) continue;
16683
+ const abs = path2.join(dir, name);
16684
+ let stat;
16685
+ try {
16686
+ stat = await fs3.stat(abs);
16687
+ } catch {
16688
+ continue;
16689
+ }
16690
+ if (stat.size > maxFileBytes) continue;
16691
+ let text;
16692
+ try {
16693
+ text = await fs3.readFile(abs, "utf8");
16694
+ } catch {
16695
+ continue;
16696
+ }
16697
+ if (text.indexOf("\0") !== -1) continue;
16698
+ const rel = path2.relative(root, abs).split(path2.sep).join("/");
16699
+ for (const chunk of chunkText(text, rel, windowLines, overlap, maxChunkChars)) {
16700
+ yield chunk;
16701
+ }
16702
+ }
16703
+ }
16704
+ }
16705
+
16706
+ // src/index/semantic/store.ts
16707
+ import { promises as fs4 } from "fs";
16708
+ import path3 from "path";
16709
+ var STORE_VERSION = 1;
16710
+ var META_FILE = "index.meta.json";
16711
+ var DATA_FILE = "index.jsonl";
16712
+ var SemanticStore = class {
16713
+ constructor(indexDir, model2) {
16714
+ this.indexDir = indexDir;
16715
+ this.model = model2;
16716
+ }
16717
+ indexDir;
16718
+ model;
16719
+ entries = [];
16720
+ byPath = /* @__PURE__ */ new Map();
16721
+ dim = 0;
16722
+ /** True when no entries are loaded — the index doesn't exist or is empty. */
16723
+ get empty() {
16724
+ return this.entries.length === 0;
16725
+ }
16726
+ /** Total number of indexed chunks. */
16727
+ get size() {
16728
+ return this.entries.length;
16729
+ }
16730
+ /** Read-only view, mostly for tests. */
16731
+ get all() {
16732
+ return this.entries;
16733
+ }
16734
+ /** Last-known mtime per indexed file (ms epoch) for incremental rebuilds. */
16735
+ fileMtimes() {
16736
+ const out = /* @__PURE__ */ new Map();
16737
+ for (const [p, group] of this.byPath) {
16738
+ const first = group[0];
16739
+ if (first) out.set(p, first.mtimeMs);
16740
+ }
16741
+ return out;
16742
+ }
16743
+ /** Append entries to in-memory state and to disk. Re-indexes the
16744
+ * `byPath` map. Caller is responsible for L2-normalizing each
16745
+ * embedding before calling — the search hot path assumes unit vectors. */
16746
+ async add(entries) {
16747
+ if (entries.length === 0) return;
16748
+ if (this.dim === 0) this.dim = entries[0].embedding.length;
16749
+ const lines = [];
16750
+ for (const e of entries) {
16751
+ if (e.embedding.length !== this.dim) {
16752
+ throw new Error(
16753
+ `embedding dim mismatch: expected ${this.dim}, got ${e.embedding.length} for ${e.path}:${e.startLine}`
16754
+ );
16755
+ }
16756
+ this.entries.push(e);
16757
+ const list = this.byPath.get(e.path);
16758
+ if (list) list.push(e);
16759
+ else this.byPath.set(e.path, [e]);
16760
+ lines.push(serializeEntry(e));
16761
+ }
16762
+ await fs4.mkdir(this.indexDir, { recursive: true });
16763
+ await fs4.appendFile(path3.join(this.indexDir, DATA_FILE), `${lines.join("\n")}
16764
+ `, "utf8");
16765
+ await this.writeMeta();
16766
+ }
16767
+ /**
16768
+ * Drop every entry whose `path` is in `paths`. Used by incremental
16769
+ * rebuild: when a file's mtime changes, the existing entries for
16770
+ * it are evicted before re-chunking + re-embedding. Implementation
16771
+ * rewrites the JSONL — append-only is fine for adds, but deletes
16772
+ * need a compaction pass.
16773
+ */
16774
+ async remove(paths) {
16775
+ if (paths.length === 0) return 0;
16776
+ const drop = new Set(paths);
16777
+ const before = this.entries.length;
16778
+ this.entries = this.entries.filter((e) => !drop.has(e.path));
16779
+ for (const p of paths) this.byPath.delete(p);
16780
+ const removed = before - this.entries.length;
16781
+ if (removed > 0) await this.flush();
16782
+ return removed;
16783
+ }
16784
+ /**
16785
+ * Top-K cosine search. `query` MUST already be L2-normalized — the
16786
+ * caller embeds + normalizes once per query. Filtering hits by
16787
+ * minimum score (`minScore`) is optional; tune via UI to suppress
16788
+ * weakly relevant snippets that distract the model.
16789
+ */
16790
+ search(query, topK = 8, minScore = 0) {
16791
+ if (this.entries.length === 0) return [];
16792
+ if (query.length !== this.dim && this.dim !== 0) {
16793
+ throw new Error(`query dim ${query.length} \u2260 index dim ${this.dim}`);
16794
+ }
16795
+ const heap = [];
16796
+ for (const entry of this.entries) {
16797
+ const score = dot(query, entry.embedding);
16798
+ if (score < minScore) continue;
16799
+ if (heap.length < topK) {
16800
+ heap.push({ entry, score });
16801
+ if (heap.length === topK) heap.sort((a, b) => a.score - b.score);
16802
+ } else if (score > heap[0].score) {
16803
+ heap[0] = { entry, score };
16804
+ for (let i = 0; i < heap.length - 1; i++) {
16805
+ if (heap[i].score > heap[i + 1].score) {
16806
+ const tmp = heap[i];
16807
+ heap[i] = heap[i + 1];
16808
+ heap[i + 1] = tmp;
16809
+ }
16810
+ }
16811
+ }
16812
+ }
16813
+ return heap.sort((a, b) => b.score - a.score);
16814
+ }
16815
+ /**
16816
+ * Rewrite the JSONL on disk with the current in-memory state. Used
16817
+ * after `remove` and from `flush`. We write to a temp file and
16818
+ * rename so a Ctrl+C mid-write never leaves the index half-empty.
16819
+ */
16820
+ async flush() {
16821
+ await fs4.mkdir(this.indexDir, { recursive: true });
16822
+ const tmp = path3.join(this.indexDir, `${DATA_FILE}.tmp`);
16823
+ const final = path3.join(this.indexDir, DATA_FILE);
16824
+ const lines = this.entries.map(serializeEntry).join("\n");
16825
+ await fs4.writeFile(tmp, lines.length > 0 ? `${lines}
16826
+ ` : "", "utf8");
16827
+ await fs4.rename(tmp, final);
16828
+ await this.writeMeta();
16829
+ }
16830
+ async writeMeta() {
16831
+ const meta = {
16832
+ version: STORE_VERSION,
16833
+ model: this.model,
16834
+ dim: this.dim,
16835
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16836
+ };
16837
+ await fs4.writeFile(
16838
+ path3.join(this.indexDir, META_FILE),
16839
+ `${JSON.stringify(meta, null, 2)}
16840
+ `,
16841
+ "utf8"
16842
+ );
16843
+ }
16844
+ /** Drop everything from disk + memory. Used by `--rebuild`. */
16845
+ async wipe() {
16846
+ this.entries = [];
16847
+ this.byPath.clear();
16848
+ this.dim = 0;
16849
+ await fs4.rm(path3.join(this.indexDir, DATA_FILE), { force: true });
16850
+ await fs4.rm(path3.join(this.indexDir, META_FILE), { force: true });
16851
+ }
16852
+ };
16853
+ async function openStore(indexDir, model2) {
16854
+ const store = new SemanticStore(indexDir, model2);
16855
+ const dataPath = path3.join(indexDir, DATA_FILE);
16856
+ const metaPath = path3.join(indexDir, META_FILE);
16857
+ let meta = null;
16858
+ try {
16859
+ const raw2 = await fs4.readFile(metaPath, "utf8");
16860
+ meta = JSON.parse(raw2);
16861
+ } catch {
16862
+ }
16863
+ if (meta) {
16864
+ if (meta.version !== STORE_VERSION) {
16865
+ throw new Error(
16866
+ `Index format version ${meta.version} does not match current ${STORE_VERSION}. Run \`reasonix index --rebuild\`.`
16867
+ );
16868
+ }
16869
+ if (meta.model !== model2) {
16870
+ throw new Error(
16871
+ `Index was built with model "${meta.model}" but current is "${model2}". Run \`reasonix index --rebuild\`.`
16872
+ );
16873
+ }
16874
+ }
16875
+ let raw;
16876
+ try {
16877
+ raw = await fs4.readFile(dataPath, "utf8");
16878
+ } catch {
16879
+ return store;
16880
+ }
16881
+ for (const line of raw.split("\n")) {
16882
+ if (line.length === 0) continue;
16883
+ try {
16884
+ const entry = deserializeEntry(line);
16885
+ store.dim = entry.embedding.length;
16886
+ store.entries.push(entry);
16887
+ const map = store.byPath;
16888
+ const list = map.get(entry.path);
16889
+ if (list) list.push(entry);
16890
+ else map.set(entry.path, [entry]);
16891
+ } catch {
16892
+ }
16893
+ }
16894
+ return store;
16895
+ }
16896
+ function normalize(v) {
16897
+ let sum = 0;
16898
+ for (let i = 0; i < v.length; i++) sum += v[i] * v[i];
16899
+ const inv = sum > 0 ? 1 / Math.sqrt(sum) : 0;
16900
+ for (let i = 0; i < v.length; i++) v[i] = v[i] * inv;
16901
+ return v;
16902
+ }
16903
+ function dot(a, b) {
16904
+ let s = 0;
16905
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
16906
+ return s;
16907
+ }
16908
+ function serializeEntry(e) {
16909
+ const buf = Buffer.from(e.embedding.buffer, e.embedding.byteOffset, e.embedding.byteLength);
16910
+ return JSON.stringify({
16911
+ p: e.path,
16912
+ s: e.startLine,
16913
+ e: e.endLine,
16914
+ m: e.mtimeMs,
16915
+ t: e.text,
16916
+ v: buf.toString("base64")
16917
+ });
16918
+ }
16919
+ function deserializeEntry(line) {
16920
+ const parsed = JSON.parse(line);
16921
+ const bytes = Buffer.from(parsed.v, "base64");
16922
+ const f32 = new Float32Array(
16923
+ bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
16924
+ );
16925
+ return {
16926
+ path: parsed.p,
16927
+ startLine: parsed.s,
16928
+ endLine: parsed.e,
16929
+ mtimeMs: parsed.m,
16930
+ text: parsed.t,
16931
+ embedding: f32
16932
+ };
16933
+ }
16934
+
16935
+ // src/index/semantic/builder.ts
16936
+ var INDEX_DIR_NAME = path4.join(".reasonix", "semantic");
16937
+ async function buildIndex(root, opts = {}) {
16938
+ const t0 = Date.now();
16939
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
16940
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
16941
+ if (!probe.ok) {
16942
+ throw new Error(
16943
+ `Ollama is not reachable: ${probe.error}. Install from https://ollama.com, then \`ollama serve\` and \`ollama pull ${opts.model ?? "nomic-embed-text"}\`.`
16944
+ );
16945
+ }
16946
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
16947
+ const store = await openStore(indexDir, model2);
16948
+ if (opts.rebuild) await store.wipe();
16949
+ const lastMtimes = store.fileMtimes();
16950
+ const seenPaths = /* @__PURE__ */ new Set();
16951
+ const fileChunks = /* @__PURE__ */ new Map();
16952
+ let filesScanned = 0;
16953
+ let filesSkipped = 0;
16954
+ for await (const chunk of walkChunks(root, {
16955
+ windowLines: opts.windowLines,
16956
+ overlap: opts.overlap,
16957
+ maxFileBytes: opts.maxFileBytes
16958
+ })) {
16959
+ seenPaths.add(chunk.path);
16960
+ let bucket = fileChunks.get(chunk.path);
16961
+ if (!bucket) {
16962
+ filesScanned++;
16963
+ const abs = path4.join(root, chunk.path);
16964
+ let mtimeMs = 0;
16965
+ try {
16966
+ const stat = await fs5.stat(abs);
16967
+ mtimeMs = stat.mtimeMs;
16968
+ } catch {
16969
+ continue;
16970
+ }
16971
+ const last = lastMtimes.get(chunk.path);
16972
+ if (last !== void 0 && last === mtimeMs && !opts.rebuild) {
16973
+ filesSkipped++;
16974
+ continue;
16975
+ }
16976
+ bucket = { chunks: [], mtimeMs };
16977
+ fileChunks.set(chunk.path, bucket);
16978
+ }
16979
+ bucket.chunks.push(chunk);
16980
+ opts.onProgress?.({ phase: "scan", filesScanned });
16981
+ }
16982
+ const deletedPaths = [];
16983
+ for (const oldPath of lastMtimes.keys()) {
16984
+ if (!seenPaths.has(oldPath)) deletedPaths.push(oldPath);
16985
+ }
16986
+ const replacePaths = [...fileChunks.keys()].filter((p) => lastMtimes.has(p));
16987
+ const removed = await store.remove([...deletedPaths, ...replacePaths]);
16988
+ let chunksAdded = 0;
16989
+ let chunksSkipped = 0;
16990
+ const filesChanged = fileChunks.size;
16991
+ let chunksTotal = 0;
16992
+ for (const { chunks } of fileChunks.values()) chunksTotal += chunks.length;
16993
+ let chunksDone = 0;
16994
+ for (const [, bucket] of fileChunks) {
16995
+ if (bucket.chunks.length === 0) continue;
16996
+ const texts = bucket.chunks.map((c) => c.text);
16997
+ const vectors = await embedAll(texts, {
16998
+ ...opts,
16999
+ onProgress: (done, total) => {
17000
+ opts.onProgress?.({
17001
+ phase: "embed",
17002
+ filesScanned,
17003
+ filesChanged,
17004
+ chunksTotal,
17005
+ chunksDone: chunksDone + done
17006
+ });
17007
+ if (done === total) chunksDone += total;
17008
+ },
17009
+ onError: (idx, err) => {
17010
+ chunksSkipped++;
17011
+ const c = bucket.chunks[idx];
17012
+ const where = c ? `${c.path}:${c.startLine}-${c.endLine}` : `chunk #${idx}`;
17013
+ const msg = err instanceof Error ? err.message : String(err);
17014
+ process.stderr.write(`
17015
+ ! skipped ${where}: ${msg}
17016
+ `);
17017
+ }
17018
+ });
17019
+ const entries = [];
17020
+ for (let i = 0; i < bucket.chunks.length; i++) {
17021
+ const vec = vectors[i];
17022
+ if (!vec) continue;
17023
+ const c = bucket.chunks[i];
17024
+ if (!c) continue;
17025
+ normalize(vec);
17026
+ entries.push({
17027
+ path: c.path,
17028
+ startLine: c.startLine,
17029
+ endLine: c.endLine,
17030
+ text: c.text,
17031
+ embedding: vec,
17032
+ mtimeMs: bucket.mtimeMs
17033
+ });
17034
+ }
17035
+ if (entries.length > 0) await store.add(entries);
17036
+ chunksAdded += entries.length;
17037
+ }
17038
+ opts.onProgress?.({
17039
+ phase: "done",
17040
+ filesScanned,
17041
+ filesSkipped,
17042
+ filesChanged,
17043
+ chunksTotal,
17044
+ chunksDone
17045
+ });
17046
+ return {
17047
+ filesScanned,
17048
+ filesChanged,
17049
+ chunksAdded,
17050
+ chunksRemoved: removed,
17051
+ chunksSkipped,
17052
+ durationMs: Date.now() - t0
17053
+ };
17054
+ }
17055
+ async function querySemantic(root, query, opts = {}) {
17056
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
17057
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17058
+ const store = await openStore(indexDir, model2);
17059
+ if (store.empty) return null;
17060
+ const qvec = await embed(query, opts);
17061
+ normalize(qvec);
17062
+ return store.search(qvec, opts.topK ?? 8, opts.minScore ?? 0.3);
17063
+ }
17064
+ async function indexExists(root) {
17065
+ const meta = path4.join(root, INDEX_DIR_NAME, "index.meta.json");
17066
+ try {
17067
+ await fs5.access(meta);
17068
+ return true;
17069
+ } catch {
17070
+ return false;
17071
+ }
17072
+ }
17073
+
17074
+ // src/index/semantic/tool.ts
17075
+ async function registerSemanticSearchTool(registry, opts) {
17076
+ if (!await indexExists(opts.root)) return false;
17077
+ const defaultTopK = opts.defaultTopK ?? 8;
17078
+ const defaultMinScore = opts.defaultMinScore ?? 0.3;
17079
+ registry.register({
17080
+ name: "semantic_search",
17081
+ description: "FIRST CHOICE for descriptive queries. Use this BEFORE search_content (grep) when the user describes WHAT code does ('where do we handle X', 'which file owns Y', 'how does Z work', 'find the logic that \u2026'). Returns ranked snippets ordered by semantic relevance \u2014 finds the right file even when your description shares no words with the code. Falls back to search_content / search_files only for: exact identifiers, regex patterns, or counting occurrences of a known token. If your first instinct is grep on a paraphrased question, you are wrong \u2014 try semantic_search first.",
17082
+ readOnly: true,
17083
+ parameters: {
17084
+ type: "object",
17085
+ properties: {
17086
+ query: {
17087
+ type: "string",
17088
+ description: "Natural-language description, phrased as a question or noun phrase: 'where do we validate the session cookie?' / 'retry backoff logic' / 'code that prevents user changes from immediately landing on disk'. Do NOT pass exact identifiers \u2014 those are search_content's job."
17089
+ },
17090
+ topK: {
17091
+ type: "integer",
17092
+ description: `Number of snippets to return (1..16). Default ${defaultTopK}.`
17093
+ },
17094
+ minScore: {
17095
+ type: "number",
17096
+ description: `Drop snippets with cosine score below this (0..1). Default ${defaultMinScore}. Raise for stricter matches; lower if the index is small.`
17097
+ }
17098
+ },
17099
+ required: ["query"]
17100
+ },
17101
+ fn: async (args, ctx) => {
17102
+ const hits = await querySemantic(opts.root, args.query, {
17103
+ topK: args.topK ?? defaultTopK,
17104
+ minScore: args.minScore ?? defaultMinScore,
17105
+ baseUrl: opts.baseUrl,
17106
+ model: opts.model,
17107
+ signal: ctx?.signal
17108
+ });
17109
+ if (hits === null) {
17110
+ return "No semantic index found for this project. Run `reasonix index` to build one.";
17111
+ }
17112
+ if (hits.length === 0) {
17113
+ return `query: ${args.query}
17114
+
17115
+ no matches above the score threshold (${args.minScore ?? defaultMinScore}).`;
17116
+ }
17117
+ return formatHits(args.query, hits);
17118
+ }
17119
+ });
17120
+ return true;
17121
+ }
17122
+ function formatHits(query, hits) {
17123
+ const lines = [`query: ${query}`, `
17124
+ results (${hits.length}):`];
17125
+ hits.forEach((h, i) => {
17126
+ const { entry, score } = h;
17127
+ lines.push(
17128
+ `
17129
+ ${i + 1}. ${entry.path}:${entry.startLine}-${entry.endLine} (score ${score.toFixed(3)})`
17130
+ );
17131
+ const preview = entry.text.split("\n").slice(0, 8).join("\n");
17132
+ lines.push(indentBlock(preview, " "));
17133
+ if (entry.text.split("\n").length > 8) {
17134
+ lines.push(
17135
+ ` \u2026(${entry.text.split("\n").length - 8} more lines \u2014 read_file ${entry.path}:${entry.startLine} for the full chunk)`
17136
+ );
17137
+ }
17138
+ });
17139
+ return lines.join("\n");
17140
+ }
17141
+ function indentBlock(text, prefix) {
17142
+ return text.split("\n").map((l) => prefix + l).join("\n");
17143
+ }
17144
+ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
17145
+ if (await indexExists(rootDir)) {
17146
+ await registerSemanticSearchTool(registry, { ...opts, root: rootDir });
17147
+ return { enabled: true };
17148
+ }
17149
+ return { enabled: false };
17150
+ }
17151
+
17152
+ // src/cli/commands/code.tsx
17153
+ async function codeCommand(opts = {}) {
17154
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-YRY4HPMZ.js");
17155
+ const rootDir = resolve10(opts.dir ?? process.cwd());
17156
+ const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
17157
+ const tools = new ToolRegistry();
17158
+ const jobs2 = new JobRegistry();
17159
+ const registerRootedTools = (root) => {
17160
+ registerFilesystemTools(tools, { rootDir: root });
17161
+ registerShellTools(tools, {
17162
+ rootDir: root,
17163
+ // Per-project "always allow" list persisted from prior ShellConfirm
17164
+ // choices; merged on top of the built-in allowlist in shell.ts.
17165
+ // GETTER form — re-read every dispatch so a prefix the user adds
17166
+ // via ShellConfirm mid-session takes effect on the next shell call
17167
+ // instead of waiting for `/new` or a relaunch.
17168
+ extraAllowed: () => loadProjectShellAllowed(root),
17169
+ // `yolo` edit-mode disables shell confirmations entirely. Re-read
17170
+ // from config on each dispatch so /mode yolo (or Shift+Tab cycling
17171
+ // through to it) flips the gate live without forcing a relaunch.
17172
+ allowAll: () => loadEditMode() === "yolo",
17173
+ jobs: jobs2
17174
+ });
17175
+ registerMemoryTools(tools, { projectRoot: root });
17176
+ };
17177
+ registerRootedTools(rootDir);
17178
+ registerPlanTool(tools);
17179
+ registerChoiceTool(tools);
17180
+ const semantic2 = await bootstrapSemanticSearchInCodeMode(tools, rootDir);
17181
+ process.stderr.write(
17182
+ `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)${semantic2.enabled ? " \xB7 semantic_search on" : ""}
17183
+ `
17184
+ );
17185
+ process.once("exit", () => {
17186
+ void jobs2.shutdown();
17187
+ });
17188
+ await chatCommand({
17189
+ model: opts.model ?? "deepseek-v4-flash",
17190
+ harvest: opts.harvest ?? false,
17191
+ system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
17192
+ transcript: opts.transcript,
17193
+ session,
17194
+ seedTools: tools,
17195
+ codeMode: { rootDir, jobs: jobs2, reregisterTools: registerRootedTools },
17196
+ forceResume: opts.forceResume,
17197
+ forceNew: opts.forceNew
17198
+ });
17199
+ }
17200
+
17201
+ // src/cli/commands/diff.ts
17202
+ import { writeFileSync as writeFileSync8 } from "fs";
17203
+ import { basename as basename3 } from "path";
17204
+ import { render as render2 } from "ink";
17205
+ import React30 from "react";
17206
+
17207
+ // src/cli/ui/DiffApp.tsx
17208
+ import { Box as Box26, Static as Static2, Text as Text24, useApp as useApp3, useInput } from "ink";
17209
+ import React29, { useState as useState13 } from "react";
17210
+
17211
+ // src/cli/ui/RecordView.tsx
17212
+ import { Box as Box25, Text as Text23 } from "ink";
17213
+ import React28 from "react";
17214
+ function RecordView({ rec, compact: compact2 = false }) {
17215
+ const toolArgsMax = compact2 ? 120 : 200;
17216
+ const toolContentMax = compact2 ? 200 : 400;
17217
+ if (rec.role === "user") {
15874
17218
  const content = rec.content.includes("\n") ? rec.content.split("\n").join("\n ") : rec.content;
15875
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React27.createElement(Text22, null, content));
17219
+ return /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { bold: true, color: "cyan" }, "you \u203A", " "), /* @__PURE__ */ React28.createElement(Text23, null, content));
15876
17220
  }
15877
17221
  if (rec.role === "assistant_final") {
15878
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React27.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React27.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React27.createElement(Text22, null, rec.content) : /* @__PURE__ */ React27.createElement(Text22, { dimColor: true, italic: true }, "(tool-call response only)"));
17222
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Box25, null, /* @__PURE__ */ React28.createElement(Text23, { bold: true, color: "green" }, "assistant"), rec.cost !== void 0 ? /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " $", rec.cost.toFixed(6)) : null, rec.usage ? /* @__PURE__ */ React28.createElement(CacheBadge, { usage: rec.usage }) : null), rec.planState ? /* @__PURE__ */ React28.createElement(PlanStateBlock, { planState: rec.planState }) : null, rec.content ? /* @__PURE__ */ React28.createElement(Text23, null, rec.content) : /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, "(tool-call response only)"));
15879
17223
  }
15880
17224
  if (rec.role === "tool") {
15881
- return /* @__PURE__ */ React27.createElement(Box24, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
17225
+ return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "tool<", rec.tool ?? "?", ">"), rec.args ? /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " args: ", truncate2(rec.args, toolArgsMax)) : null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 ", truncate2(rec.content, toolContentMax)));
15882
17226
  }
15883
17227
  if (rec.role === "error") {
15884
- return /* @__PURE__ */ React27.createElement(Box24, { marginTop: 1 }, /* @__PURE__ */ React27.createElement(Text22, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React27.createElement(Text22, { color: "red" }, rec.error ?? rec.content));
17228
+ return /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "red", bold: true }, "error", " "), /* @__PURE__ */ React28.createElement(Text23, { color: "red" }, rec.error ?? rec.content));
15885
17229
  }
15886
17230
  if (rec.role === "done" || rec.role === "assistant_delta") {
15887
17231
  return null;
15888
17232
  }
15889
- return /* @__PURE__ */ React27.createElement(Box24, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, "[", rec.role, "] ", rec.content));
17233
+ return /* @__PURE__ */ React28.createElement(Box25, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "[", rec.role, "] ", rec.content));
15890
17234
  }
15891
17235
  function CacheBadge({ usage }) {
15892
17236
  const hit = usage.prompt_cache_hit_tokens ?? 0;
@@ -15895,7 +17239,7 @@ function CacheBadge({ usage }) {
15895
17239
  if (total === 0) return null;
15896
17240
  const pct2 = hit / total * 100;
15897
17241
  const color = pct2 >= 70 ? "green" : pct2 >= 40 ? "yellow" : "red";
15898
- return /* @__PURE__ */ React27.createElement(Text22, null, /* @__PURE__ */ React27.createElement(Text22, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React27.createElement(Text22, { color }, pct2.toFixed(1), "%"));
17242
+ return /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 cache "), /* @__PURE__ */ React28.createElement(Text23, { color }, pct2.toFixed(1), "%"));
15899
17243
  }
15900
17244
  function truncate2(s, max) {
15901
17245
  return s.length <= max ? s : `${s.slice(0, max)}\u2026 (+${s.length - max} chars)`;
@@ -15929,7 +17273,7 @@ function DiffApp({ report }) {
15929
17273
  }
15930
17274
  });
15931
17275
  const pair = report.pairs[idx];
15932
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column" }, /* @__PURE__ */ React28.createElement(DiffHeader, { report }), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React28.createElement(Text23, null, pair ? /* @__PURE__ */ React28.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React28.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React28.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React28.createElement(Text23, null, pair.divergenceNote)) : null, /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "j"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "k"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "N"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "g"), "/", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React28.createElement(Text23, { bold: true }, "q"), " ", "quit")));
17276
+ return /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column" }, /* @__PURE__ */ React29.createElement(DiffHeader, { report }), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1, paddingX: 1, justifyContent: "space-between" }, /* @__PURE__ */ React29.createElement(Text24, { color: "cyan", bold: true }, "turn ", pair?.turn ?? "?", " (", idx + 1, " / ", report.pairs.length, ")"), /* @__PURE__ */ React29.createElement(Text24, null, pair ? /* @__PURE__ */ React29.createElement(KindBadge, { kind: pair.kind }) : null)), /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "row", marginTop: 1 }, /* @__PURE__ */ React29.createElement(Pane, { label: report.a.label, headerColor: "blue", records: paneRecords(pair, "a") }), /* @__PURE__ */ React29.createElement(Pane, { label: report.b.label, headerColor: "magenta", records: paneRecords(pair, "b") })), pair?.divergenceNote ? /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React29.createElement(Text24, { color: "yellow" }, "\u2605 "), /* @__PURE__ */ React29.createElement(Text24, null, pair.divergenceNote)) : null, /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "j"), "/", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "\u2193"), " next \xB7 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "k"), "/", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "\u2191"), " ", "prev \xB7 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "n"), " next-diverge \xB7 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "N"), "/", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "p"), " ", "prev-diverge \xB7 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "g"), "/", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "G"), " first/last \xB7 ", /* @__PURE__ */ React29.createElement(Text24, { bold: true }, "q"), " ", "quit")));
15933
17277
  }
15934
17278
  function DiffHeader({ report }) {
15935
17279
  const a = report.a;
@@ -15947,15 +17291,15 @@ function DiffHeader({ report }) {
15947
17291
  } else if (a.stats.prefixHashes[0] && a.stats.prefixHashes[0] === b.stats.prefixHashes[0]) {
15948
17292
  prefixLine = `shared prefix hash ${a.stats.prefixHashes[0].slice(0, 12)}\u2026 \u2014 cache delta attributable to log stability, not prompt change.`;
15949
17293
  }
15950
- return /* @__PURE__ */ React28.createElement(Box25, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React28.createElement(Box25, { justifyContent: "space-between" }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, a.label), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " vs B="), /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, b.label)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cache "), /* @__PURE__ */ React28.createElement(Text23, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React28.createElement(Text23, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "cost "), /* @__PURE__ */ React28.createElement(Text23, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React28.createElement(Text23, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React28.createElement(Text23, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React28.createElement(Text23, null, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true }, "model calls "), /* @__PURE__ */ React28.createElement(Text23, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, prefixLine)) : null);
17294
+ return /* @__PURE__ */ React29.createElement(Box26, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React29.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React29.createElement(Text24, null, /* @__PURE__ */ React29.createElement(Text24, { color: "cyan", bold: true }, "reasonix diff"), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, " \xB7 A="), /* @__PURE__ */ React29.createElement(Text24, { color: "blue" }, a.label), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, " vs B="), /* @__PURE__ */ React29.createElement(Text24, { color: "magenta" }, b.label)), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, report.pairs.length, " turns aligned")), /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1, gap: 3 }, /* @__PURE__ */ React29.createElement(Text24, null, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "cache "), /* @__PURE__ */ React29.createElement(Text24, null, (a.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React29.createElement(Text24, null, (b.stats.cacheHitRatio * 100).toFixed(1), "%"), /* @__PURE__ */ React29.createElement(Text24, { color: cacheDelta >= 0 ? "green" : "red", bold: true }, " ", cacheDelta >= 0 ? "+" : "", (cacheDelta * 100).toFixed(1), "pp")), /* @__PURE__ */ React29.createElement(Text24, null, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "cost "), /* @__PURE__ */ React29.createElement(Text24, null, "$", a.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, " \u2192 "), /* @__PURE__ */ React29.createElement(Text24, null, "$", b.stats.totalCostUsd.toFixed(6)), /* @__PURE__ */ React29.createElement(Text24, { color: costDelta2 <= 0 ? "green" : "red", bold: true }, " ", costDelta2 >= 0 ? "+" : "", costDelta2.toFixed(1), "%")), /* @__PURE__ */ React29.createElement(Text24, null, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true }, "model calls "), /* @__PURE__ */ React29.createElement(Text24, null, a.stats.turns, " \u2192 ", b.stats.turns))), prefixLine ? /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true, italic: true }, prefixLine)) : null);
15951
17295
  }
15952
17296
  function Pane({
15953
17297
  label,
15954
17298
  headerColor,
15955
17299
  records
15956
17300
  }) {
15957
- return /* @__PURE__ */ React28.createElement(
15958
- Box25,
17301
+ return /* @__PURE__ */ React29.createElement(
17302
+ Box26,
15959
17303
  {
15960
17304
  flexDirection: "column",
15961
17305
  flexGrow: 1,
@@ -15963,21 +17307,21 @@ function Pane({
15963
17307
  borderStyle: "single",
15964
17308
  borderColor: headerColor
15965
17309
  },
15966
- /* @__PURE__ */ React28.createElement(Text23, { color: headerColor, bold: true }, label),
15967
- records.length === 0 ? /* @__PURE__ */ React28.createElement(Box25, { marginTop: 1 }, /* @__PURE__ */ React28.createElement(Text23, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React28.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React28.createElement(RecordView, { key, rec, compact: true }))
17310
+ /* @__PURE__ */ React29.createElement(Text24, { color: headerColor, bold: true }, label),
17311
+ records.length === 0 ? /* @__PURE__ */ React29.createElement(Box26, { marginTop: 1 }, /* @__PURE__ */ React29.createElement(Text24, { dimColor: true, italic: true }, "(no records on this side for this turn)")) : /* @__PURE__ */ React29.createElement(Static2, { items: records.map((rec, i) => ({ key: `${label}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React29.createElement(RecordView, { key, rec, compact: true }))
15968
17312
  );
15969
17313
  }
15970
17314
  function KindBadge({ kind }) {
15971
17315
  if (kind === "match") {
15972
- return /* @__PURE__ */ React28.createElement(Text23, { color: "green" }, "\u2713 match");
17316
+ return /* @__PURE__ */ React29.createElement(Text24, { color: "green" }, "\u2713 match");
15973
17317
  }
15974
17318
  if (kind === "diverge") {
15975
- return /* @__PURE__ */ React28.createElement(Text23, { color: "yellow" }, "\u2605 diverge");
17319
+ return /* @__PURE__ */ React29.createElement(Text24, { color: "yellow" }, "\u2605 diverge");
15976
17320
  }
15977
17321
  if (kind === "only_in_a") {
15978
- return /* @__PURE__ */ React28.createElement(Text23, { color: "blue" }, "\u2190 only in A");
17322
+ return /* @__PURE__ */ React29.createElement(Text24, { color: "blue" }, "\u2190 only in A");
15979
17323
  }
15980
- return /* @__PURE__ */ React28.createElement(Text23, { color: "magenta" }, "\u2192 only in B");
17324
+ return /* @__PURE__ */ React29.createElement(Text24, { color: "magenta" }, "\u2192 only in B");
15981
17325
  }
15982
17326
  function paneRecords(pair, side) {
15983
17327
  if (!pair) return [];
@@ -16008,7 +17352,7 @@ markdown report written to ${opts.mdPath}`);
16008
17352
  return;
16009
17353
  }
16010
17354
  if (wantTui) {
16011
- const { waitUntilExit } = render2(React29.createElement(DiffApp, { report }), {
17355
+ const { waitUntilExit } = render2(React30.createElement(DiffApp, { report }), {
16012
17356
  exitOnCtrlC: true,
16013
17357
  patchConsole: false
16014
17358
  });
@@ -16018,6 +17362,200 @@ markdown report written to ${opts.mdPath}`);
16018
17362
  console.log(renderSummaryTable(report));
16019
17363
  }
16020
17364
 
17365
+ // src/cli/commands/index.ts
17366
+ import { resolve as resolve11 } from "path";
17367
+
17368
+ // src/index/semantic/preflight.ts
17369
+ import { stdin as stdin2, stdout } from "process";
17370
+ import { createInterface } from "readline/promises";
17371
+ async function ollamaPreflight(opts) {
17372
+ const log = opts.log ?? ((line) => process.stderr.write(line));
17373
+ const status2 = await checkOllamaStatus(opts.model, opts.baseUrl);
17374
+ if (!status2.binaryFound) {
17375
+ log(t("ollamaNotFound"));
17376
+ return false;
17377
+ }
17378
+ if (!status2.daemonRunning) {
17379
+ if (!opts.interactive && !opts.yesToAll) {
17380
+ log(t("daemonNotReachableHint"));
17381
+ return false;
17382
+ }
17383
+ const ok = opts.yesToAll || await confirm(t("daemonStartConfirm"), true);
17384
+ if (!ok) {
17385
+ log(t("daemonAbortStart"));
17386
+ return false;
17387
+ }
17388
+ log(t("daemonStarting"));
17389
+ const started = await startOllamaDaemon({ baseUrl: opts.baseUrl, timeoutMs: 15e3 });
17390
+ if (!started.ready) {
17391
+ log(t("daemonStartTimeout"));
17392
+ return false;
17393
+ }
17394
+ log(t("daemonReady", { pid: started.pid ? ` (pid ${started.pid})` : "" }));
17395
+ }
17396
+ const after = status2.daemonRunning ? status2 : await checkOllamaStatus(opts.model, opts.baseUrl);
17397
+ if (!after.modelPulled) {
17398
+ if (!opts.interactive && !opts.yesToAll) {
17399
+ log(t("modelNotPulledHint", { model: opts.model }));
17400
+ return false;
17401
+ }
17402
+ const ok = opts.yesToAll || await confirm(t("modelPullConfirm", { model: opts.model }), true);
17403
+ if (!ok) {
17404
+ log(t("modelAbortPull"));
17405
+ return false;
17406
+ }
17407
+ log(t("modelPulling", { model: opts.model }));
17408
+ const ESC = String.fromCharCode(27);
17409
+ const ANSI_CSI = new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "g");
17410
+ const code = await pullOllamaModel(opts.model, {
17411
+ onLine: (line) => {
17412
+ const cleaned = line.replace(ANSI_CSI, "").trim();
17413
+ if (cleaned.length === 0) return;
17414
+ log(` ${cleaned}
17415
+ `);
17416
+ }
17417
+ });
17418
+ if (code !== 0) {
17419
+ log(t("modelPullFailed", { model: opts.model, code }));
17420
+ return false;
17421
+ }
17422
+ log(t("modelPulled", { model: opts.model }));
17423
+ }
17424
+ return true;
17425
+ }
17426
+ async function confirm(question, defaultYes) {
17427
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
17428
+ const rl = createInterface({ input: stdin2, output: stdout });
17429
+ try {
17430
+ const raw = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
17431
+ if (raw === "") return defaultYes;
17432
+ return raw === "y" || raw === "yes";
17433
+ } finally {
17434
+ rl.close();
17435
+ }
17436
+ }
17437
+
17438
+ // src/cli/commands/index.ts
17439
+ async function indexCommand(opts = {}) {
17440
+ const root = resolve11(opts.dir ?? process.cwd());
17441
+ const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
17442
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17443
+ const preflightOk = await ollamaPreflight({
17444
+ model: model2,
17445
+ baseUrl: opts.ollamaUrl,
17446
+ interactive: tty && !opts.yes,
17447
+ yesToAll: opts.yes ?? false
17448
+ });
17449
+ if (!preflightOk) process.exit(1);
17450
+ const writer = makeProgressWriter(tty);
17451
+ const t0 = Date.now();
17452
+ let result;
17453
+ try {
17454
+ result = await buildIndex(root, {
17455
+ rebuild: opts.rebuild,
17456
+ model: model2,
17457
+ baseUrl: opts.ollamaUrl,
17458
+ onProgress: (p) => writer.update(p)
17459
+ });
17460
+ } catch (err) {
17461
+ writer.clear();
17462
+ const msg = err instanceof Error ? err.message : String(err);
17463
+ process.stderr.write(t("indexFailed", { msg }));
17464
+ process.exit(1);
17465
+ }
17466
+ writer.clear();
17467
+ const seconds = ((Date.now() - t0) / 1e3).toFixed(1);
17468
+ const successKey = result.chunksSkipped > 0 ? "indexSuccessWithSkips" : "indexSuccess";
17469
+ process.stderr.write(
17470
+ t(successKey, {
17471
+ scanned: result.filesScanned,
17472
+ changed: result.filesChanged,
17473
+ added: result.chunksAdded,
17474
+ removed: result.chunksRemoved,
17475
+ skipped: result.chunksSkipped,
17476
+ seconds
17477
+ })
17478
+ );
17479
+ if (result.filesChanged === 0 && !opts.rebuild) {
17480
+ process.stderr.write(t("indexNothingToDo"));
17481
+ }
17482
+ }
17483
+ var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17484
+ var SPINNER_INTERVAL_MS = 120;
17485
+ function makeProgressWriter(tty) {
17486
+ if (!tty) return makeNonTtyWriter();
17487
+ return makeTtyWriter();
17488
+ }
17489
+ function makeNonTtyWriter() {
17490
+ let lastPhase = null;
17491
+ let lastChunks = 0;
17492
+ return {
17493
+ update(p) {
17494
+ if (p.phase !== lastPhase) {
17495
+ lastPhase = p.phase;
17496
+ if (p.phase === "scan") {
17497
+ process.stderr.write(t("progressScanLine"));
17498
+ } else if (p.phase === "embed") {
17499
+ process.stderr.write(
17500
+ t("progressEmbedLine", {
17501
+ total: p.chunksTotal ?? 0,
17502
+ files: p.filesChanged ?? 0
17503
+ })
17504
+ );
17505
+ }
17506
+ }
17507
+ if (p.phase === "embed" && p.chunksDone !== void 0 && p.chunksDone - lastChunks >= 50) {
17508
+ lastChunks = p.chunksDone;
17509
+ process.stderr.write(
17510
+ t("progressEmbedHeartbeat", {
17511
+ done: p.chunksDone,
17512
+ total: p.chunksTotal ?? "?"
17513
+ })
17514
+ );
17515
+ }
17516
+ },
17517
+ clear() {
17518
+ }
17519
+ };
17520
+ }
17521
+ function makeTtyWriter() {
17522
+ let status2 = t("progressStarting");
17523
+ let lastLineLen = 0;
17524
+ let frameIdx = 0;
17525
+ const startTs = Date.now();
17526
+ const repaint = () => {
17527
+ const frame = SPINNER_FRAMES2[frameIdx % SPINNER_FRAMES2.length];
17528
+ frameIdx++;
17529
+ const elapsed = ((Date.now() - startTs) / 1e3).toFixed(1);
17530
+ const line = `${frame} ${status2} ${elapsed}s`;
17531
+ const padded = line + " ".repeat(Math.max(0, lastLineLen - line.length));
17532
+ process.stderr.write(`\r${padded}`);
17533
+ lastLineLen = line.length;
17534
+ };
17535
+ repaint();
17536
+ const interval = setInterval(repaint, SPINNER_INTERVAL_MS);
17537
+ return {
17538
+ update(p) {
17539
+ if (p.phase === "scan") {
17540
+ status2 = t("progressScan", { files: p.filesScanned ?? 0 });
17541
+ } else if (p.phase === "embed") {
17542
+ const done = p.chunksDone ?? 0;
17543
+ const total = p.chunksTotal ?? 0;
17544
+ const pct2 = total > 0 ? (done / total * 100).toFixed(0) : "0";
17545
+ status2 = t("progressEmbed", { done, total, pct: pct2 });
17546
+ }
17547
+ repaint();
17548
+ },
17549
+ clear() {
17550
+ clearInterval(interval);
17551
+ if (lastLineLen > 0) {
17552
+ process.stderr.write(`\r${" ".repeat(lastLineLen)}\r`);
17553
+ lastLineLen = 0;
17554
+ }
17555
+ }
17556
+ };
17557
+ }
17558
+
16021
17559
  // src/cli/commands/mcp-inspect.ts
16022
17560
  async function mcpInspectCommand(opts) {
16023
17561
  const spec = parseMcpSpec(opts.spec);
@@ -16064,9 +17602,9 @@ function formatSection(title, section, render5) {
16064
17602
  for (const item of section.items) lines.push(` ${render5(item)}`);
16065
17603
  return lines.join("\n");
16066
17604
  }
16067
- function toolLine(t) {
16068
- const desc = t.description ? ` \u2014 ${oneLine(t.description, 80)}` : "";
16069
- return `\xB7 ${t.name}${desc}`;
17605
+ function toolLine(t2) {
17606
+ const desc = t2.description ? ` \u2014 ${oneLine(t2.description, 80)}` : "";
17607
+ return `\xB7 ${t2.name}${desc}`;
16070
17608
  }
16071
17609
  function resourceLine(r) {
16072
17610
  const mime = r.mimeType ? ` [${r.mimeType}]` : "";
@@ -16149,11 +17687,11 @@ function pad2(s, width) {
16149
17687
 
16150
17688
  // src/cli/commands/replay.ts
16151
17689
  import { render as render3 } from "ink";
16152
- import React31 from "react";
17690
+ import React32 from "react";
16153
17691
 
16154
17692
  // src/cli/ui/ReplayApp.tsx
16155
- import { Box as Box26, Static as Static3, Text as Text24, useApp as useApp4, useInput as useInput2 } from "ink";
16156
- import React30, { useMemo as useMemo4, useState as useState14 } from "react";
17693
+ import { Box as Box27, Static as Static3, Text as Text25, useApp as useApp4, useInput as useInput2 } from "ink";
17694
+ import React31, { useMemo as useMemo4, useState as useState14 } from "react";
16157
17695
  function ReplayApp({ meta, pages }) {
16158
17696
  const { exit: exit2 } = useApp4();
16159
17697
  const maxIdx = Math.max(0, pages.length - 1);
@@ -16193,14 +17731,14 @@ function ReplayApp({ meta, pages }) {
16193
17731
  const prefixHash = cumStats.prefixHashes.length === 1 ? cumStats.prefixHashes[0].slice(0, 16) : cumStats.prefixHashes.length === 0 ? "(untracked)" : `(churned \xD7${cumStats.prefixHashes.length})`;
16194
17732
  const currentPage = pages[idx];
16195
17733
  const progressLabel = pages.length === 0 ? "empty transcript" : `turn ${idx + 1} / ${pages.length}`;
16196
- return /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column" }, /* @__PURE__ */ React30.createElement(
17734
+ return /* @__PURE__ */ React31.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React31.createElement(
16197
17735
  StatsPanel,
16198
17736
  {
16199
17737
  summary,
16200
17738
  model: cumStats.models[0] ?? meta?.model ?? "?",
16201
17739
  prefixHash
16202
17740
  }
16203
- ), /* @__PURE__ */ React30.createElement(Box26, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React30.createElement(Box26, { justifyContent: "space-between" }, /* @__PURE__ */ React30.createElement(Text24, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React30.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React30.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React30.createElement(Text24, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React30.createElement(Box26, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React30.createElement(Text24, { dimColor: true }, /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "j"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "k"), "/", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React30.createElement(Text24, { bold: true }, "q"), " quit")));
17741
+ ), /* @__PURE__ */ React31.createElement(Box27, { flexDirection: "column", marginTop: 1, paddingX: 1 }, /* @__PURE__ */ React31.createElement(Box27, { justifyContent: "space-between" }, /* @__PURE__ */ React31.createElement(Text25, { color: "cyan", bold: true }, progressLabel), meta ? /* @__PURE__ */ React31.createElement(Text25, { dimColor: true }, meta.source, meta.task ? ` \xB7 ${meta.task}` : "", meta.mode ? ` \xB7 ${meta.mode}` : "") : null), currentPage ? /* @__PURE__ */ React31.createElement(Static3, { items: currentPage.records.map((rec, i) => ({ key: `${idx}-${i}`, rec })) }, ({ key, rec }) => /* @__PURE__ */ React31.createElement(RecordView, { key, rec })) : /* @__PURE__ */ React31.createElement(Text25, { dimColor: true, italic: true }, "no records")), /* @__PURE__ */ React31.createElement(Box27, { marginTop: 1, paddingX: 1, borderStyle: "single", borderColor: "gray" }, /* @__PURE__ */ React31.createElement(Text25, { dimColor: true }, /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "j"), "/", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "\u2193"), "/", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "space"), " next \xB7 ", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "k"), "/", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "\u2191"), " prev \xB7 ", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "g"), " first \xB7 ", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "G"), " last \xB7", " ", /* @__PURE__ */ React31.createElement(Text25, { bold: true }, "q"), " quit")));
16204
17742
  }
16205
17743
 
16206
17744
  // src/cli/commands/replay.ts
@@ -16212,7 +17750,7 @@ async function replayCommand(opts) {
16212
17750
  }
16213
17751
  const { parsed } = replayFromFile(opts.path);
16214
17752
  const pages = groupRecordsByTurn(parsed.records);
16215
- const { waitUntilExit } = render3(React31.createElement(ReplayApp, { meta: parsed.meta, pages }), {
17753
+ const { waitUntilExit } = render3(React32.createElement(ReplayApp, { meta: parsed.meta, pages }), {
16216
17754
  exitOnCtrlC: true,
16217
17755
  patchConsole: false
16218
17756
  });
@@ -16306,12 +17844,12 @@ function oneLine2(s, max = 200) {
16306
17844
  }
16307
17845
 
16308
17846
  // src/cli/commands/run.ts
16309
- import { stdin as stdin2, stdout } from "process";
16310
- import { createInterface } from "readline/promises";
17847
+ import { stdin as stdin3, stdout as stdout2 } from "process";
17848
+ import { createInterface as createInterface2 } from "readline/promises";
16311
17849
  async function ensureApiKey() {
16312
17850
  const existing = loadApiKey();
16313
17851
  if (existing) return existing;
16314
- if (!stdin2.isTTY) {
17852
+ if (!stdin3.isTTY) {
16315
17853
  process.stderr.write(
16316
17854
  "DEEPSEEK_API_KEY is not set and stdin is not a TTY (cannot prompt).\nSet the env var, or run `reasonix chat` once interactively to save a key.\n"
16317
17855
  );
@@ -16320,7 +17858,7 @@ async function ensureApiKey() {
16320
17858
  process.stdout.write(
16321
17859
  "DeepSeek API key not configured.\nGet one at https://platform.deepseek.com/api_keys\n"
16322
17860
  );
16323
- const rl = createInterface({ input: stdin2, output: stdout });
17861
+ const rl = createInterface2({ input: stdin3, output: stdout2 });
16324
17862
  try {
16325
17863
  while (true) {
16326
17864
  const answer = (await rl.question("API key \u203A ")).trim();
@@ -16471,14 +18009,14 @@ function listAll() {
16471
18009
  console.log("Resume: reasonix chat --session <name>");
16472
18010
  }
16473
18011
  function inspectSession(name, verbose) {
16474
- const path = sessionPath(name);
18012
+ const path5 = sessionPath(name);
16475
18013
  const messages = loadSessionMessages(name);
16476
18014
  if (messages.length === 0) {
16477
18015
  console.error(`no session named "${name}" (or it's empty).`);
16478
- console.error(`looked at: ${path}`);
18016
+ console.error(`looked at: ${path5}`);
16479
18017
  process.exit(1);
16480
18018
  }
16481
- console.log(`[session] ${name} ${messages.length} messages ${path}`);
18019
+ console.log(`[session] ${name} ${messages.length} messages ${path5}`);
16482
18020
  console.log("");
16483
18021
  let turnIndex = 0;
16484
18022
  for (const msg of messages) {
@@ -16517,12 +18055,12 @@ function truncate3(s, max) {
16517
18055
 
16518
18056
  // src/cli/commands/setup.tsx
16519
18057
  import { render as render4 } from "ink";
16520
- import React33 from "react";
18058
+ import React34 from "react";
16521
18059
 
16522
18060
  // src/cli/ui/Wizard.tsx
16523
- import { Box as Box27, Text as Text25, useApp as useApp5, useInput as useInput3 } from "ink";
18061
+ import { Box as Box28, Text as Text26, useApp as useApp5, useInput as useInput3 } from "ink";
16524
18062
  import TextInput2 from "ink-text-input";
16525
- import React32, { useState as useState15 } from "react";
18063
+ import React33, { useState as useState15 } from "react";
16526
18064
 
16527
18065
  // src/cli/ui/presets.ts
16528
18066
  var PRESETS = {
@@ -16570,7 +18108,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16570
18108
  if (key.escape && step !== "saved" && onCancel) onCancel();
16571
18109
  });
16572
18110
  if (step === "apiKey") {
16573
- return /* @__PURE__ */ React32.createElement(
18111
+ return /* @__PURE__ */ React33.createElement(
16574
18112
  ApiKeyStep,
16575
18113
  {
16576
18114
  onSubmit: (key) => {
@@ -16584,7 +18122,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16584
18122
  );
16585
18123
  }
16586
18124
  if (step === "preset") {
16587
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React32.createElement(
18125
+ return /* @__PURE__ */ React33.createElement(StepFrame, { title: "Pick a preset", step: 1, total: 3 }, /* @__PURE__ */ React33.createElement(
16588
18126
  SingleSelect,
16589
18127
  {
16590
18128
  items: presetItems(),
@@ -16594,10 +18132,10 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16594
18132
  setStep("mcp");
16595
18133
  }
16596
18134
  }
16597
- ), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
18135
+ ), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "[\u2191\u2193] navigate \xB7 [Enter] confirm \xB7 [Esc] cancel")));
16598
18136
  }
16599
18137
  if (step === "mcp") {
16600
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(
18138
+ return /* @__PURE__ */ React33.createElement(StepFrame, { title: "Which MCP servers should Reasonix wire up for you?", step: 2, total: 3 }, /* @__PURE__ */ React33.createElement(
16601
18139
  MultiSelect,
16602
18140
  {
16603
18141
  items: mcpItems(),
@@ -16622,7 +18160,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16622
18160
  }
16623
18161
  const currentName = pending[0];
16624
18162
  const entry = CATALOG_BY_NAME.get(currentName);
16625
- return /* @__PURE__ */ React32.createElement(
18163
+ return /* @__PURE__ */ React33.createElement(
16626
18164
  McpArgsStep,
16627
18165
  {
16628
18166
  entry,
@@ -16640,7 +18178,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16640
18178
  }
16641
18179
  if (step === "review") {
16642
18180
  const specs = data.selectedCatalog.map((name) => buildSpec(name, data.catalogArgs));
16643
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React32.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React32.createElement(
18181
+ return /* @__PURE__ */ React33.createElement(StepFrame, { title: "Ready to save", step: 3, total: 3 }, /* @__PURE__ */ React33.createElement(Box28, { flexDirection: "column" }, /* @__PURE__ */ React33.createElement(SummaryLine, { label: "API key", value: redactKey(data.apiKey) }), /* @__PURE__ */ React33.createElement(SummaryLine, { label: "Preset", value: data.preset }), /* @__PURE__ */ React33.createElement(
16644
18182
  SummaryLine,
16645
18183
  {
16646
18184
  label: "MCP",
@@ -16648,8 +18186,8 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16648
18186
  }
16649
18187
  ), specs.map((spec, i) => (
16650
18188
  // biome-ignore lint/suspicious/noArrayIndexKey: review-only render, order fixed
16651
- /* @__PURE__ */ React32.createElement(Box27, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "\xB7 ", spec))
16652
- )), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React32.createElement(
18189
+ /* @__PURE__ */ React33.createElement(Box28, { key: i, paddingLeft: 14 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "\xB7 ", spec))
18190
+ )), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, null, "Saves to ", defaultConfigPath())), error ? /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { color: "red" }, error)) : null, /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "[Enter] save \xB7 [Esc] cancel"))), /* @__PURE__ */ React33.createElement(
16653
18191
  ReviewConfirm,
16654
18192
  {
16655
18193
  onConfirm: () => {
@@ -16675,7 +18213,7 @@ function Wizard({ onComplete, onCancel, existingApiKey, initial }) {
16675
18213
  }
16676
18214
  ));
16677
18215
  }
16678
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React32.createElement(ExitOnEnter, { onExit: exit2 }));
18216
+ return /* @__PURE__ */ React33.createElement(Box28, { flexDirection: "column", borderStyle: "round", borderColor: "green", paddingX: 1 }, /* @__PURE__ */ React33.createElement(Text26, { bold: true, color: "green" }, "\u25B8 Saved."), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, null, "Run `reasonix` any time to start chatting \u2014 your settings are remembered.")), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "[Enter] to exit")), /* @__PURE__ */ React33.createElement(ExitOnEnter, { onExit: exit2 }));
16679
18217
  }
16680
18218
  function ApiKeyStep({
16681
18219
  onSubmit,
@@ -16683,7 +18221,7 @@ function ApiKeyStep({
16683
18221
  onError
16684
18222
  }) {
16685
18223
  const [value, setValue] = useState15("");
16686
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React32.createElement(
18224
+ return /* @__PURE__ */ React33.createElement(Box28, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React33.createElement(Text26, { bold: true, color: "cyan" }, "Welcome to Reasonix."), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, null, "Paste your DeepSeek API key to get started.")), /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "Get one (free credit on signup): https://platform.deepseek.com/api_keys"), /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "Saved locally to ", defaultConfigPath()), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { bold: true, color: "cyan" }, "key \u203A "), /* @__PURE__ */ React33.createElement(
16687
18225
  TextInput2,
16688
18226
  {
16689
18227
  value,
@@ -16700,7 +18238,7 @@ function ApiKeyStep({
16700
18238
  mask: "\u2022",
16701
18239
  placeholder: "sk-..."
16702
18240
  }
16703
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : value ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "preview: ", redactKey(value))) : null);
18241
+ )), error ? /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { color: "red" }, error)) : value ? /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "preview: ", redactKey(value))) : null);
16704
18242
  }
16705
18243
  function McpArgsStep({
16706
18244
  entry,
@@ -16709,7 +18247,7 @@ function McpArgsStep({
16709
18247
  onError
16710
18248
  }) {
16711
18249
  const [value, setValue] = useState15("");
16712
- return /* @__PURE__ */ React32.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column" }, /* @__PURE__ */ React32.createElement(Text25, null, entry.summary), entry.note ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, null, "Required parameter: "), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, entry.userArgs)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React32.createElement(
18250
+ return /* @__PURE__ */ React33.createElement(StepFrame, { title: `Configure ${entry.name}`, step: 2, total: 3 }, /* @__PURE__ */ React33.createElement(Box28, { flexDirection: "column" }, /* @__PURE__ */ React33.createElement(Text26, null, entry.summary), entry.note ? /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, entry.note)) : null, /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, null, "Required parameter: "), /* @__PURE__ */ React33.createElement(Text26, { bold: true }, entry.userArgs)), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { bold: true, color: "cyan" }, entry.userArgs, " \u203A "), /* @__PURE__ */ React33.createElement(
16713
18251
  TextInput2,
16714
18252
  {
16715
18253
  value,
@@ -16725,7 +18263,7 @@ function McpArgsStep({
16725
18263
  },
16726
18264
  placeholder: placeholderFor(entry)
16727
18265
  }
16728
- )), error ? /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1 }, /* @__PURE__ */ React32.createElement(Text25, { color: "red" }, error)) : null));
18266
+ )), error ? /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1 }, /* @__PURE__ */ React33.createElement(Text26, { color: "red" }, error)) : null));
16729
18267
  }
16730
18268
  function ReviewConfirm({ onConfirm }) {
16731
18269
  useInput3((_i, key) => {
@@ -16745,10 +18283,10 @@ function StepFrame({
16745
18283
  total,
16746
18284
  children
16747
18285
  }) {
16748
- return /* @__PURE__ */ React32.createElement(Box27, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React32.createElement(Text25, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React32.createElement(Box27, { marginTop: 1, flexDirection: "column" }, children));
18286
+ return /* @__PURE__ */ React33.createElement(Box28, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1 }, /* @__PURE__ */ React33.createElement(Box28, null, /* @__PURE__ */ React33.createElement(Text26, { dimColor: true }, "Step ", step, "/", total, " \xB7", " "), /* @__PURE__ */ React33.createElement(Text26, { bold: true, color: "cyan" }, title)), /* @__PURE__ */ React33.createElement(Box28, { marginTop: 1, flexDirection: "column" }, children));
16749
18287
  }
16750
18288
  function SummaryLine({ label, value }) {
16751
- return /* @__PURE__ */ React32.createElement(Box27, null, /* @__PURE__ */ React32.createElement(Text25, null, label.padEnd(12)), /* @__PURE__ */ React32.createElement(Text25, { bold: true }, value));
18289
+ return /* @__PURE__ */ React33.createElement(Box28, null, /* @__PURE__ */ React33.createElement(Text26, null, label.padEnd(12)), /* @__PURE__ */ React33.createElement(Text26, { bold: true }, value));
16752
18290
  }
16753
18291
  function presetItems() {
16754
18292
  return ["fast", "smart", "max"].map((name) => ({
@@ -16804,7 +18342,7 @@ async function setupCommand(_opts = {}) {
16804
18342
  const existingKey = loadApiKey();
16805
18343
  const existing = readConfig();
16806
18344
  const { waitUntilExit, unmount } = render4(
16807
- /* @__PURE__ */ React33.createElement(
18345
+ /* @__PURE__ */ React34.createElement(
16808
18346
  Wizard,
16809
18347
  {
16810
18348
  existingApiKey: existingKey,
@@ -16822,7 +18360,7 @@ async function setupCommand(_opts = {}) {
16822
18360
  }
16823
18361
 
16824
18362
  // src/cli/commands/update.ts
16825
- import { spawn as spawn5 } from "child_process";
18363
+ import { spawn as spawn6 } from "child_process";
16826
18364
  function planUpdate(input) {
16827
18365
  const diff = compareVersions(input.current, input.latest);
16828
18366
  if (diff > 0) {
@@ -16852,13 +18390,13 @@ function planUpdate(input) {
16852
18390
  };
16853
18391
  }
16854
18392
  function defaultSpawn(argv) {
16855
- return new Promise((resolve8, reject) => {
16856
- const child = spawn5(argv[0], argv.slice(1), {
18393
+ return new Promise((resolve12, reject) => {
18394
+ const child = spawn6(argv[0], argv.slice(1), {
16857
18395
  stdio: "inherit",
16858
18396
  shell: process.platform === "win32"
16859
18397
  });
16860
18398
  child.once("error", reject);
16861
- child.once("exit", (code) => resolve8(code ?? 1));
18399
+ child.once("exit", (code) => resolve12(code ?? 1));
16862
18400
  });
16863
18401
  }
16864
18402
  async function updateCommand(opts = {}) {
@@ -17154,6 +18692,16 @@ program.command("update").description(
17154
18692
  ).option("--dry-run", "Print the plan without executing the install").action(async (opts) => {
17155
18693
  await updateCommand({ dryRun: !!opts.dryRun });
17156
18694
  });
18695
+ program.command("index").description(
18696
+ "Build (or incrementally refresh) a local semantic search index for the project so `reasonix code` can answer 'where do we\u2026' questions by meaning, not just by token. Uses Ollama as the embedding backend; missing daemon / model is offered to the user with a confirm prompt."
18697
+ ).option("--rebuild", "Wipe and rebuild from scratch").option("--model <name>", "Embedding model (default: nomic-embed-text)").option("--dir <path>", "Project root to index (default: cwd)").option("--ollama-url <url>", "Override Ollama base URL (default: http://localhost:11434)").option(
18698
+ "-y, --yes",
18699
+ "Skip preflight prompts \u2014 auto-start the daemon and pull the model if missing (use in scripts)"
18700
+ ).action(
18701
+ async (opts) => {
18702
+ await indexCommand(opts);
18703
+ }
18704
+ );
17157
18705
  program.parseAsync(process.argv).catch((err) => {
17158
18706
  console.error(err);
17159
18707
  process.exit(1);