grepmax 0.19.0 → 0.21.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.
Files changed (43) hide show
  1. package/.claude-plugin/marketplace.json +23 -0
  2. package/README.md +1 -1
  3. package/dist/commands/codex.js +5 -2
  4. package/dist/commands/context.js +1 -1
  5. package/dist/commands/doctor.js +37 -7
  6. package/dist/commands/droid.js +61 -9
  7. package/dist/commands/mcp.js +7 -7
  8. package/dist/commands/plugin.js +35 -21
  9. package/dist/commands/project.js +1 -1
  10. package/dist/commands/remove.js +9 -2
  11. package/dist/commands/repair.js +186 -0
  12. package/dist/commands/search-run.js +12 -1
  13. package/dist/commands/setup.js +4 -2
  14. package/dist/commands/status.js +1 -1
  15. package/dist/commands/symbols.js +1 -1
  16. package/dist/commands/watch.js +25 -4
  17. package/dist/config.js +45 -1
  18. package/dist/index.js +2 -0
  19. package/dist/lib/daemon/daemon.js +177 -48
  20. package/dist/lib/daemon/ipc-handler.js +12 -0
  21. package/dist/lib/daemon/process-manager.js +21 -3
  22. package/dist/lib/daemon/watcher-manager.js +18 -2
  23. package/dist/lib/graph/graph-builder.js +2 -2
  24. package/dist/lib/graph/impact.js +6 -6
  25. package/dist/lib/index/batch-processor.js +10 -1
  26. package/dist/lib/index/syncer.js +15 -4
  27. package/dist/lib/index/watcher-batch.js +10 -1
  28. package/dist/lib/llm/config.js +2 -1
  29. package/dist/lib/llm/investigate.js +40 -6
  30. package/dist/lib/llm/server.js +9 -2
  31. package/dist/lib/llm/tools.js +1 -1
  32. package/dist/lib/search/pagerank.js +1 -1
  33. package/dist/lib/search/searcher.js +6 -6
  34. package/dist/lib/store/vector-db.js +49 -10
  35. package/dist/lib/utils/daemon-client.js +87 -0
  36. package/dist/lib/utils/filter-builder.js +22 -0
  37. package/dist/lib/utils/scope-filter.js +3 -5
  38. package/dist/lib/workers/embeddings/granite.js +4 -1
  39. package/dist/lib/workers/pool.js +21 -1
  40. package/package.json +2 -1
  41. package/plugins/grepmax/.claude-plugin/plugin.json +1 -1
  42. package/plugins/grepmax/agents/semantic-explore.md +1 -1
  43. package/plugins/grepmax/skills/grepmax/SKILL.md +1 -1
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://anthropic.com/claude-code/marketplace.schema.json",
3
+ "name": "grepmax",
4
+ "owner": {
5
+ "name": "Robert Owens",
6
+ "email": "robowens@me.com"
7
+ },
8
+ "plugins": [
9
+ {
10
+ "name": "grepmax",
11
+ "source": "./plugins/grepmax",
12
+ "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
13
+ "version": "0.21.0",
14
+ "author": {
15
+ "name": "Robert Owens",
16
+ "email": "robowens@me.com"
17
+ },
18
+ "skills": [
19
+ "./skills/grepmax"
20
+ ]
21
+ }
22
+ ]
23
+ }
package/README.md CHANGED
@@ -61,7 +61,7 @@ gmax "where do we handle auth?" --agent # Semantic search (compact output)
61
61
  gmax extract handleAuth # Full function body with line numbers
62
62
  gmax peek handleAuth # Signature + callers + callees
63
63
  gmax trace handleAuth -d 2 # Call graph (2-hop)
64
- gmax skeleton src/lib/search/ # File structure (bodies collapsed)
64
+ gmax skeleton src/lib/auth.ts # File structure (bodies collapsed)
65
65
  gmax symbols auth # List indexed symbols
