reasonix 0.16.1 → 0.17.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
@@ -21,6 +21,127 @@ import { Command } from "commander";
21
21
  import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "fs";
22
22
  import { homedir } from "os";
23
23
  import { dirname, join } from "path";
24
+
25
+ // src/index/config.ts
26
+ import picomatch from "picomatch";
27
+ var DEFAULT_INDEX_EXCLUDES = {
28
+ dirs: [
29
+ "node_modules",
30
+ ".git",
31
+ ".hg",
32
+ ".svn",
33
+ "dist",
34
+ "build",
35
+ "out",
36
+ ".next",
37
+ ".nuxt",
38
+ "target",
39
+ ".venv",
40
+ "venv",
41
+ "__pycache__",
42
+ ".pytest_cache",
43
+ ".mypy_cache",
44
+ ".cache",
45
+ "coverage",
46
+ ".turbo",
47
+ ".vercel",
48
+ ".reasonix"
49
+ ],
50
+ files: [
51
+ "package-lock.json",
52
+ "yarn.lock",
53
+ "pnpm-lock.yaml",
54
+ "Cargo.lock",
55
+ "poetry.lock",
56
+ "Pipfile.lock",
57
+ "go.sum",
58
+ ".DS_Store"
59
+ ],
60
+ exts: [
61
+ ".png",
62
+ ".jpg",
63
+ ".jpeg",
64
+ ".gif",
65
+ ".webp",
66
+ ".bmp",
67
+ ".ico",
68
+ ".tiff",
69
+ ".woff",
70
+ ".woff2",
71
+ ".ttf",
72
+ ".otf",
73
+ ".eot",
74
+ ".zip",
75
+ ".tar",
76
+ ".gz",
77
+ ".bz2",
78
+ ".xz",
79
+ ".rar",
80
+ ".7z",
81
+ ".exe",
82
+ ".dll",
83
+ ".so",
84
+ ".dylib",
85
+ ".bin",
86
+ ".class",
87
+ ".jar",
88
+ ".war",
89
+ ".wasm",
90
+ ".o",
91
+ ".obj",
92
+ ".lib",
93
+ ".a",
94
+ ".pyc",
95
+ ".pyo",
96
+ ".mp3",
97
+ ".mp4",
98
+ ".wav",
99
+ ".ogg",
100
+ ".webm",
101
+ ".mov",
102
+ ".avi",
103
+ ".pdf",
104
+ ".sqlite",
105
+ ".db"
106
+ ]
107
+ };
108
+ var DEFAULT_MAX_FILE_BYTES = 256 * 1024;
109
+ var DEFAULT_RESPECT_GITIGNORE = true;
110
+ function defaultIndexConfig() {
111
+ return {
112
+ excludeDirs: [...DEFAULT_INDEX_EXCLUDES.dirs],
113
+ excludeFiles: [...DEFAULT_INDEX_EXCLUDES.files],
114
+ excludeExts: [...DEFAULT_INDEX_EXCLUDES.exts],
115
+ excludePatterns: [],
116
+ respectGitignore: DEFAULT_RESPECT_GITIGNORE,
117
+ maxFileBytes: DEFAULT_MAX_FILE_BYTES
118
+ };
119
+ }
120
+ function resolveIndexConfig(user) {
121
+ const d = defaultIndexConfig();
122
+ if (!user) return d;
123
+ return {
124
+ excludeDirs: Array.isArray(user.excludeDirs) ? [...user.excludeDirs] : d.excludeDirs,
125
+ excludeFiles: Array.isArray(user.excludeFiles) ? [...user.excludeFiles] : d.excludeFiles,
126
+ excludeExts: Array.isArray(user.excludeExts) ? user.excludeExts.map((e) => e.toLowerCase()) : d.excludeExts,
127
+ excludePatterns: Array.isArray(user.excludePatterns) ? [...user.excludePatterns] : [],
128
+ respectGitignore: typeof user.respectGitignore === "boolean" ? user.respectGitignore : d.respectGitignore,
129
+ maxFileBytes: typeof user.maxFileBytes === "number" && user.maxFileBytes > 0 ? user.maxFileBytes : d.maxFileBytes
130
+ };
131
+ }
132
+ function compileFilters(cfg) {
133
+ const matcher = cfg.excludePatterns.length === 0 ? () => false : picomatch(cfg.excludePatterns, { dot: true });
134
+ return {
135
+ dirSet: new Set(cfg.excludeDirs),
136
+ fileSet: new Set(cfg.excludeFiles),
137
+ extSet: new Set(cfg.excludeExts.map((e) => e.toLowerCase())),
138
+ patternMatch: matcher,
139
+ respectGitignore: cfg.respectGitignore,
140
+ maxFileBytes: cfg.maxFileBytes
141
+ };
142
+ }
143
+
144
+ // src/config.ts
24
145
  function defaultConfigPath() {
25
146
  return join(homedir(), ".reasonix", "config.json");
26
147
  }
@@ -116,6 +237,12 @@ function saveReasoningEffort(effort2, path5 = defaultConfigPath()) {
116
237
  cfg.reasoningEffort = effort2;
117
238
  writeConfig(cfg, path5);
118
239
  }
