@ninjaxtools/slopdex 0.19.0 → 0.20.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 (60) hide show
  1. package/.gitignore +2 -0
  2. package/README.md +134 -46
  3. package/binary-install.js +348 -0
  4. package/binary.js +124 -0
  5. package/install.js +4 -0
  6. package/npm-shrinkwrap.json +52 -0
  7. package/package.json +78 -61
  8. package/run-slopdex.js +4 -0
  9. package/dist/chunk-5BO2LLXC.js +0 -15
  10. package/dist/chunk-5BO2LLXC.js.map +0 -1
  11. package/dist/chunk-BXWO2KMC.js +0 -20
  12. package/dist/chunk-BXWO2KMC.js.map +0 -1
  13. package/dist/chunk-DKRB2XT5.js +0 -33
  14. package/dist/chunk-DKRB2XT5.js.map +0 -1
  15. package/dist/chunk-DTI7SXGL.js +0 -109
  16. package/dist/chunk-DTI7SXGL.js.map +0 -1
  17. package/dist/chunk-GBEJHETS.js +0 -1117
  18. package/dist/chunk-GBEJHETS.js.map +0 -1
  19. package/dist/chunk-HMKVMRLF.js +0 -2221
  20. package/dist/chunk-HMKVMRLF.js.map +0 -1
  21. package/dist/chunk-IQU3YVZ3.js +0 -85
  22. package/dist/chunk-IQU3YVZ3.js.map +0 -1
  23. package/dist/chunk-JAVPHCZH.js +0 -24
  24. package/dist/chunk-JAVPHCZH.js.map +0 -1
  25. package/dist/chunk-MKUTTXZB.js +0 -74
  26. package/dist/chunk-MKUTTXZB.js.map +0 -1
  27. package/dist/chunk-OIKBO3NJ.js +0 -133
  28. package/dist/chunk-OIKBO3NJ.js.map +0 -1
  29. package/dist/chunk-P57ZJREE.js +0 -426
  30. package/dist/chunk-P57ZJREE.js.map +0 -1
  31. package/dist/chunk-S225GYCL.js +0 -79
  32. package/dist/chunk-S225GYCL.js.map +0 -1
  33. package/dist/chunk-TSURHRFF.js +0 -240
  34. package/dist/chunk-TSURHRFF.js.map +0 -1
  35. package/dist/chunk-VH5VGRCI.js +0 -103
  36. package/dist/chunk-VH5VGRCI.js.map +0 -1
  37. package/dist/cli.d.ts +0 -1
  38. package/dist/cli.js +0 -1130
  39. package/dist/cli.js.map +0 -1
  40. package/dist/code-index-MWSJKV3G.js +0 -14
  41. package/dist/code-index-MWSJKV3G.js.map +0 -1
  42. package/dist/cross-search-ERBFC25T.js +0 -10
  43. package/dist/cross-search-ERBFC25T.js.map +0 -1
  44. package/dist/database-P5XWJWQL.js +0 -14
  45. package/dist/database-P5XWJWQL.js.map +0 -1
  46. package/dist/hosted-GHIUHKOU.js +0 -13
  47. package/dist/hosted-GHIUHKOU.js.map +0 -1
  48. package/dist/index.d.ts +0 -681
  49. package/dist/index.js +0 -102
  50. package/dist/index.js.map +0 -1
  51. package/dist/jina-43RL7C6M.js +0 -12
  52. package/dist/jina-43RL7C6M.js.map +0 -1
  53. package/dist/openai-CYUPNBGB.js +0 -12
  54. package/dist/openai-CYUPNBGB.js.map +0 -1
  55. package/dist/openai-EC6THXKV.js +0 -21
  56. package/dist/openai-EC6THXKV.js.map +0 -1
  57. package/dist/openai-TCRXM66O.js +0 -11
  58. package/dist/openai-TCRXM66O.js.map +0 -1
  59. package/docs/implementation.md +0 -162
  60. package/docs/reference.md +0 -187