66
66
  ```
67
67
 
@@ -69,8 +69,11 @@ function writeSkillToAgents(skill) {
69
69
  function installPlugin() {
70
70
  return __awaiter(this, void 0, void 0, function* () {
71
71
  try {
72
- // 1. Register MCP tool
73
- yield execAsync("codex mcp add gmax gmax mcp", {
72
+ // 1. Register MCP tool. Codex requires the stdio command after `--`:
73
+ // codex mcp add [OPTIONS] <NAME> -- <COMMAND>...
74
+ // Without the separator the launch command is misparsed. AGENTS.md is only
75
+ // written after this resolves, so a failed registration leaves it untouched.
76
+ yield execAsync("codex mcp add gmax -- gmax mcp", {
74
77
  shell,
75
78
  env: process.env,
76
79
  });
@@ -316,7 +316,7 @@ exports.context = new commander_1.Command("context")
316
316
  allSymbols.add(s);
317
317
  }
318
318
  if (allSymbols.size > 0) {
319
- const pathScope = `path LIKE '${(0, filter_builder_1.escapeSqlString)(projectRoot)}/%'`;
319
+ const pathScope = (0, filter_builder_1.pathStartsWith)(`${projectRoot}/`);
320
320
  const relatedCounts = new Map();
321
321
  const searchedFiles = new Set(uniqueFiles);
322
322
  for (const sym of [...allSymbols].slice(0, 20)) {
@@ -179,6 +179,13 @@ exports.doctor = new commander_1.Command("doctor")
179
179
  const db = new VectorDB(config_1.PATHS.lancedbDir);
180
180
  const table = yield db.ensureTable();
181
181
  const totalChunks = yield table.countRows();
182
+ // Physical schema-width check: the shared `chunks` table is fixed-width at
183
+ // creation, so a tier/dim change strands it at the old width and every
184
+ // write throws. This is independent of the per-project registry drift
185
+ // checked below — the table can match the registry yet still be physically
186
+ // stranded — so we surface it as its own line.
187
+ const physicalDim = yield db.getSchemaVectorDim();
188
+ const schemaGap = (0, config_1.describeSchemaDimGap)(physicalDim, globalConfig.vectorDim);
182
189
  // Summary coverage (existing check)
183
190
  if (!opts.agent && totalChunks > 0) {
184
191
  const withSummary = (yield table
@@ -275,8 +282,13 @@ exports.doctor = new commander_1.Command("doctor")
275
282
  `orphaned=${orphanedProjects.length}`,
276
283
  `stale_chunker=${staleChunkerProjects.length}`,
277
284
  `stale_embedding=${staleEmbeddingProjects.length}`,
285
+ `schema_dim=${physicalDim !== null && physicalDim !== void 0 ? physicalDim : "none"}`,
286
+ `schema_dim_ok=${schemaGap ? "false" : "true"}`,
278
287
  ];
279
288
  console.log(fields.join("\t"));
289
+ if (schemaGap) {
290
+ console.log((0, config_1.schemaDimAgentRow)(schemaGap));
291
+ }
280
292
  for (const p of staleChunkerProjects) {
281
293
  const gap = (0, config_1.describeChunkerGap)(p.chunkerVersion);
282
294
  if (!gap)
@@ -307,12 +319,24 @@ exports.doctor = new commander_1.Command("doctor")
307
319
  `current_dim=${gap.toDim}`,
308
320
  `dim_changed=${gap.dimChanged}`,
309
321
  `severity=${gap.severity}`,
310
- `fix=gmax index --reset (in ${p.root})`,
322
+ // A dim change can't be fixed by a per-project reset (the shared
323
+ // table is fixed-width) — point at the global rebuild instead.
324
+ `fix=${gap.dimChanged ? config_1.REBUILD_COMMAND : `gmax index --reset (in ${p.root})`}`,
311
325
  ].join("\t"));
312
326
  }
313
327
  }
314
328
  else {
315
329
  console.log("\nIndex Health\n");
330
+ // Physical schema width — a mismatch means every write throws until a
331
+ // global rebuild (the shared fixed-width table can't be reshaped by a
332
+ // per-project reset). Surfaced first because it's the most severe.
333
+ if (schemaGap) {
334
+ console.log(`FAIL Schema: vector table is ${schemaGap.tableDim}d, config expects ${schemaGap.configDim}d`);
335
+ console.log(` run '${config_1.REBUILD_COMMAND}' (drops + reindexes all projects at the new width)`);
336
+ }
337
+ else if (physicalDim) {
338
+ console.log(`ok Schema: vector table is ${physicalDim}d`);
339
+ }
316
340
  // Disk space
317
341
  if (diskLevel !== "ok") {
318
342
  console.log(`WARN Disk: ${formatSize(availBytes)} available (${diskLevel})`);
@@ -370,17 +394,21 @@ exports.doctor = new commander_1.Command("doctor")
370
394
  }
371
395
  // Index built with a different embedding model/dim than the current
372
396
  // config. A dim change is breaking (search scores are invalid until a
373
- // re-embed); a same-dim model swap is additive. Unlike the stale-chunker
374
- // case this is NOT auto-fixed by `--fix`: a dim change can't coexist in
375
- // the shared fixed-dim table (deferred Phase 1B), so we point at the
376
- // manual reset instead.
397
+ // re-embed); a same-dim model swap is additive. Recovery differs by kind:
398
+ // a same-dim model swap is fixed by a per-project `gmax index --reset`,
399
+ // but a dim change can't be the shared table is fixed-width, so it
400
+ // needs the global rebuild (see the Schema check above).
377
401
  if (staleEmbeddingProjects.length > 0) {
378
402
  const gaps = staleEmbeddingProjects.map((p) => (0, config_1.describeEmbeddingGap)({ modelTier: p.modelTier, vectorDim: p.vectorDim }, {
379
403
  modelTier: globalConfig.modelTier,
380
404
  vectorDim: globalConfig.vectorDim,
381
405
  }));
382
406
  const anyBreaking = gaps.some((g) => (g === null || g === void 0 ? void 0 : g.severity) === "breaking");
383
- console.log(`${anyBreaking ? "WARN" : "INFO"} Stale embedding: ${staleEmbeddingProjects.length} project(s) indexed with a different embedding model/dim run 'gmax index --reset' per project`);
407
+ const anyDimChange = gaps.some((g) => g === null || g === void 0 ? void 0 : g.dimChanged);
408
+ const headerFix = anyDimChange
409
+ ? `run '${config_1.REBUILD_COMMAND}' (dim change needs a full rebuild)`
410
+ : "run 'gmax index --reset' per project";
411
+ console.log(`${anyBreaking ? "WARN" : "INFO"} Stale embedding: ${staleEmbeddingProjects.length} project(s) indexed with a different embedding model/dim — ${headerFix}`);
384
412
  staleEmbeddingProjects.forEach((p, i) => {
385
413
  const gap = gaps[i];
386
414
  if (!gap)
@@ -389,7 +417,9 @@ exports.doctor = new commander_1.Command("doctor")
389
417
  ? `${gap.fromDim}d→${gap.toDim}d`
390
418
  : `model ${gap.fromModel}→${gap.toModel}`;
391
419
  console.log(` - ${p.name || path.basename(p.root)} (${change}, ${gap.severity})`);
392
- console.log(` run 'gmax index --reset' in ${p.root}`);
420
+ console.log(gap.dimChanged
421
+ ? ` run '${config_1.REBUILD_COMMAND}'`
422
+ : ` run 'gmax index --reset' in ${p.root}`);
393
423
  });
394
424
  }
395
425
  // Projects
@@ -69,17 +69,50 @@ function parseJsonWithComments(content) {
69
69
  .split("\n")
70
70
  .map((line) => line.replace(/^\s*\/\/.*$/, ""))
71
71
  .join("\n");
72
- try {
73
- return JSON.parse(stripped);
74
- }
75
- catch (_a) {
72
+ // An empty / whitespace-only file is a legitimate "no settings yet" -> {}.
73
+ // Anything else that fails to parse is a real, user-owned file we must NOT
74
+ // silently coerce to {} (that would clobber it on the next save) — let the
75
+ // caller decide.
76
+ if (stripped.trim() === "")
76
77
  return {};
77
- }
78
+ return JSON.parse(stripped);
78
79
  }
79
80
  function loadSettings(settingsPath) {
80
81
  if (!node_fs_1.default.existsSync(settingsPath))
81
82
  return {};
82
- return parseJsonWithComments(node_fs_1.default.readFileSync(settingsPath, "utf-8"));
83
+ const raw = node_fs_1.default.readFileSync(settingsPath, "utf-8");
84
+ try {
85
+ return parseJsonWithComments(raw);
86
+ }
87
+ catch (err) {
88
+ throw new Error(`Refusing to touch ${settingsPath}: it contains invalid JSON ` +
89
+ `(${err.message}). Fix or remove the file, then re-run.`);
90
+ }
91
+ }
92
+ /** True when a hook entry was installed by gmax — its command points at our
93
+ * hooks dir. Used to remove only gmax entries on uninstall. */
94
+ function isGmaxHookEntry(entry, hooksDir) {
95
+ var _a, _b;
96
+ return (_b = (_a = entry.hooks) === null || _a === void 0 ? void 0 : _a.some((h) => { var _a; return (_a = h.command) === null || _a === void 0 ? void 0 : _a.includes(hooksDir); })) !== null && _b !== void 0 ? _b : false;
97
+ }
98
+ /** Strip gmax hook entries from settings.hooks in place, preserving unrelated
99
+ * user hooks. Returns true if anything was removed. */
100
+ function removeGmaxHooks(settings, hooksDir) {
101
+ if (!settings.hooks)
102
+ return false;
103
+ let changed = false;
104
+ for (const [event, entries] of Object.entries(settings.hooks)) {
105
+ const kept = entries.filter((e) => !isGmaxHookEntry(e, hooksDir));
106
+ if (kept.length !== entries.length)
107
+ changed = true;
108
+ if (kept.length > 0)
109
+ settings.hooks[event] = kept;
110
+ else
111
+ delete settings.hooks[event];
112
+ }
113
+ if (Object.keys(settings.hooks).length === 0)
114
+ delete settings.hooks;
115
+ return changed;
83
116
  }
84
117
  function saveSettings(settingsPath, settings) {
85
118
  node_fs_1.default.mkdirSync(node_path_1.default.dirname(settingsPath), { recursive: true });
@@ -103,10 +136,14 @@ function mergeHooks(existing, incoming) {
103
136
  function installPlugin() {
104
137
  return __awaiter(this, void 0, void 0, function* () {
105
138
  const root = resolveDroidRoot();
139
+ const settingsPath = node_path_1.default.join(root, "settings.json");
140
+ // Validate/parse settings BEFORE writing anything. A malformed, user-owned
141
+ // settings.json aborts the install here — otherwise we'd either clobber it
142
+ // with {} or leave half-written hook scripts behind on the failure path.
143
+ const settings = loadSettings(settingsPath);
106
144
  const gmaxBin = resolveGmaxBin();
107
145
  const hooksDir = node_path_1.default.join(root, "hooks", "gmax");
108
146
  const skillsDir = node_path_1.default.join(root, "skills", "gmax");
109
- const settingsPath = node_path_1.default.join(root, "settings.json");
110
147
  // 1. Install hook scripts (start/stop daemon)
111
148
  const startScript = `