240
+ function loadIndexUserConfig(path5 = defaultConfigPath()) {
241
+ return readConfig(path5).index ?? {};
242
+ }
243
+ function loadIndexConfig(path5 = defaultConfigPath()) {
244
+ return resolveIndexConfig(readConfig(path5).index);
245
+ }
119
246
  function markEditModeHintShown(path5 = defaultConfigPath()) {
120
247
  const cfg = readConfig(path5);
121
248
  if (cfg.editModeHintShown === true) return;
@@ -3090,7 +3217,7 @@ function listFilesSync(root, opts = {}) {
3090
3217
  }
3091
3218
  function listFilesWithStatsSync(root, opts = {}) {
3092
3219
  const maxResults = Math.max(1, opts.maxResults ?? 500);
3093
- const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3220
+ const ignore2 = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3094
3221
  const rootAbs = resolve(root);
3095
3222
  const out = [];
3096
3223
  const walk3 = (dirAbs, dirRel) => {
@@ -3106,7 +3233,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3106
3233
  if (out.length >= maxResults) return;
3107
3234
  const relPath = dirRel ? `${dirRel}/${ent.name}` : ent.name;
3108
3235
  if (ent.isDirectory()) {
3109
- if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3236
+ if (ent.name.startsWith(".") || ignore2.has(ent.name)) continue;
3110
3237
  walk3(join5(dirAbs, ent.name), relPath);
3111
3238
  } else if (ent.isFile()) {
3112
3239
  let mtimeMs = 0;
@@ -3123,7 +3250,7 @@ function listFilesWithStatsSync(root, opts = {}) {
3123
3250
  }
3124
3251
  async function listFilesWithStatsAsync(root, opts = {}) {
3125
3252
  const maxResults = Math.max(1, opts.maxResults ?? 500);
3126
- const ignore = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3253
+ const ignore2 = new Set(opts.ignoreDirs ?? DEFAULT_PICKER_IGNORE_DIRS);
3127
3254
  const rootAbs = resolve(root);
3128
3255
  const out = [];
3129
3256
  const walk3 = async (dirAbs, dirRel) => {
@@ -3139,7 +3266,7 @@ async function listFilesWithStatsAsync(root, opts = {}) {
3139
3266
  for (const ent of entries) {
3140
3267
  if (out.length >= maxResults) break;
3141
3268
  if (ent.isDirectory()) {
3142
- if (ent.name.startsWith(".") || ignore.has(ent.name)) continue;
3269
+ if (ent.name.startsWith(".") || ignore2.has(ent.name)) continue;
3143
3270
  if (fileEnts.length > 0) {
3144
3271
  await statBatch(fileEnts, dirAbs, dirRel, out, maxResults);
3145
3272
  fileEnts.length = 0;
@@ -3420,69 +3547,8 @@ var DEFAULT_MAX_LIST_BYTES = 256 * 1024;
3420
3547
  var DEFAULT_AUTO_PREVIEW_LINES = 200;
3421
3548
  var AUTO_PREVIEW_HEAD_LINES = 80;
3422
3549
  var AUTO_PREVIEW_TAIL_LINES = 40;
3423
- var SKIP_DIR_NAMES = /* @__PURE__ */ new Set([
3424
- "node_modules",
3425
- ".git",
3426
- ".hg",
3427
- ".svn",
3428
- "dist",
3429
- "build",
3430
- "out",
3431
- ".next",
3432
- ".nuxt",
3433
- "target",
3434
- // Rust / Java
3435
- ".venv",
3436
- "venv",
3437
- "__pycache__",
3438
- ".pytest_cache",
3439
- ".mypy_cache",
3440
- ".cache",
3441
- "coverage"
3442
- ]);
3443
- var BINARY_EXTENSIONS = /* @__PURE__ */ new Set([
3444
- ".png",
3445
- ".jpg",
3446
- ".jpeg",
3447
- ".gif",
3448
- ".bmp",
3449
- ".ico",
3450
- ".webp",
3451
- ".tiff",
3452
- ".pdf",
3453
- ".zip",
3454
- ".tar",
3455
- ".gz",
3456
- ".bz2",
3457
- ".xz",
3458
- ".7z",
3459
- ".rar",
3460
- ".exe",
3461
- ".dll",
3462
- ".so",
3463
- ".dylib",
3464
- ".bin",
3465
- ".class",
3466
- ".jar",
3467
- ".war",
3468
- ".o",
3469
- ".obj",
3470
- ".lib",
3471
- ".a",
3472
- ".woff",
3473
- ".woff2",
3474
- ".ttf",
3475
- ".otf",
3476
- ".eot",
3477
- ".mp3",
3478
- ".mp4",
3479
- ".mov",
3480
- ".avi",
3481
- ".webm",
3482
- ".wasm",
3483
- ".pyc",
3484
- ".pyo"
3485
- ]);
3550
+ var SKIP_DIR_NAMES = new Set(DEFAULT_INDEX_EXCLUDES.dirs);
3551
+ var BINARY_EXTENSIONS = new Set(DEFAULT_INDEX_EXCLUDES.exts);
3486
3552
  function isLikelyBinaryByName(name) {
3487
3553
  const dot2 = name.lastIndexOf(".");
3488
3554
  if (dot2 < 0) return false;
@@ -8389,7 +8455,193 @@ async function handleHooks(method, rest, body, ctx) {
8389
8455
  return { status: 405, body: { error: `method ${method} not supported on this path` } };
8390
8456
  }
8391
8457
 
8392
- // src/server/api/mcp.ts
8458
+ // src/index/semantic/chunker.ts
8459
+ import { promises as fs2 } from "fs";
8460
+ import path from "path";
8461
+ import ignore from "ignore";
8462
+ var DEFAULT_MAX_CHUNK_CHARS = 4e3;
8463
+ function chunkText(text2, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
8464
+ const lines = text2.split(/\r?\n/);
8465
+ if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
8466
+ const stride = Math.max(1, windowLines - overlap);
8467
+ const chunks = [];
8468
+ for (let start = 0; start < lines.length; start += stride) {
8469
+ const end = Math.min(lines.length, start + windowLines);
8470
+ const slice2 = lines.slice(start, end).join("\n").trim();
8471
+ if (slice2.length === 0) {
8472
+ if (end >= lines.length) break;
8473
+ continue;
8474
+ }
8475
+ const window = {
8476
+ path: filePath,
8477
+ startLine: start + 1,
8478
+ endLine: end,
8479
+ text: slice2
8480
+ };
8481
+ for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
8482
+ if (end >= lines.length) break;
8483
+ }
8484
+ return chunks;
8485
+ }
8486
+ function safeSplit(chunk, maxChars) {
8487
+ if (chunk.text.length <= maxChars) return [chunk];
8488
+ const lines = chunk.text.split("\n");
8489
+ const out = [];
8490
+ let bufLines = [];
8491
+ let bufStart = chunk.startLine;
8492
+ let bufLen = 0;
8493
+ const flush = (untilLineNo) => {
8494
+ if (bufLines.length === 0) return;
8495
+ out.push({
8496
+ path: chunk.path,
8497
+ startLine: bufStart,
8498
+ endLine: untilLineNo,
8499
+ text: bufLines.join("\n")
8500
+ });
8501
+ bufLines = [];
8502
+ bufLen = 0;
8503
+ };
8504
+ for (let i = 0; i < lines.length; i++) {
8505
+ const line = lines[i] ?? "";
8506
+ const lineLen = line.length + 1;
8507
+ if (lineLen > maxChars) {
8508
+ flush(chunk.startLine + i - 1);
8509
+ out.push({
8510
+ path: chunk.path,
8511
+ startLine: chunk.startLine + i,
8512
+ endLine: chunk.startLine + i,
8513
+ text: line.slice(0, maxChars)
8514
+ });
8515
+ bufStart = chunk.startLine + i + 1;
8516
+ continue;
8517
+ }
8518
+ if (bufLen + lineLen > maxChars && bufLines.length > 0) {
8519
+ flush(chunk.startLine + i - 1);
8520
+ bufStart = chunk.startLine + i;
8521
+ }
8522
+ bufLines.push(line);
8523
+ bufLen += lineLen;
8524
+ }
8525
+ flush(chunk.endLine);
8526
+ return out;
8527
+ }
8528
+ async function loadGitignoreAt(dirAbs) {
8529
+ try {
8530
+ const text2 = await fs2.readFile(path.join(dirAbs, ".gitignore"), "utf8");
8531
+ return ignore().add(text2);
8532
+ } catch {
8533
+ return null;
8534
+ }
8535
+ }
8536
+ function toForwardRel(root, abs) {
8537
+ return path.relative(root, abs).split(path.sep).join("/");
8538
+ }
8539
+ function ignoredByLayers(layers, abs, isDir) {
8540
+ for (const layer of layers) {
8541
+ const rel = path.relative(layer.dirAbs, abs).split(path.sep).join("/");
8542
+ if (!rel || rel.startsWith("..")) continue;
8543
+ if (layer.ig.ignores(isDir ? `${rel}/` : rel)) return true;
8544
+ }
8545
+ return false;
8546
+ }
8547
+ async function* walkChunks(root, opts = {}) {
8548
+ const windowLines = opts.windowLines ?? 60;
8549
+ const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
8550
+ const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
8551
+ const filters = compileFilters(opts.config ?? defaultIndexConfig());
8552
+ const onSkip = opts.onSkip ?? (() => {
8553
+ });
8554
+ const initial = [];
8555
+ if (filters.respectGitignore) {
8556
+ const rootIg = await loadGitignoreAt(root);
8557
+ if (rootIg) initial.push({ dirAbs: root, ig: rootIg });
8558
+ }
8559
+ const stack = [{ dir: root, layers: initial }];
8560
+ while (stack.length > 0) {
8561
+ const frame = stack.pop();
8562
+ if (!frame) break;
8563
+ const { dir, layers } = frame;
8564
+ let entries;
8565
+ try {
8566
+ entries = await fs2.readdir(dir, { withFileTypes: true });
8567
+ } catch {
8568
+ continue;
8569
+ }
8570
+ for (const entry of entries) {
8571
+ const name = entry.name;
8572
+ const abs = path.join(dir, name);
8573
+ const rel = toForwardRel(root, abs);
8574
+ if (entry.isDirectory()) {
8575
+ if (filters.dirSet.has(name)) {
8576
+ onSkip(rel, "defaultDir");
8577
+ continue;
8578
+ }
8579
+ if (filters.respectGitignore && ignoredByLayers(layers, abs, true)) {
8580
+ onSkip(rel, "gitignore");
8581
+ continue;
8582
+ }
8583
+ if (filters.patternMatch(`${rel}/`) || filters.patternMatch(rel)) {
8584
+ onSkip(rel, "pattern");
8585
+ continue;
8586
+ }
8587
+ const childLayers = filters.respectGitignore ? await extendLayers(layers, abs) : layers;
8588
+ stack.push({ dir: abs, layers: childLayers });
8589
+ continue;
8590
+ }
8591
+ if (!entry.isFile()) continue;
8592
+ if (filters.fileSet.has(name)) {
8593
+ onSkip(rel, "defaultFile");
8594
+ continue;
8595
+ }
8596
+ const ext = path.extname(name).toLowerCase();
8597
+ if (filters.extSet.has(ext)) {
8598
+ onSkip(rel, "binaryExt");
8599
+ continue;
8600
+ }
8601
+ if (filters.respectGitignore && ignoredByLayers(layers, abs, false)) {
8602
+ onSkip(rel, "gitignore");
8603
+ continue;
8604
+ }
8605
+ if (filters.patternMatch(rel)) {
8606
+ onSkip(rel, "pattern");
8607
+ continue;
8608
+ }
8609
+ let stat2;
8610
+ try {
8611
+ stat2 = await fs2.stat(abs);
8612
+ } catch {
8613
+ onSkip(rel, "readError");
8614
+ continue;
8615
+ }
8616
+ if (stat2.size > filters.maxFileBytes) {
8617
+ onSkip(rel, "tooLarge");
8618
+ continue;
8619
+ }
8620
+ let text2;
8621
+ try {
8622
+ text2 = await fs2.readFile(abs, "utf8");
8623
+ } catch {
8624
+ onSkip(rel, "readError");
8625
+ continue;
8626
+ }
8627
+ if (text2.indexOf("\0") !== -1) {
8628
+ onSkip(rel, "binaryContent");
8629
+ continue;
8630
+ }
8631
+ for (const chunk of chunkText(text2, rel, windowLines, overlap, maxChunkChars)) {
8632
+ yield chunk;
8633
+ }
8634
+ }
8635
+ }
8636
+ }
8637
+ async function extendLayers(layers, dirAbs) {
8638
+ const ig = await loadGitignoreAt(dirAbs);
8639
+ return ig ? [...layers, { dirAbs, ig }] : layers;
8640
+ }
8641
+
8642
+ // src/server/api/index-config.ts
8643
+ var PREVIEW_INCLUDED_CAP = 50;
8644
+ var PREVIEW_PER_REASON_CAP = 10;
8393
8645
  function parseBody4(raw) {
8394
8646
  if (!raw) return {};
8395
8647
  try {
@@ -8399,6 +8651,165 @@ function parseBody4(raw) {
8399
8651
  return {};
8400
8652
  }
8401
8653
  }
8654
+ function isStringArray(v) {
8655
+ return Array.isArray(v) && v.every((x) => typeof x === "string");
8656
+ }
8657
+ async function handleIndexConfig(method, rest, body, ctx) {
8658
+ if (rest[0] === "preview" && method === "POST") {
8659
+ return await handlePreview(body, ctx);
8660
+ }
8661
+ if (method === "GET") {
8662
+ const user = loadIndexUserConfig(ctx.configPath);
8663
+ const resolved = resolveIndexConfig(user);
8664
+ return {
8665
+ status: 200,
8666
+ body: {
8667
+ user,
8668
+ resolved,
8669
+ defaults: {
8670
+ excludeDirs: [...DEFAULT_INDEX_EXCLUDES.dirs],
8671
+ excludeFiles: [...DEFAULT_INDEX_EXCLUDES.files],
8672
+ excludeExts: [...DEFAULT_INDEX_EXCLUDES.exts],
8673
+ excludePatterns: [],
8674
+ respectGitignore: DEFAULT_RESPECT_GITIGNORE,
8675
+ maxFileBytes: DEFAULT_MAX_FILE_BYTES
8676
+ }
8677
+ }
8678
+ };
8679
+ }
8680
+ if (method === "POST") {
8681
+ const fields = parseBody4(body);
8682
+ const next = {};
8683
+ const changed = [];
8684
+ if (fields.excludeDirs !== void 0) {
8685
+ if (!isStringArray(fields.excludeDirs)) {
8686
+ return { status: 400, body: { error: "excludeDirs must be string[]" } };
8687
+ }
8688
+ next.excludeDirs = fields.excludeDirs;
8689
+ changed.push("excludeDirs");
8690
+ }
8691
+ if (fields.excludeFiles !== void 0) {
8692
+ if (!isStringArray(fields.excludeFiles)) {
8693
+ return { status: 400, body: { error: "excludeFiles must be string[]" } };
8694
+ }
8695
+ next.excludeFiles = fields.excludeFiles;
8696
+ changed.push("excludeFiles");
8697
+ }
8698
+ if (fields.excludeExts !== void 0) {
8699
+ if (!isStringArray(fields.excludeExts)) {
8700
+ return { status: 400, body: { error: "excludeExts must be string[]" } };
8701
+ }
8702
+ next.excludeExts = fields.excludeExts;
8703
+ changed.push("excludeExts");
8704
+ }
8705
+ if (fields.excludePatterns !== void 0) {
8706
+ if (!isStringArray(fields.excludePatterns)) {
8707
+ return { status: 400, body: { error: "excludePatterns must be string[]" } };
8708
+ }
8709
+ next.excludePatterns = fields.excludePatterns;
8710
+ changed.push("excludePatterns");
8711
+ }
8712
+ if (fields.respectGitignore !== void 0) {
8713
+ if (typeof fields.respectGitignore !== "boolean") {
8714
+ return { status: 400, body: { error: "respectGitignore must be boolean" } };
8715
+ }
8716
+ next.respectGitignore = fields.respectGitignore;
8717
+ changed.push("respectGitignore");
8718
+ }
8719
+ if (fields.maxFileBytes !== void 0) {
8720
+ if (typeof fields.maxFileBytes !== "number" || fields.maxFileBytes <= 0) {
8721
+ return { status: 400, body: { error: "maxFileBytes must be a positive number" } };
8722
+ }
8723
+ next.maxFileBytes = fields.maxFileBytes;
8724
+ changed.push("maxFileBytes");
8725
+ }
8726
+ const cfg = readConfig(ctx.configPath);
8727
+ cfg.index = { ...cfg.index ?? {}, ...next };
8728
+ writeConfig(cfg, ctx.configPath);
8729
+ if (changed.length > 0) {
8730
+ ctx.audit?.({ ts: Date.now(), action: "set-index-config", payload: { fields: changed } });
8731
+ }
8732
+ return { status: 200, body: { changed, resolved: resolveIndexConfig(cfg.index) } };
8733
+ }
8734
+ return { status: 405, body: { error: "GET or POST only" } };
8735
+ }
8736
+ async function handlePreview(body, ctx) {
8737
+ const root = ctx.getCurrentCwd?.();
8738
+ if (!root) {
8739
+ return {
8740
+ status: 400,
8741
+ body: { error: "preview requires a code-mode session (no project root attached)" }
8742
+ };
8743
+ }
8744
+ const fields = parseBody4(body);
8745
+ const draft = {};
8746
+ if (isStringArray(fields.excludeDirs)) draft.excludeDirs = fields.excludeDirs;
8747
+ if (isStringArray(fields.excludeFiles)) draft.excludeFiles = fields.excludeFiles;
8748
+ if (isStringArray(fields.excludeExts)) draft.excludeExts = fields.excludeExts;
8749
+ if (isStringArray(fields.excludePatterns)) draft.excludePatterns = fields.excludePatterns;
8750
+ if (typeof fields.respectGitignore === "boolean")
8751
+ draft.respectGitignore = fields.respectGitignore;
8752
+ if (typeof fields.maxFileBytes === "number" && fields.maxFileBytes > 0) {
8753
+ draft.maxFileBytes = fields.maxFileBytes;
8754
+ }
8755
+ const resolved = resolveIndexConfig(draft);
8756
+ const skipBuckets = {
8757
+ defaultDir: 0,
8758
+ defaultFile: 0,
8759
+ binaryExt: 0,
8760
+ binaryContent: 0,
8761
+ tooLarge: 0,
8762
+ gitignore: 0,
8763
+ pattern: 0,
8764
+ readError: 0
8765
+ };
8766
+ const skipSamples = {
8767
+ defaultDir: [],
8768
+ defaultFile: [],
8769
+ binaryExt: [],
8770
+ binaryContent: [],
8771
+ tooLarge: [],
8772
+ gitignore: [],
8773
+ pattern: [],
8774
+ readError: []
8775
+ };
8776
+ const includedFiles = /* @__PURE__ */ new Set();
8777
+ const sampleIncluded = [];
8778
+ for await (const chunk of walkChunks(root, {
8779
+ config: resolved,
8780
+ onSkip: (rel, reason) => {
8781
+ skipBuckets[reason]++;
8782
+ const bucket = skipSamples[reason];
8783
+ if (bucket.length < PREVIEW_PER_REASON_CAP) bucket.push(rel);
8784
+ }
8785
+ })) {
8786
+ if (!includedFiles.has(chunk.path)) {
8787
+ includedFiles.add(chunk.path);
8788
+ if (sampleIncluded.length < PREVIEW_INCLUDED_CAP) sampleIncluded.push(chunk.path);
8789
+ }
8790
+ }
8791
+ return {
8792
+ status: 200,
8793
+ body: {
8794
+ filesIncluded: includedFiles.size,
8795
+ sampleIncluded,
8796
+ skipBuckets,
8797
+ skipSamples,
8798
+ resolved
8799
+ }
8800
+ };
8801
+ }
8802
+
8803
+ // src/server/api/mcp.ts
8804
+ function parseBody5(raw) {
8805
+ if (!raw) return {};
8806
+ try {
8807
+ const parsed = JSON.parse(raw);
8808
+ return typeof parsed === "object" && parsed !== null ? parsed : {};
8809
+ } catch {
8810
+ return {};
8811
+ }
8812
+ }
8402
8813
  async function handleMcp(method, rest, body, ctx) {
8403
8814
  if (method === "GET" && rest.length === 0) {
8404
8815
  const servers = (ctx.mcpServers ?? []).map((s) => ({
@@ -8427,7 +8838,7 @@ async function handleMcp(method, rest, body, ctx) {
8427
8838
  return { status: 200, body: { specs: cfg.mcp ?? [] } };
8428
8839
  }
8429
8840
  if (method === "POST" && rest[0] === "specs") {
8430
- const { spec } = parseBody4(body);
8841
+ const { spec } = parseBody5(body);
8431
8842
  if (typeof spec !== "string" || !spec.trim()) {
8432
8843
  return { status: 400, body: { error: "spec (non-empty string) required" } };
8433
8844
  }
@@ -8442,7 +8853,7 @@ async function handleMcp(method, rest, body, ctx) {
8442
8853
  return { status: 200, body: { added: true, requiresRestart: !ctx.reloadMcp } };
8443
8854
  }
8444
8855
  if (method === "DELETE" && rest[0] === "specs") {
8445
- const { spec } = parseBody4(body);
8856
+ const { spec } = parseBody5(body);
8446
8857
  if (typeof spec !== "string") {
8447
8858
  return { status: 400, body: { error: "spec (string) required" } };
8448
8859
  }
@@ -8475,7 +8886,7 @@ async function handleMcp(method, rest, body, ctx) {
8475
8886
  body: { error: "MCP invocation requires an attached session." }
8476
8887
  };
8477
8888
  }
8478
- const { server, tool: tool2, args } = parseBody4(body);
8889
+ const { server, tool: tool2, args } = parseBody5(body);
8479
8890
  if (typeof server !== "string" || typeof tool2 !== "string") {
8480
8891
  return { status: 400, body: { error: "server + tool (strings) required" } };
8481
8892
  }
@@ -8515,7 +8926,7 @@ function globalMemoryDir() {
8515
8926
  function projectMemoryDir(rootDir) {
8516
8927
  return join14(homedir7(), ".reasonix", "memory", projectHash2(rootDir));
8517
8928
  }
8518
- function parseBody5(raw) {
8929
+ function parseBody6(raw) {
8519
8930
  if (!raw) return {};
8520
8931
  try {
8521
8932
  const parsed = JSON.parse(raw);
@@ -8585,7 +8996,7 @@ async function handleMemory(method, rest, body, ctx) {
8585
8996
  return { status: 400, body: { error: "bad scope or name" } };
8586
8997
  }
8587
8998
  if (method === "POST") {
8588
- const { body: contents } = parseBody5(body);
8999
+ const { body: contents } = parseBody6(body);
8589
9000
  if (typeof contents !== "string") {
8590
9001
  return { status: 400, body: { error: "body (string) required" } };
8591
9002
  }
@@ -8651,7 +9062,7 @@ async function handleMessages(method, _rest, _body, ctx) {
8651
9062
  }
8652
9063
 
8653
9064
  // src/server/api/modal.ts
8654
- function parseBody6(raw) {
9065
+ function parseBody7(raw) {
8655
9066
  if (!raw) return {};
8656
9067
  try {
8657
9068
  const parsed = JSON.parse(raw);
@@ -8668,7 +9079,7 @@ async function handleModal(method, rest, body, ctx) {
8668
9079
  };
8669
9080
  }
8670
9081
  if (method === "POST" && rest[0] === "resolve") {
8671
- const { kind, choice, text: text2 } = parseBody6(body);
9082
+ const { kind, choice, text: text2 } = parseBody7(body);
8672
9083
  if (kind === "shell") {
8673
9084
  if (!ctx.resolveShellConfirm) {
8674
9085
  return { status: 503, body: { error: "shell modal resolution not wired" } };
@@ -8769,199 +9180,6 @@ async function handleModal(method, rest, body, ctx) {
8769
9180
  import { promises as fs4 } from "fs";
8770
9181
  import path3 from "path";
8771
9182
 
8772
- // src/index/semantic/chunker.ts
8773
- import { promises as fs2 } from "fs";
8774
- import path from "path";
8775
- var DEFAULT_MAX_CHUNK_CHARS = 4e3;
8776
- var SKIP_DIRS = /* @__PURE__ */ new Set([
8777
- "node_modules",
8778
- ".git",
8779
- "dist",
8780
- "build",
8781
- "out",
8782
- ".next",
8783
- ".nuxt",
8784
- "target",
8785
- ".venv",
8786
- "venv",
8787
- "__pycache__",
8788
- ".pytest_cache",
8789
- ".mypy_cache",
8790
- ".cache",
8791
- "coverage",
8792
- ".turbo",
8793
- ".vercel",
8794
- ".reasonix"
8795
- ]);
8796
- var SKIP_FILES = /* @__PURE__ */ new Set([
8797
- "package-lock.json",
8798
- "yarn.lock",
8799
- "pnpm-lock.yaml",
8800
- "Cargo.lock",
8801
- "poetry.lock",
8802
- "Pipfile.lock",
8803
- "go.sum",
8804
- ".DS_Store"
8805
- ]);
8806
- var BINARY_EXTS = /* @__PURE__ */ new Set([
8807
- // Images
8808
- ".png",
8809
- ".jpg",
8810
- ".jpeg",
8811
- ".gif",
8812
- ".webp",
8813
- ".bmp",
8814
- ".ico",
8815
- ".tiff",
8816
- // Fonts
8817
- ".woff",
8818
- ".woff2",
8819
- ".ttf",
8820
- ".otf",
8821
- ".eot",
8822
- // Archives / binaries
8823
- ".zip",
8824
- ".tar",
8825
- ".gz",
8826
- ".rar",
8827
- ".7z",
8828
- ".exe",
8829
- ".dll",
8830
- ".so",
8831
- ".dylib",
8832
- ".class",
8833
- ".jar",
8834
- ".wasm",
8835
- ".o",
8836
- ".a",
8837
- // Media
8838
- ".mp3",
8839
- ".mp4",
8840
- ".wav",
8841
- ".ogg",
8842
- ".webm",
8843
- ".mov",
8844
- // Other
8845
- ".pdf",
8846
- ".sqlite",
8847
- ".db"
8848
- ]);
8849
- function chunkText(text2, filePath, windowLines, overlap, maxChunkChars = DEFAULT_MAX_CHUNK_CHARS) {
8850
- const lines = text2.split(/\r?\n/);
8851
- if (lines.length === 0 || lines.length === 1 && lines[0] === "") return [];
8852
- const stride = Math.max(1, windowLines - overlap);
8853
- const chunks = [];
8854
- for (let start = 0; start < lines.length; start += stride) {
8855
- const end = Math.min(lines.length, start + windowLines);
8856
- const slice2 = lines.slice(start, end).join("\n").trim();
8857
- if (slice2.length === 0) {
8858
- if (end >= lines.length) break;
8859
- continue;
8860
- }
8861
- const window = {
8862
- path: filePath,
8863
- startLine: start + 1,
8864
- endLine: end,
8865
- text: slice2
8866
- };
8867
- for (const sub of safeSplit(window, maxChunkChars)) chunks.push(sub);
8868
- if (end >= lines.length) break;
8869
- }
8870
- return chunks;
8871
- }
8872
- function safeSplit(chunk, maxChars) {
8873
- if (chunk.text.length <= maxChars) return [chunk];
8874
- const lines = chunk.text.split("\n");
8875
- const out = [];
8876
- let bufLines = [];
8877
- let bufStart = chunk.startLine;
8878
- let bufLen = 0;
8879
- const flush = (untilLineNo) => {
8880
- if (bufLines.length === 0) return;
8881
- out.push({
8882
- path: chunk.path,
8883
- startLine: bufStart,
8884
- endLine: untilLineNo,
8885
- text: bufLines.join("\n")
8886
- });
8887
- bufLines = [];
8888
- bufLen = 0;
8889
- };
8890
- for (let i = 0; i < lines.length; i++) {
8891
- const line = lines[i] ?? "";
8892
- const lineLen = line.length + 1;
8893
- if (lineLen > maxChars) {
8894
- flush(chunk.startLine + i - 1);
8895
- out.push({
8896
- path: chunk.path,
8897
- startLine: chunk.startLine + i,
8898
- endLine: chunk.startLine + i,
8899
- text: line.slice(0, maxChars)
8900
- });
8901
- bufStart = chunk.startLine + i + 1;
8902
- continue;
8903
- }
8904
- if (bufLen + lineLen > maxChars && bufLines.length > 0) {
8905
- flush(chunk.startLine + i - 1);
8906
- bufStart = chunk.startLine + i;
8907
- }
8908
- bufLines.push(line);
8909
- bufLen += lineLen;
8910
- }
8911
- flush(chunk.endLine);
8912
- return out;
8913
- }
8914
- async function* walkChunks(root, opts = {}) {
8915
- const windowLines = opts.windowLines ?? 60;
8916
- const overlap = Math.min(opts.overlap ?? 12, Math.max(0, windowLines - 1));
8917
- const maxFileBytes = opts.maxFileBytes ?? 256 * 1024;
8918
- const maxChunkChars = opts.maxChunkChars ?? DEFAULT_MAX_CHUNK_CHARS;
8919
- const stack = [root];
8920
- while (stack.length > 0) {
8921
- const dir = stack.pop();
8922
- if (!dir) break;
8923
- let entries;
8924
- try {
8925
- entries = await fs2.readdir(dir, { withFileTypes: true });
8926
- } catch {
8927
- continue;
8928
- }
8929
- for (const entry of entries) {
8930
- const name = entry.name;
8931
- if (entry.isDirectory()) {
8932
- if (SKIP_DIRS.has(name) || name.startsWith(".")) {
8933
- if (SKIP_DIRS.has(name) || name === ".git") continue;
8934
- }
8935
- stack.push(path.join(dir, name));
8936
- continue;
8937
- }
8938
- if (!entry.isFile()) continue;
8939
- if (SKIP_FILES.has(name)) continue;
8940
- const ext = path.extname(name).toLowerCase();
8941
- if (BINARY_EXTS.has(ext)) continue;
8942
- const abs = path.join(dir, name);
8943
- let stat2;
8944
- try {
8945
- stat2 = await fs2.stat(abs);
8946
- } catch {
8947
- continue;
8948
- }
8949
- if (stat2.size > maxFileBytes) continue;
8950
- let text2;
8951
- try {
8952
- text2 = await fs2.readFile(abs, "utf8");
8953
- } catch {
8954
- continue;
8955
- }
8956
- if (text2.indexOf("\0") !== -1) continue;
8957
- const rel = path.relative(root, abs).split(path.sep).join("/");
8958
- for (const chunk of chunkText(text2, rel, windowLines, overlap, maxChunkChars)) {
8959
- yield chunk;
8960
- }
8961
- }
8962
- }
8963
- }
8964
-
8965
9183
  // src/index/semantic/embedding.ts
8966
9184
  var DEFAULT_OLLAMA_URL = "http://localhost:11434";
8967
9185
  var DEFAULT_EMBED_MODEL = "nomic-embed-text";
@@ -9278,6 +9496,18 @@ function deserializeEntry(line) {
9278
9496
 
9279
9497
  // src/index/semantic/builder.ts
9280
9498
  var INDEX_DIR_NAME = path3.join(".reasonix", "semantic");
9499
+ function emptyBuckets() {
9500
+ return {
9501
+ defaultDir: 0,
9502
+ defaultFile: 0,
9503
+ binaryExt: 0,
9504
+ binaryContent: 0,
9505
+ tooLarge: 0,
9506
+ gitignore: 0,
9507
+ pattern: 0,
9508
+ readError: 0
9509
+ };
9510
+ }
9281
9511
  async function buildIndex(root, opts = {}) {
9282
9512
  const t0 = Date.now();
9283
9513
  const indexDir = path3.join(root, INDEX_DIR_NAME);
@@ -9295,10 +9525,14 @@ async function buildIndex(root, opts = {}) {
9295
9525
  const fileChunks = /* @__PURE__ */ new Map();
9296
9526
  let filesScanned = 0;
9297
9527
  let filesSkipped = 0;
9528
+ const skipBuckets = emptyBuckets();
9298
9529
  for await (const chunk of walkChunks(root, {
9299
9530
  windowLines: opts.windowLines,
9300
9531
  overlap: opts.overlap,
9301
- maxFileBytes: opts.maxFileBytes
9532
+ config: opts.indexConfig ?? defaultIndexConfig(),
9533
+ onSkip: (_p, reason) => {
9534
+ skipBuckets[reason]++;
9535
+ }
9302
9536
  })) {
9303
9537
  seenPaths.add(chunk.path);
9304
9538
  let bucket = fileChunks.get(chunk.path);
@@ -9385,7 +9619,8 @@ async function buildIndex(root, opts = {}) {
9385
9619
  filesSkipped,
9386
9620
  filesChanged,
9387
9621
  chunksTotal,
9388
- chunksDone
9622
+ chunksDone,
9623
+ skipBuckets
9389
9624
  });
9390
9625
  return {
9391
9626
  filesScanned,
@@ -9393,6 +9628,7 @@ async function buildIndex(root, opts = {}) {
9393
9628
  chunksAdded,
9394
9629
  chunksRemoved: removed,
9395
9630
  chunksSkipped,
9631
+ skipBuckets,
9396
9632
  durationMs: Date.now() - t0
9397
9633
  };
9398
9634
  }
@@ -9444,7 +9680,7 @@ async function handleOverview(method, _rest, _body, ctx) {
9444
9680
  }
9445
9681
 
9446
9682
  // src/server/api/permissions.ts
9447
- function parseBody7(raw) {
9683
+ function parseBody8(raw) {
9448
9684
  if (!raw) return {};
9449
9685
  try {
9450
9686
  const parsed = JSON.parse(raw);
@@ -9476,7 +9712,7 @@ async function handlePermissions(method, rest, body, ctx) {
9476
9712
  };
9477
9713
  }
9478
9714
  if (method === "POST" && rest.length === 0) {
9479
- const { prefix } = parseBody7(body);
9715
+ const { prefix } = parseBody8(body);
9480
9716
  if (typeof prefix !== "string" || !prefix.trim()) {
9481
9717
  return { status: 400, body: { error: "prefix (string) required" } };
9482
9718
  }
@@ -9502,7 +9738,7 @@ async function handlePermissions(method, rest, body, ctx) {
9502
9738
  return { status: 200, body: { added: true, prefix: trimmed } };
9503
9739
  }
9504
9740
  if (method === "DELETE" && rest.length === 0) {
9505
- const { prefix } = parseBody7(body);
9741
+ const { prefix } = parseBody8(body);
9506
9742
  if (typeof prefix !== "string" || !prefix.trim()) {
9507
9743
  return { status: 400, body: { error: "prefix (string) required" } };
9508
9744
  }
@@ -9526,7 +9762,7 @@ async function handlePermissions(method, rest, body, ctx) {
9526
9762
  return { status: 200, body: { removed, prefix: trimmed } };
9527
9763
  }
9528
9764
  if (method === "POST" && rest[0] === "clear") {
9529
- const { confirm: confirm2 } = parseBody7(body);
9765
+ const { confirm: confirm2 } = parseBody8(body);
9530
9766
  if (confirm2 !== true) {
9531
9767
  return {
9532
9768
  status: 400,
@@ -9911,6 +10147,7 @@ async function runIndex(root, job, ctx) {
9911
10147
  try {
9912
10148
  const result = await buildIndex(root, {
9913
10149
  rebuild: job.rebuild,
10150
+ indexConfig: loadIndexConfig(ctx.configPath),
9914
10151
  onProgress: (p) => {
9915
10152
  job.phase = p.phase;
9916
10153
  if (p.filesScanned !== void 0) job.filesScanned = p.filesScanned;
@@ -10014,7 +10251,7 @@ async function handleSessions(method, rest, _body, _ctx) {
10014
10251
  }
10015
10252
 
10016
10253
  // src/server/api/settings.ts
10017
- function parseBody8(raw) {
10254
+ function parseBody9(raw) {
10018
10255
  if (!raw) return {};
10019
10256
  try {
10020
10257
  const parsed = JSON.parse(raw);
@@ -10052,7 +10289,7 @@ async function handleSettings(method, _rest, body, ctx) {
10052
10289
  };
10053
10290
  }
10054
10291
  if (method === "POST") {
10055
- const fields = parseBody8(body);
10292
+ const fields = parseBody9(body);
10056
10293
  const cfg = readConfig(ctx.configPath);
10057
10294
  const changed = [];
10058
10295
  if (fields.apiKey !== void 0) {
@@ -10115,7 +10352,7 @@ import {
10115
10352
  } from "fs";
10116
10353
  import { homedir as homedir8 } from "os";
10117
10354
  import { dirname as dirname15, join as join15 } from "path";
10118
- function parseBody9(raw) {
10355
+ function parseBody10(raw) {
10119
10356
  if (!raw) return {};
10120
10357
  try {
10121
10358
  const parsed = JSON.parse(raw);
@@ -10221,7 +10458,7 @@ async function handleSkills(method, rest, body, ctx) {
10221
10458
  return { status: 200, body: { path: skillPath, body: readFileSync18(skillPath, "utf8") } };
10222
10459
  }
10223
10460
  if (method === "POST") {
10224
- const { body: contents } = parseBody9(body);
10461
+ const { body: contents } = parseBody10(body);
10225
10462
  if (typeof contents !== "string") {
10226
10463
  return { status: 400, body: { error: "body (string) required" } };
10227
10464
  }
@@ -10244,7 +10481,7 @@ async function handleSkills(method, rest, body, ctx) {
10244
10481
  }
10245
10482
 
10246
10483
  // src/server/api/submit.ts
10247
- function parseBody10(raw) {
10484
+ function parseBody11(raw) {
10248
10485
  if (!raw) return {};
10249
10486
  try {
10250
10487
  const parsed = JSON.parse(raw);
@@ -10265,7 +10502,7 @@ async function handleSubmit(method, _rest, body, ctx) {
10265
10502
  }
10266
10503
  };
10267
10504
  }
10268
- const { prompt } = parseBody10(body);
10505
+ const { prompt } = parseBody11(body);
10269
10506
  if (typeof prompt !== "string" || !prompt.trim()) {
10270
10507
  return { status: 400, body: { error: "prompt (non-empty string) required" } };
10271
10508
  }
@@ -10428,6 +10665,8 @@ async function handleApi(pathTail, method, body, ctx) {
10428
10665
  return await handleFile(method, rest, body, ctx);
10429
10666
  case "semantic":
10430
10667
  return await handleSemantic(method, rest, body, ctx);
10668
+ case "index-config":
10669
+ return await handleIndexConfig(method, rest, body, ctx);
10431
10670
  default:
10432
10671
  return { status: 404, body: { error: `no such endpoint: /${head}` } };
10433
10672
  }
@@ -24064,6 +24303,7 @@ async function indexCommand(opts = {}) {
24064
24303
  rebuild: opts.rebuild,
24065
24304
  model: model2,
24066
24305
  baseUrl: opts.ollamaUrl,
24306
+ indexConfig: loadIndexConfig(),
24067
24307
  onProgress: (p) => writer.update(p)
24068
24308
  });
24069
24309
  } catch (err) {
@@ -24085,10 +24325,27 @@ async function indexCommand(opts = {}) {
24085
24325
  seconds
24086
24326
  })
24087
24327
  );
24328
+ const breakdown = renderSkipBreakdown(result.skipBuckets);
24329
+ if (breakdown) process.stderr.write(`${breakdown}
24330
+ `);
24088
24331
  if (result.filesChanged === 0 && !opts.rebuild) {
24089
24332
  process.stderr.write(t("indexNothingToDo"));
24090
24333
  }
24091
24334
  }
24335
+ function renderSkipBreakdown(buckets) {
24336
+ const total = Object.values(buckets).reduce((a, b) => a + b, 0);
24337
+ if (total === 0) return "";
24338
+ const parts = [];
24339
+ if (buckets.gitignore) parts.push(`gitignore: ${buckets.gitignore}`);
24340
+ if (buckets.pattern) parts.push(`pattern: ${buckets.pattern}`);
24341
+ if (buckets.defaultDir) parts.push(`defaultDir: ${buckets.defaultDir}`);
24342
+ if (buckets.defaultFile) parts.push(`defaultFile: ${buckets.defaultFile}`);
24343
+ if (buckets.binaryExt) parts.push(`binaryExt: ${buckets.binaryExt}`);
24344
+ if (buckets.binaryContent) parts.push(`binaryContent: ${buckets.binaryContent}`);
24345
+ if (buckets.tooLarge) parts.push(`tooLarge: ${buckets.tooLarge}`);
24346
+ if (buckets.readError) parts.push(`readError: ${buckets.readError}`);
24347
+ return ` \xB7 skipped ${total} files (${parts.join(", ")})`;
24348
+ }
24092
24349
  var SPINNER_FRAMES2 = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
24093
24350
  var SPINNER_INTERVAL_MS = 120;
24094
24351
  function makeProgressWriter(tty) {