package/dist/cli.js DELETED
@@ -1,1130 +0,0 @@
1
- #!/usr/bin/env node
2
- import {
3
- TerminalProgress,
4
- clearProgress
5
- } from "./chunk-S225GYCL.js";
6
- import {
7
- DEFAULT_PARALLELISM,
8
- compileNameRegex
9
- } from "./chunk-VH5VGRCI.js";
10
- import {
11
- CodeIndexError,
12
- GitUnavailableError,
13
- IncompatibleIndexError
14
- } from "./chunk-DKRB2XT5.js";
15
-
16
- // src/cli.ts
17
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
18
- import path from "node:path";
19
- import { DatabaseSync } from "node:sqlite";
20
- import { parseArgs } from "node:util";
21
-
22
- // src/format.ts
23
- function functionName(value) {
24
- return `${value.path} :: ${value.qualifiedName}`;
25
- }
26
- function formatSimilaritySummary(matches, source) {
27
- const lines = matches.map((match) => `${source ? " " : ""}${rankDetails(match)} ${functionName(match.function)}${distanceDetails(match)}${scoreDetails(match)}`);
28
- if (!source) return lines.length > 0 ? lines.join("\n") : "No matches.";
29
- return [functionName(source), ...lines.length > 0 ? lines : [" No matches."]].join("\n");
30
- }
31
- function rankDetails(match) {
32
- return match.rerankScore === void 0 ? match.similarity.toFixed(4) : `${match.rerankScore.toFixed(4)} rerank (${match.similarity.toFixed(4)} similarity)`;
33
- }
34
- function distanceDetails(match) {
35
- return "physicalDistance" in match && match.physicalDistance !== void 0 ? ` [distance ${match.physicalDistance}]` : "";
36
- }
37
- function formatSimilarityClusters(results, sameIndex, limit) {
38
- const combined = results.some((result) => result.matches.some((match) => match.descriptionSimilarity !== void 0));
39
- const nodes = /* @__PURE__ */ new Map();
40
- const neighbors = /* @__PURE__ */ new Map();
41
- const edges = [];
42
- const connect = (left, right) => {
43
- const leftNeighbors = neighbors.get(left) ?? /* @__PURE__ */ new Set();
44
- const rightNeighbors = neighbors.get(right) ?? /* @__PURE__ */ new Set();
45
- leftNeighbors.add(right);
46
- rightNeighbors.add(left);
47
- neighbors.set(left, leftNeighbors);
48
- neighbors.set(right, rightNeighbors);
49
- };
50
- for (const result of results) {
51
- const sourceKey = `${sameIndex ? "index" : "source"}:${result.source.id}`;
52
- nodes.set(sourceKey, { function: result.source, role: "source" });
53
- for (const match of result.matches) {
54
- const matchKey = `${sameIndex ? "index" : "target"}:${match.function.id}`;
55
- nodes.set(matchKey, { function: match.function, role: sameIndex ? "source" : "target" });
56
- connect(sourceKey, matchKey);
57
- edges.push({ left: sourceKey, right: matchKey, similarity: match.similarity });
58
- }
59
- }
60
- const seen = /* @__PURE__ */ new Set();
61
- const clusters = [];
62
- for (const start of neighbors.keys()) {
63
- if (seen.has(start)) continue;
64
- const pending = [start];
65
- const keys = /* @__PURE__ */ new Set();
66
- while (pending.length > 0) {
67
- const key = pending.pop();
68
- if (seen.has(key)) continue;
69
- seen.add(key);
70
- keys.add(key);
71
- for (const neighbor of neighbors.get(key) ?? []) pending.push(neighbor);
72
- }
73
- const similarities = edges.filter((edge) => keys.has(edge.left) && keys.has(edge.right)).map((edge) => edge.similarity);
74
- clusters.push({
75
- members: [...keys].map((key) => nodes.get(key)).sort((left, right) => clusterFunctionName(left, sameIndex).localeCompare(clusterFunctionName(right, sameIndex))),
76
- min: Math.min(...similarities),
77
- max: Math.max(...similarities)
78
- });
79
- }
80
- clusters.sort((left, right) => right.members.length - left.members.length || clusterFunctionName(left.members[0], sameIndex).localeCompare(clusterFunctionName(right.members[0], sameIndex)));
81
- const limited = limit !== void 0 ? clusters.slice(0, limit) : clusters;
82
- if (limited.length === 0) return "No clusters.";
83
- return limited.map((cluster, index) => {
84
- const similarity = cluster.min === cluster.max ? cluster.min.toFixed(4) : `${cluster.min.toFixed(4)}-${cluster.max.toFixed(4)}`;
85
- return [
86
- `Cluster ${index + 1} (${cluster.members.length} functions, similarity ${similarity}${combined ? ", combined code + callable description + file description" : ""})`,
87
- ...cluster.members.map((member) => ` ${clusterFunctionName(member, sameIndex)}`)
88
- ].join("\n");
89
- }).join("\n\n");
90
- }
91
- function scoreDetails(scores) {
92
- if (scores.descriptionSimilarity === void 0 || scores.fileDescriptionSimilarity === void 0) return "";
93
- if (scores.codeSimilarity === void 0) {
94
- return ` [combined 50/50; description ${scores.descriptionSimilarity.toFixed(4)}, file ${scores.fileDescriptionSimilarity.toFixed(4)}]`;
95
- }
96
- return ` [combined thirds; code ${scores.codeSimilarity.toFixed(4)}, description ${scores.descriptionSimilarity.toFixed(4)}, file ${scores.fileDescriptionSimilarity.toFixed(4)}]`;
97
- }
98
- function clusterFunctionName(member, sameIndex) {
99
- const role = sameIndex ? "" : `[${member.role}] `;
100
- return `${role}${member.function.path}:${member.function.startLine}:${member.function.startColumn} :: ${member.function.qualifiedName}`;
101
- }
102
-
103
- // src/cli.ts
104
- var DESCRIPTION_PROVIDER_NAMES = ["openai", "opencode", "opencode-go"];
105
- function isDescriptionProviderName(value) {
106
- return DESCRIPTION_PROVIDER_NAMES.includes(value);
107
- }
108
- var parsed = (() => {
109
- try {
110
- return parseArgs({
111
- args: process.argv.slice(2),
112
- allowPositionals: true,
113
- strict: true,
114
- options: {
115
- root: { type: "string", default: process.cwd() },
116
- config: { type: "string" },
117
- index: { type: "string" },
118
- provider: { type: "string" },
119
- model: { type: "string" },
120
- "description-provider": { type: "string" },
121
- "description-model": { type: "string" },
122
- "reranker-candidates": { type: "string" },
123
- dimensions: { type: "string" },
124
- target: { type: "string" },
125
- "target-root": { type: "string" },
126
- "target-index": { type: "string" },
127
- "target-config": { type: "string" },
128
- "changed-since": { type: "string" },
129
- uncommitted: { type: "boolean", default: false },
130
- "source-path": { type: "string" },
131
- "min-lines": { type: "string" },
132
- regex: { type: "string" },
133
- regexp: { type: "string", short: "e" },
134
- limit: { type: "string" },
135
- matches: { type: "string" },
136
- threshold: { type: "string" },
137
- format: { type: "string" },
138
- cohesion: { type: "boolean", default: false },
139
- "include-symmetric-duplicates": { type: "boolean", default: false },
140
- "cross-file-only": { type: "boolean", default: false },
141
- "rebuild-on-divergence": { type: "boolean", default: false },
142
- "force-reindex": { type: "boolean", default: false },
143
- "no-reindex": { type: "boolean", default: false },
144
- callables: { type: "boolean", default: false },
145
- "ignore-errors": { type: "boolean", default: false },
146
- verbose: { type: "boolean", default: false },
147
- version: { type: "boolean", default: false },
148
- help: { type: "boolean", short: "h", default: false }
149
- }
150
- });
151
- } catch (error) {
152
- process.stderr.write(`slopdex: ${error instanceof Error ? error.message : String(error)}
153
- `);
154
- process.exit(2);
155
- }
156
- })();
157
- var [command, ...positionals] = parsed.positionals;
158
- var descriptionsAction = command === "descriptions" ? positionals[0] : void 0;
159
- if (parsed.values.version) {
160
- const version = true ? "0.19.0" : JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
161
- process.stdout.write(`${version}
162
- `);
163
- process.exit(0);
164
- }
165
- if (parsed.values.help || !command) {
166
- printHelp();
167
- process.exit(parsed.values.help ? 0 : 1);
168
- }
169
- var diagnosticIndexes = /* @__PURE__ */ new Set();
170
- function readErrorCountsLight(indexPath) {
171
- const database = new DatabaseSync(indexPath, { readOnly: true });
172
- try {
173
- if (!database.prepare("SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'indexing_errors'").get()) {
174
- return { errors: 0, files: 0 };
175
- }
176
- const counts = database.prepare("SELECT COUNT(*) AS errors, COUNT(DISTINCT path) AS files FROM indexing_errors").get();
177
- return { errors: Number(counts.errors), files: Number(counts.files) };
178
- } finally {
179
- database.close();
180
- }
181
- }
182
- process.on("exit", () => {
183
- if (parsed.values["ignore-errors"]) return;
184
- try {
185
- const rootDir = path.resolve(parsed.values.root);
186
- const config = loadConfig(rootDir, parsed.values.config);
187
- diagnosticIndexes.add(path.resolve(parsed.values.index ?? config.indexPath ?? path.join(rootDir, ".slopdex/index.sqlite")));
188
- if (parsed.values["target-index"]) diagnosticIndexes.add(path.resolve(parsed.values["target-index"]));
189
- } catch {
190
- }
191
- for (const indexPath of diagnosticIndexes) {
192
- try {
193
- if (!existsSync(indexPath)) continue;
194
- const { errors, files } = readErrorCountsLight(indexPath);
195
- if (errors === 0) continue;
196
- process.stderr.write(`slopdex: warning: ${errors} unresolved indexing error(s) in ${files} file(s); run slopdex index-errors --index ${JSON.stringify(indexPath)} to inspect, or use --ignore-errors to silence this warning.
197
- `);
198
- } catch {
199
- }
200
- }
201
- });
202
- void main().catch((error) => {
203
- clearProgress();
204
- const message = error instanceof Error ? error.message : String(error);
205
- process.stderr.write(`slopdex: ${message}
206
- `);
207
- process.exitCode = error instanceof CodeIndexError ? 2 : 1;
208
- });
209
- async function main() {
210
- validateInvocation();
211
- const rootDir = path.resolve(parsed.values.root);
212
- if (command === "models") {
213
- await runModels();
214
- return;
215
- }
216
- if (command === "config") {
217
- await runConfig(rootDir);
218
- return;
219
- }
220
- const config = loadConfig(rootDir, parsed.values.config);
221
- if (command === "index-errors") {
222
- const indexPath = path.resolve(parsed.values.index ?? config.indexPath ?? path.join(rootDir, ".slopdex/index.sqlite"));
223
- const { readIndexErrors } = await import("./database-P5XWJWQL.js");
224
- const errors = existsSync(indexPath) ? readIndexErrors(indexPath) : [];
225
- if (outputFormat("summary") === "summary") {
226
- process.stdout.write(errors.length === 0 ? "No indexing errors.\n" : `${errors.map(
227
- (error) => `${error.path}${error.startLine === null ? "" : `:${error.startLine}:${error.startColumn}`} ${error.qualifiedName ? `:: ${error.qualifiedName} ` : ""}[${error.code}]
228
- ${error.message}`
229
- ).join("\n")}
230
- `);
231
- } else printJson(errors);
232
- return;
233
- }
234
- const provider = await createProvider(config);
235
- const reranker = command === "search" || command === "search-description" ? await createReranker(config) : void 0;
236
- const descriptionProvider = await createDescriptionProvider(config);
237
- const progress = new TerminalProgress();
238
- const indexOptions = {
239
- rootDir,
240
- provider,
241
- parallelism: config.parallelism ?? DEFAULT_PARALLELISM,
242
- onProgress: (value) => progress.update(value),
243
- ...reranker ? { reranker } : {},
244
- ...descriptionProvider ? { descriptionProvider } : {},
245
- onWarning: () => {
246
- },
247
- // Persisted diagnostics are reported once per index at exit.
248
- ...parsed.values.index || config.indexPath ? { indexPath: parsed.values.index ?? config.indexPath } : {},
249
- ...config.include ? { include: config.include } : {},
250
- ...config.exclude ? { exclude: config.exclude } : {},
251
- ...config.maxFileSize ? { maxFileSize: config.maxFileSize } : {},
252
- ...config.embeddingBatchSize ? { embeddingBatchSize: config.embeddingBatchSize } : {},
253
- ...config.verbose ? { verbose: true } : {}
254
- };
255
- const updateTarget = command === "update-git" ? parsed.values.target ?? "HEAD" : "HEAD";
256
- const updateStats = await ensureIndexUpdated(
257
- indexOptions,
258
- "index",
259
- updateTarget,
260
- parsed.values["rebuild-on-divergence"],
261
- parsed.values["force-reindex"],
262
- parsed.values["no-reindex"],
263
- descriptionRefresh(config)
264
- );
265
- const { CodeIndex } = await import("./code-index-MWSJKV3G.js");
266
- const index = new CodeIndex(indexOptions);
267
- try {
268
- switch (command) {
269
- case "status":
270
- printJson(index.status());
271
- break;
272
- case "update-files":
273
- if (positionals.length === 0) throw new CodeIndexError("update-files requires at least one path.");
274
- printJson(await index.updateFiles({ upsert: positionals }));
275
- break;
276
- case "delete-files":
277
- if (positionals.length === 0) throw new CodeIndexError("delete-files requires at least one path.");
278
- printJson(await index.updateFiles({ delete: positionals }));
279
- break;
280
- case "update-git": {
281
- printJson(updateStats);
282
- break;
283
- }
284
- case "reindex-files":
285
- printJson(await index.reindexFiles({ includeCallables: parsed.values.callables }));
286
- break;
287
- case "descriptions":
288
- printJson(descriptionsAction === "enable" ? await index.useDescriptions() : index.disableDescriptions());
289
- break;
290
- case "search":
291
- case "search-description": {
292
- const query = positionals.join(" ").trim();
293
- if (!query) throw new CodeIndexError(`${command} requires a query.`);
294
- const threshold = similarityThreshold();
295
- const nameRegex = qualifiedNameRegex();
296
- const results = await (command === "search-description" ? index.searchDescription.bind(index) : index.similaritySearch.bind(index))({
297
- query,
298
- ...nameRegex !== void 0 ? { nameRegex } : {},
299
- ...parsed.values.limit === void 0 ? {} : { limit: positiveIntegerOption(parsed.values.limit, 1, "limit") },
300
- minSimilarity: threshold.min,
301
- ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {}
302
- });
303
- const format = outputFormat("summary");
304
- if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
305
- if (format === "summary") {
306
- process.stdout.write(`${command === "search-description" ? results.map((match) => `${formatSimilaritySummary([match])}
307
- ${match.function.description}`).join("\n") : formatSimilaritySummary(results)}
308
- `);
309
- } else printJson(results.map(presentMatch));
310
- break;
311
- }
312
- case "cross-search":
313
- await runCrossSearch(index, indexOptions, provider);
314
- break;
315
- default:
316
- throw new CodeIndexError(`Unknown command: ${command}`);
317
- }
318
- } finally {
319
- index.close();
320
- }
321
- }
322
- async function runCrossSearch(source, sourceOptions, provider) {
323
- const targetRoot = parsed.values["target-root"];
324
- const targetPath = parsed.values["target-index"];
325
- if (targetRoot && !targetPath || !targetRoot && targetPath) {
326
- throw new CodeIndexError("Cross-index search requires both --target-root and --target-index.");
327
- }
328
- const targetRootDir = targetRoot ? path.resolve(targetRoot) : void 0;
329
- const resolvedTargetPath = targetPath ? path.resolve(targetPath) : void 0;
330
- const usesSourceAsTarget = targetRootDir === source.rootDir && resolvedTargetPath === source.indexPath;
331
- const targetConfig = targetRootDir && !usesSourceAsTarget ? loadConfig(targetRootDir, parsed.values["target-config"]) : void 0;
332
- const targetDescriptionProvider = targetConfig ? await createDescriptionProvider(targetConfig) : void 0;
333
- const targetOptions = targetRootDir && resolvedTargetPath && !usesSourceAsTarget ? {
334
- rootDir: targetRootDir,
335
- indexPath: resolvedTargetPath,
336
- provider,
337
- ...sourceOptions.parallelism !== void 0 ? { parallelism: sourceOptions.parallelism } : {},
338
- ...sourceOptions.onProgress ? { onProgress: sourceOptions.onProgress } : {},
339
- ...sourceOptions.onWarning ? { onWarning: sourceOptions.onWarning } : {},
340
- ...targetConfig?.include ? { include: targetConfig.include } : {},
341
- ...targetConfig?.exclude ? { exclude: targetConfig.exclude } : {},
342
- ...targetConfig?.maxFileSize ? { maxFileSize: targetConfig.maxFileSize } : {},
343
- ...targetConfig?.embeddingBatchSize ? { embeddingBatchSize: targetConfig.embeddingBatchSize } : {},
344
- ...targetConfig?.verbose ? { verbose: true } : {},
345
- ...targetDescriptionProvider ? { descriptionProvider: targetDescriptionProvider } : {}
346
- } : void 0;
347
- if (targetOptions) {
348
- await ensureIndexUpdated(
349
- targetOptions,
350
- "target index",
351
- "HEAD",
352
- parsed.values["rebuild-on-divergence"],
353
- parsed.values["force-reindex"],
354
- parsed.values["no-reindex"],
355
- descriptionRefresh(targetConfig)
356
- );
357
- }
358
- const { CodeIndex: CodeIndexClass } = await import("./code-index-MWSJKV3G.js");
359
- const { crossSearch } = await import("./cross-search-ERBFC25T.js");
360
- const target = targetOptions ? new CodeIndexClass({ ...targetOptions, readOnly: true }) : void 0;
361
- const format = outputFormat(parsed.values.cohesion ? "summary" : "clusters");
362
- const threshold = similarityThreshold();
363
- const indexProgress = sourceOptions.onProgress;
364
- const outputLimit = parsed.values.limit === void 0 ? void 0 : positiveIntegerOption(parsed.values.limit, 1, "limit");
365
- const searchOptions = {
366
- source,
367
- ...target ? { target } : {},
368
- sourceFilter: crossSearchSourceFilter(),
369
- limitPerFunction: positiveIntegerOption(parsed.values.matches, 5, "matches"),
370
- minSimilarity: threshold.min,
371
- ...threshold.max !== void 0 ? { maxSimilarity: threshold.max } : {},
372
- includeSymmetricDuplicates: parsed.values["include-symmetric-duplicates"],
373
- crossFileOnly: parsed.values["cross-file-only"],
374
- cohesion: parsed.values.cohesion,
375
- minLines: minimumLines(),
376
- // Per-source read progress (including read-repair scans) on the terminal bar.
377
- ...indexProgress ? {
378
- onProgress: (value) => indexProgress({ phase: "cross-search", ...value })
379
- } : {}
380
- };
381
- try {
382
- if (format === "clusters") {
383
- const results = [];
384
- for await (const result of crossSearch(searchOptions)) results.push(result);
385
- process.stdout.write(`${formatSimilarityClusters(results, !target, outputLimit)}
386
- `);
387
- return;
388
- }
389
- let emitted = 0;
390
- for await (const result of crossSearch(searchOptions)) {
391
- if (outputLimit !== void 0 && emitted >= outputLimit) break;
392
- if (format === "summary") {
393
- process.stdout.write(`${formatSimilaritySummary(result.matches, result.source)}
394
- `);
395
- } else {
396
- process.stdout.write(`${JSON.stringify({
397
- source: presentFunction(result.source),
398
- matches: result.matches.map(presentMatch),
399
- scoring: result.scoring
400
- })}
401
- `);
402
- }
403
- emitted += 1;
404
- }
405
- } finally {
406
- target?.close();
407
- }
408
- }
409
- async function ensureIndexUpdated(options, label, target, rebuildOnDivergence, forceRebuild, noReindex, hooks) {
410
- diagnosticIndexes.add(resolveIndexPath(options));
411
- const initialized = await initializeMissingIndex(options, label, target, noReindex, hooks);
412
- if (initialized) return initialized;
413
- const { CodeIndex } = await import("./code-index-MWSJKV3G.js");
414
- try {
415
- const index = new CodeIndex(options);
416
- try {
417
- await hooks?.beforeRefresh?.(index);
418
- const stats = await refreshIndex(index, label, target, rebuildOnDivergence, noReindex);
419
- await hooks?.afterRefresh?.(index);
420
- return stats;
421
- } finally {
422
- index.close();
423
- }
424
- } catch (error) {
425
- if (!forceRebuild || !(error instanceof IncompatibleIndexError)) throw error;
426
- process.stderr.write(
427
- `slopdex: warning: ${label} is incompatible (${error.message}); rebuilding automatically because --force-reindex was specified.
428
- `
429
- );
430
- const indexPath = resolveIndexPath(options);
431
- const descriptionProfile = descriptionProfileForRebuild(indexPath, options.rootDir);
432
- try {
433
- const { resetIndexState } = await import("./database-P5XWJWQL.js");
434
- resetIndexState(indexPath, options.rootDir, options.provider.profile);
435
- } catch (resetError) {
436
- if (!(resetError instanceof IncompatibleIndexError)) throw resetError;
437
- removeIndexArtifacts(indexPath);
438
- }
439
- return initializeIndex(options, indexPath, label, target, noReindex, descriptionProfile, hooks);
440
- }
441
- }
442
- async function initializeMissingIndex(options, label, target, noReindex, hooks) {
443
- const indexPath = resolveIndexPath(options);
444
- if (existsSync(indexPath)) return null;
445
- process.stderr.write(
446
- `slopdex: ${label} not found at ${indexPath}; initializing automatically from ${target}${noReindex ? "" : " and the working tree"}.
447
- `
448
- );
449
- return initializeIndex(options, indexPath, label, target, noReindex, null, hooks);
450
- }
451
- async function initializeIndex(options, indexPath, label, target, noReindex, descriptionProfile = null, hooks) {
452
- const { CodeIndex } = await import("./code-index-MWSJKV3G.js");
453
- const rebuiltDescriptionProvider = descriptionProfile && !options.descriptionProvider ? await createDescriptionProvider({
454
- descriptionProvider: descriptionProfile.provider,
455
- descriptionModel: descriptionProfile.model,
456
- parallelism: options.parallelism ?? DEFAULT_PARALLELISM,
457
- ...options.verbose ? { verbose: true } : {}
458
- }) : void 0;
459
- const index = new CodeIndex({
460
- ...options,
461
- indexPath,
462
- ...rebuiltDescriptionProvider ? { descriptionProvider: rebuiltDescriptionProvider } : {}
463
- });
464
- try {
465
- if (descriptionProfile) await index.useDescriptions();
466
- await hooks?.beforeRefresh?.(index);
467
- const stats = await refreshIndex(index, label, target, false, noReindex);
468
- await hooks?.afterRefresh?.(index);
469
- return stats;
470
- } finally {
471
- index.close();
472
- }
473
- }
474
- function descriptionProfileForRebuild(indexPath, rootDir) {
475
- const db = new DatabaseSync(indexPath, { readOnly: true });
476
- try {
477
- const metadata = new Map(db.prepare("SELECT key, value FROM metadata").all().map((row) => [row.key, row.value]));
478
- if (metadata.get("root_dir") !== path.resolve(rootDir) || metadata.get("descriptions_enabled") !== "true") return null;
479
- const profile = JSON.parse(metadata.get("description_profile"));
480
- if (!isDescriptionProviderName(profile.provider)) {
481
- throw new CodeIndexError("Rebuilding this description index requires its custom description provider through the library API.");
482
- }
483
- return profile;
484
- } finally {
485
- db.close();
486
- }
487
- }
488
- async function refreshIndex(index, label, target, rebuildOnDivergence, noReindex) {
489
- try {
490
- return await index.updateFromGit({ target, rebuildOnDivergence, includeWorkingTree: !noReindex });
491
- } catch (error) {
492
- if (!(error instanceof GitUnavailableError)) throw error;
493
- const status = index.status();
494
- if (noReindex && status.fileCount > 0) {
495
- process.stderr.write(
496
- `slopdex: warning: no Git repository is available for ${label}; full working-tree re-index skipped because --no-reindex was specified.
497
- `
498
- );
499
- return {
500
- filesUpdated: 0,
501
- filesDeleted: 0,
502
- functionsAdded: 0,
503
- functionsUpdated: 0,
504
- functionsDeleted: 0,
505
- embeddingsCreated: 0,
506
- checkpoint: status.gitCheckpoint
507
- };
508
- }
509
- process.stderr.write(
510
- `slopdex: warning: no Git repository is available for ${label}; re-indexing all source files from the working tree.
511
- `
512
- );
513
- return await index.updateFromWorkingTree();
514
- }
515
- }
516
- function resolveIndexPath(options) {
517
- return path.resolve(options.indexPath ?? path.join(path.resolve(options.rootDir), ".slopdex", "index.sqlite"));
518
- }
519
- function removeIndexArtifacts(indexPath) {
520
- for (const suffix of ["", "-shm", "-wal", "-journal"]) rmSync(`${indexPath}${suffix}`, { force: true });
521
- }
522
- function loadConfig(rootDir, configuredPath) {
523
- const configPath = path.resolve(configuredPath ?? path.join(rootDir, ".slopdex", "config.json"));
524
- if (!existsSync(configPath)) return commandLineConfig({});
525
- const value = JSON.parse(readFileSync(configPath, "utf8"));
526
- return commandLineConfig(value);
527
- }
528
- function commandLineConfig(config) {
529
- const dimensions = parsed.values.dimensions ? numberOption(parsed.values.dimensions, 0, "dimensions") : config.dimensions;
530
- const providerValue = parsed.values.provider ?? config.provider;
531
- if (providerValue !== void 0 && providerValue !== "openai" && providerValue !== "jina") {
532
- throw new CodeIndexError(`Unsupported provider: ${providerValue}`);
533
- }
534
- const provider = providerValue === "openai" || providerValue === "jina" ? providerValue : void 0;
535
- const descriptionProviderValue = parsed.values["description-provider"] ?? config.descriptionProvider;
536
- if (descriptionProviderValue !== void 0 && !isDescriptionProviderName(descriptionProviderValue)) {
537
- throw new CodeIndexError(`Unsupported description provider: ${descriptionProviderValue}`);
538
- }
539
- if (config.rerankingEnabled !== void 0 && typeof config.rerankingEnabled !== "boolean") {
540
- throw new CodeIndexError("rerankingEnabled must be a boolean.");
541
- }
542
- if (config.parallelism !== void 0 && (!Number.isInteger(config.parallelism) || config.parallelism < 1)) {
543
- throw new CodeIndexError("parallelism must be a positive integer.");
544
- }
545
- if (config.verbose !== void 0 && typeof config.verbose !== "boolean") {
546
- throw new CodeIndexError("verbose must be a boolean.");
547
- }
548
- if (config.rerankingEnabled === true) {
549
- if (config.rerankerProvider !== "cohere" && config.rerankerProvider !== "jina" && config.rerankerProvider !== "openai") {
550
- throw new CodeIndexError(`Unsupported reranker provider: ${String(config.rerankerProvider)}`);
551
- }
552
- if (config.rerankerModel !== void 0 && (typeof config.rerankerModel !== "string" || !config.rerankerModel.trim())) {
553
- throw new CodeIndexError("rerankerModel must be a non-empty string.");
554
- }
555
- if (config.rerankerProvider === "openai" && config.rerankerCandidates !== void 0 && (typeof config.rerankerCandidates !== "number" || !Number.isInteger(config.rerankerCandidates) || config.rerankerCandidates < 1)) {
556
- throw new CodeIndexError("rerankerCandidates must be a positive integer.");
557
- }
558
- if (config.rerankerProvider === "openai" && typeof config.rerankerCandidates === "number" && config.rerankerCandidates > 100) {
559
- throw new CodeIndexError("rerankerCandidates must not exceed 100.");
560
- }
561
- }
562
- return {
563
- ...config,
564
- ...provider ? { provider } : {},
565
- ...parsed.values.model ? { model: parsed.values.model } : {},
566
- ...descriptionProviderValue ? { descriptionProvider: descriptionProviderValue } : {},
567
- ...parsed.values["description-model"] ? { descriptionModel: parsed.values["description-model"] } : {},
568
- ...dimensions ? { dimensions } : {},
569
- ...parsed.values.verbose ? { verbose: true } : {}
570
- };
571
- }
572
- async function createDescriptionProvider(config) {
573
- if (!config.descriptionProvider && !config.descriptionModel) return void 0;
574
- const { OpenAIDescriptionProvider } = await import("./openai-EC6THXKV.js");
575
- return new OpenAIDescriptionProvider({
576
- ...config.descriptionProvider ? { provider: config.descriptionProvider } : {},
577
- ...config.descriptionModel ? { model: config.descriptionModel } : {},
578
- parallelism: config.parallelism ?? DEFAULT_PARALLELISM,
579
- ...config.verbose ? { verbose: true } : {}
580
- });
581
- }
582
- function descriptionRefresh(config) {
583
- if (command === "descriptions") {
584
- return descriptionsAction === "disable" ? { beforeRefresh: (index) => {
585
- index.disableDescriptions();
586
- } } : enableDescriptionRefresh(false);
587
- }
588
- if (config.descriptionsEnabled === true) return enableDescriptionRefresh();
589
- if (config.descriptionsEnabled === false) return { beforeRefresh: (index) => {
590
- index.disableDescriptions();
591
- } };
592
- return void 0;
593
- }
594
- function enableDescriptionRefresh(afterRefresh = true) {
595
- return {
596
- ...afterRefresh ? { afterRefresh: async (index) => {
597
- await index.useDescriptions();
598
- } } : {}
599
- };
600
- }
601
- async function runModels() {
602
- const positionalProvider = positionals[0];
603
- const optionProvider = parsed.values["description-provider"];
604
- if (positionalProvider && optionProvider && positionalProvider !== optionProvider) {
605
- throw new CodeIndexError("models provider and --description-provider must match when both are supplied.");
606
- }
607
- const requested = positionalProvider ?? optionProvider;
608
- if (requested !== void 0 && !isOpenCodeDescriptionProvider(requested)) {
609
- throw new CodeIndexError("models provider must be opencode or opencode-go.");
610
- }
611
- const providers = requested ? [requested] : ["opencode", "opencode-go"];
612
- const models = (await Promise.all(providers.map(fetchPublishedModels))).flat();
613
- const format = outputFormat("summary");
614
- if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
615
- if (format === "json") printJson(models);
616
- else process.stdout.write(`${models.map(({ provider, model }) => `${provider}/${model}`).join("\n")}
617
- `);
618
- }
619
- async function runConfig(rootDir) {
620
- const configPath = path.resolve(parsed.values.config ?? path.join(rootDir, ".slopdex", "config.json"));
621
- const config = readConfigFile(configPath);
622
- const action = positionals[0];
623
- let result;
624
- if (action === "descriptions") {
625
- config.descriptionsEnabled = positionals[1] === "enable";
626
- result = { descriptionsEnabled: config.descriptionsEnabled };
627
- } else if (action === "model") {
628
- const selected = await resolveConfiguredModel(positionals[1]);
629
- config.descriptionProvider = selected.provider;
630
- config.descriptionModel = selected.model;
631
- result = { descriptionProvider: selected.provider, descriptionModel: selected.model };
632
- } else if (action === "parallelism") {
633
- const parallelism = Number(positionals[1]);
634
- config.parallelism = parallelism;
635
- result = { parallelism };
636
- } else {
637
- const provider = positionals[1];
638
- if (provider === "disable") {
639
- config.rerankingEnabled = false;
640
- result = { rerankingEnabled: false };
641
- } else {
642
- const sameProvider = config.rerankerProvider === provider;
643
- const defaultModel = provider === "cohere" ? "rerank-v4.0-pro" : provider === "jina" ? "jina-reranker-v3.5" : "gpt-5.6-luna";
644
- const model = positionals[2] ?? (sameProvider && typeof config.rerankerModel === "string" && config.rerankerModel.trim() ? config.rerankerModel : defaultModel);
645
- config.rerankerProvider = provider;
646
- config.rerankerModel = model;
647
- if (provider === "openai") {
648
- const savedCandidates = sameProvider && typeof config.rerankerCandidates === "number" && Number.isInteger(config.rerankerCandidates) && config.rerankerCandidates > 0 ? config.rerankerCandidates : 10;
649
- config.rerankerCandidates = openAIRerankerCandidateCount(
650
- parsed.values["reranker-candidates"],
651
- savedCandidates,
652
- "reranker candidate count"
653
- );
654
- } else {
655
- delete config.rerankerCandidates;
656
- }
657
- config.rerankingEnabled = true;
658
- result = {
659
- rerankingEnabled: true,
660
- rerankerProvider: provider,
661
- rerankerModel: model,
662
- ...provider === "openai" ? { rerankerCandidates: config.rerankerCandidates } : {}
663
- };
664
- }
665
- }
666
- writeConfigFile(configPath, config);
667
- const format = outputFormat("summary");
668
- if (format === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
669
- if (format === "json") printJson({ configPath, ...result });
670
- else process.stdout.write(`Updated ${configPath}: ${Object.entries(result).map(([key, value]) => `${key}=${value}`).join(" ")}
671
- `);
672
- }
673
- async function resolveConfiguredModel(positionalModel) {
674
- const optionModel = parsed.values["description-model"];
675
- if (positionalModel && optionModel && positionalModel !== optionModel) {
676
- throw new CodeIndexError("config model argument and --description-model must match when both are supplied.");
677
- }
678
- let reference = positionalModel ?? optionModel;
679
- if (!reference) throw new CodeIndexError("config model requires a model ID or provider/model reference.");
680
- let provider = parsed.values["description-provider"];
681
- const separator = reference.indexOf("/");
682
- if (separator !== -1) {
683
- const qualifiedProvider = reference.slice(0, separator);
684
- reference = reference.slice(separator + 1);
685
- if (provider && provider !== qualifiedProvider) {
686
- throw new CodeIndexError("Model reference and --description-provider must select the same provider.");
687
- }
688
- provider = qualifiedProvider;
689
- }
690
- if (!reference) throw new CodeIndexError("config model requires a non-empty model ID.");
691
- if (provider !== void 0) {
692
- if (!isOpenCodeDescriptionProvider(provider)) {
693
- throw new CodeIndexError("config model provider must be opencode or opencode-go.");
694
- }
695
- const models = await fetchPublishedModels(provider);
696
- if (!models.some(({ model }) => model === reference)) {
697
- throw new CodeIndexError(`Unknown ${provider} model: ${reference}`);
698
- }
699
- return { provider, model: reference };
700
- }
701
- const matches = (await Promise.all([
702
- fetchPublishedModels("opencode"),
703
- fetchPublishedModels("opencode-go")
704
- ])).flat().filter(({ model }) => model === reference);
705
- if (matches.length === 0) throw new CodeIndexError(`Unknown OpenCode model: ${reference}`);
706
- if (matches.length > 1) {
707
- throw new CodeIndexError(`Model ${reference} is available from multiple providers; use provider/model.`);
708
- }
709
- return matches[0];
710
- }
711
- async function fetchPublishedModels(provider) {
712
- const { descriptionProviderBaseUrl } = await import("./openai-EC6THXKV.js");
713
- const url = `${descriptionProviderBaseUrl(provider)}/models`;
714
- let response;
715
- try {
716
- response = await fetch(url);
717
- } catch (error) {
718
- throw new CodeIndexError(`Could not fetch ${provider} models: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
719
- }
720
- if (!response.ok) {
721
- throw new CodeIndexError(`Could not fetch ${provider} models (${response.status}): ${(await response.text()).slice(0, 500)}`);
722
- }
723
- let body;
724
- try {
725
- body = await response.json();
726
- } catch (error) {
727
- throw new CodeIndexError(`${provider} returned a malformed model list.`, { cause: error });
728
- }
729
- if (!Array.isArray(body.data) || body.data.some((item) => typeof item.id !== "string" || item.id.length === 0)) {
730
- throw new CodeIndexError(`${provider} returned a malformed model list.`);
731
- }
732
- return [...new Set(body.data.map((item) => item.id))].map((model) => ({ provider, model }));
733
- }
734
- function isOpenCodeDescriptionProvider(value) {
735
- return value === "opencode" || value === "opencode-go";
736
- }
737
- function readConfigFile(configPath) {
738
- if (!existsSync(configPath)) return {};
739
- const value = JSON.parse(readFileSync(configPath, "utf8"));
740
- if (!value || typeof value !== "object" || Array.isArray(value)) {
741
- throw new CodeIndexError(`Config must contain a JSON object: ${configPath}`);
742
- }
743
- return value;
744
- }
745
- function writeConfigFile(configPath, config) {
746
- mkdirSync(path.dirname(configPath), { recursive: true });
747
- const temporaryPath = `${configPath}.tmp-${process.pid}`;
748
- writeFileSync(temporaryPath, `${JSON.stringify(config, null, 2)}
749
- `);
750
- renameSync(temporaryPath, configPath);
751
- }
752
- async function createProvider(config) {
753
- if ((config.provider ?? "openai") === "jina") {
754
- const { JinaEmbeddingProvider } = await import("./jina-43RL7C6M.js");
755
- return new JinaEmbeddingProvider({
756
- ...config.model ? { model: config.model } : {},
757
- ...config.dimensions ? { dimensions: config.dimensions } : {},
758
- parallelism: config.parallelism ?? DEFAULT_PARALLELISM,
759
- ...config.verbose ? { verbose: true } : {}
760
- });
761
- }
762
- const { OpenAIEmbeddingProvider } = await import("./openai-CYUPNBGB.js");
763
- return new OpenAIEmbeddingProvider({
764
- ...config.model ? { model: config.model } : {},
765
- ...config.dimensions ? { dimensions: config.dimensions } : {},
766
- parallelism: config.parallelism ?? DEFAULT_PARALLELISM,
767
- ...config.verbose ? { verbose: true } : {}
768
- });
769
- }
770
- async function createReranker(config) {
771
- if (config.rerankingEnabled !== true) return void 0;
772
- if (config.rerankerProvider === "cohere" || config.rerankerProvider === "jina") {
773
- const { CohereReranker, JinaReranker } = await import("./hosted-GHIUHKOU.js");
774
- if (config.rerankerProvider === "cohere") {
775
- return new CohereReranker({
776
- ...config.rerankerModel ? { model: config.rerankerModel } : {},
777
- ...config.verbose ? { verbose: true } : {}
778
- });
779
- }
780
- return new JinaReranker({
781
- ...config.rerankerModel ? { model: config.rerankerModel } : {},
782
- ...config.verbose ? { verbose: true } : {}
783
- });
784
- }
785
- if (config.rerankerProvider === "openai") {
786
- const { OpenAILLMReranker } = await import("./openai-TCRXM66O.js");
787
- return new OpenAILLMReranker({
788
- ...config.rerankerModel ? { model: config.rerankerModel } : {},
789
- ...config.rerankerCandidates ? { candidateCount: config.rerankerCandidates } : {},
790
- ...config.verbose ? { verbose: true } : {}
791
- });
792
- }
793
- throw new CodeIndexError("rerankerProvider is required when rerankingEnabled is true.");
794
- }
795
- function numberOption(value, defaultValue, name) {
796
- if (value === void 0) return defaultValue;
797
- const parsedValue = Number(value);
798
- if (!Number.isFinite(parsedValue)) throw new CodeIndexError(`${name} must be a number.`);
799
- return parsedValue;
800
- }
801
- function validateInvocation() {
802
- if (parsed.values["reranker-candidates"] !== void 0 && (command !== "config" || positionals[0] !== "reranker" || positionals[1] !== "openai")) {
803
- throw new CodeIndexError("--reranker-candidates is only available with config reranker openai.");
804
- }
805
- if (parsed.values.cohesion && command !== "cross-search") {
806
- throw new CodeIndexError("--cohesion is only available for cross-search.");
807
- }
808
- if (parsed.values.matches !== void 0 && command !== "cross-search") {
809
- throw new CodeIndexError("--matches is only available for cross-search.");
810
- }
811
- const nameRegex = qualifiedNameRegex();
812
- if (nameRegex !== void 0) {
813
- if (!["search", "search-description", "cross-search"].includes(command)) {
814
- throw new CodeIndexError("-e/--regexp/--regex is only available for search, search-description, and cross-search.");
815
- }
816
- compileNameRegex(nameRegex, parsed.values.regex !== void 0 ? "--regex value" : "-e/--regexp value");
817
- }
818
- switch (command) {
819
- case "models":
820
- if (positionals.length > 1) throw new CodeIndexError("models accepts at most one provider.");
821
- if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
822
- return;
823
- case "config":
824
- if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
825
- if (positionals[0] === "descriptions") {
826
- if (positionals.length !== 2 || positionals[1] !== "enable" && positionals[1] !== "disable") {
827
- throw new CodeIndexError("config descriptions requires enable or disable.");
828
- }
829
- return;
830
- }
831
- if (positionals[0] === "model") {
832
- if (positionals.length > 2 || !positionals[1] && !parsed.values["description-model"]) {
833
- throw new CodeIndexError("config model requires a model ID or provider/model reference.");
834
- }
835
- return;
836
- }
837
- if (positionals[0] === "parallelism") {
838
- const value = Number(positionals[1]);
839
- if (positionals.length !== 2 || !Number.isInteger(value) || value < 1) {
840
- throw new CodeIndexError("config parallelism requires a positive integer.");
841
- }
842
- return;
843
- }
844
- if (positionals[0] === "reranker") {
845
- const provider = positionals[1];
846
- if (provider !== "cohere" && provider !== "jina" && provider !== "openai" && provider !== "disable" || positionals.length > (provider === "disable" ? 2 : 3) || positionals[2] !== void 0 && !positionals[2].trim()) {
847
- throw new CodeIndexError("config reranker requires cohere, jina, openai, or disable, optionally followed by a model ID.");
848
- }
849
- if (provider === "openai") openAIRerankerCandidateCount(parsed.values["reranker-candidates"], 10, "reranker candidate count");
850
- return;
851
- }
852
- throw new CodeIndexError("config requires descriptions, model, parallelism, or reranker.");
853
- case "index-errors":
854
- if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
855
- return;
856
- case "status":
857
- case "update-git":
858
- return;
859
- case "reindex-files":
860
- if (positionals.length > 0) throw new CodeIndexError("reindex-files does not accept positional arguments.");
861
- return;
862
- case "descriptions":
863
- if (positionals.length !== 1 || descriptionsAction !== "enable" && descriptionsAction !== "disable") {
864
- throw new CodeIndexError("descriptions requires enable or disable.");
865
- }
866
- return;
867
- case "update-files":
868
- if (positionals.length === 0) throw new CodeIndexError("update-files requires at least one path.");
869
- return;
870
- case "delete-files":
871
- if (positionals.length === 0) throw new CodeIndexError("delete-files requires at least one path.");
872
- return;
873
- case "search":
874
- case "search-description": {
875
- if (!positionals.join(" ").trim()) throw new CodeIndexError(`${command} requires a query.`);
876
- validateOutputLimit();
877
- similarityThreshold();
878
- if (outputFormat("summary") === "clusters") throw new CodeIndexError("clusters format is only available for cross-search.");
879
- return;
880
- }
881
- case "cross-search":
882
- if (Boolean(parsed.values["target-root"]) !== Boolean(parsed.values["target-index"])) {
883
- throw new CodeIndexError("Cross-index search requires both --target-root and --target-index.");
884
- }
885
- if (parsed.values["target-config"] && !parsed.values["target-root"]) {
886
- throw new CodeIndexError("--target-config requires --target-root and --target-index.");
887
- }
888
- validateMatches(5);
889
- validateOutputLimit();
890
- similarityThreshold();
891
- if (parsed.values.cohesion && outputFormat("summary") === "clusters") {
892
- throw new CodeIndexError("clusters format does not preserve cohesion match order; use summary or json.");
893
- }
894
- outputFormat(parsed.values.cohesion ? "summary" : "clusters");
895
- minimumLines();
896
- crossSearchSourceFilter();
897
- return;
898
- default:
899
- throw new CodeIndexError(`Unknown command: ${command}`);
900
- }
901
- }
902
- function validateMatches(defaultValue) {
903
- positiveIntegerOption(parsed.values.matches, defaultValue, "matches");
904
- }
905
- function validateOutputLimit() {
906
- if (parsed.values.limit !== void 0) positiveIntegerOption(parsed.values.limit, 1, "limit");
907
- }
908
- function positiveIntegerOption(value, defaultValue, name) {
909
- const parsedValue = numberOption(value, defaultValue, name);
910
- if (!Number.isInteger(parsedValue) || parsedValue < 1) throw new CodeIndexError(`${name} must be a positive integer.`);
911
- return parsedValue;
912
- }
913
- function openAIRerankerCandidateCount(value, defaultValue, name) {
914
- const count = positiveIntegerOption(value, defaultValue, name);
915
- if (count > 100) throw new CodeIndexError(`${name} must not exceed 100.`);
916
- return count;
917
- }
918
- function similarityThreshold(defaultMin = 0.3) {
919
- const threshold = parsed.values.threshold;
920
- if (threshold === void 0) return { min: defaultMin };
921
- const number = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:e[+-]?\\d+)?";
922
- const range = new RegExp(`^\\s*(${number})\\s*-\\s*(${number})\\s*$`, "i").exec(threshold);
923
- if (!range) return { min: numberOption(threshold, defaultMin, "threshold") };
924
- const min = Number(range[1]);
925
- const max = Number(range[2]);
926
- if (!Number.isFinite(min) || !Number.isFinite(max)) throw new CodeIndexError("threshold range bounds must be numbers.");
927
- if (min >= max) throw new CodeIndexError("threshold range minimum must be less than its maximum.");
928
- return { min, max };
929
- }
930
- function outputFormat(defaultValue) {
931
- const value = parsed.values.format ?? defaultValue;
932
- if (value !== "json" && value !== "summary" && value !== "clusters") {
933
- throw new CodeIndexError("format must be json, summary, or clusters.");
934
- }
935
- return value;
936
- }
937
- function minimumLines() {
938
- const value = numberOption(parsed.values["min-lines"], 2, "min-lines");
939
- if (!Number.isInteger(value) || value < 1) throw new CodeIndexError("min-lines must be a positive integer.");
940
- return value;
941
- }
942
- function qualifiedNameRegex() {
943
- if (parsed.values.regexp !== void 0 && parsed.values.regex !== void 0 && parsed.values.regexp !== parsed.values.regex) {
944
- throw new CodeIndexError("-e/--regexp and --regex are aliases and cannot use different values.");
945
- }
946
- return parsed.values.regexp ?? parsed.values.regex;
947
- }
948
- function crossSearchSourceFilter() {
949
- const changedSince = parsed.values["changed-since"];
950
- const uncommitted = parsed.values.uncommitted;
951
- const nameRegex = qualifiedNameRegex();
952
- const restrictions = {
953
- ...parsed.values["source-path"] ? { path: parsed.values["source-path"] } : {},
954
- ...nameRegex !== void 0 ? { nameRegex } : {}
955
- };
956
- if (changedSince) return { type: "changed-since", commit: changedSince, ...uncommitted ? { uncommitted: true } : {}, ...restrictions };
957
- if (uncommitted) return { type: "uncommitted", ...restrictions };
958
- return { type: "all", ...restrictions };
959
- }
960
- function presentFunction(value) {
961
- const { embeddingInput: _embeddingInput, embeddingId: _embeddingId, descriptionEmbeddingId: _descriptionEmbeddingId, ...result } = value;
962
- return result;
963
- }
964
- function presentMatch(value) {
965
- const { function: callable, ...scores } = value;
966
- return { ...scores, function: presentFunction(callable) };
967
- }
968
- function printJson(value) {
969
- process.stdout.write(`${JSON.stringify(value, null, 2)}
970
- `);
971
- }
972
- function printHelp() {
973
- process.stdout.write(`Usage: slopdex <command> [arguments] [options]
974
-
975
- Commands:
976
- models [opencode|opencode-go] List current published OpenCode models
977
- config model <model|provider/model> Validate and save an OpenCode description model
978
- config descriptions <enable|disable> Save description state without opening an index
979
- config parallelism <count> Save the concurrent provider request limit (default: 10)
980
- config reranker <provider|disable> Save Cohere, Jina, or OpenAI reranking settings
981
- status Show index metadata
982
- index-errors List persisted file and function indexing failures
983
- update-files <path...> Index specific working-tree files
984
- reindex-files Regenerate stale file descriptions
985
- delete-files <path...> Remove specific files from the index
986
- update-git Index a Git snapshot plus working-tree changes
987
- search <query> Search functions by semantic similarity
988
- descriptions <enable|disable> Enable or disable automatic purpose descriptions
989
- search-description <query> Search functions using description embeddings
990
- cross-search Find nearest functions for each source function
991
-
992
- Examples:
993
- Search code:
994
- slopdex search "validate an authenticated session"
995
-
996
- slopdex search "keep the repository index synchronized"
997
- 0.4284 tests/languages.test.ts :: refresh
998
- 0.4200 src/cli.ts :: refreshIndex
999
- 0.4113 src/code-index.ts :: CodeIndex.updateFromGit
1000
- ...
1001
-
1002
- Enable descriptions, then search by their meaning:
1003
- slopdex descriptions enable
1004
- slopdex search-description "keep the repository index synchronized"
1005
-
1006
- slopdex models opencode-go
1007
- slopdex config model opencode-go/gpt-5.6-luna
1008
-
1009
- Find duplicate code:
1010
- Compare functions across files, exclude short wrappers, and group matches into clusters:
1011
- slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
1012
-
1013
- Cluster 1 (3 functions, similarity 0.9124-0.9568)
1014
- src/auth/session.ts:18:1 :: validateSession
1015
- src/http/middleware.ts:42:1 :: authenticate
1016
- src/users/user-service.ts:27:3 :: UserService.authenticate
1017
-
1018
- This found three substantial authentication functions in separate files with very
1019
- high similarity. Review them for repeated validation or session logic that could be shared.
1020
- Middleware and service locations may be intentional architectural layers, so this is
1021
- evidence to inspect rather than proof they should merge. Connected components may use
1022
- transitive links, so every function need not directly match every other function.
1023
-
1024
- Using --cross-file-only is useful to exclude similar code in the same file.
1025
-
1026
- Review adjacent bands with threshold ranges:
1027
- slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.9
1028
- slopdex cross-search --cross-file-only --min-lines 4 --threshold 0.85-0.9
1029
-
1030
- Restrict functions used in cross-search:
1031
- Only use uncommitted working-tree functions as sources:
1032
- slopdex cross-search --uncommitted --cross-file-only --min-lines 4 --threshold 0.9
1033
-
1034
- Only use functions changed since origin/main as sources:
1035
- slopdex cross-search --changed-since origin/main --threshold 0.9
1036
-
1037
- Only use matching symbols under src/services as sources:
1038
- slopdex cross-search --source-path src/services -e '^UserService\\.' --threshold 0.9
1039
-
1040
- Find related code stored far apart:
1041
- --cohesion orders matches from farthest to nearest, which can highlight similar code
1042
- that could be made more cohesive through an abstraction:
1043
- slopdex cross-search --cross-file-only --cohesion --threshold 0.8
1044
-
1045
- src/auth/session.ts :: validateSession
1046
- 0.9400 packages/http/middleware.ts :: authenticate [distance 4]
1047
- 0.9300 src/auth/token.ts :: validateToken [distance 1]
1048
-
1049
- --cohesion keeps cross-search's semantic matches but orders each source's matches
1050
- from greatest to least path distance. Similarity breaks distance ties. This highlights
1051
- related functions stored far apart without introducing a separate analysis command.
1052
-
1053
- Use with agents:
1054
- slopdex search "..." --threshold 0.5
1055
- slopdex cross-search --uncommitted --threshold 0.8
1056
-
1057
- Reranking (second-stage reranker for search and search-description):
1058
- slopdex config reranker cohere
1059
- slopdex config reranker jina
1060
- slopdex config reranker openai
1061
-
1062
- Compare repositories:
1063
- slopdex cross-search --target-root /path/to/other/repo --target-index /path/to/other/repo/.slopdex/index.sqlite --threshold 0.9
1064
-
1065
- Inspect index health:
1066
- slopdex status
1067
- slopdex index-errors --format summary
1068
- slopdex --version
1069
-
1070
- Options:
1071
- --version Show the package version
1072
- --root <path> Repository root (default: current directory)
1073
- --config <path> Config file (default: .slopdex/config.json)
1074
- --index <path> SQLite index path
1075
- --provider <openai|jina> Embedding provider
1076
- --model <name> Embedding model
1077
- --description-provider <name> Description provider: openai, opencode, or opencode-go
1078
- --description-model <name> Description model (provider default: gpt-5.6-luna or muse-spark-1.3-contributor)
1079
- --reranker-candidates <number> Candidates sent to the OpenAI LLM reranker (default: 10)
1080
- --dimensions <number> Embedding dimensions
1081
- --target <ref> Target ref for update-git (default: HEAD)
1082
- --rebuild-on-divergence Rebuild after a rebase or branch change
1083
- --force-reindex Rebuild an incompatible existing index
1084
- --no-reindex Skip worktree overlays or reuse a non-Git index
1085
- --callables With reindex-files, also regenerate callable descriptions
1086
- --ignore-errors Silence warnings about persisted indexing errors
1087
- --verbose Log every external model call instead of one per kind/model
1088
- --limit <number> Output limit (default: unlimited; threshold filters results)
1089
- --matches <number> Cross-search matches per source function (default: 5)
1090
- --threshold <number|range> Show similarities at/above a value or within a range (default: 0.3)
1091
- --format <json|summary|clusters> Output format (default: summary; cross-search: clusters)
1092
- --cohesion Re-rank cross-search matches by physical distance
1093
- --include-symmetric-duplicates Show both directions of same-index matches
1094
- --cross-file-only Exclude matches from the source file
1095
- --min-lines <number> Minimum callable length for cross-search (default: 2)
1096
- -e, --regexp <regex> Filter qualified symbols (analysis: sources only)
1097
- --regex <regex> Alias for -e/--regexp
1098
- --changed-since <commit> Search added, modified, or moved functions
1099
- --uncommitted Search functions from uncommitted files
1100
- --source-path <path> Restrict cross-search sources to a file or directory
1101
- --target-root <path> Root of a second indexed codebase
1102
- --target-index <path> SQLite path of a second index
1103
- --target-config <path> Config file for a second indexed codebase
1104
-
1105
- Other Examples:
1106
- Save description state and parallelism without opening an index:
1107
- slopdex config descriptions enable
1108
- slopdex config parallelism 10
1109
-
1110
- Index specific working-tree files:
1111
- slopdex update-files src/service.ts src/model.ts
1112
-
1113
- Regenerate stale file descriptions, optionally continuing through callable descriptions:
1114
- slopdex reindex-files
1115
- slopdex reindex-files --callables
1116
-
1117
- Remove deleted files from the index:
1118
- slopdex delete-files src/removed.ts
1119
-
1120
- Index HEAD and overlay uncommitted working-tree changes:
1121
- slopdex update-git --target HEAD
1122
-
1123
- Combine source restrictions (all must match):
1124
- slopdex cross-search -e 'validate' --source-path src --changed-since origin/main --uncommitted
1125
-
1126
- Filter semantic search results by qualified symbol before applying the limit:
1127
- slopdex search "validate session" -e '^Session\\.' --limit 10
1128
- `);
1129
- }
1130
- //# sourceMappingURL=cli.js.map