112
149
  const { execFileSync } = require("child_process");
@@ -166,7 +203,6 @@ try { execFileSync("gmax", ["watch", "stop", "--all"], { stdio: "ignore", timeou
166
203
  },
167
204
  ],
168
205
  };
169
- const settings = loadSettings(settingsPath);
170
206
  settings.enableHooks = true;
171
207
  settings.allowBackgroundProcesses = true;
172
208
  settings.hooks = mergeHooks(settings.hooks, hookConfig);
@@ -179,12 +215,28 @@ function uninstallPlugin() {
179
215
  const root = resolveDroidRoot();
180
216
  const hooksDir = node_path_1.default.join(root, "hooks", "gmax");
181
217
  const skillsDir = node_path_1.default.join(root, "skills", "gmax");
218
+ const settingsPath = node_path_1.default.join(root, "settings.json");
182
219
  if (node_fs_1.default.existsSync(hooksDir))
183
220
  node_fs_1.default.rmSync(hooksDir, { recursive: true, force: true });
184
221
  if (node_fs_1.default.existsSync(skillsDir))
185
222
  node_fs_1.default.rmSync(skillsDir, { recursive: true, force: true });
223
+ // Remove only gmax hook entries from settings.json, preserving unrelated user
224
+ // hooks. enableHooks/allowBackgroundProcesses are left alone — other hooks may
225
+ // depend on them.
226
+ if (node_fs_1.default.existsSync(settingsPath)) {
227
+ try {
228
+ const settings = loadSettings(settingsPath);
229
+ if (removeGmaxHooks(settings, hooksDir)) {
230
+ saveSettings(settingsPath, settings);
231
+ console.log("✅ Removed gmax hooks from settings.json");
232
+ }
233
+ }
234
+ catch (err) {
235
+ // Don't clobber an invalid settings file on uninstall either.
236
+ console.warn(`⚠️ Skipped settings cleanup: ${err.message}`);
237
+ }
238
+ }
186
239
  console.log("✅ gmax removed from Factory Droid");
187
- console.log("NOTE: You may want to manually clean up 'hooks' in ~/.factory/settings.json");
188
240
  });
