@unlimiting/qsc 0.1.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 (117) hide show
  1. package/dist/chunker/ast.d.ts +7 -0
  2. package/dist/chunker/ast.d.ts.map +1 -0
  3. package/dist/chunker/ast.js +302 -0
  4. package/dist/chunker/ast.js.map +1 -0
  5. package/dist/chunker/index.d.ts +15 -0
  6. package/dist/chunker/index.d.ts.map +1 -0
  7. package/dist/chunker/index.js +26 -0
  8. package/dist/chunker/index.js.map +1 -0
  9. package/dist/chunker/languages/dart.d.ts +3 -0
  10. package/dist/chunker/languages/dart.d.ts.map +1 -0
  11. package/dist/chunker/languages/dart.js +22 -0
  12. package/dist/chunker/languages/dart.js.map +1 -0
  13. package/dist/chunker/languages/go.d.ts +3 -0
  14. package/dist/chunker/languages/go.d.ts.map +1 -0
  15. package/dist/chunker/languages/go.js +20 -0
  16. package/dist/chunker/languages/go.js.map +1 -0
  17. package/dist/chunker/languages/index.d.ts +12 -0
  18. package/dist/chunker/languages/index.d.ts.map +1 -0
  19. package/dist/chunker/languages/index.js +35 -0
  20. package/dist/chunker/languages/index.js.map +1 -0
  21. package/dist/chunker/languages/kotlin.d.ts +3 -0
  22. package/dist/chunker/languages/kotlin.d.ts.map +1 -0
  23. package/dist/chunker/languages/kotlin.js +23 -0
  24. package/dist/chunker/languages/kotlin.js.map +1 -0
  25. package/dist/chunker/languages/python.d.ts +3 -0
  26. package/dist/chunker/languages/python.d.ts.map +1 -0
  27. package/dist/chunker/languages/python.js +21 -0
  28. package/dist/chunker/languages/python.js.map +1 -0
  29. package/dist/chunker/languages/swift.d.ts +3 -0
  30. package/dist/chunker/languages/swift.d.ts.map +1 -0
  31. package/dist/chunker/languages/swift.js +24 -0
  32. package/dist/chunker/languages/swift.js.map +1 -0
  33. package/dist/chunker/languages/typescript.d.ts +4 -0
  34. package/dist/chunker/languages/typescript.d.ts.map +1 -0
  35. package/dist/chunker/languages/typescript.js +34 -0
  36. package/dist/chunker/languages/typescript.js.map +1 -0
  37. package/dist/chunker/token.d.ts +6 -0
  38. package/dist/chunker/token.d.ts.map +1 -0
  39. package/dist/chunker/token.js +107 -0
  40. package/dist/chunker/token.js.map +1 -0
  41. package/dist/collection.d.ts +22 -0
  42. package/dist/collection.d.ts.map +1 -0
  43. package/dist/collection.js +154 -0
  44. package/dist/collection.js.map +1 -0
  45. package/dist/config/index.d.ts +95 -0
  46. package/dist/config/index.d.ts.map +1 -0
  47. package/dist/config/index.js +103 -0
  48. package/dist/config/index.js.map +1 -0
  49. package/dist/embedder/index.d.ts +14 -0
  50. package/dist/embedder/index.d.ts.map +1 -0
  51. package/dist/embedder/index.js +18 -0
  52. package/dist/embedder/index.js.map +1 -0
  53. package/dist/embedder/local.d.ts +11 -0
  54. package/dist/embedder/local.d.ts.map +1 -0
  55. package/dist/embedder/local.js +60 -0
  56. package/dist/embedder/local.js.map +1 -0
  57. package/dist/embedder/openai.d.ts +10 -0
  58. package/dist/embedder/openai.d.ts.map +1 -0
  59. package/dist/embedder/openai.js +69 -0
  60. package/dist/embedder/openai.js.map +1 -0
  61. package/dist/index.d.ts +3 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +824 -0
  64. package/dist/index.js.map +1 -0
  65. package/dist/llm/index.d.ts +17 -0
  66. package/dist/llm/index.d.ts.map +1 -0
  67. package/dist/llm/index.js +18 -0
  68. package/dist/llm/index.js.map +1 -0
  69. package/dist/llm/local.d.ts +10 -0
  70. package/dist/llm/local.d.ts.map +1 -0
  71. package/dist/llm/local.js +76 -0
  72. package/dist/llm/local.js.map +1 -0
  73. package/dist/llm/openai.d.ts +10 -0
  74. package/dist/llm/openai.d.ts.map +1 -0
  75. package/dist/llm/openai.js +76 -0
  76. package/dist/llm/openai.js.map +1 -0
  77. package/dist/mcp.d.ts +3 -0
  78. package/dist/mcp.d.ts.map +1 -0
  79. package/dist/mcp.js +393 -0
  80. package/dist/mcp.js.map +1 -0
  81. package/dist/scanner/git.d.ts +26 -0
  82. package/dist/scanner/git.d.ts.map +1 -0
  83. package/dist/scanner/git.js +134 -0
  84. package/dist/scanner/git.js.map +1 -0
  85. package/dist/scanner/index.d.ts +17 -0
  86. package/dist/scanner/index.d.ts.map +1 -0
  87. package/dist/scanner/index.js +174 -0
  88. package/dist/scanner/index.js.map +1 -0
  89. package/dist/search/bm25.d.ts +17 -0
  90. package/dist/search/bm25.d.ts.map +1 -0
  91. package/dist/search/bm25.js +27 -0
  92. package/dist/search/bm25.js.map +1 -0
  93. package/dist/search/expander.d.ts +12 -0
  94. package/dist/search/expander.d.ts.map +1 -0
  95. package/dist/search/expander.js +60 -0
  96. package/dist/search/expander.js.map +1 -0
  97. package/dist/search/fusion.d.ts +32 -0
  98. package/dist/search/fusion.d.ts.map +1 -0
  99. package/dist/search/fusion.js +80 -0
  100. package/dist/search/fusion.js.map +1 -0
  101. package/dist/search/index.d.ts +61 -0
  102. package/dist/search/index.d.ts.map +1 -0
  103. package/dist/search/index.js +137 -0
  104. package/dist/search/index.js.map +1 -0
  105. package/dist/search/reranker.d.ts +18 -0
  106. package/dist/search/reranker.d.ts.map +1 -0
  107. package/dist/search/reranker.js +56 -0
  108. package/dist/search/reranker.js.map +1 -0
  109. package/dist/search/vector.d.ts +23 -0
  110. package/dist/search/vector.d.ts.map +1 -0
  111. package/dist/search/vector.js +47 -0
  112. package/dist/search/vector.js.map +1 -0
  113. package/dist/store.d.ts +119 -0
  114. package/dist/store.d.ts.map +1 -0
  115. package/dist/store.js +500 -0
  116. package/dist/store.js.map +1 -0
  117. package/package.json +48 -0
