reasonix 0.10.0 → 0.11.0

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-GXABXQMU.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((resolve9, reject) => {
160
+ const timer = setTimeout(resolve9, 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((resolve9) => {
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
+ resolve9({
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
+ resolve9({
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
723
  const cwd = hook.cwd ?? opts.payload.cwd;
724
- const raw = await spawner({ command: hook.command, cwd, stdin: stdin3, timeoutMs });
724
+ const raw = await spawner({ command: hook.command, cwd, 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;
@@ -2198,13 +2198,13 @@ var CacheFirstLoop = class {
2198
2198
  * with `<`.
2199
2199
  */
2200
2200
  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);
2201
+ const t2 = buf.trimStart();
2202
+ if (t2.length === 0) return true;
2203
+ if (t2.length <= NEEDS_PRO_MARKER_PREFIX.length) {
2204
+ return NEEDS_PRO_MARKER_PREFIX.startsWith(t2);
2205
2205
  }
2206
- if (!t.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
- const rest = t.slice(NEEDS_PRO_MARKER_PREFIX.length);
2206
+ if (!t2.startsWith(NEEDS_PRO_MARKER_PREFIX)) return false;
2207
+ const rest = t2.slice(NEEDS_PRO_MARKER_PREFIX.length);
2208
2208
  if (rest[0] !== ">" && rest[0] !== ":") return false;
2209
2209
  return true;
2210
2210
  }
@@ -2312,7 +2312,9 @@ var CacheFirstLoop = class {
2312
2312
  this._proArmedForNextTurn = false;
2313
2313
  armedConsumed = true;
2314
2314
  }
2315
+ const carryAbort = this._turnAbort.signal.aborted;
2315
2316
  this._turnAbort = new AbortController();
2317
+ if (carryAbort) this._turnAbort.abort();
2316
2318
  const signal = this._turnAbort.signal;
2317
2319
  if (armedConsumed) {
2318
2320
  yield {
@@ -2435,8 +2437,8 @@ var CacheFirstLoop = class {
2435
2437
  }
2436
2438
  );
2437
2439
  for (let k = 0; k < budget; k++) {
2438
- const sample = queue.shift() ?? await new Promise((resolve8) => {
2439
- waiter = resolve8;
2440
+ const sample = queue.shift() ?? await new Promise((resolve9) => {
2441
+ waiter = resolve9;
2440
2442
  });
2441
2443
  yield {
2442
2444
  turn: this._turn,
@@ -3259,7 +3261,7 @@ function rankPickerCandidates(files, query, limitOrOpts) {
3259
3261
  var AT_MENTION_PATTERN = /(?<=^|\s)@([a-zA-Z0-9_./\\-]+)/g;
3260
3262
  function expandAtMentions(text, rootDir, opts = {}) {
3261
3263
  const maxBytes = opts.maxBytes ?? DEFAULT_AT_MENTION_MAX_BYTES;
3262
- const fs2 = opts.fs ?? defaultFs;
3264
+ const fs6 = opts.fs ?? defaultFs;
3263
3265
  const root = resolve(rootDir);
3264
3266
  const seen = /* @__PURE__ */ new Map();
3265
3267
  const expansions = [];
@@ -3269,7 +3271,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3269
3271
  if (!cleaned) continue;
3270
3272
  const token = `@${cleaned}`;
3271
3273
  if (seen.has(token)) continue;
3272
- const expansion = resolveMention(cleaned, root, maxBytes, fs2);
3274
+ const expansion = resolveMention(cleaned, root, maxBytes, fs6);
3273
3275
  seen.set(token, expansion);
3274
3276
  expansions.push(expansion);
3275
3277
  }
@@ -3277,7 +3279,7 @@ function expandAtMentions(text, rootDir, opts = {}) {
3277
3279
  const blocks = [];
3278
3280
  for (const ex of expansions) {
3279
3281
  if (ex.ok) {
3280
- const content = readSafe(root, ex.path, fs2);
3282
+ const content = readSafe(root, ex.path, fs6);
3281
3283
  blocks.push(`<file path="${ex.path}">
3282
3284
  ${content}
3283
3285
  </file>`);
@@ -3291,7 +3293,7 @@ ${content}
3291
3293
  ${blocks.join("\n\n")}`;
3292
3294
  return { text: augmented, expansions };
3293
3295
  }
3294
- function resolveMention(rawPath, root, maxBytes, fs2) {
3296
+ function resolveMention(rawPath, root, maxBytes, fs6) {
3295
3297
  if (isAbsolute(rawPath)) {
3296
3298
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3297
3299
  }
@@ -3300,22 +3302,22 @@ function resolveMention(rawPath, root, maxBytes, fs2) {
3300
3302
  if (rel.startsWith("..") || isAbsolute(rel)) {
3301
3303
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "escape" };
3302
3304
  }
3303
- if (!fs2.exists(resolved)) {
3305
+ if (!fs6.exists(resolved)) {
3304
3306
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "missing" };
3305
3307
  }
3306
- if (!fs2.isFile(resolved)) {
3308
+ if (!fs6.isFile(resolved)) {
3307
3309
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "not-file" };
3308
3310
  }
3309
- const size = fs2.size(resolved);
3311
+ const size = fs6.size(resolved);
3310
3312
  if (size > maxBytes) {
3311
3313
  return { token: `@${rawPath}`, path: rawPath, ok: false, skip: "too-large", bytes: size };
3312
3314
  }
3313
3315
  return { token: `@${rawPath}`, path: rawPath, ok: true, bytes: size };
3314
3316
  }
3315
- function readSafe(root, rawPath, fs2) {
3317
+ function readSafe(root, rawPath, fs6) {
3316
3318
  const resolved = resolve(root, rawPath);
3317
3319
  try {
3318
- return fs2.read(resolved);
3320
+ return fs6.read(resolved);
3319
3321
  } catch {
3320
3322
  return "(read failed)";
3321
3323
  }
@@ -3513,9 +3515,9 @@ var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3513
3515
  ".pyo"
3514
3516
  ]);
3515
3517
  function isLikelyBinaryByName(name) {
3516
- const dot = name.lastIndexOf(".");
3517
- if (dot < 0) return false;
3518
- return BINARY_EXTENSIONS.has(name.slice(dot).toLowerCase());
3518
+ const dot2 = name.lastIndexOf(".");
3519
+ if (dot2 < 0) return false;
3520
+ return BINARY_EXTENSIONS.has(name.slice(dot2).toLowerCase());
3519
3521
  }
3520
3522
  function registerFilesystemTools(registry, opts) {
3521
3523
  const rootDir = pathMod.resolve(opts.rootDir);
@@ -4077,7 +4079,7 @@ function registerMemoryTools(registry, opts = {}) {
4077
4079
  });
4078
4080
  }
4079
4081
  try {
4080
- const path = store.write({
4082
+ const path5 = store.write({
4081
4083
  name: args.name,
4082
4084
  type: args.type,
4083
4085
  scope: args.scope,
@@ -4090,7 +4092,7 @@ function registerMemoryTools(registry, opts = {}) {
4090
4092
  "",
4091
4093
  "TREAT THIS AS ESTABLISHED FACT for the rest of this session.",
4092
4094
  "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.)`
4095
+ `(Saved to ${path5}; pins into the system prompt on next /new or launch.)`
4094
4096
  ].join("\n");
4095
4097
  } catch (err) {
4096
4098
  return JSON.stringify({ error: `remember failed: ${err.message}` });
@@ -4568,7 +4570,11 @@ async function spawnSubagent(opts) {
4568
4570
  stream: false
4569
4571
  });
4570
4572
  const onParentAbort = () => childLoop.abort();
4571
- opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4573
+ if (opts.parentSignal?.aborted) {
4574
+ childLoop.abort();
4575
+ } else {
4576
+ opts.parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4577
+ }
4572
4578
  let final = "";
4573
4579
  let errorMessage;
4574
4580
  let toolIter = 0;
@@ -4586,7 +4592,11 @@ async function spawnSubagent(opts) {
4586
4592
  });
4587
4593
  }
4588
4594
  if (ev.role === "assistant_final") {
4589
- final = ev.content ?? "";
4595
+ if (ev.forcedSummary) {
4596
+ errorMessage = ev.content?.trim() || "subagent ended without producing an answer";
4597
+ } else {
4598
+ final = ev.content ?? "";
4599
+ }
4590
4600
  }
4591
4601
  if (ev.role === "error") {
4592
4602
  errorMessage = ev.error ?? "subagent error";
@@ -4635,12 +4645,12 @@ async function spawnSubagent(opts) {
4635
4645
  }
4636
4646
  function aggregateChildUsage(loop2) {
4637
4647
  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;
4648
+ for (const t2 of loop2.stats.turns) {
4649
+ agg.promptTokens += t2.usage.promptTokens;
4650
+ agg.completionTokens += t2.usage.completionTokens;
4651
+ agg.totalTokens += t2.usage.totalTokens;
4652
+ agg.promptCacheHitTokens += t2.usage.promptCacheHitTokens;
4653
+ agg.promptCacheMissTokens += t2.usage.promptCacheMissTokens;
4644
4654
  }
4645
4655
  return agg;
4646
4656
  }
@@ -5158,7 +5168,7 @@ async function runCommand(cmd, opts) {
5158
5168
  };
5159
5169
  const { bin, args, spawnOverrides } = prepareSpawn(argv);
5160
5170
  const effectiveSpawnOpts = { ...spawnOpts, ...spawnOverrides };
5161
- return await new Promise((resolve8, reject) => {
5171
+ return await new Promise((resolve9, reject) => {
5162
5172
  let child;
5163
5173
  try {
5164
5174
  child = spawn3(bin, args, effectiveSpawnOpts);
@@ -5191,7 +5201,7 @@ async function runCommand(cmd, opts) {
5191
5201
  const output = buf.length > maxChars ? `${buf.slice(0, maxChars)}
5192
5202
 
5193
5203
  [\u2026 truncated ${buf.length - maxChars} chars \u2026]` : buf;
5194
- resolve8({ exitCode: code, output, timedOut });
5204
+ resolve9({ exitCode: code, output, timedOut });
5195
5205
  });
5196
5206
  });
5197
5207
  }
@@ -5664,10 +5674,10 @@ ${i + 1}. ${r.title}`);
5664
5674
  // src/env.ts
5665
5675
  import { readFileSync as readFileSync6 } from "fs";
5666
5676
  import { resolve as resolve5 } from "path";
5667
- function loadDotenv(path = ".env") {
5677
+ function loadDotenv(path5 = ".env") {
5668
5678
  let raw;
5669
5679
  try {
5670
- raw = readFileSync6(resolve5(process.cwd(), path), "utf8");
5680
+ raw = readFileSync6(resolve5(process.cwd(), path5), "utf8");
5671
5681
  } catch {
5672
5682
  return;
5673
5683
  }
@@ -5731,13 +5741,13 @@ function writeMeta(stream, meta) {
5731
5741
  stream.write(`${JSON.stringify(line)}
5732
5742
  `);
5733
5743
  }
5734
- function openTranscriptFile(path, meta) {
5735
- const stream = createWriteStream(path, { flags: "a" });
5744
+ function openTranscriptFile(path5, meta) {
5745
+ const stream = createWriteStream(path5, { flags: "a" });
5736
5746
  writeMeta(stream, meta);
5737
5747
  return stream;
5738
5748
  }
5739
- function readTranscript(path) {
5740
- const raw = readFileSync7(path, "utf8");
5749
+ function readTranscript(path5) {
5750
+ const raw = readFileSync7(path5, "utf8");
5741
5751
  return parseTranscript(raw);
5742
5752
  }
5743
5753
  function isPlanStateEmptyShape(s) {
@@ -5786,8 +5796,8 @@ function computeCumulativeStats(pages, upToIdx) {
5786
5796
  }
5787
5797
  return computeReplayStats(flat);
5788
5798
  }
5789
- function replayFromFile(path) {
5790
- const parsed = readTranscript(path);
5799
+ function replayFromFile(path5) {
5800
+ const parsed = readTranscript(path5);
5791
5801
  return { parsed, stats: computeReplayStats(parsed.records) };
5792
5802
  }
5793
5803
  function computeReplayStats(records) {
@@ -5844,15 +5854,15 @@ function computeReplayStats(records) {
5844
5854
  };
5845
5855
  }
5846
5856
  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);
5857
+ const totalCost = turns.reduce((s, t2) => s + t2.cost, 0);
5858
+ const totalInput = turns.reduce((s, t2) => s + inputCostUsd(t2.model, t2.usage), 0);
5859
+ const totalOutput = turns.reduce((s, t2) => s + outputCostUsd(t2.model, t2.usage), 0);
5860
+ const totalClaude = turns.reduce((s, t2) => s + claudeEquivalentCost(t2.usage), 0);
5851
5861
  let hit = 0;
5852
5862
  let miss = 0;
5853
- for (const t of turns) {
5854
- hit += t.usage.promptCacheHitTokens;
5855
- miss += t.usage.promptCacheMissTokens;
5863
+ for (const t2 of turns) {
5864
+ hit += t2.usage.promptCacheHitTokens;
5865
+ miss += t2.usage.promptCacheMissTokens;
5856
5866
  }
5857
5867
  const cacheHitRatio = hit + miss > 0 ? hit / (hit + miss) : 0;
5858
5868
  const savingsVsClaude = totalClaude > 0 ? 1 - totalCost / totalClaude : 0;
@@ -5929,8 +5939,8 @@ function diffTranscripts(a, b) {
5929
5939
  return { a: aSide, b: bSide, pairs, firstDivergenceTurn };
5930
5940
  }
5931
5941
  function classifyDivergence(a, b, aTools, bTools) {
5932
- const aNames = aTools.map((t) => t.tool ?? "").sort();
5933
- const bNames = bTools.map((t) => t.tool ?? "").sort();
5942
+ const aNames = aTools.map((t2) => t2.tool ?? "").sort();
5943
+ const bNames = bTools.map((t2) => t2.tool ?? "").sort();
5934
5944
  if (aNames.join(",") !== bNames.join(",")) {
5935
5945
  return `tool calls differ: A=[${aNames.join(",") || "\u2014"}] B=[${bNames.join(",") || "\u2014"}]`;
5936
5946
  }
@@ -5960,7 +5970,7 @@ function tokenOverlap(a, b) {
5960
5970
  const tb = new Set(b.toLowerCase().split(/\s+/).filter(Boolean));
5961
5971
  if (ta.size === 0 && tb.size === 0) return 1;
5962
5972
  let shared = 0;
5963
- for (const t of ta) if (tb.has(t)) shared++;
5973
+ for (const t2 of ta) if (tb.has(t2)) shared++;
5964
5974
  return 2 * shared / (ta.size + tb.size);
5965
5975
  }
5966
5976
  function levenshtein(a, b) {
@@ -6153,8 +6163,8 @@ function renderMarkdown(report) {
6153
6163
  out.push(`| turn | kind | ${a.label} tool calls | ${b.label} tool calls | note |`);
6154
6164
  out.push("|---:|:---:|---|---|---|");
6155
6165
  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";
6166
+ const aTools = p.aTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6167
+ const bTools = p.bTools.map((t2) => t2.tool).filter(Boolean).join(", ") || "\u2014";
6158
6168
  out.push(`| ${p.turn} | ${p.kind} | ${aTools} | ${bTools} | ${p.divergenceNote ?? ""} |`);
6159
6169
  }
6160
6170
  out.push("");
@@ -6381,7 +6391,7 @@ var McpClient = class {
6381
6391
  const id = this.nextId++;
6382
6392
  const frame = { jsonrpc: "2.0", id, method, params };
6383
6393
  let abortHandler = null;
6384
- const promise = new Promise((resolve8, reject) => {
6394
+ const promise = new Promise((resolve9, reject) => {
6385
6395
  const timeout = setTimeout(() => {
6386
6396
  this.pending.delete(id);
6387
6397
  if (abortHandler && signal) signal.removeEventListener("abort", abortHandler);
@@ -6390,7 +6400,7 @@ var McpClient = class {
6390
6400
  );
6391
6401
  }, this.requestTimeoutMs);
6392
6402
  this.pending.set(id, {
6393
- resolve: resolve8,
6403
+ resolve: resolve9,
6394
6404
  reject,
6395
6405
  timeout
6396
6406
  });
@@ -6513,12 +6523,12 @@ var StdioTransport = class {
6513
6523
  }
6514
6524
  async send(message) {
6515
6525
  if (this.closed) throw new Error("MCP transport is closed");
6516
- return new Promise((resolve8, reject) => {
6526
+ return new Promise((resolve9, reject) => {
6517
6527
  const line = `${JSON.stringify(message)}
6518
6528
  `;
6519
6529
  this.child.stdin.write(line, "utf8", (err) => {
6520
6530
  if (err) reject(err);
6521
- else resolve8();
6531
+ else resolve9();
6522
6532
  });
6523
6533
  });
6524
6534
  }
@@ -6529,8 +6539,8 @@ var StdioTransport = class {
6529
6539
  continue;
6530
6540
  }
6531
6541
  if (this.closed) return;
6532
- const next = await new Promise((resolve8) => {
6533
- this.waiters.push(resolve8);
6542
+ const next = await new Promise((resolve9) => {
6543
+ this.waiters.push(resolve9);
6534
6544
  });
6535
6545
  if (next === null) return;
6536
6546
  yield next;
@@ -6596,8 +6606,8 @@ var SseTransport = class {
6596
6606
  constructor(opts) {
6597
6607
  this.url = opts.url;
6598
6608
  this.headers = opts.headers ?? {};
6599
- this.endpointReady = new Promise((resolve8, reject) => {
6600
- this.resolveEndpoint = resolve8;
6609
+ this.endpointReady = new Promise((resolve9, reject) => {
6610
+ this.resolveEndpoint = resolve9;
6601
6611
  this.rejectEndpoint = reject;
6602
6612
  });
6603
6613
  this.endpointReady.catch(() => void 0);
@@ -6624,8 +6634,8 @@ var SseTransport = class {
6624
6634
  continue;
6625
6635
  }
6626
6636
  if (this.closed) return;
6627
- const next = await new Promise((resolve8) => {
6628
- this.waiters.push(resolve8);
6637
+ const next = await new Promise((resolve9) => {
6638
+ this.waiters.push(resolve9);
6629
6639
  });
6630
6640
  if (next === null) return;
6631
6641
  yield next;
@@ -6893,8 +6903,8 @@ function applyEditBlock(block, rootDir) {
6893
6903
  function applyEditBlocks(blocks, rootDir) {
6894
6904
  return blocks.map((b) => applyEditBlock(b, rootDir));
6895
6905
  }
6896
- function toWholeFileEditBlock(path, content, rootDir) {
6897
- const abs = resolve6(rootDir, path);
6906
+ function toWholeFileEditBlock(path5, content, rootDir) {
6907
+ const abs = resolve6(rootDir, path5);
6898
6908
  let search = "";
6899
6909
  if (existsSync6(abs)) {
6900
6910
  try {
@@ -6903,7 +6913,7 @@ function toWholeFileEditBlock(path, content, rootDir) {
6903
6913
  search = "";
6904
6914
  }
6905
6915
  }
6906
- return { path, search, replace: content, offset: 0 };
6916
+ return { path: path5, search, replace: content, offset: 0 };
6907
6917
  }
6908
6918
  function snapshotBeforeEdits(blocks, rootDir) {
6909
6919
  const absRoot = resolve6(rootDir);
@@ -7082,20 +7092,20 @@ function appendUsage(input) {
7082
7092
  };
7083
7093
  if (input.kind === "subagent") record.kind = "subagent";
7084
7094
  if (input.subagent) record.subagent = input.subagent;
7085
- const path = input.path ?? defaultUsageLogPath();
7095
+ const path5 = input.path ?? defaultUsageLogPath();
7086
7096
  try {
7087
- mkdirSync5(dirname7(path), { recursive: true });
7088
- appendFileSync2(path, `${JSON.stringify(record)}
7097
+ mkdirSync5(dirname7(path5), { recursive: true });
7098
+ appendFileSync2(path5, `${JSON.stringify(record)}
7089
7099
  `, "utf8");
7090
7100
  } catch {
7091
7101
  }
7092
7102
  return record;
7093
7103
  }
7094
- function readUsageLog(path = defaultUsageLogPath()) {
7095
- if (!existsSync8(path)) return [];
7104
+ function readUsageLog(path5 = defaultUsageLogPath()) {
7105
+ if (!existsSync8(path5)) return [];
7096
7106
  let raw;
7097
7107
  try {
7098
- raw = readFileSync10(path, "utf8");
7108
+ raw = readFileSync10(path5, "utf8");
7099
7109
  } catch {
7100
7110
  return [];
7101
7111
  }
@@ -7199,10 +7209,10 @@ function aggregateUsage(records, opts = {}) {
7199
7209
  subagents
7200
7210
  };
7201
7211
  }
7202
- function formatLogSize(path = defaultUsageLogPath()) {
7203
- if (!existsSync8(path)) return "";
7212
+ function formatLogSize(path5 = defaultUsageLogPath()) {
7213
+ if (!existsSync8(path5)) return "";
7204
7214
  try {
7205
- const s = statSync4(path);
7215
+ const s = statSync4(path5);
7206
7216
  const bytes = s.size;
7207
7217
  if (bytes < 1024) return `${bytes} B`;
7208
7218
  if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
@@ -7229,24 +7239,24 @@ function pendingEditsPath(sessionName) {
7229
7239
  }
7230
7240
  function savePendingEdits(sessionName, blocks) {
7231
7241
  if (!sessionName) return;
7232
- const path = pendingEditsPath(sessionName);
7242
+ const path5 = pendingEditsPath(sessionName);
7233
7243
  try {
7234
7244
  if (blocks.length === 0) {
7235
- if (existsSync9(path)) unlinkSync3(path);
7245
+ if (existsSync9(path5)) unlinkSync3(path5);
7236
7246
  return;
7237
7247
  }
7238
- mkdirSync6(dirname8(path), { recursive: true });
7239
- writeFileSync5(path, JSON.stringify(blocks, null, 2), "utf8");
7248
+ mkdirSync6(dirname8(path5), { recursive: true });
7249
+ writeFileSync5(path5, JSON.stringify(blocks, null, 2), "utf8");
7240
7250
  } catch {
7241
7251
  }
7242
7252
  }
7243
7253
  function loadPendingEdits(sessionName) {
7244
7254
  if (!sessionName) return null;
7245
- const path = pendingEditsPath(sessionName);
7246
- if (!existsSync9(path)) return null;
7255
+ const path5 = pendingEditsPath(sessionName);
7256
+ if (!existsSync9(path5)) return null;
7247
7257
  let raw;
7248
7258
  try {
7249
- raw = readFileSync11(path, "utf8");
7259
+ raw = readFileSync11(path5, "utf8");
7250
7260
  } catch {
7251
7261
  return null;
7252
7262
  }
@@ -7266,9 +7276,9 @@ function loadPendingEdits(sessionName) {
7266
7276
  }
7267
7277
  function clearPendingEdits(sessionName) {
7268
7278
  if (!sessionName) return;
7269
- const path = pendingEditsPath(sessionName);
7279
+ const path5 = pendingEditsPath(sessionName);
7270
7280
  try {
7271
- if (existsSync9(path)) unlinkSync3(path);
7281
+ if (existsSync9(path5)) unlinkSync3(path5);
7272
7282
  } catch {
7273
7283
  }
7274
7284
  }
@@ -7289,10 +7299,10 @@ function planStatePath(sessionName) {
7289
7299
  return join10(sessionsDir(), `${sanitizeName(sessionName)}.plan.json`);
7290
7300
  }
7291
7301
  function loadPlanState(sessionName) {
7292
- const path = planStatePath(sessionName);
7293
- if (!existsSync10(path)) return null;
7302
+ const path5 = planStatePath(sessionName);
7303
+ if (!existsSync10(path5)) return null;
7294
7304
  try {
7295
- const raw = readFileSync12(path, "utf8");
7305
+ const raw = readFileSync12(path5, "utf8");
7296
7306
  const parsed = JSON.parse(raw);
7297
7307
  if (!parsed || typeof parsed !== "object") return null;
7298
7308
  if (parsed.version !== 1) return null;
@@ -7328,9 +7338,9 @@ function loadPlanState(sessionName) {
7328
7338
  }
7329
7339
  }
7330
7340
  function savePlanState(sessionName, steps, completedStepIds, extras) {
7331
- const path = planStatePath(sessionName);
7341
+ const path5 = planStatePath(sessionName);
7332
7342
  try {
7333
- mkdirSync7(dirname9(path), { recursive: true });
7343
+ mkdirSync7(dirname9(path5), { recursive: true });
7334
7344
  const state = {
7335
7345
  version: 1,
7336
7346
  steps,
@@ -7339,7 +7349,7 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7339
7349
  };
7340
7350
  if (extras?.body) state.body = extras.body;
7341
7351
  if (extras?.summary) state.summary = extras.summary;
7342
- writeFileSync6(path, `${JSON.stringify(state, null, 2)}
7352
+ writeFileSync6(path5, `${JSON.stringify(state, null, 2)}
7343
7353
  `, "utf8");
7344
7354
  } catch (err) {
7345
7355
  process.stderr.write(
@@ -7349,9 +7359,9 @@ function savePlanState(sessionName, steps, completedStepIds, extras) {
7349
7359
  }
7350
7360
  }
7351
7361
  function clearPlanState(sessionName) {
7352
- const path = planStatePath(sessionName);
7362
+ const path5 = planStatePath(sessionName);
7353
7363
  try {
7354
- if (existsSync10(path)) unlinkSync4(path);
7364
+ if (existsSync10(path5)) unlinkSync4(path5);
7355
7365
  } catch {
7356
7366
  }
7357
7367
  }
@@ -7419,9 +7429,9 @@ function listPlanArchives(sessionName) {
7419
7429
  return summaries;
7420
7430
  }
7421
7431
  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);
7432
+ const t2 = Date.parse(updatedAt);
7433
+ if (Number.isNaN(t2)) return updatedAt;
7434
+ const diffMs = Math.max(0, now - t2);
7425
7435
  const sec = Math.floor(diffMs / 1e3);
7426
7436
  if (sec < 60) return `${sec}s ago`;
7427
7437
  const min = Math.floor(sec / 60);
@@ -7466,7 +7476,7 @@ function registerSkillTools(registry, opts = {}) {
7466
7476
  }
7467
7477
  const stripped = raw.replace(/\[[^\]]*\]/g, " ").trim();
7468
7478
  const tokens = stripped.split(/\s+/).filter(Boolean);
7469
- const name = tokens.find((t) => /^[a-zA-Z0-9]/.test(t)) ?? "";
7479
+ const name = tokens.find((t2) => /^[a-zA-Z0-9]/.test(t2)) ?? "";
7470
7480
  if (!name) {
7471
7481
  return JSON.stringify({
7472
7482
  error: "run_skill requires a 'name' argument",
@@ -7529,12 +7539,12 @@ function AtMentionSuggestions({
7529
7539
  const shown = matches.slice(windowStart, windowStart + MAX);
7530
7540
  const hiddenAbove = windowStart;
7531
7541
  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"));
7542
+ 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
7543
  }
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;
7544
+ function FileRow({ path: path5, isSelected }) {
7545
+ const slash = path5.lastIndexOf("/");
7546
+ const dir = slash >= 0 ? `${path5.slice(0, slash)}/` : "";
7547
+ const base = slash >= 0 ? path5.slice(slash + 1) : path5;
7538
7548
  if (isSelected) {
7539
7549
  return /* @__PURE__ */ React.createElement(Box, null, /* @__PURE__ */ React.createElement(Text, { backgroundColor: "#67e8f9", color: "black", bold: true }, ` \u25B8 ${base}${dir ? ` ${dir}` : ""} `));
7540
7550
  }
@@ -7555,8 +7565,8 @@ function ModalCard({
7555
7565
  icon,
7556
7566
  children
7557
7567
  }) {
7558
- const { stdout: stdout2 } = useStdout();
7559
- const cols = stdout2?.columns ?? 80;
7568
+ const { stdout: stdout3 } = useStdout();
7569
+ const cols = stdout3?.columns ?? 80;
7560
7570
  const ruleWidth = Math.min(80, Math.max(28, cols - 4));
7561
7571
  const titleText = icon ? ` ${icon} ${title} ` : ` ${title} `;
7562
7572
  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 +8129,8 @@ function capLines(lines, maxLines, indent) {
8119
8129
  var MODAL_OVERHEAD_ROWS = 18;
8120
8130
  var MIN_DIFF_ROWS = 8;
8121
8131
  function EditConfirm({ block, onChoose }) {
8122
- const { stdout: stdout2 } = useStdout2();
8123
- const rows = stdout2?.rows ?? 40;
8132
+ const { stdout: stdout3 } = useStdout2();
8133
+ const rows = stdout3?.rows ?? 40;
8124
8134
  const budget = Math.max(MIN_DIFF_ROWS, rows - MODAL_OVERHEAD_ROWS);
8125
8135
  const allLines = useMemo(
8126
8136
  () => formatEditBlockDiff(block, { contextLines: 2, maxLines: 1e5, indent: " " }),
@@ -9110,10 +9120,10 @@ function gradientCells(width, glyph = GLYPH.block) {
9110
9120
  if (width <= 0) return cells;
9111
9121
  const last = GRADIENT.length - 1;
9112
9122
  for (let i = 0; i < width; i++) {
9113
- const t = width === 1 ? 0 : i * last / (width - 1);
9114
- const lo = Math.floor(t);
9123
+ const t2 = width === 1 ? 0 : i * last / (width - 1);
9124
+ const lo = Math.floor(t2);
9115
9125
  const hi = Math.min(last, lo + 1);
9116
- const color = t - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9126
+ const color = t2 - lo < 0.5 ? GRADIENT[lo] : GRADIENT[hi];
9117
9127
  cells.push({ ch: glyph, color });
9118
9128
  }
9119
9129
  return cells;
@@ -9127,7 +9137,7 @@ function TickerProvider({ children, disabled }) {
9127
9137
  const [tick, setTick] = useState3(0);
9128
9138
  useEffect2(() => {
9129
9139
  if (disabled) return;
9130
- const id = setInterval(() => setTick((t) => t + 1), TICK_MS);
9140
+ const id = setInterval(() => setTick((t2) => t2 + 1), TICK_MS);
9131
9141
  return () => clearInterval(id);
9132
9142
  }, [disabled]);
9133
9143
  return /* @__PURE__ */ React10.createElement(TickContext.Provider, { value: tick }, children);
@@ -9416,8 +9426,8 @@ var EventRow = React11.memo(function EventRow2({
9416
9426
  return /* @__PURE__ */ React11.createElement(Box9, null, /* @__PURE__ */ React11.createElement(Text8, null, event.text));
9417
9427
  });
9418
9428
  function TurnSeparator() {
9419
- const { stdout: stdout2 } = useStdout3();
9420
- const cols = stdout2?.columns ?? 80;
9429
+ const { stdout: stdout3 } = useStdout3();
9430
+ const cols = stdout3?.columns ?? 80;
9421
9431
  const width = Math.max(16, cols - 2);
9422
9432
  const sideWidth = Math.max(2, Math.floor((width - 5) / 2));
9423
9433
  const leftCells = gradientCells(sideWidth, "\u2500");
@@ -9448,10 +9458,10 @@ function EditFileDiff({ text }) {
9448
9458
  function BranchBlock({ branch: branch2 }) {
9449
9459
  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
9460
  const chosen = i === branch2.chosenIndex;
9451
- const t = (branch2.temperatures[i] ?? 0).toFixed(1);
9461
+ const t2 = (branch2.temperatures[i] ?? 0).toFixed(1);
9452
9462
  return (
9453
9463
  // 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} `))
9464
+ /* @__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
9465
  );
9456
9466
  })));
9457
9467
  }
@@ -9575,8 +9585,8 @@ function ModeStatusBar({
9575
9585
  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
9586
  }
9577
9587
  function ModeBarFrame({ children }) {
9578
- const { stdout: stdout2 } = useStdout4();
9579
- const cols = stdout2?.columns ?? 80;
9588
+ const { stdout: stdout3 } = useStdout4();
9589
+ const cols = stdout3?.columns ?? 80;
9580
9590
  const ruleWidth = Math.max(20, cols - 2);
9581
9591
  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
9592
  }
@@ -9635,26 +9645,26 @@ function summarizeToolArgs(name, args) {
9635
9645
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9636
9646
  }
9637
9647
  const hasSuffix = (s) => name === s || name.endsWith(`_${s}`);
9638
- const path = typeof parsed.path === "string" ? parsed.path : void 0;
9648
+ const path5 = typeof parsed.path === "string" ? parsed.path : void 0;
9639
9649
  if (hasSuffix("read_file")) {
9640
9650
  const head = typeof parsed.head === "number" ? `, head=${parsed.head}` : "";
9641
9651
  const tail = typeof parsed.tail === "number" ? `, tail=${parsed.tail}` : "";
9642
- return `path: ${path ?? "?"}${head}${tail}`;
9652
+ return `path: ${path5 ?? "?"}${head}${tail}`;
9643
9653
  }
9644
9654
  if (hasSuffix("write_file")) {
9645
9655
  const content = typeof parsed.content === "string" ? parsed.content : "";
9646
- return `path: ${path ?? "?"} (${content.length} chars)`;
9656
+ return `path: ${path5 ?? "?"} (${content.length} chars)`;
9647
9657
  }
9648
9658
  if (hasSuffix("edit_file")) {
9649
9659
  const edits = Array.isArray(parsed.edits) ? parsed.edits.length : 0;
9650
- return `path: ${path ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9660
+ return `path: ${path5 ?? "?"} (${edits} edit${edits === 1 ? "" : "s"})`;
9651
9661
  }
9652
9662
  if (hasSuffix("list_directory") || hasSuffix("directory_tree")) {
9653
- return `path: ${path ?? "?"}`;
9663
+ return `path: ${path5 ?? "?"}`;
9654
9664
  }
9655
9665
  if (hasSuffix("search_files")) {
9656
9666
  const pattern = typeof parsed.pattern === "string" ? parsed.pattern : "?";
9657
- return `path: ${path ?? "?"} \xB7 pattern: ${pattern}`;
9667
+ return `path: ${path5 ?? "?"} \xB7 pattern: ${pattern}`;
9658
9668
  }
9659
9669
  if (hasSuffix("move_file")) {
9660
9670
  const src = typeof parsed.source === "string" ? parsed.source : "?";
@@ -9662,7 +9672,7 @@ function summarizeToolArgs(name, args) {
9662
9672
  return `${src} \u2192 ${dst}`;
9663
9673
  }
9664
9674
  if (hasSuffix("get_file_info")) {
9665
- return `path: ${path ?? "?"}`;
9675
+ return `path: ${path5 ?? "?"}`;
9666
9676
  }
9667
9677
  return args.length > 80 ? `${args.slice(0, 80)}\u2026` : args;
9668
9678
  }
@@ -10377,8 +10387,8 @@ function PromptInput({
10377
10387
  if (action.historyHandoff === "prev") onHistoryPrev?.();
10378
10388
  if (action.historyHandoff === "next") onHistoryNext?.();
10379
10389
  }, !disabled);
10380
- const { stdout: stdout2 } = useStdout5();
10381
- const cols = stdout2?.columns ?? 80;
10390
+ const { stdout: stdout3 } = useStdout5();
10391
+ const cols = stdout3?.columns ?? 80;
10382
10392
  const narrow = cols <= 90;
10383
10393
  const promptBody = narrow ? "\u203A " : "you \u203A ";
10384
10394
  const promptPrefix = BAR + promptBody;
@@ -10749,8 +10759,8 @@ function StatsPanel({
10749
10759
  const branchOn = (branchBudget ?? 1) > 1;
10750
10760
  const ctxMax = DEEPSEEK_CONTEXT_TOKENS[model2] ?? DEFAULT_CONTEXT_TOKENS;
10751
10761
  const ctxRatio = summary.lastPromptTokens / ctxMax;
10752
- const { stdout: stdout2 } = useStdout6();
10753
- const columns = stdout2?.columns ?? 80;
10762
+ const { stdout: stdout3 } = useStdout6();
10763
+ const columns = stdout3?.columns ?? 80;
10754
10764
  const narrow = columns < NARROW_BREAKPOINT;
10755
10765
  const coldStart = summary.turns <= COLD_START_TURNS;
10756
10766
  return /* @__PURE__ */ React21.createElement(Box19, { flexDirection: "column", paddingX: 1, marginBottom: 1 }, /* @__PURE__ */ React21.createElement(
@@ -10916,8 +10926,8 @@ function formatTokens(n) {
10916
10926
  import { Box as Box20, Text as Text18, useStdout as useStdout7 } from "ink";
10917
10927
  import React22 from "react";
10918
10928
  function WelcomeBanner({ inCodeMode }) {
10919
- const { stdout: stdout2 } = useStdout7();
10920
- const cols = stdout2?.columns ?? 80;
10929
+ const { stdout: stdout3 } = useStdout7();
10930
+ const cols = stdout3?.columns ?? 80;
10921
10931
  const ruleWidth = Math.min(60, Math.max(28, cols - 4));
10922
10932
  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
10933
  }
@@ -10978,7 +10988,7 @@ function parseEditIndices(raw, max) {
10978
10988
  if (!trimmed) return { ok: [] };
10979
10989
  if (max <= 0) return { error: "no pending edits to address" };
10980
10990
  const seen = /* @__PURE__ */ new Set();
10981
- const tokens = trimmed.split(",").map((t) => t.trim()).filter((t) => t.length > 0);
10991
+ const tokens = trimmed.split(",").map((t2) => t2.trim()).filter((t2) => t2.length > 0);
10982
10992
  if (tokens.length === 0) return { ok: [] };
10983
10993
  for (const tok of tokens) {
10984
10994
  const range = tok.match(/^(\d+)-(\d+)$/);
@@ -11072,24 +11082,24 @@ function globalMemoryPath(homeDir = homedir6()) {
11072
11082
  function appendGlobalMemory(note, homeDir) {
11073
11083
  return appendBulletToFile(globalMemoryPath(homeDir), note, GLOBAL_HEADER);
11074
11084
  }
11075
- function appendBulletToFile(path, note, newFileHeader) {
11085
+ function appendBulletToFile(path5, note, newFileHeader) {
11076
11086
  const trimmed = note.trim();
11077
11087
  if (!trimmed) throw new Error("note body cannot be empty");
11078
11088
  const bullet = `- ${trimmed}
11079
11089
  `;
11080
- if (!existsSync11(path)) {
11081
- mkdirSync8(dirname10(path), { recursive: true });
11082
- writeFileSync7(path, `${newFileHeader}${bullet}`, "utf8");
11083
- return { path, created: true };
11090
+ if (!existsSync11(path5)) {
11091
+ mkdirSync8(dirname10(path5), { recursive: true });
11092
+ writeFileSync7(path5, `${newFileHeader}${bullet}`, "utf8");
11093
+ return { path: path5, created: true };
11084
11094
  }
11085
11095
  let prefix = "";
11086
11096
  try {
11087
- const existing = readFileSync14(path, "utf8");
11097
+ const existing = readFileSync14(path5, "utf8");
11088
11098
  if (existing.length > 0 && !existing.endsWith("\n")) prefix = "\n";
11089
11099
  } catch {
11090
11100
  }
11091
- appendFileSync3(path, `${prefix}${bullet}`, "utf8");
11092
- return { path, created: false };
11101
+ appendFileSync3(path5, `${prefix}${bullet}`, "utf8");
11102
+ return { path: path5, created: false };
11093
11103
  }
11094
11104
 
11095
11105
  // src/cli/ui/loop.ts
@@ -11454,6 +11464,10 @@ var SLASH_COMMANDS = [
11454
11464
  { cmd: "sessions", summary: "list saved sessions (current marked with \u25B8)" },
11455
11465
  { cmd: "forget", summary: "delete the current session from disk" },
11456
11466
  { cmd: "setup", summary: "reminds you to exit and run `reasonix setup`" },
11467
+ {
11468
+ cmd: "semantic",
11469
+ summary: "show semantic_search status \u2014 built? Ollama installed? how to enable"
11470
+ },
11457
11471
  { cmd: "clear", summary: "clear visible scrollback only (log/context kept)" },
11458
11472
  { cmd: "new", summary: "start a fresh conversation (clear context + scrollback)" },
11459
11473
  {
@@ -11576,12 +11590,12 @@ function statsCommand(opts) {
11576
11590
  }
11577
11591
  dashboard(opts);
11578
11592
  }
11579
- function transcriptSummary(path) {
11580
- if (!existsSync12(path)) {
11581
- console.error(`no such transcript: ${path}`);
11593
+ function transcriptSummary(path5) {
11594
+ if (!existsSync12(path5)) {
11595
+ console.error(`no such transcript: ${path5}`);
11582
11596
  process.exit(1);
11583
11597
  }
11584
- const lines = readFileSync15(path, "utf8").split(/\r?\n/).filter(Boolean);
11598
+ const lines = readFileSync15(path5, "utf8").split(/\r?\n/).filter(Boolean);
11585
11599
  let assistantTurns = 0;
11586
11600
  let toolCalls = 0;
11587
11601
  let lastTurn = 0;
@@ -11594,25 +11608,25 @@ function transcriptSummary(path) {
11594
11608
  } catch {
11595
11609
  }
11596
11610
  }
11597
- console.log(`transcript: ${path}`);
11611
+ console.log(`transcript: ${path5}`);
11598
11612
  console.log(`assistant turns: ${assistantTurns}`);
11599
11613
  console.log(`tool invocations: ${toolCalls}`);
11600
11614
  console.log(`last turn index: ${lastTurn}`);
11601
11615
  }
11602
11616
  function dashboard(opts) {
11603
- const path = opts.logPath ?? defaultUsageLogPath();
11604
- const records = readUsageLog(path);
11617
+ const path5 = opts.logPath ?? defaultUsageLogPath();
11618
+ const records = readUsageLog(path5);
11605
11619
  if (records.length === 0) {
11606
11620
  console.log("no usage data yet.");
11607
11621
  console.log("");
11608
- console.log(` ${path}`);
11622
+ console.log(` ${path5}`);
11609
11623
  console.log("");
11610
11624
  console.log("run `reasonix chat`, `reasonix code`, or `reasonix run <task>` \u2014 every turn");
11611
11625
  console.log("appends one line to the log and `reasonix stats` will roll it up.");
11612
11626
  return;
11613
11627
  }
11614
11628
  const agg = aggregateUsage(records, { now: opts.now });
11615
- console.log(renderDashboard(agg, path));
11629
+ console.log(renderDashboard(agg, path5));
11616
11630
  }
11617
11631
  function renderDashboard(agg, logPath) {
11618
11632
  const lines = [];
@@ -11784,14 +11798,14 @@ var update = (_args, _loop, ctx) => {
11784
11798
  return { info: lines.join("\n") };
11785
11799
  };
11786
11800
  var stats = () => {
11787
- const path = defaultUsageLogPath();
11788
- const records = readUsageLog(path);
11801
+ const path5 = defaultUsageLogPath();
11802
+ const records = readUsageLog(path5);
11789
11803
  if (records.length === 0) {
11790
11804
  return {
11791
11805
  info: [
11792
11806
  "no usage data yet.",
11793
11807
  "",
11794
- ` ${path}`,
11808
+ ` ${path5}`,
11795
11809
  "",
11796
11810
  "every turn you run here appends one record \u2014 this session's turns",
11797
11811
  "will show up in the dashboard once you send a message."
@@ -11799,7 +11813,7 @@ var stats = () => {
11799
11813
  };
11800
11814
  }
11801
11815
  const agg = aggregateUsage(records);
11802
- return { info: renderDashboard(agg, path) };
11816
+ return { info: renderDashboard(agg, path5) };
11803
11817
  };
11804
11818
  var handlers = {
11805
11819
  hook: hooks,
@@ -12101,8 +12115,8 @@ ${gitTail(commit2)}` };
12101
12115
  }
12102
12116
  function gitTail(res) {
12103
12117
  const stderr = res.stderr ?? "";
12104
- const stdout2 = res.stdout ?? "";
12105
- const body = stderr.trim() || stdout2.trim();
12118
+ const stdout3 = res.stdout ?? "";
12119
+ const body = stderr.trim() || stdout3.trim();
12106
12120
  if (body) return body;
12107
12121
  if (res.error) return res.error.message;
12108
12122
  return "(no output from git)";
@@ -12361,7 +12375,7 @@ var mcp = (_args, loop2, ctx) => {
12361
12375
  }
12362
12376
  if (toolSpecs.length > 0) {
12363
12377
  lines.push(`Tools in registry (${toolSpecs.length}):`);
12364
- for (const t of toolSpecs) lines.push(` \xB7 ${t.function.name}`);
12378
+ for (const t2 of toolSpecs) lines.push(` \xB7 ${t2.function.name}`);
12365
12379
  }
12366
12380
  lines.push("");
12367
12381
  lines.push("To change this set, exit and run `reasonix setup`.");
@@ -12757,9 +12771,9 @@ var context = (_args, loop2) => {
12757
12771
  const top = [...toolBreakdown].sort((a, b) => b.tokens - a.tokens).slice(0, 5);
12758
12772
  lines.push("");
12759
12773
  lines.push(`Top tool results by cost (of ${toolBreakdown.length} total):`);
12760
- for (const t of top) {
12774
+ for (const t2 of top) {
12761
12775
  lines.push(
12762
- ` turn ${String(t.turn).padStart(3)} ${t.name.padEnd(22)} ${compactNum(t.tokens).padStart(8)} tokens`
12776
+ ` turn ${String(t2.turn).padStart(3)} ${t2.name.padEnd(22)} ${compactNum(t2.tokens).padStart(8)} tokens`
12763
12777
  );
12764
12778
  }
12765
12779
  }
@@ -12901,6 +12915,378 @@ var handlers9 = {
12901
12915
  replay
12902
12916
  };
12903
12917
 
12918
+ // src/cli/ui/slash/handlers/semantic.ts
12919
+ import { promises as fs2 } from "fs";
12920
+ import path from "path";
12921
+
12922
+ // src/index/semantic/embedding.ts
12923
+ var DEFAULT_OLLAMA_URL = "http://localhost:11434";
12924
+ var DEFAULT_EMBED_MODEL = "nomic-embed-text";
12925
+ var DEFAULT_TIMEOUT_MS = 3e4;
12926
+ var EmbeddingError = class extends Error {
12927
+ constructor(message, cause) {
12928
+ super(message);
12929
+ this.cause = cause;
12930
+ this.name = "EmbeddingError";
12931
+ }
12932
+ cause;
12933
+ };
12934
+ async function embed(text, opts = {}) {
12935
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
12936
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? DEFAULT_EMBED_MODEL;
12937
+ const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
12938
+ const controller = new AbortController();
12939
+ const onCallerAbort = () => controller.abort(opts.signal?.reason);
12940
+ if (opts.signal) {
12941
+ if (opts.signal.aborted) controller.abort(opts.signal.reason);
12942
+ else opts.signal.addEventListener("abort", onCallerAbort, { once: true });
12943
+ }
12944
+ const timer = setTimeout(() => controller.abort(new Error("embedding timeout")), timeoutMs);
12945
+ let res;
12946
+ try {
12947
+ res = await fetch(`${baseUrl}/api/embeddings`, {
12948
+ method: "POST",
12949
+ headers: { "content-type": "application/json" },
12950
+ body: JSON.stringify({ model: model2, prompt: text }),
12951
+ signal: controller.signal
12952
+ });
12953
+ } catch (err) {
12954
+ clearTimeout(timer);
12955
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
12956
+ const msg = err instanceof Error ? err.message : String(err);
12957
+ if (/ECONNREFUSED|connect ECONNREFUSED|fetch failed/i.test(msg)) {
12958
+ throw new EmbeddingError(
12959
+ `Cannot reach Ollama at ${baseUrl}. Install from https://ollama.com, then run \`ollama pull ${model2}\` and \`ollama serve\`. Override the URL via OLLAMA_URL.`,
12960
+ err
12961
+ );
12962
+ }
12963
+ throw new EmbeddingError(`embedding request failed: ${msg}`, err);
12964
+ } finally {
12965
+ clearTimeout(timer);
12966
+ if (opts.signal) opts.signal.removeEventListener("abort", onCallerAbort);
12967
+ }
12968
+ if (!res.ok) {
12969
+ const body = await res.text().catch(() => "");
12970
+ if (res.status === 404 && /model.*not found/i.test(body)) {
12971
+ throw new EmbeddingError(
12972
+ `Embedding model "${model2}" not pulled. Run \`ollama pull ${model2}\` once, then retry.`
12973
+ );
12974
+ }
12975
+ throw new EmbeddingError(`Ollama returned ${res.status}: ${body.slice(0, 200)}`);
12976
+ }
12977
+ const json = await res.json();
12978
+ if (!json.embedding || !Array.isArray(json.embedding)) {
12979
+ throw new EmbeddingError(`Ollama response missing 'embedding' array`);
12980
+ }
12981
+ const out = new Float32Array(json.embedding.length);
12982
+ for (let i = 0; i < json.embedding.length; i++) {
12983
+ const v = json.embedding[i];
12984
+ if (typeof v !== "number" || !Number.isFinite(v)) {
12985
+ throw new EmbeddingError(`embedding[${i}] is not a finite number`);
12986
+ }
12987
+ out[i] = v;
12988
+ }
12989
+ return out;
12990
+ }
12991
+ async function embedAll(texts, opts = {}) {
12992
+ const out = new Array(texts.length).fill(null);
12993
+ for (let i = 0; i < texts.length; i++) {
12994
+ if (opts.signal?.aborted) {
12995
+ throw new EmbeddingError("embedding aborted");
12996
+ }
12997
+ const text = texts[i];
12998
+ if (text === void 0) continue;
12999
+ try {
13000
+ out[i] = await embed(text, opts);
13001
+ } catch (err) {
13002
+ opts.onError?.(i, err);
13003
+ }
13004
+ opts.onProgress?.(i + 1, texts.length);
13005
+ }
13006
+ return out;
13007
+ }
13008
+ async function probeOllama(opts = {}) {
13009
+ const baseUrl = opts.baseUrl ?? process.env.OLLAMA_URL ?? DEFAULT_OLLAMA_URL;
13010
+ try {
13011
+ const res = await fetch(`${baseUrl}/api/tags`, { signal: opts.signal });
13012
+ if (!res.ok) return { ok: false, error: `Ollama returned ${res.status}` };
13013
+ const json = await res.json();
13014
+ const models2 = (json.models ?? []).map((m) => m.name).filter((n) => typeof n === "string");
13015
+ return { ok: true, models: models2 };
13016
+ } catch (err) {
13017
+ const msg = err instanceof Error ? err.message : String(err);
13018
+ return { ok: false, error: msg };
13019
+ }
13020
+ }
13021
+
13022
+ // src/index/semantic/i18n.ts
13023
+ var cachedLocale = null;
13024
+ function detectLocale() {
13025
+ if (cachedLocale) return cachedLocale;
13026
+ const override = (process.env.REASONIX_LANG ?? "").toLowerCase();
13027
+ if (override === "zh" || override === "en") {
13028
+ cachedLocale = override;
13029
+ return cachedLocale;
13030
+ }
13031
+ const env = process.env.LANG ?? process.env.LC_ALL ?? process.env.LC_MESSAGES ?? "";
13032
+ if (/^zh[-_]/i.test(env)) {
13033
+ cachedLocale = "zh";
13034
+ return "zh";
13035
+ }
13036
+ try {
13037
+ const sys = new Intl.DateTimeFormat().resolvedOptions().locale ?? "";
13038
+ if (/^zh[-_]/i.test(sys)) {
13039
+ cachedLocale = "zh";
13040
+ return "zh";
13041
+ }
13042
+ } catch {
13043
+ }
13044
+ cachedLocale = "en";
13045
+ return "en";
13046
+ }
13047
+ function t(key, vars = {}) {
13048
+ const loc = detectLocale();
13049
+ const dict = loc === "zh" ? ZH : EN;
13050
+ const tpl = dict[key] ?? EN[key];
13051
+ return tpl.replace(/\{(\w+)\}/g, (_m, name) => {
13052
+ const v = vars[name];
13053
+ return v === void 0 ? `{${name}}` : String(v);
13054
+ });
13055
+ }
13056
+ var EN = {
13057
+ // ── preflight ─────────────────────────────────────────────────────
13058
+ ollamaNotFound: "\u2717 `ollama` not found on PATH.\n Install from https://ollama.com (one-time, ~150 MB), then retry.\n",
13059
+ daemonNotReachableHint: "\u2717 Ollama daemon not reachable. Run `ollama serve` and retry, or pass --yes to start it automatically.\n",
13060
+ daemonStartConfirm: "Ollama daemon isn't running. Start `ollama serve` now?",
13061
+ daemonAbortStart: "\u2717 aborted \u2014 start `ollama serve` yourself and retry.\n",
13062
+ daemonStarting: "\u25B8 starting `ollama serve`\u2026\n",
13063
+ daemonStartTimeout: "\u2717 daemon didn't come up within 15s. Try `ollama serve` in a separate terminal and retry.\n",
13064
+ daemonReady: "\u2713 daemon up{pid}\n",
13065
+ modelNotPulledHint: '\u2717 embedding model "{model}" not pulled. Run `ollama pull {model}` and retry, or pass --yes to pull it automatically.\n',
13066
+ modelPullConfirm: `Embedding model "{model}" isn't pulled yet. Pull it now? (~274 MB for nomic-embed-text)`,
13067
+ modelAbortPull: "\u2717 aborted \u2014 pull the model yourself and retry.\n",
13068
+ modelPulling: "\u25B8 pulling {model}\u2026\n",
13069
+ modelPullFailed: "\u2717 `ollama pull {model}` failed (exit {code}).\n",
13070
+ modelPulled: "\u2713 {model} pulled\n",
13071
+ // ── progress ─────────────────────────────────────────────────────
13072
+ // The TTY-mode progress writer paints `<spinner> <status> <elapsed>s`
13073
+ // every 120ms. The status itself comes from one of these keys based
13074
+ // on the current phase. {files}, {done}, {total}, {pct} are
13075
+ // substituted by the writer.
13076
+ progressStarting: "starting\u2026",
13077
+ progressScan: "scanning project \xB7 {files} files",
13078
+ progressEmbed: "embedding {done}/{total} chunks \xB7 {pct}%",
13079
+ progressEmbedHeartbeat: " {done}/{total}\n",
13080
+ progressScanLine: "scanning files\u2026\n",
13081
+ progressEmbedLine: "embedding {total} chunks across {files} files\u2026\n",
13082
+ // Final result line after a successful build.
13083
+ indexSuccess: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed) in {seconds}s\n",
13084
+ indexSuccessWithSkips: "\u2713 indexed {scanned} files ({changed} changed, {added} new chunks, {removed} stale removed, {skipped} skipped due to embed errors) in {seconds}s\n",
13085
+ indexNothingToDo: " (nothing to do \u2014 re-run with --rebuild to force a full rebuild)\n",
13086
+ indexFailed: "\u2717 index failed: {msg}\n",
13087
+ // ── /semantic slash ──────────────────────────────────────────────
13088
+ slashHeader: "semantic_search status",
13089
+ slashEnabled: "\u2713 enabled \u2014 index built, tool registered.",
13090
+ slashEnabledDetail: " index size: {chunks} chunks across {files} files",
13091
+ slashEnabledHowto: " the model will call semantic_search automatically when it fits.",
13092
+ slashIndexMissing: "\u2717 no index built yet for this project.",
13093
+ slashHowToBuild: " to enable, exit Reasonix and run in your shell:\n reasonix index",
13094
+ slashOllamaMissing: " prerequisite: install Ollama from https://ollama.com",
13095
+ slashDaemonDown: " Ollama is installed but the daemon isn't running. start it with: ollama serve",
13096
+ 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."
13097
+ };
13098
+ var ZH = {
13099
+ 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",
13100
+ 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",
13101
+ daemonStartConfirm: "Ollama \u5B88\u62A4\u8FDB\u7A0B\u672A\u8FD0\u884C\u3002\u73B0\u5728\u542F\u52A8 `ollama serve` \u5417\uFF1F",
13102
+ daemonAbortStart: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u8FD0\u884C `ollama serve` \u540E\u91CD\u8BD5\u3002\n",
13103
+ daemonStarting: "\u25B8 \u6B63\u5728\u542F\u52A8 `ollama serve`\u2026\n",
13104
+ 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",
13105
+ daemonReady: "\u2713 \u5B88\u62A4\u8FDB\u7A0B\u5DF2\u542F\u52A8{pid}\n",
13106
+ 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',
13107
+ modelPullConfirm: '\u5D4C\u5165\u6A21\u578B "{model}" \u8FD8\u672A\u4E0B\u8F7D\u3002\u73B0\u5728\u4E0B\u8F7D\u5417\uFF1F\uFF08nomic-embed-text \u7EA6 274 MB\uFF09',
13108
+ modelAbortPull: "\u2717 \u5DF2\u53D6\u6D88\u2014\u2014\u8BF7\u81EA\u884C\u4E0B\u8F7D\u6A21\u578B\u540E\u91CD\u8BD5\u3002\n",
13109
+ modelPulling: "\u25B8 \u6B63\u5728\u4E0B\u8F7D {model}\u2026\n",
13110
+ modelPullFailed: "\u2717 `ollama pull {model}` \u5931\u8D25\uFF08\u9000\u51FA\u7801 {code}\uFF09\u3002\n",
13111
+ modelPulled: "\u2713 {model} \u4E0B\u8F7D\u5B8C\u6210\n",
13112
+ progressStarting: "\u6B63\u5728\u542F\u52A8\u2026",
13113
+ progressScan: "\u626B\u63CF\u9879\u76EE \xB7 \u5DF2\u626B\u63CF {files} \u4E2A\u6587\u4EF6",
13114
+ progressEmbed: "\u6B63\u5728\u5411\u91CF\u5316 {done}/{total} \u4E2A\u7247\u6BB5 \xB7 {pct}%",
13115
+ progressEmbedHeartbeat: " {done}/{total}\n",
13116
+ progressScanLine: "\u6B63\u5728\u626B\u63CF\u6587\u4EF6\u2026\n",
13117
+ progressEmbedLine: "\u6B63\u5728\u5411\u91CF\u5316 {total} \u4E2A\u7247\u6BB5\uFF08\u6D89\u53CA {files} \u4E2A\u6587\u4EF6\uFF09\u2026\n",
13118
+ 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",
13119
+ 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",
13120
+ indexNothingToDo: " \uFF08\u6CA1\u6709\u53D8\u5316\u2014\u2014\u52A0 --rebuild \u5F3A\u5236\u91CD\u5EFA\uFF09\n",
13121
+ indexFailed: "\u2717 \u5EFA\u7ACB\u7D22\u5F15\u5931\u8D25\uFF1A{msg}\n",
13122
+ slashHeader: "semantic_search \u72B6\u6001",
13123
+ slashEnabled: "\u2713 \u5DF2\u542F\u7528\u2014\u2014\u7D22\u5F15\u5DF2\u5EFA\u597D\uFF0C\u5DE5\u5177\u5DF2\u6CE8\u518C\u3002",
13124
+ slashEnabledDetail: " \u7D22\u5F15\u89C4\u6A21\uFF1A{chunks} \u4E2A\u7247\u6BB5\uFF0C{files} \u4E2A\u6587\u4EF6",
13125
+ slashEnabledHowto: " \u6A21\u578B\u5728\u5408\u9002\u7684\u65F6\u5019\u4F1A\u81EA\u52A8\u8C03\u7528 semantic_search\u3002",
13126
+ slashIndexMissing: "\u2717 \u5F53\u524D\u9879\u76EE\u8FD8\u6CA1\u6709\u7D22\u5F15\u3002",
13127
+ slashHowToBuild: " \u542F\u7528\u65B9\u5F0F\uFF1A\u9000\u51FA Reasonix\uFF0C\u5728\u7EC8\u7AEF\u8FD0\u884C\uFF1A\n reasonix index",
13128
+ slashOllamaMissing: " \u524D\u7F6E\u4F9D\u8D56\uFF1A\u4ECE https://ollama.com \u5B89\u88C5 Ollama",
13129
+ slashDaemonDown: " \u5DF2\u88C5 Ollama \u4F46\u5B88\u62A4\u8FDB\u7A0B\u672A\u542F\u52A8\uFF0C\u8BF7\u8FD0\u884C\uFF1Aollama serve",
13130
+ 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'
13131
+ };
13132
+
13133
+ // src/index/semantic/ollama-launcher.ts
13134
+ import { spawn as spawn5, spawnSync as spawnSync2 } from "child_process";
13135
+ import { setTimeout as sleep2 } from "timers/promises";
13136
+ function findOllamaBinary() {
13137
+ const cmd = process.platform === "win32" ? "where" : "which";
13138
+ const out = spawnSync2(cmd, ["ollama"], { encoding: "utf8" });
13139
+ if (out.status !== 0) return null;
13140
+ const first = out.stdout.split(/\r?\n/).find((l) => l.trim().length > 0);
13141
+ return first ? first.trim() : null;
13142
+ }
13143
+ async function checkOllamaStatus(modelName, baseUrl) {
13144
+ const binary = findOllamaBinary();
13145
+ const probe = await probeOllama({ baseUrl });
13146
+ const installedModels = probe.ok ? probe.models : [];
13147
+ const wanted = modelName.includes(":") ? modelName : `${modelName}:latest`;
13148
+ const modelPulled = installedModels.some((m) => m === modelName || m === wanted);
13149
+ return {
13150
+ binaryFound: binary !== null,
13151
+ daemonRunning: probe.ok,
13152
+ modelPulled,
13153
+ modelName,
13154
+ installedModels
13155
+ };
13156
+ }
13157
+ async function startOllamaDaemon(opts = {}) {
13158
+ const timeoutMs = opts.timeoutMs ?? 15e3;
13159
+ const child = spawn5("ollama", ["serve"], {
13160
+ detached: true,
13161
+ stdio: "ignore",
13162
+ windowsHide: true
13163
+ });
13164
+ child.unref();
13165
+ const pid = child.pid ?? null;
13166
+ const start = Date.now();
13167
+ while (Date.now() - start < timeoutMs) {
13168
+ if (opts.signal?.aborted) return { ready: false, pid };
13169
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
13170
+ if (probe.ok) return { ready: true, pid };
13171
+ await sleep2(500);
13172
+ }
13173
+ return { ready: false, pid };
13174
+ }
13175
+ async function pullOllamaModel(modelName, opts = {}) {
13176
+ return new Promise((resolve9) => {
13177
+ const child = spawn5("ollama", ["pull", modelName], {
13178
+ stdio: ["ignore", "pipe", "pipe"],
13179
+ windowsHide: true
13180
+ });
13181
+ if (opts.signal) {
13182
+ const onAbort = () => child.kill();
13183
+ opts.signal.addEventListener("abort", onAbort, { once: true });
13184
+ child.once("exit", () => opts.signal?.removeEventListener("abort", onAbort));
13185
+ }
13186
+ streamLines(child.stdout, (l) => opts.onLine?.(l, "stdout"));
13187
+ streamLines(child.stderr, (l) => opts.onLine?.(l, "stderr"));
13188
+ child.once("exit", (code) => resolve9(code ?? -1));
13189
+ child.once("error", () => resolve9(-1));
13190
+ });
13191
+ }
13192
+ function streamLines(stream, cb) {
13193
+ if (!stream) return;
13194
+ let buf = "";
13195
+ stream.setEncoding("utf8");
13196
+ stream.on("data", (chunk) => {
13197
+ buf += chunk;
13198
+ let nl = buf.indexOf("\n");
13199
+ while (nl !== -1) {
13200
+ const line = buf.slice(0, nl).replace(/\r$/, "");
13201
+ buf = buf.slice(nl + 1);
13202
+ if (line.length > 0) cb(line);
13203
+ nl = buf.indexOf("\n");
13204
+ }
13205
+ });
13206
+ stream.on("end", () => {
13207
+ if (buf.length > 0) cb(buf.replace(/\r$/, ""));
13208
+ });
13209
+ }
13210
+
13211
+ // src/cli/ui/slash/handlers/semantic.ts
13212
+ var semantic = (_args, _loop, ctx) => {
13213
+ const root = ctx.codeRoot;
13214
+ if (!root) {
13215
+ return {
13216
+ info: "/semantic is only available inside `reasonix code` (needs a project root)."
13217
+ };
13218
+ }
13219
+ void (async () => {
13220
+ const status2 = await renderSemanticStatus(root);
13221
+ ctx.postInfo?.(status2);
13222
+ })();
13223
+ return { info: "\u25B8 checking semantic_search status\u2026" };
13224
+ };
13225
+ async function renderSemanticStatus(rootDir) {
13226
+ const lines = [t("slashHeader"), ""];
13227
+ const indexExists2 = await indexFileExists(rootDir);
13228
+ if (indexExists2) {
13229
+ const meta = await readIndexMeta(rootDir);
13230
+ lines.push(t("slashEnabled"));
13231
+ if (meta) {
13232
+ lines.push(
13233
+ t("slashEnabledDetail", {
13234
+ chunks: meta.chunks,
13235
+ files: meta.files
13236
+ })
13237
+ );
13238
+ }
13239
+ lines.push(t("slashEnabledHowto"));
13240
+ return lines.join("\n");
13241
+ }
13242
+ lines.push(t("slashIndexMissing"));
13243
+ lines.push(t("slashIndexInfo"));
13244
+ lines.push("");
13245
+ if (findOllamaBinary() === null) {
13246
+ lines.push(t("slashOllamaMissing"));
13247
+ } else {
13248
+ const probe = await probeOllama();
13249
+ if (!probe.ok) lines.push(t("slashDaemonDown"));
13250
+ }
13251
+ lines.push(t("slashHowToBuild"));
13252
+ return lines.join("\n");
13253
+ }
13254
+ async function indexFileExists(rootDir) {
13255
+ try {
13256
+ await fs2.access(path.join(rootDir, ".reasonix", "semantic", "index.meta.json"));
13257
+ return true;
13258
+ } catch {
13259
+ return false;
13260
+ }
13261
+ }
13262
+ async function readIndexMeta(rootDir) {
13263
+ const dataPath = path.join(rootDir, ".reasonix", "semantic", "index.jsonl");
13264
+ try {
13265
+ const stat = await fs2.stat(dataPath);
13266
+ if (stat.size > 10 * 1024 * 1024) {
13267
+ return { chunks: Math.round(stat.size / 500), files: 0 };
13268
+ }
13269
+ const raw = await fs2.readFile(dataPath, "utf8");
13270
+ const seenPaths = /* @__PURE__ */ new Set();
13271
+ let chunks = 0;
13272
+ for (const line of raw.split("\n")) {
13273
+ if (line.length === 0) continue;
13274
+ chunks++;
13275
+ try {
13276
+ const parsed = JSON.parse(line);
13277
+ if (parsed.p) seenPaths.add(parsed.p);
13278
+ } catch {
13279
+ }
13280
+ }
13281
+ return { chunks, files: seenPaths.size };
13282
+ } catch {
13283
+ return null;
13284
+ }
13285
+ }
13286
+ var handlers10 = {
13287
+ semantic
13288
+ };
13289
+
12904
13290
  // src/cli/ui/slash/handlers/sessions.ts
12905
13291
  var sessions = (_args, loop2) => {
12906
13292
  const items = listSessions();
@@ -12932,7 +13318,7 @@ var forget = (_args, loop2) => {
12932
13318
  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
13319
  };
12934
13320
  };
12935
- var handlers10 = {
13321
+ var handlers11 = {
12936
13322
  sessions,
12937
13323
  forget
12938
13324
  };
@@ -13008,7 +13394,7 @@ ${found.body}${argsLine}`;
13008
13394
  resubmit: payload
13009
13395
  };
13010
13396
  };
13011
- var handlers11 = {
13397
+ var handlers12 = {
13012
13398
  skill,
13013
13399
  skills: skill
13014
13400
  };
@@ -13025,7 +13411,8 @@ var HANDLERS = {
13025
13411
  ...handlers8,
13026
13412
  ...handlers9,
13027
13413
  ...handlers10,
13028
- ...handlers11
13414
+ ...handlers11,
13415
+ ...handlers12
13029
13416
  };
13030
13417
  function handleSlash(cmd, args, loop2, ctx = {}) {
13031
13418
  const h = HANDLERS[cmd];
@@ -13339,16 +13726,16 @@ function useEditHistory(codeMode) {
13339
13726
  const status2 = entryStatus(entry);
13340
13727
  const header2 = `\u25B8 edit #${entry.id} \xB7 ${when} \xB7 ${entry.source} \xB7 ${status2} \xB7 ${files.length} file(s)`;
13341
13728
  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);
13729
+ const fileLines = files.map((path5) => {
13730
+ const fileBlocks = entry.blocks.filter((b) => b.path === path5);
13344
13731
  let removed = 0;
13345
13732
  let added = 0;
13346
13733
  for (const b of fileBlocks) {
13347
13734
  removed += countLines3(b.search);
13348
13735
  added += countLines3(b.replace);
13349
13736
  }
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"})`;
13737
+ const state = entry.undoneFiles.has(path5) ? "UNDONE" : "applied";
13738
+ return ` ${state.padEnd(7)} -${String(removed).padStart(3)}/+${String(added).padStart(3)} ${path5} (${fileBlocks.length} block${fileBlocks.length === 1 ? "" : "s"})`;
13352
13739
  });
13353
13740
  return [
13354
13741
  header2,
@@ -13517,7 +13904,7 @@ function LoopStatusRow({
13517
13904
  }) {
13518
13905
  const [, setTick] = React23.useState(0);
13519
13906
  React23.useEffect(() => {
13520
- const id = setInterval(() => setTick((t) => t + 1), 1e3);
13907
+ const id = setInterval(() => setTick((t2) => t2 + 1), 1e3);
13521
13908
  return () => clearInterval(id);
13522
13909
  }, []);
13523
13910
  const nextFireMs = Math.max(0, loop2.nextFireAt - Date.now());
@@ -13547,19 +13934,19 @@ function App({
13547
13934
  }, [busy]);
13548
13935
  const [ongoingTool, setOngoingTool] = useState10(null);
13549
13936
  const [toolProgress, setToolProgress] = useState10(null);
13550
- const { stdout: stdout2 } = useStdout8();
13937
+ const { stdout: stdout3 } = useStdout8();
13551
13938
  useEffect6(() => {
13552
- if (!stdout2 || !stdout2.isTTY) return;
13553
- stdout2.write("\x1B[?2004h");
13554
- stdout2.write("\x1B[>4;2m");
13939
+ if (!stdout3 || !stdout3.isTTY) return;
13940
+ stdout3.write("\x1B[?2004h");
13941
+ stdout3.write("\x1B[>4;2m");
13555
13942
  return () => {
13556
- stdout2.write("\x1B[?2004l");
13557
- stdout2.write("\x1B[>4m");
13943
+ stdout3.write("\x1B[?2004l");
13944
+ stdout3.write("\x1B[>4m");
13558
13945
  };
13559
- }, [stdout2]);
13946
+ }, [stdout3]);
13560
13947
  const [isResizing, setIsResizing] = useState10(false);
13561
13948
  useEffect6(() => {
13562
- if (!stdout2 || !stdout2.isTTY) return;
13949
+ if (!stdout3 || !stdout3.isTTY) return;
13563
13950
  let timer = null;
13564
13951
  const onResize = () => {
13565
13952
  setIsResizing(true);
@@ -13569,12 +13956,12 @@ function App({
13569
13956
  timer = null;
13570
13957
  }, 400);
13571
13958
  };
13572
- stdout2.on("resize", onResize);
13959
+ stdout3.on("resize", onResize);
13573
13960
  return () => {
13574
- stdout2.off("resize", onResize);
13961
+ stdout3.off("resize", onResize);
13575
13962
  if (timer) clearTimeout(timer);
13576
13963
  };
13577
- }, [stdout2]);
13964
+ }, [stdout3]);
13578
13965
  const { activity: subagentActivity, sinkRef: subagentSinkRef } = useSubagent({
13579
13966
  session,
13580
13967
  setHistorical
@@ -13598,7 +13985,7 @@ function App({
13598
13985
  const [pendingCount, setPendingCount] = useState10(0);
13599
13986
  const syncPendingCount = useCallback4(() => {
13600
13987
  setPendingCount(pendingEdits.current.length);
13601
- setPendingTick((t) => t + 1);
13988
+ setPendingTick((t2) => t2 + 1);
13602
13989
  }, []);
13603
13990
  const [editMode, setEditMode] = useState10(() => codeMode ? loadEditMode() : "review");
13604
13991
  const editModeRef = useRef6(editMode);
@@ -13888,11 +14275,11 @@ function App({
13888
14275
  if (key.escape && busy) {
13889
14276
  if (abortedThisTurn.current) return;
13890
14277
  abortedThisTurn.current = true;
13891
- const resolve8 = editReviewResolveRef.current;
13892
- if (resolve8) {
14278
+ const resolve9 = editReviewResolveRef.current;
14279
+ if (resolve9) {
13893
14280
  editReviewResolveRef.current = null;
13894
14281
  setPendingEditReview(null);
13895
- resolve8("reject");
14282
+ resolve9("reject");
13896
14283
  }
13897
14284
  if (activeLoopRef.current) stopLoop();
13898
14285
  loop2.abort();
@@ -14403,7 +14790,7 @@ function App({
14403
14790
  return;
14404
14791
  }
14405
14792
  if (result.clear && result.info) {
14406
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
14793
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14407
14794
  setHistorical([
14408
14795
  {
14409
14796
  id: `sys-${Date.now()}`,
@@ -14420,7 +14807,7 @@ function App({
14420
14807
  return;
14421
14808
  }
14422
14809
  if (result.clear) {
14423
- stdout2?.write("\x1B[2J\x1B[3J\x1B[H");
14810
+ stdout3?.write("\x1B[2J\x1B[3J\x1B[H");
14424
14811
  setHistorical([]);
14425
14812
  if (codeMode) {
14426
14813
  pendingEdits.current = [];
@@ -14950,7 +15337,7 @@ function App({
14950
15337
  refreshModels,
14951
15338
  proArmed,
14952
15339
  persistPlanState,
14953
- stdout2,
15340
+ stdout3,
14954
15341
  stopLoop,
14955
15342
  startLoop,
14956
15343
  getLoopStatus,
@@ -15500,10 +15887,10 @@ Continue executing from the next pending step. Call mark_step_complete after eac
15500
15887
  {
15501
15888
  block: pendingEditReview,
15502
15889
  onChoose: (choice) => {
15503
- const resolve8 = editReviewResolveRef.current;
15504
- if (resolve8) {
15890
+ const resolve9 = editReviewResolveRef.current;
15891
+ if (resolve9) {
15505
15892
  editReviewResolveRef.current = null;
15506
- resolve8(choice);
15893
+ resolve9(choice);
15507
15894
  }
15508
15895
  }
15509
15896
  }
@@ -15810,8 +16197,653 @@ async function chatCommand(opts) {
15810
16197
 
15811
16198
  // src/cli/commands/code.tsx
15812
16199
  import { basename as basename2, resolve as resolve7 } from "path";
16200
+
16201
+ // src/index/semantic/builder.ts
16202
+ import { promises as fs5 } from "fs";
16203
+ import path4 from "path";
16204
+
16205
+ // src/index/semantic/chunker.ts
16206
+ import { promises as fs3 } from "fs";
16207
+ import path2 from "path";
16208
+ var DEFAULT_MAX_CHUNK_CHARS = 4e3;
16209
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
16210
+ "node_modules",
16211
+ ".git",
16212
+ "dist",
16213
+ "build",
16214
+ "out",
16215
+ ".next",
16216
+ ".nuxt",
16217
+ "target",
16218
+ ".venv",
16219
+ "venv",
16220
+ "__pycache__",
16221
+ ".pytest_cache",
16222
+ ".mypy_cache",
16223
+ ".cache",
16224
+ "coverage",
16225
+ ".turbo",
16226
+ ".vercel",
16227
+ ".reasonix"
16228
+ ]);
16229
+ var SKIP_FILES = /* @__PURE__ */ new Set([
16230
+ "package-lock.json",
16231
+ "yarn.lock",
16232
+ "pnpm-lock.yaml",
16233
+ "Cargo.lock",
16234
+ "poetry.lock",
16235
+ "Pipfile.lock",
16236
+ "go.sum",
16237
+ ".DS_Store"
16238
+ ]);
16239
+ var BINARY_EXTS = /* @__PURE__ */ new Set([
16240
+ // Images
16241
+ ".png",
16242
+ ".jpg",
16243
+ ".jpeg",
16244
+ ".gif",
16245
+ ".webp",
16246
+ ".bmp",
16247
+ ".ico",
16248
+ ".tiff",
16249
+ // Fonts
16250
+ ".woff",
16251
+ ".woff2",
16252
+ ".ttf",
16253
+ ".otf",
16254
+ ".eot",
16255
+ // Archives / binaries
16256
+ ".zip",
16257
+ ".tar",
16258
+ ".gz",
16259
+ ".rar",
16260
+ ".7z",
16261
+ ".exe",
16262
+ ".dll",
16263
+ ".so",
16264
+ ".dylib",
16265
+ ".class",
16266
+ ".jar",
16267
+ ".wasm",
16268
+ ".o",
16269
+ ".a",
16270
+ // Media
16271
+ ".mp3",
16272
+ ".mp4",
16273
+ ".wav",
16274
+ ".ogg",
16275
+ ".webm",
16276
+ ".mov",
16277
+ // Other
16278
+ ".pdf",
16279
+ ".sqlite",
16280
+ ".db"
16281
+ ]);
16282
+ function chunkText(text, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
16283
+ const lines = text.split(/\r?\n/);
16284
+ if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
16285
+ const stride = Math.max(1, windowLines - overlap);
16286
+ const chunks = [];
16287
+ for (let start = 0; start < lines.length; start += stride) {
16288
+ const end = Math.min(lines.length, start + windowLines);
16289
+ const slice = lines.slice(start, end).join("\n").trim();
16290
+ if (slice.length === 0) {
16291
+ if (end >= lines.length) break;
16292
+ continue;
16293
+ }
16294
+ const window = {
16295
+ path: filePath,
16296
+ startLine: start + 1,
16297
+ endLine: end,
16298
+ text: slice
16299
+ };
16300
+ for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
16301
+ if (end >= lines.length) break;
16302
+ }
16303
+ return chunks;
16304
+ }
16305
+ function safeSplit(chunk, maxChars) {
16306
+ if (chunk.text.length <= maxChars) return [chunk];
16307
+ const lines = chunk.text.split("\n");
16308
+ const out = [];
16309
+ let bufLines = [];
16310
+ let bufStart = chunk.startLine;
16311
+ let bufLen = 0;
16312
+ const flush = (untilLineNo) => {
16313
+ if (bufLines.length === 0) return;
16314
+ out.push({
16315
+ path: chunk.path,
16316
+ startLine: bufStart,
16317
+ endLine: untilLineNo,
16318
+ text: bufLines.join("\n")
16319
+ });
16320
+ bufLines = [];
16321
+ bufLen = 0;
16322
+ };
16323
+ for (let i = 0; i < lines.length; i++) {
16324
+ const line = lines[i] ?? "";
16325
+ const lineLen = line.length + 1;
16326
+ if (lineLen > maxChars) {
16327
+ flush(chunk.startLine + i - 1);
16328
+ out.push({
16329
+ path: chunk.path,
16330
+ startLine: chunk.startLine + i,
16331
+ endLine: chunk.startLine + i,
16332
+ text: line.slice(0, maxChars)
16333
+ });
16334
+ bufStart = chunk.startLine + i + 1;
16335
+ continue;
16336
+ }
16337
+ if (bufLen + lineLen > maxChars && bufLines.length > 0) {
16338
+ flush(chunk.startLine + i - 1);
16339
+ bufStart = chunk.startLine + i;
16340
+ }
16341
+ bufLines.push(line);
16342
+ bufLen += lineLen;
16343
+ }
16344
+ flush(chunk.endLine);
16345
+ return out;
16346
+ }
16347
+ async function* walkChunks(root, opts = {}) {
16348
+ const windowLines = opts.windowLines ?? 60;
16349
+ const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
16350
+ const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
16351
+ const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
16352
+ const stack = [root];
16353
+ while (stack.length > 0) {
16354
+ const dir = stack.pop();
16355
+ if (!dir) break;
16356
+ let entries;
16357
+ try {
16358
+ entries = await fs3.readdir(dir, { withFileTypes: true });
16359
+ } catch {
16360
+ continue;
16361
+ }
16362
+ for (const entry of entries) {
16363
+ const name = entry.name;
16364
+ if (entry.isDirectory()) {
16365
+ if (SKIP_DIRS.has(name) || name.startsWith(".")) {
16366
+ if (SKIP_DIRS.has(name) || name === ".git") continue;
16367
+ }
16368
+ stack.push(path2.join(dir, name));
16369
+ continue;
16370
+ }
16371
+ if (!entry.isFile()) continue;
16372
+ if (SKIP_FILES.has(name)) continue;
16373
+ const ext = path2.extname(name).toLowerCase();
16374
+ if (BINARY_EXTS.has(ext)) continue;
16375
+ const abs = path2.join(dir, name);
16376
+ let stat;
16377
+ try {
16378
+ stat = await fs3.stat(abs);
16379
+ } catch {
16380
+ continue;
16381
+ }
16382
+ if (stat.size > maxFileBytes) continue;
16383
+ let text;
16384
+ try {
16385
+ text = await fs3.readFile(abs, "utf8");
16386
+ } catch {
16387
+ continue;
16388
+ }
16389
+ if (text.indexOf("\0") !== -1) continue;
16390
+ const rel = path2.relative(root, abs).split(path2.sep).join("/");
16391
+ for (const chunk of chunkText(text, rel, windowLines, overlap, maxChunkChars)) {
16392
+ yield chunk;
16393
+ }
16394
+ }
16395
+ }
16396
+ }
16397
+
16398
+ // src/index/semantic/store.ts
16399
+ import { promises as fs4 } from "fs";
16400
+ import path3 from "path";
16401
+ var STORE_VERSION = 1;
16402
+ var META_FILE = "index.meta.json";
16403
+ var DATA_FILE = "index.jsonl";
16404
+ var SemanticStore = class {
16405
+ constructor(indexDir, model2) {
16406
+ this.indexDir = indexDir;
16407
+ this.model = model2;
16408
+ }
16409
+ indexDir;
16410
+ model;
16411
+ entries = [];
16412
+ byPath = /* @__PURE__ */ new Map();
16413
+ dim = 0;
16414
+ /** True when no entries are loaded — the index doesn't exist or is empty. */
16415
+ get empty() {
16416
+ return this.entries.length === 0;
16417
+ }
16418
+ /** Total number of indexed chunks. */
16419
+ get size() {
16420
+ return this.entries.length;
16421
+ }
16422
+ /** Read-only view, mostly for tests. */
16423
+ get all() {
16424
+ return this.entries;
16425
+ }
16426
+ /** Last-known mtime per indexed file (ms epoch) for incremental rebuilds. */
16427
+ fileMtimes() {
16428
+ const out = /* @__PURE__ */ new Map();
16429
+ for (const [p, group] of this.byPath) {
16430
+ const first = group[0];
16431
+ if (first) out.set(p, first.mtimeMs);
16432
+ }
16433
+ return out;
16434
+ }
16435
+ /** Append entries to in-memory state and to disk. Re-indexes the
16436
+ * `byPath` map. Caller is responsible for L2-normalizing each
16437
+ * embedding before calling — the search hot path assumes unit vectors. */
16438
+ async add(entries) {
16439
+ if (entries.length === 0) return;
16440
+ if (this.dim === 0) this.dim = entries[0].embedding.length;
16441
+ const lines = [];
16442
+ for (const e of entries) {
16443
+ if (e.embedding.length !== this.dim) {
16444
+ throw new Error(
16445
+ `embedding dim mismatch: expected ${this.dim}, got ${e.embedding.length} for ${e.path}:${e.startLine}`
16446
+ );
16447
+ }
16448
+ this.entries.push(e);
16449
+ const list = this.byPath.get(e.path);
16450
+ if (list) list.push(e);
16451
+ else this.byPath.set(e.path, [e]);
16452
+ lines.push(serializeEntry(e));
16453
+ }
16454
+ await fs4.mkdir(this.indexDir, { recursive: true });
16455
+ await fs4.appendFile(path3.join(this.indexDir, DATA_FILE), `${lines.join("\n")}
16456
+ `, "utf8");
16457
+ await this.writeMeta();
16458
+ }
16459
+ /**
16460
+ * Drop every entry whose `path` is in `paths`. Used by incremental
16461
+ * rebuild: when a file's mtime changes, the existing entries for
16462
+ * it are evicted before re-chunking + re-embedding. Implementation
16463
+ * rewrites the JSONL — append-only is fine for adds, but deletes
16464
+ * need a compaction pass.
16465
+ */
16466
+ async remove(paths) {
16467
+ if (paths.length === 0) return 0;
16468
+ const drop = new Set(paths);
16469
+ const before = this.entries.length;
16470
+ this.entries = this.entries.filter((e) => !drop.has(e.path));
16471
+ for (const p of paths) this.byPath.delete(p);
16472
+ const removed = before - this.entries.length;
16473
+ if (removed > 0) await this.flush();
16474
+ return removed;
16475
+ }
16476
+ /**
16477
+ * Top-K cosine search. `query` MUST already be L2-normalized — the
16478
+ * caller embeds + normalizes once per query. Filtering hits by
16479
+ * minimum score (`minScore`) is optional; tune via UI to suppress
16480
+ * weakly relevant snippets that distract the model.
16481
+ */
16482
+ search(query, topK = 8, minScore = 0) {
16483
+ if (this.entries.length === 0) return [];
16484
+ if (query.length !== this.dim && this.dim !== 0) {
16485
+ throw new Error(`query dim ${query.length} \u2260 index dim ${this.dim}`);
16486
+ }
16487
+ const heap = [];
16488
+ for (const entry of this.entries) {
16489
+ const score = dot(query, entry.embedding);
16490
+ if (score < minScore) continue;
16491
+ if (heap.length < topK) {
16492
+ heap.push({ entry, score });
16493
+ if (heap.length === topK) heap.sort((a, b) => a.score - b.score);
16494
+ } else if (score > heap[0].score) {
16495
+ heap[0] = { entry, score };
16496
+ for (let i = 0; i < heap.length - 1; i++) {
16497
+ if (heap[i].score > heap[i + 1].score) {
16498
+ const tmp = heap[i];
16499
+ heap[i] = heap[i + 1];
16500
+ heap[i + 1] = tmp;
16501
+ }
16502
+ }
16503
+ }
16504
+ }
16505
+ return heap.sort((a, b) => b.score - a.score);
16506
+ }
16507
+ /**
16508
+ * Rewrite the JSONL on disk with the current in-memory state. Used
16509
+ * after `remove` and from `flush`. We write to a temp file and
16510
+ * rename so a Ctrl+C mid-write never leaves the index half-empty.
16511
+ */
16512
+ async flush() {
16513
+ await fs4.mkdir(this.indexDir, { recursive: true });
16514
+ const tmp = path3.join(this.indexDir, `${DATA_FILE}.tmp`);
16515
+ const final = path3.join(this.indexDir, DATA_FILE);
16516
+ const lines = this.entries.map(serializeEntry).join("\n");
16517
+ await fs4.writeFile(tmp, lines.length > 0 ? `${lines}
16518
+ ` : "", "utf8");
16519
+ await fs4.rename(tmp, final);
16520
+ await this.writeMeta();
16521
+ }
16522
+ async writeMeta() {
16523
+ const meta = {
16524
+ version: STORE_VERSION,
16525
+ model: this.model,
16526
+ dim: this.dim,
16527
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
16528
+ };
16529
+ await fs4.writeFile(
16530
+ path3.join(this.indexDir, META_FILE),
16531
+ `${JSON.stringify(meta, null, 2)}
16532
+ `,
16533
+ "utf8"
16534
+ );
16535
+ }
16536
+ /** Drop everything from disk + memory. Used by `--rebuild`. */
16537
+ async wipe() {
16538
+ this.entries = [];
16539
+ this.byPath.clear();
16540
+ this.dim = 0;
16541
+ await fs4.rm(path3.join(this.indexDir, DATA_FILE), { force: true });
16542
+ await fs4.rm(path3.join(this.indexDir, META_FILE), { force: true });
16543
+ }
16544
+ };
16545
+ async function openStore(indexDir, model2) {
16546
+ const store = new SemanticStore(indexDir, model2);
16547
+ const dataPath = path3.join(indexDir, DATA_FILE);
16548
+ const metaPath = path3.join(indexDir, META_FILE);
16549
+ let meta = null;
16550
+ try {
16551
+ const raw2 = await fs4.readFile(metaPath, "utf8");
16552
+ meta = JSON.parse(raw2);
16553
+ } catch {
16554
+ }
16555
+ if (meta) {
16556
+ if (meta.version !== STORE_VERSION) {
16557
+ throw new Error(
16558
+ `Index format version ${meta.version} does not match current ${STORE_VERSION}. Run \`reasonix index --rebuild\`.`
16559
+ );
16560
+ }
16561
+ if (meta.model !== model2) {
16562
+ throw new Error(
16563
+ `Index was built with model "${meta.model}" but current is "${model2}". Run \`reasonix index --rebuild\`.`
16564
+ );
16565
+ }
16566
+ }
16567
+ let raw;
16568
+ try {
16569
+ raw = await fs4.readFile(dataPath, "utf8");
16570
+ } catch {
16571
+ return store;
16572
+ }
16573
+ for (const line of raw.split("\n")) {
16574
+ if (line.length === 0) continue;
16575
+ try {
16576
+ const entry = deserializeEntry(line);
16577
+ store.dim = entry.embedding.length;
16578
+ store.entries.push(entry);
16579
+ const map = store.byPath;
16580
+ const list = map.get(entry.path);
16581
+ if (list) list.push(entry);
16582
+ else map.set(entry.path, [entry]);
16583
+ } catch {
16584
+ }
16585
+ }
16586
+ return store;
16587
+ }
16588
+ function normalize(v) {
16589
+ let sum = 0;
16590
+ for (let i = 0; i < v.length; i++) sum += v[i] * v[i];
16591
+ const inv = sum > 0 ? 1 / Math.sqrt(sum) : 0;
16592
+ for (let i = 0; i < v.length; i++) v[i] = v[i] * inv;
16593
+ return v;
16594
+ }
16595
+ function dot(a, b) {
16596
+ let s = 0;
16597
+ for (let i = 0; i < a.length; i++) s += a[i] * b[i];
16598
+ return s;
16599
+ }
16600
+ function serializeEntry(e) {
16601
+ const buf = Buffer.from(e.embedding.buffer, e.embedding.byteOffset, e.embedding.byteLength);
16602
+ return JSON.stringify({
16603
+ p: e.path,
16604
+ s: e.startLine,
16605
+ e: e.endLine,
16606
+ m: e.mtimeMs,
16607
+ t: e.text,
16608
+ v: buf.toString("base64")
16609
+ });
16610
+ }
16611
+ function deserializeEntry(line) {
16612
+ const parsed = JSON.parse(line);
16613
+ const bytes = Buffer.from(parsed.v, "base64");
16614
+ const f32 = new Float32Array(
16615
+ bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength)
16616
+ );
16617
+ return {
16618
+ path: parsed.p,
16619
+ startLine: parsed.s,
16620
+ endLine: parsed.e,
16621
+ mtimeMs: parsed.m,
16622
+ text: parsed.t,
16623
+ embedding: f32
16624
+ };
16625
+ }
16626
+
16627
+ // src/index/semantic/builder.ts
16628
+ var INDEX_DIR_NAME = path4.join(".reasonix", "semantic");
16629
+ async function buildIndex(root, opts = {}) {
16630
+ const t0 = Date.now();
16631
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
16632
+ const probe = await probeOllama({ baseUrl: opts.baseUrl, signal: opts.signal });
16633
+ if (!probe.ok) {
16634
+ throw new Error(
16635
+ `Ollama is not reachable: ${probe.error}. Install from https://ollama.com, then \`ollama serve\` and \`ollama pull ${opts.model ?? "nomic-embed-text"}\`.`
16636
+ );
16637
+ }
16638
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
16639
+ const store = await openStore(indexDir, model2);
16640
+ if (opts.rebuild) await store.wipe();
16641
+ const lastMtimes = store.fileMtimes();
16642
+ const seenPaths = /* @__PURE__ */ new Set();
16643
+ const fileChunks = /* @__PURE__ */ new Map();
16644
+ let filesScanned = 0;
16645
+ let filesSkipped = 0;
16646
+ for await (const chunk of walkChunks(root, {
16647
+ windowLines: opts.windowLines,
16648
+ overlap: opts.overlap,
16649
+ maxFileBytes: opts.maxFileBytes
16650
+ })) {
16651
+ seenPaths.add(chunk.path);
16652
+ let bucket = fileChunks.get(chunk.path);
16653
+ if (!bucket) {
16654
+ filesScanned++;
16655
+ const abs = path4.join(root, chunk.path);
16656
+ let mtimeMs = 0;
16657
+ try {
16658
+ const stat = await fs5.stat(abs);
16659
+ mtimeMs = stat.mtimeMs;
16660
+ } catch {
16661
+ continue;
16662
+ }
16663
+ const last = lastMtimes.get(chunk.path);
16664
+ if (last !== void 0 && last === mtimeMs && !opts.rebuild) {
16665
+ filesSkipped++;
16666
+ continue;
16667
+ }
16668
+ bucket = { chunks: [], mtimeMs };
16669
+ fileChunks.set(chunk.path, bucket);
16670
+ }
16671
+ bucket.chunks.push(chunk);
16672
+ opts.onProgress?.({ phase: "scan", filesScanned });
16673
+ }
16674
+ const deletedPaths = [];
16675
+ for (const oldPath of lastMtimes.keys()) {
16676
+ if (!seenPaths.has(oldPath)) deletedPaths.push(oldPath);
16677
+ }
16678
+ const replacePaths = [...fileChunks.keys()].filter((p) => lastMtimes.has(p));
16679
+ const removed = await store.remove([...deletedPaths, ...replacePaths]);
16680
+ let chunksAdded = 0;
16681
+ let chunksSkipped = 0;
16682
+ const filesChanged = fileChunks.size;
16683
+ let chunksTotal = 0;
16684
+ for (const { chunks } of fileChunks.values()) chunksTotal += chunks.length;
16685
+ let chunksDone = 0;
16686
+ for (const [, bucket] of fileChunks) {
16687
+ if (bucket.chunks.length === 0) continue;
16688
+ const texts = bucket.chunks.map((c) => c.text);
16689
+ const vectors = await embedAll(texts, {
16690
+ ...opts,
16691
+ onProgress: (done, total) => {
16692
+ opts.onProgress?.({
16693
+ phase: "embed",
16694
+ filesScanned,
16695
+ filesChanged,
16696
+ chunksTotal,
16697
+ chunksDone: chunksDone + done
16698
+ });
16699
+ if (done === total) chunksDone += total;
16700
+ },
16701
+ onError: (idx, err) => {
16702
+ chunksSkipped++;
16703
+ const c = bucket.chunks[idx];
16704
+ const where = c ? `${c.path}:${c.startLine}-${c.endLine}` : `chunk #${idx}`;
16705
+ const msg = err instanceof Error ? err.message : String(err);
16706
+ process.stderr.write(`
16707
+ ! skipped ${where}: ${msg}
16708
+ `);
16709
+ }
16710
+ });
16711
+ const entries = [];
16712
+ for (let i = 0; i < bucket.chunks.length; i++) {
16713
+ const vec = vectors[i];
16714
+ if (!vec) continue;
16715
+ const c = bucket.chunks[i];
16716
+ if (!c) continue;
16717
+ normalize(vec);
16718
+ entries.push({
16719
+ path: c.path,
16720
+ startLine: c.startLine,
16721
+ endLine: c.endLine,
16722
+ text: c.text,
16723
+ embedding: vec,
16724
+ mtimeMs: bucket.mtimeMs
16725
+ });
16726
+ }
16727
+ if (entries.length > 0) await store.add(entries);
16728
+ chunksAdded += entries.length;
16729
+ }
16730
+ opts.onProgress?.({
16731
+ phase: "done",
16732
+ filesScanned,
16733
+ filesSkipped,
16734
+ filesChanged,
16735
+ chunksTotal,
16736
+ chunksDone
16737
+ });
16738
+ return {
16739
+ filesScanned,
16740
+ filesChanged,
16741
+ chunksAdded,
16742
+ chunksRemoved: removed,
16743
+ chunksSkipped,
16744
+ durationMs: Date.now() - t0
16745
+ };
16746
+ }
16747
+ async function querySemantic(root, query, opts = {}) {
16748
+ const indexDir = path4.join(root, INDEX_DIR_NAME);
16749
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
16750
+ const store = await openStore(indexDir, model2);
16751
+ if (store.empty) return null;
16752
+ const qvec = await embed(query, opts);
16753
+ normalize(qvec);
16754
+ return store.search(qvec, opts.topK ?? 8, opts.minScore ?? 0.3);
16755
+ }
16756
+ async function indexExists(root) {
16757
+ const meta = path4.join(root, INDEX_DIR_NAME, "index.meta.json");
16758
+ try {
16759
+ await fs5.access(meta);
16760
+ return true;
16761
+ } catch {
16762
+ return false;
16763
+ }
16764
+ }
16765
+
16766
+ // src/index/semantic/tool.ts
16767
+ async function registerSemanticSearchTool(registry, opts) {
16768
+ if (!await indexExists(opts.root)) return false;
16769
+ const defaultTopK = opts.defaultTopK ?? 8;
16770
+ const defaultMinScore = opts.defaultMinScore ?? 0.3;
16771
+ registry.register({
16772
+ name: "semantic_search",
16773
+ 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.",
16774
+ readOnly: true,
16775
+ parameters: {
16776
+ type: "object",
16777
+ properties: {
16778
+ query: {
16779
+ type: "string",
16780
+ 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."
16781
+ },
16782
+ topK: {
16783
+ type: "integer",
16784
+ description: `Number of snippets to return (1..16). Default ${defaultTopK}.`
16785
+ },
16786
+ minScore: {
16787
+ type: "number",
16788
+ description: `Drop snippets with cosine score below this (0..1). Default ${defaultMinScore}. Raise for stricter matches; lower if the index is small.`
16789
+ }
16790
+ },
16791
+ required: ["query"]
16792
+ },
16793
+ fn: async (args, ctx) => {
16794
+ const hits = await querySemantic(opts.root, args.query, {
16795
+ topK: args.topK ?? defaultTopK,
16796
+ minScore: args.minScore ?? defaultMinScore,
16797
+ baseUrl: opts.baseUrl,
16798
+ model: opts.model,
16799
+ signal: ctx?.signal
16800
+ });
16801
+ if (hits === null) {
16802
+ return "No semantic index found for this project. Run `reasonix index` to build one.";
16803
+ }
16804
+ if (hits.length === 0) {
16805
+ return `query: ${args.query}
16806
+
16807
+ no matches above the score threshold (${args.minScore ?? defaultMinScore}).`;
16808
+ }
16809
+ return formatHits(args.query, hits);
16810
+ }
16811
+ });
16812
+ return true;
16813
+ }
16814
+ function formatHits(query, hits) {
16815
+ const lines = [`query: ${query}`, `
16816
+ results (${hits.length}):`];
16817
+ hits.forEach((h, i) => {
16818
+ const { entry, score } = h;
16819
+ lines.push(
16820
+ `
16821
+ ${i + 1}. ${entry.path}:${entry.startLine}-${entry.endLine} (score ${score.toFixed(3)})`
16822
+ );
16823
+ const preview = entry.text.split("\n").slice(0, 8).join("\n");
16824
+ lines.push(indentBlock(preview, " "));
16825
+ if (entry.text.split("\n").length > 8) {
16826
+ lines.push(
16827
+ ` \u2026(${entry.text.split("\n").length - 8} more lines \u2014 read_file ${entry.path}:${entry.startLine} for the full chunk)`
16828
+ );
16829
+ }
16830
+ });
16831
+ return lines.join("\n");
16832
+ }
16833
+ function indentBlock(text, prefix) {
16834
+ return text.split("\n").map((l) => prefix + l).join("\n");
16835
+ }
16836
+ async function bootstrapSemanticSearchInCodeMode(registry, rootDir, opts = {}) {
16837
+ if (await indexExists(rootDir)) {
16838
+ await registerSemanticSearchTool(registry, { ...opts, root: rootDir });
16839
+ return { enabled: true };
16840
+ }
16841
+ return { enabled: false };
16842
+ }
16843
+
16844
+ // src/cli/commands/code.tsx
15813
16845
  async function codeCommand(opts = {}) {
15814
- const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-LJ44NWSU.js");
16846
+ const { codeSystemPrompt: codeSystemPrompt2 } = await import("./prompt-FMYQ7IDW.js");
15815
16847
  const rootDir = resolve7(opts.dir ?? process.cwd());
15816
16848
  const session = opts.noSession ? void 0 : `code-${sanitizeName(basename2(rootDir))}`;
15817
16849
  const tools = new ToolRegistry();
@@ -15834,8 +16866,9 @@ async function codeCommand(opts = {}) {
15834
16866
  registerPlanTool(tools);
15835
16867
  registerChoiceTool(tools);
15836
16868
  registerMemoryTools(tools, { projectRoot: rootDir });
16869
+ const semantic2 = await bootstrapSemanticSearchInCodeMode(tools, rootDir);
15837
16870
  process.stderr.write(
15838
- `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)
16871
+ `\u25B8 reasonix code: rooted at ${rootDir}, session "${session ?? "(ephemeral)"}" \xB7 ${tools.size} native tool(s)${semantic2.enabled ? " \xB7 semantic_search on" : ""}
15839
16872
  `
15840
16873
  );
15841
16874
  process.once("exit", () => {
@@ -15844,7 +16877,7 @@ async function codeCommand(opts = {}) {
15844
16877
  await chatCommand({
15845
16878
  model: opts.model ?? "deepseek-v4-flash",
15846
16879
  harvest: opts.harvest ?? false,
15847
- system: codeSystemPrompt2(rootDir),
16880
+ system: codeSystemPrompt2(rootDir, { hasSemanticSearch: semantic2.enabled }),
15848
16881
  transcript: opts.transcript,
15849
16882
  session,
15850
16883
  seedTools: tools,
@@ -16018,6 +17051,200 @@ markdown report written to ${opts.mdPath}`);
16018
17051
  console.log(renderSummaryTable(report));
16019
17052
  }
16020
17053
 
17054
+ // src/cli/commands/index.ts
17055
+ import { resolve as resolve8 } from "path";
17056
+
17057
+ // src/index/semantic/preflight.ts
17058
+ import { stdin as stdin2, stdout } from "process";
17059
+ import { createInterface } from "readline/promises";
17060
+ async function ollamaPreflight(opts) {
17061
+ const log = opts.log ?? ((line) => process.stderr.write(line));
17062
+ const status2 = await checkOllamaStatus(opts.model, opts.baseUrl);
17063
+ if (!status2.binaryFound) {
17064
+ log(t("ollamaNotFound"));
17065
+ return false;
17066
+ }
17067
+ if (!status2.daemonRunning) {
17068
+ if (!opts.interactive && !opts.yesToAll) {
17069
+ log(t("daemonNotReachableHint"));
17070
+ return false;
17071
+ }
17072
+ const ok = opts.yesToAll || await confirm(t("daemonStartConfirm"), true);
17073
+ if (!ok) {
17074
+ log(t("daemonAbortStart"));
17075
+ return false;
17076
+ }
17077
+ log(t("daemonStarting"));
17078
+ const started = await startOllamaDaemon({ baseUrl: opts.baseUrl, timeoutMs: 15e3 });
17079
+ if (!started.ready) {
17080
+ log(t("daemonStartTimeout"));
17081
+ return false;
17082
+ }
17083
+ log(t("daemonReady", { pid: started.pid ? ` (pid ${started.pid})` : "" }));
17084
+ }
17085
+ const after = status2.daemonRunning ? status2 : await checkOllamaStatus(opts.model, opts.baseUrl);
17086
+ if (!after.modelPulled) {
17087
+ if (!opts.interactive && !opts.yesToAll) {
17088
+ log(t("modelNotPulledHint", { model: opts.model }));
17089
+ return false;
17090
+ }
17091
+ const ok = opts.yesToAll || await confirm(t("modelPullConfirm", { model: opts.model }), true);
17092
+ if (!ok) {
17093
+ log(t("modelAbortPull"));
17094
+ return false;
17095
+ }
17096
+ log(t("modelPulling", { model: opts.model }));
17097
+ const ESC = String.fromCharCode(27);
17098
+ const ANSI_CSI = new RegExp(`${ESC}\\[[0-9;]*[A-Za-z]`, "g");
17099
+ const code = await pullOllamaModel(opts.model, {
17100
+ onLine: (line) => {
17101
+ const cleaned = line.replace(ANSI_CSI, "").trim();
17102
+ if (cleaned.length === 0) return;
17103
+ log(` ${cleaned}
17104
+ `);
17105
+ }
17106
+ });
17107
+ if (code !== 0) {
17108
+ log(t("modelPullFailed", { model: opts.model, code }));
17109
+ return false;
17110
+ }
17111
+ log(t("modelPulled", { model: opts.model }));
17112
+ }
17113
+ return true;
17114
+ }
17115
+ async function confirm(question, defaultYes) {
17116
+ const suffix = defaultYes ? "[Y/n]" : "[y/N]";
17117
+ const rl = createInterface({ input: stdin2, output: stdout });
17118
+ try {
17119
+ const raw = (await rl.question(`${question} ${suffix} `)).trim().toLowerCase();
17120
+ if (raw === "") return defaultYes;
17121
+ return raw === "y" || raw === "yes";
17122
+ } finally {
17123
+ rl.close();
17124
+ }
17125
+ }
17126
+
17127
+ // src/cli/commands/index.ts
17128
+ async function indexCommand(opts = {}) {
17129
+ const root = resolve8(opts.dir ?? process.cwd());
17130
+ const tty = process.stderr.isTTY === true && process.stdin.isTTY === true;
17131
+ const model2 = opts.model ?? process.env.REASONIX_EMBED_MODEL ?? "nomic-embed-text";
17132
+ const preflightOk = await ollamaPreflight({
17133
+ model: model2,
17134
+ baseUrl: opts.ollamaUrl,
17135
+ interactive: tty && !opts.yes,
17136
+ yesToAll: opts.yes ?? false
17137
+ });
17138
+ if (!preflightOk) process.exit(1);
17139
+ const writer = makeProgressWriter(tty);
17140
+ const t0 = Date.now();
17141
+ let result;
17142
+ try {
17143
+ result = await buildIndex(root, {
17144
+ rebuild: opts.rebuild,
17145
+ model: model2,
17146
+ baseUrl: opts.ollamaUrl,
17147
+ onProgress: (p) => writer.update(p)
17148
+ });
17149
+ } catch (err) {
17150
+ writer.clear();
17151
+ const msg = err instanceof Error ? err.message : String(err);
17152
+ process.stderr.write(t("indexFailed", { msg }));
17153
+ process.exit(1);
17154
+ }
17155
+ writer.clear();
17156
+ const seconds = ((Date.now() - t0) / 1e3).toFixed(1);
17157
+ const successKey = result.chunksSkipped > 0 ? "indexSuccessWithSkips" : "indexSuccess";
17158
+ process.stderr.write(
17159
+ t(successKey, {
17160
+ scanned: result.filesScanned,
17161
+ changed: result.filesChanged,
17162
+ added: result.chunksAdded,
17163
+ removed: result.chunksRemoved,
17164
+ skipped: result.chunksSkipped,
17165
+ seconds
17166
+ })
17167
+ );
17168
+ if (result.filesChanged === 0 && !opts.rebuild) {
17169
+ process.stderr.write(t("indexNothingToDo"));
17170
+ }
17171
+ }
17172
+ var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
17173
+ var SPINNER_INTERVAL_MS = 120;
17174
+ function makeProgressWriter(tty) {
17175
+ if (!tty) return makeNonTtyWriter();
17176
+ return makeTtyWriter();
17177
+ }
17178
+ function makeNonTtyWriter() {
17179
+ let lastPhase = null;
17180
+ let lastChunks = 0;
17181
+ return {
17182
+ update(p) {
17183
+ if (p.phase !== lastPhase) {
17184
+ lastPhase = p.phase;
17185
+ if (p.phase === "scan") {
17186
+ process.stderr.write(t("progressScanLine"));
17187
+ } else if (p.phase === "embed") {
17188
+ process.stderr.write(
17189
+ t("progressEmbedLine", {
17190
+ total: p.chunksTotal ?? 0,
17191
+ files: p.filesChanged ?? 0
17192
+ })
17193
+ );
17194
+ }
17195
+ }
17196
+ if (p.phase === "embed" && p.chunksDone !== void 0 && p.chunksDone - lastChunks >= 50) {
17197
+ lastChunks = p.chunksDone;
17198
+ process.stderr.write(
17199
+ t("progressEmbedHeartbeat", {
17200
+ done: p.chunksDone,
17201
+ total: p.chunksTotal ?? "?"
17202
+ })
17203
+ );
17204
+ }
17205
+ },
17206
+ clear() {
17207
+ }
17208
+ };
17209
+ }
17210
+ function makeTtyWriter() {
17211
+ let status2 = t("progressStarting");
17212
+ let lastLineLen = 0;
17213
+ let frameIdx = 0;
17214
+ const startTs = Date.now();
17215
+ const repaint = () => {
17216
+ const frame = SPINNER_FRAMES2[frameIdx % SPINNER_FRAMES2.length];
17217
+ frameIdx++;
17218
+ const elapsed = ((Date.now() - startTs) / 1e3).toFixed(1);
17219
+ const line = `${frame} ${status2} ${elapsed}s`;
17220
+ const padded = line + " ".repeat(Math.max(0, lastLineLen - line.length));
17221
+ process.stderr.write(`\r${padded}`);
17222
+ lastLineLen = line.length;
17223
+ };
17224
+ repaint();
17225
+ const interval = setInterval(repaint, SPINNER_INTERVAL_MS);
17226
+ return {
17227
+ update(p) {
17228
+ if (p.phase === "scan") {
17229
+ status2 = t("progressScan", { files: p.filesScanned ?? 0 });
17230
+ } else if (p.phase === "embed") {
17231
+ const done = p.chunksDone ?? 0;
17232
+ const total = p.chunksTotal ?? 0;
17233
+ const pct2 = total > 0 ? (done / total * 100).toFixed(0) : "0";
17234
+ status2 = t("progressEmbed", { done, total, pct: pct2 });
17235
+ }
17236
+ repaint();
17237
+ },
17238
+ clear() {
17239
+ clearInterval(interval);
17240
+ if (lastLineLen > 0) {
17241
+ process.stderr.write(`\r${" ".repeat(lastLineLen)}\r`);
17242
+ lastLineLen = 0;
17243
+ }
17244
+ }
17245
+ };
17246
+ }
17247
+
16021
17248
  // src/cli/commands/mcp-inspect.ts
16022
17249
  async function mcpInspectCommand(opts) {
16023
17250
  const spec = parseMcpSpec(opts.spec);
@@ -16064,9 +17291,9 @@ function formatSection(title, section, render5) {
16064
17291
  for (const item of section.items) lines.push(` ${render5(item)}`);
16065
17292
  return lines.join("\n");
16066
17293
  }
16067
- function toolLine(t) {
16068
- const desc = t.description ? ` \u2014 ${oneLine(t.description, 80)}` : "";
16069
- return `\xB7 ${t.name}${desc}`;
17294
+ function toolLine(t2) {
17295
+ const desc = t2.description ? ` \u2014 ${oneLine(t2.description, 80)}` : "";
17296
+ return `\xB7 ${t2.name}${desc}`;
16070
17297
  }
16071
17298
  function resourceLine(r) {
16072
17299
  const mime = r.mimeType ? ` [${r.mimeType}]` : "";
@@ -16306,12 +17533,12 @@ function oneLine2(s, max = 200) {
16306
17533
  }
16307
17534
 
16308
17535
  // src/cli/commands/run.ts
16309
- import { stdin as stdin2, stdout } from "process";
16310
- import { createInterface } from "readline/promises";
17536
+ import { stdin as stdin3, stdout as stdout2 } from "process";
17537
+ import { createInterface as createInterface2 } from "readline/promises";
16311
17538
  async function ensureApiKey() {
16312
17539
  const existing = loadApiKey();
16313
17540
  if (existing) return existing;
16314
- if (!stdin2.isTTY) {
17541
+ if (!stdin3.isTTY) {
16315
17542
  process.stderr.write(
16316
17543
  "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
17544
  );
@@ -16320,7 +17547,7 @@ async function ensureApiKey() {
16320
17547
  process.stdout.write(
16321
17548
  "DeepSeek API key not configured.\nGet one at https://platform.deepseek.com/api_keys\n"
16322
17549
  );
16323
- const rl = createInterface({ input: stdin2, output: stdout });
17550
+ const rl = createInterface2({ input: stdin3, output: stdout2 });
16324
17551
  try {
16325
17552
  while (true) {
16326
17553
  const answer = (await rl.question("API key \u203A ")).trim();
@@ -16471,14 +17698,14 @@ function listAll() {
16471
17698
  console.log("Resume: reasonix chat --session <name>");
16472
17699
  }
16473
17700
  function inspectSession(name, verbose) {
16474
- const path = sessionPath(name);
17701
+ const path5 = sessionPath(name);
16475
17702
  const messages = loadSessionMessages(name);
16476
17703
  if (messages.length === 0) {
16477
17704
  console.error(`no session named "${name}" (or it's empty).`);
16478
- console.error(`looked at: ${path}`);
17705
+ console.error(`looked at: ${path5}`);
16479
17706
  process.exit(1);
16480
17707
  }
16481
- console.log(`[session] ${name} ${messages.length} messages ${path}`);
17708
+ console.log(`[session] ${name} ${messages.length} messages ${path5}`);
16482
17709
  console.log("");
16483
17710
  let turnIndex = 0;
16484
17711
  for (const msg of messages) {
@@ -16822,7 +18049,7 @@ async function setupCommand(_opts = {}) {
16822
18049
  }
16823
18050
 
16824
18051
  // src/cli/commands/update.ts
16825
- import { spawn as spawn5 } from "child_process";
18052
+ import { spawn as spawn6 } from "child_process";
16826
18053
  function planUpdate(input) {
16827
18054
  const diff = compareVersions(input.current, input.latest);
16828
18055
  if (diff > 0) {
@@ -16852,13 +18079,13 @@ function planUpdate(input) {
16852
18079
  };
16853
18080
  }
16854
18081
  function defaultSpawn(argv) {
16855
- return new Promise((resolve8, reject) => {
16856
- const child = spawn5(argv[0], argv.slice(1), {
18082
+ return new Promise((resolve9, reject) => {
18083
+ const child = spawn6(argv[0], argv.slice(1), {
16857
18084
  stdio: "inherit",
16858
18085
  shell: process.platform === "win32"
16859
18086
  });
16860
18087
  child.once("error", reject);
16861
- child.once("exit", (code) => resolve8(code ?? 1));
18088
+ child.once("exit", (code) => resolve9(code ?? 1));
16862
18089
  });
16863
18090
  }
16864
18091
  async function updateCommand(opts = {}) {
@@ -17154,6 +18381,16 @@ program.command("update").description(
17154
18381
  ).option("--dry-run", "Print the plan without executing the install").action(async (opts) => {
17155
18382
  await updateCommand({ dryRun: !!opts.dryRun });
17156
18383
  });
18384
+ program.command("index").description(
18385
+ "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."
18386
+ ).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(
18387
+ "-y, --yes",
18388
+ "Skip preflight prompts \u2014 auto-start the daemon and pull the model if missing (use in scripts)"
18389
+ ).action(
18390
+ async (opts) => {
18391
+ await indexCommand(opts);
18392
+ }
18393
+ );
17157
18394
  program.parseAsync(process.argv).catch((err) => {
17158
18395
  console.error(err);
17159
18396
  process.exit(1);