189
241
  }
190
242
  exports.installDroid = new commander_1.Command("install-droid")
@@ -773,7 +773,7 @@ exports.mcp = new commander_1.Command("mcp")
773
773
  "is_exported",
774
774
  "defined_symbols",
775
775
  ])
776
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
776
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
777
777
  .limit(10)
778
778
  .toArray();
779
779
  if (rows.length === 0) {
@@ -867,7 +867,7 @@ exports.mcp = new commander_1.Command("mcp")
867
867
  const metaRows = yield table
868
868
  .query()
869
869
  .select(["is_exported", "start_line", "end_line"])
870
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
870
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
871
871
  .limit(1)
872
872
  .toArray();
873
873
  const exported = metaRows.length > 0 && Boolean(metaRows[0].is_exported);
@@ -982,7 +982,7 @@ exports.mcp = new commander_1.Command("mcp")
982
982
  const defRows = yield table
983
983
  .query()
984
984
  .select(["path", "start_line", "is_exported"])
985
- .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
985
+ .where(`array_contains(defined_symbols, '${(0, filter_builder_1.escapeSqlString)(symbol)}') AND ${(0, filter_builder_1.pathStartsWith)(prefix)}`)
986
986
  .limit(1)
987
987
  .toArray();
988
988
  if (defRows.length === 0) {
@@ -1037,7 +1037,7 @@ exports.mcp = new commander_1.Command("mcp")
1037
1037
  "referenced_symbols",
1038
1038
  "is_exported",
1039
1039
  ])