package/dist/index.js ADDED
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+ import { resolve, basename } from "node:path";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { createStore, formatChunkForEmbedding } from "./store.js";
5
+ import { createChunker } from "./chunker/index.js";
6
+ import { createEmbedder } from "./embedder/index.js";
7
+ import { createLLMProvider } from "./llm/index.js";
8
+ import { scanRepository, detectLanguage } from "./scanner/index.js";
9
+ import { detectChanges, getCurrentCommit, isGitRepository } from "./scanner/git.js";
10
+ import { createSearchPipeline } from "./search/index.js";
11
+ import { loadConfig } from "./config/index.js";
12
+ import { createHash } from "node:crypto";
13
+ import { registerCollection, resolveCollectionDb, resolveCollectionSourcePath, getCollection, listCollections, ensureQscHome, getCollectionDbPath, copyCollection, importCollection, exportCollection, updateCollectionMeta, } from "./collection.js";
14
+ import { execSync } from "node:child_process";
15
+ function parseArgs(argv) {
16
+ const args = argv.slice(2);
17
+ if (args.length === 0 || (args.length === 1 && args[0] === "--help")) {
18
+ return { command: "help", positional: [], flags: {} };
19
+ }
20
+ const command = args[0].startsWith("--") ? "help" : args[0];
21
+ const positional = [];
22
+ const flags = {};
23
+ const startIdx = args[0].startsWith("--") ? 0 : 1;
24
+ for (let i = startIdx; i < args.length; i++) {
25
+ const arg = args[i];
26
+ if (arg.startsWith("--")) {
27
+ const key = arg.slice(2);
28
+ const eqIdx = key.indexOf("=");
29
+ if (eqIdx !== -1) {
30
+ flags[key.slice(0, eqIdx)] = key.slice(eqIdx + 1);
31
+ }
32
+ else if (i + 1 < args.length && !args[i + 1].startsWith("--")) {
33
+ if (key.startsWith("no-")) {
34
+ flags[key] = true;
35
+ }
36
+ else {
37
+ flags[key] = args[++i];
38
+ }
39
+ }
40
+ else {
41
+ flags[key] = true;
42
+ }
43
+ }
44
+ else {
45
+ positional.push(arg);
46
+ }
47
+ }
48
+ return { command, positional, flags };
49
+ }
50
+ function getFlag(flags, key, defaultVal) {
51
+ const v = flags[key];
52
+ if (v === undefined || v === true)
53
+ return defaultVal;
54
+ return String(v);
55
+ }
56
+ function hasFlag(flags, key) {
57
+ return flags[key] !== undefined;
58
+ }
59
+ // --- Helpers ---
60
+ function openStore(dbPath, config) {
61
+ if (!existsSync(dbPath)) {
62
+ throw new Error(`Database not found: ${dbPath}\nRun 'qsc init <name> <path>' first.`);
63
+ }
64
+ const store = createStore(dbPath);
65
+ store.initDb(config.embedder.dimensions);
66
+ return store;
67
+ }
68
+ function getRepoId(repoPath) {
69
+ return basename(resolve(repoPath));
70
+ }
71
+ function formatScore(score) {
72
+ return score.toFixed(4);
73
+ }
74
+ function truncate(s, maxLen) {
75
+ if (s.length <= maxLen)
76
+ return s;
77
+ return s.slice(0, maxLen - 3) + "...";
78
+ }
79
+ function printResults(results, mode) {
80
+ if (results.length === 0) {
81
+ console.log("No results found.");
82
+ return;
83
+ }
84
+ console.log(`\n${results.length} result(s) [${mode}]:\n`);
85
+ for (let i = 0; i < results.length; i++) {
86
+ const r = results[i];
87
+ const lineInfo = r.startLine != null ? `:${r.startLine}` : "";
88
+ const nameInfo = r.name ? ` (${r.name})` : "";
89
+ const typeInfo = r.chunkType ? ` [${r.chunkType}]` : "";
90
+ console.log(`--- #${i + 1} | ${r.filePath}${lineInfo}${nameInfo}${typeInfo} | score: ${formatScore(r.score)} ---`);
91
+ const parts = [];
92
+ if (r.scores.bm25 != null)
93
+ parts.push(`bm25=${formatScore(r.scores.bm25)}`);
94
+ if (r.scores.vector != null)
95
+ parts.push(`vector=${formatScore(r.scores.vector)}`);
96
+ if (r.scores.rrf != null)
97
+ parts.push(`rrf=${formatScore(r.scores.rrf)}`);
98
+ if (r.scores.rerank != null)
99
+ parts.push(`rerank=${formatScore(r.scores.rerank)}`);
100
+ if (parts.length > 0)
101
+ console.log(` scores: ${parts.join(", ")}`);
102
+ const lines = r.content.split("\n");
103
+ const preview = lines.slice(0, 8).join("\n");
104
+ console.log(preview);
105
+ if (lines.length > 8)
106
+ console.log(` ... (${lines.length - 8} more lines)`);
107
+ console.log();
108
+ }
109
+ }
110
+ function printBenchmark(response) {
111
+ if (!response.timing)
112
+ return;
113
+ const t = response.timing;
114
+ console.log("--- Benchmark ---");
115
+ if (t.expand != null) {
116
+ console.log(`Query Expansion: ${Math.round(t.expand)}ms`);
117
+ }
118
+ if (t.bm25 != null) {
119
+ const countInfo = response.counts?.bm25 != null ? ` (${response.counts.bm25} results)` : "";
120
+ console.log(`BM25: ${Math.round(t.bm25)}ms${countInfo}`);
121
+ }
122
+ if (t.vector != null) {
123
+ const countInfo = response.counts?.vector != null ? ` (${response.counts.vector} results)` : "";
124
+ console.log(`Vector: ${Math.round(t.vector)}ms${countInfo}`);
125
+ }
126
+ if (t.fusion != null) {
127
+ console.log(`RRF Fusion: ${Math.round(t.fusion)}ms`);
128
+ }
129
+ if (t.rerank != null) {
130
+ console.log(`LLM Reranking: ${Math.round(t.rerank)}ms`);
131
+ }
132
+ console.log(`Total: ${Math.round(t.total)}ms`);
133
+ }
134
+ // --- Commands ---
135
+ async function cmdInit(positional, flags) {
136
+ const name = positional[0];
137
+ const sourcePath = positional[1];
138
+ if (!name || !sourcePath) {
139
+ console.error("Usage: qsc init <name> <path> [--update-cmd <command>]");
140
+ process.exit(1);
141
+ }
142
+ const absSourcePath = resolve(sourcePath);
143
+ if (!existsSync(absSourcePath)) {
144
+ console.error(`Source path does not exist: ${absSourcePath}`);
145
+ process.exit(1);
146
+ }
147
+ ensureQscHome();
148
+ const dbPath = getCollectionDbPath(name);
149
+ const config = loadConfig(absSourcePath);
150
+ // Create DB and initialize schema
151
+ const store = createStore(dbPath);
152
+ store.initDb(config.embedder.dimensions);
153
+ // Register repository in DB
154
+ const repoId = getRepoId(absSourcePath);
155
+ store.upsertRepository({
156
+ id: repoId,
157
+ path: absSourcePath,
158
+ });
159
+ store.close();
160
+ // Register collection with optional updateCommand
161
+ const updateCmd = hasFlag(flags, "update-cmd")
162
+ ? getFlag(flags, "update-cmd", "")
163
+ : undefined;
164
+ registerCollection(name, absSourcePath, dbPath, updateCmd || undefined);
165
+ console.log(`Collection '${name}' initialized.`);
166
+ console.log(` Database: ${dbPath}`);
167
+ console.log(` Source: ${absSourcePath}`);
168
+ console.log(` Embedding dimensions: ${config.embedder.dimensions}`);
169
+ if (updateCmd) {
170
+ console.log(` Update command: ${updateCmd}`);
171
+ }
172
+ }
173
+ async function cmdIndex(positional, _flags) {
174
+ const name = positional[0];
175
+ if (!name) {
176
+ console.error("Usage: qsc index <name>");
177
+ process.exit(1);
178
+ }
179
+ const dbPath = resolveCollectionDb(name);
180
+ const sourcePath = resolveCollectionSourcePath(name);
181
+ const config = loadConfig(sourcePath);
182
+ const store = openStore(dbPath, config);
183
+ const repoId = getRepoId(sourcePath);
184
+ try {
185
+ console.log(`Scanning ${sourcePath}...`);
186
+ const scanResult = await scanRepository(sourcePath, config.scanner);
187
+ console.log(`Found ${scanResult.files.length} files (${(scanResult.totalSize / 1024).toFixed(1)} KB)`);
188
+ const chunker = createChunker(config.chunker);
189
+ let indexed = 0;
190
+ let skipped = 0;
191
+ for (let i = 0; i < scanResult.files.length; i++) {
192
+ const file = scanResult.files[i];
193
+ const progress = `[${i + 1}/${scanResult.files.length}]`;
194
+ const { id: fileId, changed } = store.upsertFile({
195
+ repo_id: repoId,
196
+ path: file.path,
197
+ hash: file.hash,
198
+ language: file.language ?? null,
199
+ active: 1,
200
+ indexed_at: new Date().toISOString(),
201
+ });
202
+ if (!changed) {
203
+ skipped++;
204
+ continue;
205
+ }
206
+ const content = readFileSync(file.absolutePath, "utf-8");
207
+ const chunks = await chunker.chunk(content, file.path);
208
+ store.insertChunks(fileId, chunks.map((c, seq) => ({
209
+ hash: createHash("sha256").update(c.content).digest("hex"),
210
+ seq,
211
+ start_line: c.startLine,
212
+ end_line: c.endLine,
213
+ chunk_type: c.type,
214
+ name: c.name ?? null,
215
+ content: c.content,
216
+ metadata: c.metadata ? JSON.stringify(c.metadata) : null,
217
+ })));
218
+ indexed++;
219
+ process.stdout.write(`\r${progress} Indexed: ${indexed}, Skipped: ${skipped}`);
220
+ }
221
+ console.log(`\nIndexing complete. Indexed: ${indexed}, Skipped (unchanged): ${skipped}`);
222
+ store.upsertRepository({
223
+ id: repoId,
224
+ path: sourcePath,
225
+ indexed_at: new Date().toISOString(),
226
+ });
227
+ }
228
+ finally {
229
+ store.close();
230
+ }
231
+ }
232
+ async function cmdEmbed(positional, flags) {
233
+ const name = positional[0];
234
+ if (!name) {
235
+ console.error("Usage: qsc embed <name>");
236
+ process.exit(1);
237
+ }
238
+ const dbPath = resolveCollectionDb(name);
239
+ const sourcePath = resolveCollectionSourcePath(name);
240
+ const config = loadConfig(sourcePath);
241
+ const store = openStore(dbPath, config);
242
+ try {
243
+ const batchSize = parseInt(getFlag(flags, "batch", "100"), 10);
244
+ const embedder = await createEmbedder(config.embedder);
245
+ console.log(`Embedder: ${embedder.modelName} (${embedder.dimensions}d)`);
246
+ let totalEmbedded = 0;
247
+ while (true) {
248
+ const chunks = store.getUnembeddedChunks(batchSize);
249
+ if (chunks.length === 0)
250
+ break;
251
+ const texts = chunks.map((c) => formatChunkForEmbedding(c));
252
+ const vectors = await embedder.embed(texts);
253
+ store.insertEmbeddings(chunks.map((c, i) => ({
254
+ chunk_id: c.chunk_id,
255
+ embedding: new Float32Array(vectors[i]),
256
+ model: embedder.modelName,
257
+ })));
258
+ totalEmbedded += chunks.length;
259
+ process.stdout.write(`\rEmbedded: ${totalEmbedded} chunks`);
260
+ }
261
+ console.log(`\nEmbedding complete. Total: ${totalEmbedded} chunks`);
262
+ }
263
+ finally {
264
+ store.close();
265
+ }
266
+ }
267
+ async function cmdUpdate(positional, flags) {
268
+ const name = positional[0];
269
+ if (!name) {
270
+ console.error("Usage: qsc update <name>");
271
+ process.exit(1);
272
+ }
273
+ // Run pre-update command if configured
274
+ const collectionMeta = getCollection(name);
275
+ if (!collectionMeta) {
276
+ console.error(`Collection '${name}' not found. Run 'qsc init ${name} <path>' first.`);
277
+ process.exit(1);
278
+ }
279
+ if (collectionMeta.updateCommand) {
280
+ const cmd = collectionMeta.updateCommand;
281
+ console.log(`Running pre-update command: ${cmd}`);
282
+ console.log(` Working directory: ${collectionMeta.sourcePath}`);
283
+ try {
284
+ const output = execSync(cmd, {
285
+ cwd: collectionMeta.sourcePath,
286
+ timeout: 60_000,
287
+ stdio: "pipe",
288
+ encoding: "utf-8",
289
+ });
290
+ if (output.trim()) {
291
+ console.log(output.trimEnd());
292
+ }
293
+ console.log("Pre-update command completed successfully.\n");
294
+ }
295
+ catch (err) {
296
+ const execErr = err;
297
+ console.error(`Warning: Pre-update command failed (exit code: ${execErr.status ?? "unknown"})`);
298
+ if (execErr.stderr) {
299
+ console.error(execErr.stderr.trimEnd());
300
+ }
301
+ if (execErr.stdout) {
302
+ console.log(execErr.stdout.trimEnd());
303
+ }
304
+ console.error("Continuing with update...\n");
305
+ }
306
+ }
307
+ const dbPath = resolveCollectionDb(name);
308
+ const sourcePath = resolveCollectionSourcePath(name);
309
+ const config = loadConfig(sourcePath);
310
+ const store = openStore(dbPath, config);
311
+ const repoId = getRepoId(sourcePath);
312
+ let closed = false;
313
+ try {
314
+ const isGit = isGitRepository(sourcePath);
315
+ // Try git-optimized path first
316
+ if (isGit) {
317
+ const repo = store.getRepository(repoId);
318
+ const lastCommit = repo?.last_commit ?? undefined;
319
+ if (lastCommit) {
320
+ const gitInfo = detectChanges(sourcePath, lastCommit);
321
+ if (!gitInfo.isFullScan) {
322
+ console.log(`Git incremental update (${lastCommit.slice(0, 8)} -> ${gitInfo.currentCommit.slice(0, 8)})`);
323
+ const added = gitInfo.changes.filter((c) => c.status === "added" || c.status === "renamed");
324
+ const modified = gitInfo.changes.filter((c) => c.status === "modified");
325
+ const deleted = gitInfo.changes.filter((c) => c.status === "deleted");
326
+ console.log(` Added: ${added.length}, Modified: ${modified.length}, Deleted: ${deleted.length}`);
327
+ if (deleted.length > 0) {
328
+ store.deactivateFiles(repoId, deleted.map((c) => c.path));
329
+ console.log(`Deactivated ${deleted.length} deleted files`);
330
+ }
331
+ const changedPaths = [...added, ...modified].map((c) => c.path);
332
+ if (changedPaths.length > 0) {
333
+ const chunker = createChunker(config.chunker);
334
+ let indexed = 0;
335
+ for (const relPath of changedPaths) {
336
+ const absPath = resolve(sourcePath, relPath);
337
+ if (!existsSync(absPath))
338
+ continue;
339
+ const content = readFileSync(absPath, "utf-8");
340
+ const hash = createHash("sha256").update(content).digest("hex");
341
+ const language = detectLanguage(relPath);
342
+ const { id: fileId, changed } = store.upsertFile({
343
+ repo_id: repoId,
344
+ path: relPath,
345
+ hash,
346
+ language: language ?? null,
347
+ active: 1,
348
+ indexed_at: new Date().toISOString(),
349
+ });
350
+ if (!changed)
351
+ continue;
352
+ const chunks = await chunker.chunk(content, relPath);
353
+ store.insertChunks(fileId, chunks.map((c, seq) => ({
354
+ hash: createHash("sha256").update(c.content).digest("hex"),
355
+ seq,
356
+ start_line: c.startLine,
357
+ end_line: c.endLine,
358
+ chunk_type: c.type,
359
+ name: c.name ?? null,
360
+ content: c.content,
361
+ metadata: c.metadata ? JSON.stringify(c.metadata) : null,
362
+ })));
363
+ indexed++;
364
+ }
365
+ console.log(`Re-indexed ${indexed} files`);
366
+ }
367
+ // Cleanup and update commit
368
+ const cleaned = store.cleanup();
369
+ if (cleaned.deletedChunks > 0) {
370
+ console.log(`Cleaned up ${cleaned.deletedChunks} orphan chunks, ${cleaned.deletedVectors} vectors`);
371
+ }
372
+ store.upsertRepository({
373
+ id: repoId,
374
+ path: sourcePath,
375
+ last_commit: gitInfo.currentCommit,
376
+ indexed_at: new Date().toISOString(),
377
+ });
378
+ console.log("Git incremental update complete.");
379
+ // Auto-embed
380
+ const unembedded = store.getUnembeddedChunks(1);
381
+ store.close();
382
+ closed = true;
383
+ if (unembedded.length > 0) {
384
+ console.log("Embedding new chunks...");
385
+ await cmdEmbed([name], flags);
386
+ }
387
+ return;
388
+ }
389
+ }
390
+ }
391
+ // Fallback: hash-based full scan (works for both git and non-git)
392
+ console.log(isGit ? "Full hash-based scan (no previous commit)..." : "Hash-based scan (non-git directory)...");
393
+ const scanResult = await scanRepository(sourcePath, config.scanner);
394
+ console.log(`Found ${scanResult.files.length} files (${(scanResult.totalSize / 1024).toFixed(1)} KB)`);
395
+ const chunker = createChunker(config.chunker);
396
+ let indexed = 0;
397
+ let skipped = 0;
398
+ // Track scanned paths to detect deleted files
399
+ const scannedPaths = new Set();
400
+ for (let i = 0; i < scanResult.files.length; i++) {
401
+ const file = scanResult.files[i];
402
+ scannedPaths.add(file.path);
403
+ const progress = `[${i + 1}/${scanResult.files.length}]`;
404
+ const { id: fileId, changed } = store.upsertFile({
405
+ repo_id: repoId,
406
+ path: file.path,
407
+ hash: file.hash,
408
+ language: file.language ?? null,
409
+ active: 1,
410
+ indexed_at: new Date().toISOString(),
411
+ });
412
+ if (!changed) {
413
+ skipped++;
414
+ continue;
415
+ }
416
+ const content = readFileSync(file.absolutePath, "utf-8");
417
+ const chunks = await chunker.chunk(content, file.path);
418
+ store.insertChunks(fileId, chunks.map((c, seq) => ({
419
+ hash: createHash("sha256").update(c.content).digest("hex"),
420
+ seq,
421
+ start_line: c.startLine,
422
+ end_line: c.endLine,
423
+ chunk_type: c.type,
424
+ name: c.name ?? null,
425
+ content: c.content,
426
+ metadata: c.metadata ? JSON.stringify(c.metadata) : null,
427
+ })));
428
+ indexed++;
429
+ process.stdout.write(`\r${progress} Indexed: ${indexed}, Skipped: ${skipped}`);
430
+ }
431
+ console.log(`\nIndexed: ${indexed}, Skipped (unchanged): ${skipped}`);
432
+ // Deactivate files that are no longer present on disk
433
+ const activeFiles = store.getActiveFiles(repoId);
434
+ const deletedPaths = activeFiles
435
+ .filter((f) => !scannedPaths.has(f.path))
436
+ .map((f) => f.path);
437
+ if (deletedPaths.length > 0) {
438
+ store.deactivateFiles(repoId, deletedPaths);
439
+ console.log(`Deactivated ${deletedPaths.length} deleted files`);
440
+ }
441
+ // Cleanup orphaned data
442
+ const cleaned = store.cleanup();
443
+ if (cleaned.deletedChunks > 0) {
444
+ console.log(`Cleaned up ${cleaned.deletedChunks} orphan chunks, ${cleaned.deletedVectors} vectors`);
445
+ }
446
+ // Update repository metadata
447
+ const updateData = {
448
+ id: repoId,
449
+ path: sourcePath,
450
+ indexed_at: new Date().toISOString(),
451
+ };
452
+ if (isGit) {
453
+ try {
454
+ updateData.last_commit = getCurrentCommit(sourcePath);
455
+ }
456
+ catch {
457
+ // ignore git errors
458
+ }
459
+ }
460
+ store.upsertRepository(updateData);
461
+ console.log("Update complete.");
462
+ // Auto-embed
463
+ const unembedded = store.getUnembeddedChunks(1);
464
+ store.close();
465
+ closed = true;
466
+ if (unembedded.length > 0) {
467
+ console.log("Embedding new chunks...");
468
+ await cmdEmbed([name], flags);
469
+ }
470
+ }
471
+ finally {
472
+ if (!closed) {
473
+ store.close();
474
+ }
475
+ }
476
+ }
477
+ async function cmdSearch(positional, flags) {
478
+ const name = positional[0];
479
+ const query = positional.slice(1).join(" ");
480
+ if (!name || !query) {
481
+ console.error("Usage: qsc search <name> <query>");
482
+ process.exit(1);
483
+ }
484
+ const dbPath = resolveCollectionDb(name);
485
+ const sourcePath = resolveCollectionSourcePath(name);
486
+ const config = loadConfig(sourcePath);
487
+ const store = openStore(dbPath, config);
488
+ try {
489
+ const limit = parseInt(getFlag(flags, "limit", "10"), 10);
490
+ const benchmark = hasFlag(flags, "benchmark");
491
+ const pipeline = createSearchPipeline(store);
492
+ const response = await pipeline.search(query, {
493
+ mode: "bm25",
494
+ limit,
495
+ benchmark,
496
+ });
497
+ printResults(response.results, "BM25");
498
+ if (benchmark)
499
+ printBenchmark(response);
500
+ }
501
+ finally {
502
+ store.close();
503
+ }
504
+ }
505
+ async function cmdQuery(positional, flags) {
506
+ const name = positional[0];
507
+ const query = positional.slice(1).join(" ");
508
+ if (!name || !query) {
509
+ console.error("Usage: qsc query <name> <query>");
510
+ process.exit(1);
511
+ }
512
+ const dbPath = resolveCollectionDb(name);
513
+ const sourcePath = resolveCollectionSourcePath(name);
514
+ const config = loadConfig(sourcePath);
515
+ const store = openStore(dbPath, config);
516
+ try {
517
+ const limit = parseInt(getFlag(flags, "limit", "10"), 10);
518
+ const noExpand = hasFlag(flags, "no-expand");
519
+ const noRerank = hasFlag(flags, "no-rerank");
520
+ const benchmark = hasFlag(flags, "benchmark");
521
+ let embedder;
522
+ let llm;
523
+ try {
524
+ embedder = await createEmbedder(config.embedder);
525
+ }
526
+ catch (err) {
527
+ console.error(`Warning: Could not create embedder (${err.message}). Vector search disabled.`);
528
+ }
529
+ try {
530
+ llm = await createLLMProvider(config.llm);
531
+ }
532
+ catch (err) {
533
+ console.error(`Warning: Could not create LLM provider (${err.message}). Expansion/reranking disabled.`);
534
+ }
535
+ const pipeline = createSearchPipeline(store, embedder, llm);
536
+ const mode = embedder ? "hybrid" : "bm25";
537
+ const response = await pipeline.search(query, {
538
+ mode,
539
+ limit,
540
+ expand: !noExpand && !!llm,
541
+ rerank: !noRerank && !!llm,
542
+ benchmark,
543
+ });
544
+ printResults(response.results, mode);
545
+ if (benchmark)
546
+ printBenchmark(response);
547
+ }
548
+ finally {
549
+ store.close();
550
+ }
551
+ }
552
+ async function cmdGet(positional, _flags) {
553
+ const name = positional[0];
554
+ const filePath = positional[1];
555
+ if (!name || !filePath) {
556
+ console.error("Usage: qsc get <name> <file-path>");
557
+ process.exit(1);
558
+ }
559
+ const dbPath = resolveCollectionDb(name);
560
+ const sourcePath = resolveCollectionSourcePath(name);
561
+ const config = loadConfig(sourcePath);
562
+ const store = openStore(dbPath, config);
563
+ const repoId = getRepoId(sourcePath);
564
+ try {
565
+ const file = store.getFileByPath(repoId, filePath);
566
+ if (!file) {
567
+ console.error(`File not found in index: ${filePath}`);
568
+ process.exit(1);
569
+ }
570
+ console.log(`File: ${file.path}`);
571
+ console.log(` ID: ${file.id}`);
572
+ console.log(` Hash: ${file.hash}`);
573
+ console.log(` Language: ${file.language ?? "unknown"}`);
574
+ console.log(` Active: ${file.active ? "yes" : "no"}`);
575
+ console.log(` Indexed at: ${file.indexed_at ?? "unknown"}`);
576
+ const chunks = store.getChunksByFileId(file.id);
577
+ console.log(`\nChunks (${chunks.length}):\n`);
578
+ for (const chunk of chunks) {
579
+ const lineInfo = chunk.start_line != null
580
+ ? `L${chunk.start_line}-${chunk.end_line}`
581
+ : "";
582
+ const nameInfo = chunk.name ? ` ${chunk.name}` : "";
583
+ const typeInfo = chunk.chunk_type ? ` [${chunk.chunk_type}]` : "";
584
+ console.log(` #${chunk.seq} ${lineInfo}${nameInfo}${typeInfo}`);
585
+ console.log(` ${truncate(chunk.content.split("\n")[0], 80)}`);
586
+ }
587
+ }
588
+ finally {
589
+ store.close();
590
+ }
591
+ }
592
+ async function cmdStatus(positional, _flags) {
593
+ const name = positional[0];
594
+ if (!name) {
595
+ console.error("Usage: qsc status <name>");
596
+ process.exit(1);
597
+ }
598
+ const dbPath = resolveCollectionDb(name);
599
+ const sourcePath = resolveCollectionSourcePath(name);
600
+ const config = loadConfig(sourcePath);
601
+ const store = openStore(dbPath, config);
602
+ try {
603
+ const stats = store.getStats();
604
+ console.log("QSC Index Status");
605
+ console.log("================");
606
+ console.log(`Collection: ${name}`);
607
+ console.log(`Database: ${dbPath}`);
608
+ console.log(`Source: ${sourcePath}`);
609
+ console.log(`Repositories: ${stats.repositories}`);
610
+ console.log(`Files (total): ${stats.files}`);
611
+ console.log(`Files (active): ${stats.active_files}`);
612
+ console.log(`Chunks: ${stats.chunks}`);
613
+ console.log(`Embedded: ${stats.embedded_chunks}`);
614
+ console.log(`Pending embed: ${stats.pending_chunks}`);
615
+ }
616
+ finally {
617
+ store.close();
618
+ }
619
+ }
620
+ async function cmdConfig() {
621
+ const config = loadConfig();
622
+ console.log("QSC Configuration");
623
+ console.log("==================");
624
+ console.log("\nEmbedder:");
625
+ console.log(` Provider: ${config.embedder.provider}`);
626
+ console.log(` Model: ${config.embedder.model}`);
627
+ console.log(` Dimensions: ${config.embedder.dimensions}`);
628
+ console.log(` API Key: ${config.embedder.api_key_env} (env var)`);
629
+ console.log("\nLLM:");
630
+ console.log(` Provider: ${config.llm.provider}`);
631
+ console.log(` Model: ${config.llm.model}`);
632
+ console.log(` API Key: ${config.llm.api_key_env} (env var)`);
633
+ console.log("\nChunker:");
634
+ console.log(` Max Tokens: ${config.chunker.max_tokens}`);
635
+ console.log(` Overlap: ${config.chunker.overlap}`);
636
+ console.log("\nScanner:");
637
+ console.log(` Max File Size: ${(config.scanner.max_file_size / 1024).toFixed(0)} KB`);
638
+ console.log(` Exclude:`);
639
+ for (const pattern of config.scanner.exclude) {
640
+ console.log(` - ${pattern}`);
641
+ }
642
+ }
643
+ async function cmdList() {
644
+ const collections = listCollections();
645
+ const names = Object.keys(collections);
646
+ if (names.length === 0) {
647
+ console.log("No collections found. Run 'qsc init <name> <path>' to create one.");
648
+ return;
649
+ }
650
+ console.log("Collections:");
651
+ console.log("============");
652
+ for (const name of names.sort()) {
653
+ const meta = collections[name];
654
+ console.log(` ${name}`);
655
+ console.log(` Source: ${meta.sourcePath}`);
656
+ console.log(` DB: ${meta.dbPath}`);
657
+ console.log(` Created: ${meta.createdAt}`);
658
+ if (meta.updateCommand) {
659
+ console.log(` Update cmd: ${meta.updateCommand}`);
660
+ }
661
+ }
662
+ }
663
+ async function cmdSetUpdateCmd(positional, _flags) {
664
+ const name = positional[0];
665
+ const cmd = positional.slice(1).join(" ");
666
+ if (!name) {
667
+ console.error("Usage: qsc set-update-cmd <collection> <command>");
668
+ console.error(" qsc set-update-cmd <collection> (removes the command)");
669
+ process.exit(1);
670
+ }
671
+ const updateCommand = cmd || "";
672
+ const meta = updateCollectionMeta(name, { updateCommand });
673
+ if (updateCommand) {
674
+ console.log(`Update command for '${name}' set to: ${updateCommand}`);
675
+ }
676
+ else {
677
+ console.log(`Update command for '${name}' has been removed.`);
678
+ }
679
+ }
680
+ async function cmdCopy(positional) {
681
+ const sourceName = positional[0];
682
+ const destName = positional[1];
683
+ const path = positional[2];
684
+ if (!sourceName || !destName || !path) {
685
+ console.error("Usage: qsc copy <source-name> <dest-name> <path>");
686
+ process.exit(1);
687
+ }
688
+ const meta = copyCollection(sourceName, destName, path);
689
+ console.log(`Collection '${destName}' created (copied from '${sourceName}').`);
690
+ console.log(` Database: ${meta.dbPath}`);
691
+ console.log(` Source: ${meta.sourcePath}`);
692
+ }
693
+ async function cmdImport(positional) {
694
+ const name = positional[0];
695
+ const sqlitePath = positional[1];
696
+ const sourcePath = positional[2];
697
+ if (!name || !sqlitePath || !sourcePath) {
698
+ console.error("Usage: qsc import <name> <sqlite-path> <source-path>");
699
+ process.exit(1);
700
+ }
701
+ const meta = importCollection(name, sqlitePath, sourcePath);
702
+ console.log(`Collection '${name}' imported.`);
703
+ console.log(` Database: ${meta.dbPath}`);
704
+ console.log(` Source: ${meta.sourcePath}`);
705
+ }
706
+ async function cmdExport(positional) {
707
+ const name = positional[0];
708
+ const outputPath = positional[1];
709
+ if (!name || !outputPath) {
710
+ console.error("Usage: qsc export <name> <output-path>");
711
+ process.exit(1);
712
+ }
713
+ exportCollection(name, outputPath);
714
+ console.log(`Collection '${name}' exported to ${resolve(outputPath)}`);
715
+ }
716
+ function printHelp() {
717
+ console.log(`
718
+ QSC - Query Source Code
719
+ =======================
720
+
721
+ Usage: qsc <command> [options]
722
+
723
+ Commands:
724
+ init <name> <path> Create a collection for the source at <path>
725
+ index <name> Index source code (scan -> chunk -> store)
726
+ embed <name> Generate vector embeddings for unembedded chunks
727
+ update <name> Incremental update (hash-based, git-optimized if available)
728
+ search <name> <query> BM25 full-text search
729
+ query <name> <query> Hybrid search (BM25 + Vector + LLM reranking)
730
+ get <name> <file-path> Get file info and chunks
731
+ status <name> Show index statistics
732
+ list List all collections
733
+ set-update-cmd <name> <command> Set a pre-update command for a collection
734
+ copy <source> <dest> <path> Copy a collection DB to a new collection
735
+ import <name> <sqlite-path> <path> Import an external SQLite DB as a collection
736
+ export <name> <output-path> Export a collection's SQLite DB
737
+ config Show current configuration
738
+ mcp Start MCP server (stdio transport)
739
+ help Show this help message
740
+
741
+ Options:
742
+ --limit <n> Max results for search/query (default: 10)
743
+ --batch <n> Batch size for embed (default: 100)
744
+ --update-cmd <cmd> Set pre-update command (init command)
745
+ --no-expand Disable query expansion (query command)
746
+ --no-rerank Disable LLM reranking (query command)
747
+ --benchmark Show timing info for search/query
748
+ --collection <name> Collection name for MCP server
749
+ --help Show help
750
+ `);
751
+ }
752
+ // --- Main ---
753
+ async function main() {
754
+ const { command, positional, flags } = parseArgs(process.argv);
755
+ if (hasFlag(flags, "help")) {
756
+ printHelp();
757
+ return;
758
+ }
759
+ try {
760
+ switch (command) {
761
+ case "init":
762
+ await cmdInit(positional, flags);
763
+ break;
764
+ case "index":
765
+ await cmdIndex(positional, flags);
766
+ break;
767
+ case "embed":
768
+ await cmdEmbed(positional, flags);
769
+ break;
770
+ case "update":
771
+ await cmdUpdate(positional, flags);
772
+ break;
773
+ case "search":
774
+ await cmdSearch(positional, flags);
775
+ break;
776
+ case "query":
777
+ await cmdQuery(positional, flags);
778
+ break;
779
+ case "get":
780
+ await cmdGet(positional, flags);
781
+ break;
782
+ case "status":
783
+ await cmdStatus(positional, flags);
784
+ break;
785
+ case "list":
786
+ await cmdList();
787
+ break;
788
+ case "set-update-cmd":
789
+ await cmdSetUpdateCmd(positional, flags);
790
+ break;
791
+ case "copy":
792
+ await cmdCopy(positional);
793
+ break;
794
+ case "import":
795
+ await cmdImport(positional);
796
+ break;
797
+ case "export":
798
+ await cmdExport(positional);
799
+ break;
800
+ case "config":
801
+ await cmdConfig();
802
+ break;
803
+ case "mcp": {
804
+ const { startMcpServer } = await import("./mcp.js");
805
+ await startMcpServer();
806
+ break;
807
+ }
808
+ case "help":
809
+ printHelp();
810
+ break;
811
+ default:
812
+ console.error(`Unknown command: ${command}`);
813
+ printHelp();
814
+ process.exit(1);
815
+ }
816
+ }
817
+ catch (err) {
818
+ const message = err instanceof Error ? err.message : String(err);
819
+ console.error(`Error: ${message}`);
820
+ process.exit(1);
821
+ }
822
+ }
823
+ main();
824
+ //# sourceMappingURL=index.js.map