1040
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
1040
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
1041
1041
  .limit(500000)
1042
1042
  .toArray();
1043
1043
  if (rows.length === 0) {
@@ -1221,7 +1221,7 @@ exports.mcp = new commander_1.Command("mcp")
1221
1221
  const absPrefix = path.isAbsolute(pathPrefix)
1222
1222
  ? pathPrefix
1223
1223
  : path.resolve(projectRoot, pathPrefix);
1224
- query = query.where(`path LIKE '${(0, filter_builder_1.escapeSqlString)((0, filter_builder_1.normalizePath)(absPrefix))}%'`);
1224
+ query = query.where((0, filter_builder_1.pathStartsWith)((0, filter_builder_1.normalizePath)(absPrefix)));
1225
1225
  }
1226
1226
  const rows = yield query.toArray();
1227
1227
  const map = new Map();
@@ -1388,7 +1388,7 @@ exports.mcp = new commander_1.Command("mcp")
1388
1388
  "defined_symbols",
1389
1389
  "referenced_symbols",
1390
1390
  ])
1391
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
1391
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
1392
1392
  .limit(200000)
1393
1393
  .toArray();
1394
1394
  if (rows.length === 0) {
@@ -1829,7 +1829,7 @@ exports.mcp = new commander_1.Command("mcp")
1829
1829
  "role",
1830
1830
  "_distance",
1831
1831
  ])
1832
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(projectRoot)}/%'`)
1832
+ .where((0, filter_builder_1.pathStartsWith)(`${projectRoot}/`))
1833
1833
  .limit(limit + 5)
1834
1834
  .toArray();
1835
1835
  let filtered = results.filter((r) => !(r.path === source.path && r.start_line === source.start_line));
@@ -43,6 +43,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
43
43
  };
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.plugin = void 0;
46
+ exports.installAll = installAll;
46
47
  const node_child_process_1 = require("node:child_process");
47
48
  const fs = __importStar(require("node:fs"));
48
49
  const os = __importStar(require("node:os"));
@@ -139,6 +140,39 @@ function getClients() {
139
140
  },
140
141
  ];
141
142
  }
143
+ /**
144
+ * Install gmax plugins for every detected client; returns the number installed.
145
+ * Deliberately does NOT call gracefulExit — callers decide how to finish. This
146
+ * lets `gmax setup` keep running (and print its outro) after installing, while
147
+ * the `add` subcommand still exits the process itself.
148
+ */
149
+ function installAll() {
150
+ return __awaiter(this, void 0, void 0, function* () {
151
+ const clients = getClients();
152
+ console.log("gmax plugin add — detecting clients...\n");
153
+ let installed = 0;
154
+ for (const client of clients) {
155
+ if (!client.detect()) {
156
+ console.log(` skip ${client.name} — not found`);
157
+ continue;
158
+ }
159
+ try {
160
+ yield client.install();
161
+ installed++;
162
+ }
163
+ catch (err) {
164
+ console.error(` FAIL ${client.name} — ${err instanceof Error ? err.message : String(err)}`);
165
+ }
166
+ }
167
+ if (installed === 0) {
168
+ console.log("\nNo supported clients found. Install one of: claude, opencode, codex, droid");
169
+ }
170
+ else {
171
+ console.log(`\n${installed} plugin(s) installed.`);
172
+ }
173
+ return installed;
174
+ });
175
+ }
142
176
  // --- Subcommands ---
143
177
  const addCmd = new commander_1.Command("add")
144
178
  .description("Install or update gmax plugins")
@@ -164,27 +198,7 @@ const addCmd = new commander_1.Command("add")
164
198
  return;
165
199
  }
166
200
  // Install all detected
167
- console.log("gmax plugin add — detecting clients...\n");
168
- let installed = 0;
169
- for (const client of clients) {
170
- if (!client.detect()) {
171
- console.log(` skip ${client.name} — not found`);
172
- continue;
173
- }
174
- try {
175
- yield client.install();
176
- installed++;
177
- }
178
- catch (err) {
179
- console.error(` FAIL ${client.name} — ${err instanceof Error ? err.message : String(err)}`);
180
- }
181
- }
182
- if (installed === 0) {
183
- console.log("\nNo supported clients found. Install one of: claude, opencode, codex, droid");
184
- }
185
- else {
186
- console.log(`\n${installed} plugin(s) installed.`);
187
- }
201
+ yield installAll();
188
202
  yield (0, exit_1.gracefulExit)();
189
203
  }));
190
204
  const removeCmd = new commander_1.Command("remove")
@@ -79,7 +79,7 @@ exports.project = new commander_1.Command("project")
79
79
  "defined_symbols",
80
80
  "referenced_symbols",
81
81
  ])
82
- .where(`path LIKE '${(0, filter_builder_1.escapeSqlString)(prefix)}%'`)
82
+ .where((0, filter_builder_1.pathStartsWith)(prefix))
83
83
  .limit(200000)
84
84
  .toArray();
85
85
  if (rows.length === 0) {
@@ -133,10 +133,17 @@ Examples:
133
133
  (0, watcher_store_1.unregisterWatcher)(watcher.pid);
134
134
  }
135
135
  const paths = (0, project_root_1.ensureProjectPaths)(projectRoot);
136
+ // Slash-terminate so removing /repo/app can't also delete the sibling
137
+ // /repo/app2 — VectorDB matches `path LIKE prefix%` and MetaCache
138
+ // matches `key.startsWith(prefix)`, both of which bleed across siblings
139
+ // without the trailing slash. Mirrors the daemon remove path.
140
+ const rootPrefix = projectRoot.endsWith("/")
141
+ ? projectRoot
142
+ : `${projectRoot}/`;
136
143
  vectorDb = new vector_db_1.VectorDB(paths.lancedbDir);
137
- yield vectorDb.deletePathsWithPrefix(projectRoot);
144
+ yield vectorDb.deletePathsWithPrefix(rootPrefix);
138
145
  metaCache = new meta_cache_1.MetaCache(paths.lmdbPath);
139
- const keys = yield metaCache.getKeysWithPrefix(projectRoot);
146
+ const keys = yield metaCache.getKeysWithPrefix(rootPrefix);
140
147
  for (const key of keys) {
141
148
  metaCache.delete(key);
142
149
  }
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
47
+ Object.defineProperty(exports, "__esModule", { value: true });
48
+ exports.repair = void 0;
49
+ const readline = __importStar(require("node:readline"));
50
+ const commander_1 = require("commander");
51
+ const ora_1 = __importDefault(require("ora"));
52
+ const config_1 = require("../config");
53
+ const index_config_1 = require("../lib/index/index-config");
54
+ const vector_db_1 = require("../lib/store/vector-db");
55
+ const exit_1 = require("../lib/utils/exit");
56
+ const project_registry_1 = require("../lib/utils/project-registry");
57
+ function confirm(message) {
58
+ const rl = readline.createInterface({
59
+ input: process.stdin,
60
+ output: process.stdout,
61
+ });
62
+ return new Promise((resolve) => {
63
+ rl.question(`${message} [y/N] `, (answer) => {
64
+ rl.close();
65
+ resolve(answer.toLowerCase() === "y");
66
+ });
67
+ });
68
+ }
69
+ exports.repair = new commander_1.Command("repair")
70
+ .description("Repair the centralized index (recover from a schema mismatch)")
71
+ .option("--rebuild", "Drop the shared vector table and re-index every project at the configured embedding dim", false)
72
+ .option("-y, --yes", "Skip the confirmation prompt", false)
73
+ .addHelpText("after", `
74
+ The shared LanceDB \`chunks\` table is fixed-width at creation. Switching model
75
+ tiers (e.g. small 384d -> standard 768d) strands the table at the old width, so
76
+ every write then fails. A per-project \`gmax index --reset\` only deletes rows —
77
+ it can't change the table width. \`${config_1.REBUILD_COMMAND}\` drops the table and
78
+ re-embeds all projects at the current dim.
79
+
80
+ Examples:
81
+ gmax repair Show schema status and what a rebuild would do
82
+ gmax repair --rebuild Drop the table and re-index every project
83
+ gmax repair --rebuild -y Rebuild without the confirmation prompt
84
+ `)
85
+ .action((opts) => __awaiter(void 0, void 0, void 0, function* () {
86
+ var _a, _b, _c;
87
+ try {
88
+ const globalConfig = (0, index_config_1.readGlobalConfig)();
89
+ const configDim = globalConfig.vectorDim;
90
+ // Physical width of the on-disk table (null if no table yet). Opened
91
+ // read-only; safe to inspect even while the daemon holds the table.
92
+ let physicalDim = null;
93
+ const probe = new vector_db_1.VectorDB(config_1.PATHS.lancedbDir);
94
+ try {
95
+ physicalDim = yield probe.getSchemaVectorDim();
96
+ }
97
+ finally {
98
+ yield probe.close();
99
+ }
100
+ const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed" || p.status === "pending");
101
+ const dimLine = physicalDim == null
102
+ ? "Vector table: none yet"
103
+ : physicalDim === configDim
104
+ ? `Vector table: ${physicalDim}d (matches config)`
105
+ : `Vector table: ${physicalDim}d, config expects ${configDim}d (MISMATCH)`;
106
+ console.log(dimLine);
107
+ if (!opts.rebuild) {
108
+ if (physicalDim != null && physicalDim !== configDim) {
109
+ console.log(`\nRun '${config_1.REBUILD_COMMAND}' to drop the table and re-index ` +
110
+ `${projects.length} project(s) at ${configDim}d.`);
111
+ }
112
+ else {
113
+ console.log(`\nNothing to repair. Pass --rebuild to force a full drop + re-index anyway.`);
114
+ }
115
+ return;
116
+ }
117
+ if (projects.length === 0) {
118
+ console.log("\nNo indexed projects to rebuild.");
119
+ return;
120
+ }
121
+ const totalChunks = projects.reduce((sum, p) => { var _a; return sum + ((_a = p.chunkCount) !== null && _a !== void 0 ? _a : 0); }, 0);
122
+ console.log(`\nThis will DROP the shared vector table and re-embed ${projects.length} project(s)` +
123
+ (totalChunks > 0
124
+ ? ` (~${totalChunks.toLocaleString()} chunks).`
125
+ : "."));
126
+ for (const p of projects) {
127
+ console.log(` - ${p.name}`);
128
+ }
129
+ if (!opts.yes) {
130
+ const ok = yield confirm("\nContinue?");
131
+ if (!ok) {
132
+ console.log("Cancelled.");
133
+ return;
134
+ }
135
+ }
136
+ // The rebuild must run through the daemon — it is the single writer for
137
+ // the shared table. Refuse rather than risk a torn drop alongside a
138
+ // daemon mid-flush.
139
+ const { ensureDaemonRunning, sendStreamingCommand } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
140
+ if (!(yield ensureDaemonRunning())) {
141
+ console.error("Could not start the gmax daemon. Start it with 'gmax watch --daemon -b' and retry.");
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ const spinner = (0, ora_1.default)({
146
+ text: "Dropping vector table...",
147
+ isSilent: !process.stdout.isTTY,
148
+ }).start();
149
+ try {
150
+ const done = yield sendStreamingCommand({ cmd: "repair" }, (msg) => {
151
+ var _a, _b, _c, _d;
152
+ if (msg.phase === "drop") {
153
+ spinner.text = "Dropping vector table...";
154
+ }
155
+ else if (msg.phase === "reindex") {
156
+ const doneN = (_a = msg.projectsDone) !== null && _a !== void 0 ? _a : 0;
157
+ const totalN = (_b = msg.projectsTotal) !== null && _b !== void 0 ? _b : projects.length;
158
+ const processed = (_c = msg.processed) !== null && _c !== void 0 ? _c : 0;
159
+ const total = (_d = msg.total) !== null && _d !== void 0 ? _d : 0;
160
+ const counts = total > 0 ? ` ${processed}/${total}` : "";
161
+ spinner.text = `Re-indexing ${msg.project} (${doneN + 1}/${totalN})${counts}`;
162
+ }
163
+ });
164
+ if (!done.ok) {
165
+ spinner.fail("Rebuild failed");
166
+ throw new Error((_a = done.error) !== null && _a !== void 0 ? _a : "daemon repair failed");
167
+ }
168
+ const rebuilt = (_b = done.projects) !== null && _b !== void 0 ? _b : 0;
169
+ const indexed = (_c = done.indexed) !== null && _c !== void 0 ? _c : 0;
170
+ spinner.succeed(`Rebuilt ${rebuilt} project(s) at ${configDim}d • ${indexed.toLocaleString()} chunks`);
171
+ }
172
+ catch (e) {
173
+ if (spinner.isSpinning)
174
+ spinner.fail("Rebuild failed");
175
+ throw e;
176
+ }
177
+ }
178
+ catch (error) {
179
+ const message = error instanceof Error ? error.message : "Unknown error";
180
+ console.error("Failed to repair:", message);
181
+ process.exitCode = 1;
182
+ }
183
+ finally {
184
+ yield (0, exit_1.gracefulExit)();
185
+ }
186
+ }));
@@ -130,7 +130,18 @@ function runSearch(params) {
130
130
  if (!indexState && (locked || projectStatus === "pending")) {
131
131
  indexState = { indexing: true, pendingFiles: 0 };
132
132
  }
133
- const hasRows = yield vectorDb.hasAnyRows();
133
+ // Decide first-run auto-index by whether the project being searched has
134
+ // rows — NOT whether the shared store has any rows at all. The store is
135
+ // centralized, so a global hasAnyRows() lets a sibling project's rows
136
+ // suppress this project's first-run index. Cross-project mode keeps the
137
+ // global check: it spans already-indexed projects and must not first-run
138
+ // a single directory just because the cwd happens to be unindexed.
139
+ const crossProject = !!options.allProjects ||
140
+ !!options.projects ||
141
+ !!options.excludeProjects;
142
+ const hasRows = crossProject
143
+ ? yield vectorDb.hasAnyRows()
144
+ : yield vectorDb.hasRowsForPath(effectiveRoot);
134
145
  const needsSync = options.sync || !hasRows;
135
146
  if (needsSync) {
136
147
  const isTTY = process.stdout.isTTY;
@@ -154,8 +154,10 @@ exports.setup = new commander_1.Command("setup")
154
154
  initialValue: true,
155
155
  });
156
156
  if (!p.isCancel(installPlugins) && installPlugins) {
157
- const { plugin: pluginCmd } = yield Promise.resolve().then(() => __importStar(require("./plugin")));
158
- yield pluginCmd.parseAsync(["node", "gmax"]);
157
+ // Call installAll() directly, not `plugin` (bare status) or the `add`
158
+ // subcommand — both gracefulExit() before setup's outro can run.
159
+ const { installAll } = yield Promise.resolve().then(() => __importStar(require("./plugin")));
160
+ yield installAll();
159
161
  }
160
162
  p.outro(`Ready — ${selectedTier.label}, ${embedMode === "gpu" ? "GPU" : "CPU"} mode`);
161
163
  yield (0, exit_1.gracefulExit)();