@tobilu/qmd 1.1.5 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +240 -6
- package/bin/qmd +12 -0
- package/dist/{formatter.d.ts → cli/formatter.d.ts} +1 -1
- package/dist/{formatter.js → cli/formatter.js} +1 -1
- package/dist/{qmd.js → cli/qmd.js} +100 -143
- package/dist/collections.d.ts +16 -2
- package/dist/collections.js +57 -8
- package/dist/index.d.ts +220 -0
- package/dist/index.js +229 -0
- package/dist/llm.d.ts +6 -0
- package/dist/llm.js +23 -0
- package/dist/maintenance.d.ts +23 -0
- package/dist/maintenance.js +37 -0
- package/dist/{mcp.js → mcp/server.js} +41 -61
- package/dist/store.d.ts +83 -19
- package/dist/store.js +561 -84
- package/package.json +20 -11
- /package/dist/{qmd.d.ts → cli/qmd.d.ts} +0 -0
- /package/dist/{mcp.d.ts → mcp/server.d.ts} +0 -0
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { openDatabase } from "
|
|
2
|
+
import { openDatabase } from "../db.js";
|
|
3
3
|
import fastGlob from "fast-glob";
|
|
4
4
|
import { execSync, spawn as nodeSpawn } from "child_process";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { dirname, join as pathJoin } from "path";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
8
|
import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync } from "fs";
|
|
9
|
-
import { getPwd, getRealPath, homedir, resolve, enableProductionMode, searchFTS, extractSnippet, getContextForFile, getContextForPath, listCollections, removeCollection, renameCollection, findSimilarFiles, findDocumentByDocid, isDocid, matchFilesByGlob, getHashesNeedingEmbedding, getHashesForEmbedding, clearAllEmbeddings, insertEmbedding, getStatus, hashContent, extractTitle, formatDocForEmbedding, chunkDocumentByTokens, clearCache, getCacheKey, getCachedResult, setCachedResult, getIndexHealth, parseVirtualPath, buildVirtualPath, isVirtualPath, resolveVirtualPath, toVirtualPath, insertContent, insertDocument, findActiveDocument, updateDocumentTitle, updateDocument, deactivateDocument, getActiveDocumentPaths, cleanupOrphanedContent, deleteLLMCache, deleteInactiveDocuments, cleanupOrphanedVectors, vacuumDatabase, getCollectionsWithoutContext, getTopLevelPathsWithoutContext, handelize, hybridQuery, vectorSearchQuery, structuredSearch, addLineNumbers, DEFAULT_EMBED_MODEL, DEFAULT_RERANK_MODEL, DEFAULT_GLOB, DEFAULT_MULTI_GET_MAX_BYTES, createStore, getDefaultDbPath, } from "
|
|
10
|
-
import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "
|
|
9
|
+
import { getPwd, getRealPath, homedir, resolve, enableProductionMode, searchFTS, extractSnippet, getContextForFile, getContextForPath, listCollections, removeCollection, renameCollection, findSimilarFiles, findDocumentByDocid, isDocid, matchFilesByGlob, getHashesNeedingEmbedding, getHashesForEmbedding, clearAllEmbeddings, insertEmbedding, getStatus, hashContent, extractTitle, formatDocForEmbedding, chunkDocumentByTokens, clearCache, getCacheKey, getCachedResult, setCachedResult, getIndexHealth, parseVirtualPath, buildVirtualPath, isVirtualPath, resolveVirtualPath, toVirtualPath, insertContent, insertDocument, findActiveDocument, updateDocumentTitle, updateDocument, deactivateDocument, getActiveDocumentPaths, cleanupOrphanedContent, deleteLLMCache, deleteInactiveDocuments, cleanupOrphanedVectors, vacuumDatabase, getCollectionsWithoutContext, getTopLevelPathsWithoutContext, handelize, hybridQuery, vectorSearchQuery, structuredSearch, addLineNumbers, DEFAULT_EMBED_MODEL, DEFAULT_RERANK_MODEL, DEFAULT_GLOB, DEFAULT_MULTI_GET_MAX_BYTES, createStore, getDefaultDbPath, reindexCollection, generateEmbeddings, syncConfigToDb, } from "../store.js";
|
|
10
|
+
import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
|
|
11
11
|
import { formatSearchResults, formatDocuments, escapeXml, escapeCSV, } from "./formatter.js";
|
|
12
|
-
import { getCollection as getCollectionFromYaml, listCollections as yamlListCollections, getDefaultCollectionNames, addContext as yamlAddContext, removeContext as yamlRemoveContext, setGlobalContext, listAllContexts, setConfigIndexName, } from "
|
|
12
|
+
import { getCollection as getCollectionFromYaml, listCollections as yamlListCollections, getDefaultCollectionNames, addContext as yamlAddContext, removeContext as yamlRemoveContext, removeCollection as yamlRemoveCollectionFn, renameCollection as yamlRenameCollectionFn, setGlobalContext, listAllContexts, setConfigIndexName, loadConfig, } from "../collections.js";
|
|
13
13
|
// Enable production mode - allows using default database path
|
|
14
14
|
// Tests must set INDEX_PATH or use createStore() with explicit path
|
|
15
15
|
enableProductionMode();
|
|
@@ -21,12 +21,33 @@ let storeDbPathOverride;
|
|
|
21
21
|
function getStore() {
|
|
22
22
|
if (!store) {
|
|
23
23
|
store = createStore(storeDbPathOverride);
|
|
24
|
+
// Sync YAML config into SQLite store_collections so store.ts reads from DB
|
|
25
|
+
try {
|
|
26
|
+
const config = loadConfig();
|
|
27
|
+
syncConfigToDb(store.db, config);
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Config may not exist yet — that's fine, DB works without it
|
|
31
|
+
}
|
|
24
32
|
}
|
|
25
33
|
return store;
|
|
26
34
|
}
|
|
27
35
|
function getDb() {
|
|
28
36
|
return getStore().db;
|
|
29
37
|
}
|
|
38
|
+
/** Re-sync YAML config into SQLite after CLI mutations (add/remove/rename collection, context changes) */
|
|
39
|
+
function resyncConfig() {
|
|
40
|
+
const s = getStore();
|
|
41
|
+
try {
|
|
42
|
+
const config = loadConfig();
|
|
43
|
+
// Clear config hash to force re-sync
|
|
44
|
+
s.db.prepare(`DELETE FROM store_config WHERE key = 'config_hash'`).run();
|
|
45
|
+
syncConfigToDb(s.db, config);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Config may not exist — that's fine
|
|
49
|
+
}
|
|
50
|
+
}
|
|
30
51
|
function closeDb() {
|
|
31
52
|
if (store) {
|
|
32
53
|
store.close();
|
|
@@ -354,6 +375,7 @@ async function showStatus() {
|
|
|
354
375
|
}
|
|
355
376
|
async function updateCollections() {
|
|
356
377
|
const db = getDb();
|
|
378
|
+
const storeInstance = getStore();
|
|
357
379
|
// Collections are defined in YAML; no duplicate cleanup needed.
|
|
358
380
|
// Clear Ollama cache on update
|
|
359
381
|
clearCache(db);
|
|
@@ -363,7 +385,6 @@ async function updateCollections() {
|
|
|
363
385
|
closeDb();
|
|
364
386
|
return;
|
|
365
387
|
}
|
|
366
|
-
// Don't close db here - indexFiles will reuse it and close at the end
|
|
367
388
|
console.log(`${c.bold}Updating ${collections.length} collection(s)...${c.reset}\n`);
|
|
368
389
|
for (let i = 0; i < collections.length; i++) {
|
|
369
390
|
const col = collections[i];
|
|
@@ -403,12 +424,30 @@ async function updateCollections() {
|
|
|
403
424
|
process.exit(1);
|
|
404
425
|
}
|
|
405
426
|
}
|
|
406
|
-
|
|
427
|
+
const startTime = Date.now();
|
|
428
|
+
console.log(`Collection: ${col.pwd} (${col.glob_pattern})`);
|
|
429
|
+
progress.indeterminate();
|
|
430
|
+
const result = await reindexCollection(storeInstance, col.pwd, col.glob_pattern, col.name, {
|
|
431
|
+
ignorePatterns: yamlCol?.ignore,
|
|
432
|
+
onProgress: (info) => {
|
|
433
|
+
progress.set((info.current / info.total) * 100);
|
|
434
|
+
const elapsed = (Date.now() - startTime) / 1000;
|
|
435
|
+
const rate = info.current / elapsed;
|
|
436
|
+
const remaining = (info.total - info.current) / rate;
|
|
437
|
+
const eta = info.current > 2 ? ` ETA: ${formatETA(remaining)}` : "";
|
|
438
|
+
if (isTTY)
|
|
439
|
+
process.stderr.write(`\rIndexing: ${info.current}/${info.total}${eta} `);
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
progress.clear();
|
|
443
|
+
console.log(`\nIndexed: ${result.indexed} new, ${result.updated} updated, ${result.unchanged} unchanged, ${result.removed} removed`);
|
|
444
|
+
if (result.orphanedCleaned > 0) {
|
|
445
|
+
console.log(`Cleaned up ${result.orphanedCleaned} orphaned content hash(es)`);
|
|
446
|
+
}
|
|
407
447
|
console.log("");
|
|
408
448
|
}
|
|
409
449
|
// Check if any documents need embedding (show once at end)
|
|
410
|
-
const
|
|
411
|
-
const needsEmbedding = getHashesNeedingEmbedding(finalDb);
|
|
450
|
+
const needsEmbedding = getHashesNeedingEmbedding(db);
|
|
412
451
|
closeDb();
|
|
413
452
|
console.log(`${c.green}✓ All collections updated.${c.reset}`);
|
|
414
453
|
if (needsEmbedding > 0) {
|
|
@@ -452,6 +491,7 @@ async function contextAdd(pathArg, contextText) {
|
|
|
452
491
|
// Handle "/" as global context (applies to all collections)
|
|
453
492
|
if (pathArg === '/') {
|
|
454
493
|
setGlobalContext(contextText);
|
|
494
|
+
resyncConfig();
|
|
455
495
|
console.log(`${c.green}✓${c.reset} Set global context`);
|
|
456
496
|
console.log(`${c.dim}Context: ${contextText}${c.reset}`);
|
|
457
497
|
closeDb();
|
|
@@ -481,6 +521,7 @@ async function contextAdd(pathArg, contextText) {
|
|
|
481
521
|
process.exit(1);
|
|
482
522
|
}
|
|
483
523
|
yamlAddContext(parsed.collectionName, parsed.path, contextText);
|
|
524
|
+
resyncConfig();
|
|
484
525
|
const displayPath = parsed.path
|
|
485
526
|
? `qmd://${parsed.collectionName}/${parsed.path}`
|
|
486
527
|
: `qmd://${parsed.collectionName}/ (collection root)`;
|
|
@@ -497,6 +538,7 @@ async function contextAdd(pathArg, contextText) {
|
|
|
497
538
|
process.exit(1);
|
|
498
539
|
}
|
|
499
540
|
yamlAddContext(detected.collectionName, detected.relativePath, contextText);
|
|
541
|
+
resyncConfig();
|
|
500
542
|
const displayPath = detected.relativePath ? `qmd://${detected.collectionName}/${detected.relativePath}` : `qmd://${detected.collectionName}/`;
|
|
501
543
|
console.log(`${c.green}✓${c.reset} Added context for: ${displayPath}`);
|
|
502
544
|
console.log(`${c.dim}Context: ${contextText}${c.reset}`);
|
|
@@ -527,6 +569,10 @@ function contextRemove(pathArg) {
|
|
|
527
569
|
if (pathArg === '/') {
|
|
528
570
|
// Remove global context
|
|
529
571
|
setGlobalContext(undefined);
|
|
572
|
+
// Resync so SQLite store_config is updated
|
|
573
|
+
const s = getStore();
|
|
574
|
+
resyncConfig();
|
|
575
|
+
closeDb();
|
|
530
576
|
console.log(`${c.green}✓${c.reset} Removed global context`);
|
|
531
577
|
return;
|
|
532
578
|
}
|
|
@@ -1141,9 +1187,10 @@ async function collectionAdd(pwd, globPattern, name) {
|
|
|
1141
1187
|
console.error(`\nUse 'qmd update' to re-index it, or remove it first with 'qmd collection remove ${existingPwdGlob.name}'`);
|
|
1142
1188
|
process.exit(1);
|
|
1143
1189
|
}
|
|
1144
|
-
// Add to YAML config
|
|
1145
|
-
const { addCollection } = await import("
|
|
1190
|
+
// Add to YAML config + sync to SQLite
|
|
1191
|
+
const { addCollection } = await import("../collections.js");
|
|
1146
1192
|
addCollection(collName, pwd, globPattern);
|
|
1193
|
+
resyncConfig();
|
|
1147
1194
|
// Create the collection and index files
|
|
1148
1195
|
console.log(`Creating collection '${collName}'...`);
|
|
1149
1196
|
const newColl = getCollectionFromYaml(collName);
|
|
@@ -1160,6 +1207,8 @@ function collectionRemove(name) {
|
|
|
1160
1207
|
}
|
|
1161
1208
|
const db = getDb();
|
|
1162
1209
|
const result = removeCollection(db, name);
|
|
1210
|
+
// Also remove from YAML config
|
|
1211
|
+
yamlRemoveCollectionFn(name);
|
|
1163
1212
|
closeDb();
|
|
1164
1213
|
console.log(`${c.green}✓${c.reset} Removed collection '${name}'`);
|
|
1165
1214
|
console.log(` Deleted ${result.deletedDocs} documents`);
|
|
@@ -1184,6 +1233,8 @@ function collectionRename(oldName, newName) {
|
|
|
1184
1233
|
}
|
|
1185
1234
|
const db = getDb();
|
|
1186
1235
|
renameCollection(db, oldName, newName);
|
|
1236
|
+
// Also rename in YAML config
|
|
1237
|
+
yamlRenameCollectionFn(oldName, newName);
|
|
1187
1238
|
closeDb();
|
|
1188
1239
|
console.log(`${c.green}✓${c.reset} Renamed collection '${oldName}' to '${newName}'`);
|
|
1189
1240
|
console.log(` Virtual paths updated: ${c.cyan}qmd://${oldName}/${c.reset} → ${c.cyan}qmd://${newName}/${c.reset}`);
|
|
@@ -1315,150 +1366,56 @@ function renderProgressBar(percent, width = 30) {
|
|
|
1315
1366
|
return bar;
|
|
1316
1367
|
}
|
|
1317
1368
|
async function vectorIndex(model = DEFAULT_EMBED_MODEL, force = false) {
|
|
1318
|
-
const
|
|
1319
|
-
const
|
|
1320
|
-
// If force, clear all vectors
|
|
1369
|
+
const storeInstance = getStore();
|
|
1370
|
+
const db = storeInstance.db;
|
|
1321
1371
|
if (force) {
|
|
1322
1372
|
console.log(`${c.yellow}Force re-indexing: clearing all vectors...${c.reset}`);
|
|
1323
|
-
clearAllEmbeddings(db);
|
|
1324
1373
|
}
|
|
1325
|
-
//
|
|
1374
|
+
// Check if there's work to do before starting
|
|
1326
1375
|
const hashesToEmbed = getHashesForEmbedding(db);
|
|
1327
|
-
if (hashesToEmbed.length === 0) {
|
|
1376
|
+
if (hashesToEmbed.length === 0 && !force) {
|
|
1328
1377
|
console.log(`${c.green}✓ All content hashes already have embeddings.${c.reset}`);
|
|
1329
1378
|
closeDb();
|
|
1330
1379
|
return;
|
|
1331
1380
|
}
|
|
1332
|
-
const allChunks = [];
|
|
1333
|
-
let multiChunkDocs = 0;
|
|
1334
|
-
// Chunk all documents using actual token counts
|
|
1335
|
-
process.stderr.write(`Chunking ${hashesToEmbed.length} documents by token count...\n`);
|
|
1336
|
-
for (const item of hashesToEmbed) {
|
|
1337
|
-
const encoder = new TextEncoder();
|
|
1338
|
-
const bodyBytes = encoder.encode(item.body).length;
|
|
1339
|
-
if (bodyBytes === 0)
|
|
1340
|
-
continue; // Skip empty
|
|
1341
|
-
const title = extractTitle(item.body, item.path);
|
|
1342
|
-
const displayName = item.path;
|
|
1343
|
-
const chunks = await chunkDocumentByTokens(item.body); // Uses actual tokenizer
|
|
1344
|
-
if (chunks.length > 1)
|
|
1345
|
-
multiChunkDocs++;
|
|
1346
|
-
for (let seq = 0; seq < chunks.length; seq++) {
|
|
1347
|
-
allChunks.push({
|
|
1348
|
-
hash: item.hash,
|
|
1349
|
-
title,
|
|
1350
|
-
text: chunks[seq].text, // Chunk is guaranteed to exist by seq loop
|
|
1351
|
-
seq,
|
|
1352
|
-
pos: chunks[seq].pos,
|
|
1353
|
-
tokens: chunks[seq].tokens,
|
|
1354
|
-
bytes: encoder.encode(chunks[seq].text).length,
|
|
1355
|
-
displayName,
|
|
1356
|
-
});
|
|
1357
|
-
}
|
|
1358
|
-
}
|
|
1359
|
-
if (allChunks.length === 0) {
|
|
1360
|
-
console.log(`${c.green}✓ No non-empty documents to embed.${c.reset}`);
|
|
1361
|
-
closeDb();
|
|
1362
|
-
return;
|
|
1363
|
-
}
|
|
1364
|
-
const totalBytes = allChunks.reduce((sum, chk) => sum + chk.bytes, 0);
|
|
1365
|
-
const totalChunks = allChunks.length;
|
|
1366
|
-
const totalDocs = hashesToEmbed.length;
|
|
1367
|
-
console.log(`${c.bold}Embedding ${totalDocs} documents${c.reset} ${c.dim}(${totalChunks} chunks, ${formatBytes(totalBytes)})${c.reset}`);
|
|
1368
|
-
if (multiChunkDocs > 0) {
|
|
1369
|
-
console.log(`${c.dim}${multiChunkDocs} documents split into multiple chunks${c.reset}`);
|
|
1370
|
-
}
|
|
1371
1381
|
console.log(`${c.dim}Model: ${model}${c.reset}\n`);
|
|
1372
|
-
// Hide cursor during embedding
|
|
1373
1382
|
cursor.hide();
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
await
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
const firstText = formatDocForEmbedding(firstChunk.text, firstChunk.title);
|
|
1384
|
-
const firstResult = await session.embed(firstText);
|
|
1385
|
-
if (!firstResult) {
|
|
1386
|
-
throw new Error("Failed to get embedding dimensions from first chunk");
|
|
1387
|
-
}
|
|
1388
|
-
ensureVecTable(db, firstResult.embedding.length);
|
|
1389
|
-
let chunksEmbedded = 0, errors = 0, bytesProcessed = 0;
|
|
1390
|
-
const startTime = Date.now();
|
|
1391
|
-
// Batch embedding for better throughput
|
|
1392
|
-
// Process in batches of 32 to balance memory usage and efficiency
|
|
1393
|
-
const BATCH_SIZE = 32;
|
|
1394
|
-
for (let batchStart = 0; batchStart < allChunks.length; batchStart += BATCH_SIZE) {
|
|
1395
|
-
const batchEnd = Math.min(batchStart + BATCH_SIZE, allChunks.length);
|
|
1396
|
-
const batch = allChunks.slice(batchStart, batchEnd);
|
|
1397
|
-
// Format texts for embedding
|
|
1398
|
-
const texts = batch.map(chunk => formatDocForEmbedding(chunk.text, chunk.title));
|
|
1399
|
-
try {
|
|
1400
|
-
// Batch embed all texts at once
|
|
1401
|
-
const embeddings = await session.embedBatch(texts);
|
|
1402
|
-
// Insert each embedding
|
|
1403
|
-
for (let i = 0; i < batch.length; i++) {
|
|
1404
|
-
const chunk = batch[i];
|
|
1405
|
-
const embedding = embeddings[i];
|
|
1406
|
-
if (embedding) {
|
|
1407
|
-
insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(embedding.embedding), model, now);
|
|
1408
|
-
chunksEmbedded++;
|
|
1409
|
-
}
|
|
1410
|
-
else {
|
|
1411
|
-
errors++;
|
|
1412
|
-
console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}${c.reset}`);
|
|
1413
|
-
}
|
|
1414
|
-
bytesProcessed += chunk.bytes;
|
|
1415
|
-
}
|
|
1416
|
-
}
|
|
1417
|
-
catch (err) {
|
|
1418
|
-
// If batch fails, try individual embeddings as fallback
|
|
1419
|
-
for (const chunk of batch) {
|
|
1420
|
-
try {
|
|
1421
|
-
const text = formatDocForEmbedding(chunk.text, chunk.title);
|
|
1422
|
-
const result = await session.embed(text);
|
|
1423
|
-
if (result) {
|
|
1424
|
-
insertEmbedding(db, chunk.hash, chunk.seq, chunk.pos, new Float32Array(result.embedding), model, now);
|
|
1425
|
-
chunksEmbedded++;
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
errors++;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
|
-
catch (innerErr) {
|
|
1432
|
-
errors++;
|
|
1433
|
-
console.error(`\n${c.yellow}⚠ Error embedding "${chunk.displayName}" chunk ${chunk.seq}: ${innerErr}${c.reset}`);
|
|
1434
|
-
}
|
|
1435
|
-
bytesProcessed += chunk.bytes;
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
const percent = (bytesProcessed / totalBytes) * 100;
|
|
1383
|
+
progress.indeterminate();
|
|
1384
|
+
const startTime = Date.now();
|
|
1385
|
+
const result = await generateEmbeddings(storeInstance, {
|
|
1386
|
+
force,
|
|
1387
|
+
model,
|
|
1388
|
+
onProgress: (info) => {
|
|
1389
|
+
if (info.totalBytes === 0)
|
|
1390
|
+
return;
|
|
1391
|
+
const percent = (info.bytesProcessed / info.totalBytes) * 100;
|
|
1439
1392
|
progress.set(percent);
|
|
1440
1393
|
const elapsed = (Date.now() - startTime) / 1000;
|
|
1441
|
-
const bytesPerSec = bytesProcessed / elapsed;
|
|
1442
|
-
const remainingBytes = totalBytes - bytesProcessed;
|
|
1394
|
+
const bytesPerSec = info.bytesProcessed / elapsed;
|
|
1395
|
+
const remainingBytes = info.totalBytes - info.bytesProcessed;
|
|
1443
1396
|
const etaSec = remainingBytes / bytesPerSec;
|
|
1444
1397
|
const bar = renderProgressBar(percent);
|
|
1445
1398
|
const percentStr = percent.toFixed(0).padStart(3);
|
|
1446
1399
|
const throughput = `${formatBytes(bytesPerSec)}/s`;
|
|
1447
1400
|
const eta = elapsed > 2 ? formatETA(etaSec) : "...";
|
|
1448
|
-
const errStr = errors > 0 ? ` ${c.yellow}${errors} err${c.reset}` : "";
|
|
1401
|
+
const errStr = info.errors > 0 ? ` ${c.yellow}${info.errors} err${c.reset}` : "";
|
|
1449
1402
|
if (isTTY)
|
|
1450
|
-
process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${chunksEmbedded}/${totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `);
|
|
1451
|
-
}
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1403
|
+
process.stderr.write(`\r${c.cyan}${bar}${c.reset} ${c.bold}${percentStr}%${c.reset} ${c.dim}${info.chunksEmbedded}/${info.totalChunks}${c.reset}${errStr} ${c.dim}${throughput} ETA ${eta}${c.reset} `);
|
|
1404
|
+
},
|
|
1405
|
+
});
|
|
1406
|
+
progress.clear();
|
|
1407
|
+
cursor.show();
|
|
1408
|
+
const totalTimeSec = result.durationMs / 1000;
|
|
1409
|
+
if (result.chunksEmbedded === 0 && result.docsProcessed === 0) {
|
|
1410
|
+
console.log(`${c.green}✓ No non-empty documents to embed.${c.reset}`);
|
|
1411
|
+
}
|
|
1412
|
+
else {
|
|
1456
1413
|
console.log(`\r${c.green}${renderProgressBar(100)}${c.reset} ${c.bold}100%${c.reset} `);
|
|
1457
|
-
console.log(`\n${c.green}✓ Done!${c.reset} Embedded ${c.bold}${chunksEmbedded}${c.reset} chunks from ${c.bold}${
|
|
1458
|
-
if (errors > 0) {
|
|
1459
|
-
console.log(`${c.yellow}⚠ ${errors} chunks failed${c.reset}`);
|
|
1414
|
+
console.log(`\n${c.green}✓ Done!${c.reset} Embedded ${c.bold}${result.chunksEmbedded}${c.reset} chunks from ${c.bold}${result.docsProcessed}${c.reset} documents in ${c.bold}${formatETA(totalTimeSec)}${c.reset}`);
|
|
1415
|
+
if (result.errors > 0) {
|
|
1416
|
+
console.log(`${c.yellow}⚠ ${result.errors} chunks failed${c.reset}`);
|
|
1460
1417
|
}
|
|
1461
|
-
}
|
|
1418
|
+
}
|
|
1462
1419
|
closeDb();
|
|
1463
1420
|
}
|
|
1464
1421
|
// Sanitize a term for FTS5: remove punctuation except apostrophes
|
|
@@ -1820,7 +1777,7 @@ function logExpansionTree(originalQuery, expanded) {
|
|
|
1820
1777
|
const lines = [];
|
|
1821
1778
|
lines.push(`${c.dim}├─ ${originalQuery}${c.reset}`);
|
|
1822
1779
|
for (const q of expanded) {
|
|
1823
|
-
let preview = q.
|
|
1780
|
+
let preview = q.query.replace(/\n/g, ' ');
|
|
1824
1781
|
if (preview.length > 72)
|
|
1825
1782
|
preview = preview.substring(0, 69) + '...';
|
|
1826
1783
|
lines.push(`${c.dim}├─ ${q.type}: ${preview}${c.reset}`);
|
|
@@ -2093,7 +2050,7 @@ function parseCLI() {
|
|
|
2093
2050
|
function showSkill() {
|
|
2094
2051
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
2095
2052
|
const relativePath = pathJoin("skills", "qmd", "SKILL.md");
|
|
2096
|
-
const skillPath = pathJoin(scriptDir, "..", relativePath);
|
|
2053
|
+
const skillPath = pathJoin(scriptDir, "..", "..", relativePath);
|
|
2097
2054
|
console.log(`QMD Skill (${relativePath})`);
|
|
2098
2055
|
console.log(`Location: ${skillPath}`);
|
|
2099
2056
|
console.log("");
|
|
@@ -2192,7 +2149,7 @@ function showHelp() {
|
|
|
2192
2149
|
}
|
|
2193
2150
|
async function showVersion() {
|
|
2194
2151
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
2195
|
-
const pkgPath = resolve(scriptDir, "..", "package.json");
|
|
2152
|
+
const pkgPath = resolve(scriptDir, "..", "..", "package.json");
|
|
2196
2153
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2197
2154
|
let commit = "";
|
|
2198
2155
|
try {
|
|
@@ -2365,7 +2322,7 @@ if (isMain) {
|
|
|
2365
2322
|
console.error(" Omit command to clear it");
|
|
2366
2323
|
process.exit(1);
|
|
2367
2324
|
}
|
|
2368
|
-
const { updateCollectionSettings, getCollection } = await import("
|
|
2325
|
+
const { updateCollectionSettings, getCollection } = await import("../collections.js");
|
|
2369
2326
|
const col = getCollection(name);
|
|
2370
2327
|
if (!col) {
|
|
2371
2328
|
console.error(`Collection not found: ${name}`);
|
|
@@ -2388,7 +2345,7 @@ if (isMain) {
|
|
|
2388
2345
|
console.error(` ${subcommand === 'include' ? 'Include' : 'Exclude'} collection in default queries`);
|
|
2389
2346
|
process.exit(1);
|
|
2390
2347
|
}
|
|
2391
|
-
const { updateCollectionSettings, getCollection } = await import("
|
|
2348
|
+
const { updateCollectionSettings, getCollection } = await import("../collections.js");
|
|
2392
2349
|
const col = getCollection(name);
|
|
2393
2350
|
if (!col) {
|
|
2394
2351
|
console.error(`Collection not found: ${name}`);
|
|
@@ -2406,7 +2363,7 @@ if (isMain) {
|
|
|
2406
2363
|
console.error("Usage: qmd collection show <name>");
|
|
2407
2364
|
process.exit(1);
|
|
2408
2365
|
}
|
|
2409
|
-
const { getCollection } = await import("
|
|
2366
|
+
const { getCollection } = await import("../collections.js");
|
|
2410
2367
|
const col = getCollection(name);
|
|
2411
2368
|
if (!col) {
|
|
2412
2369
|
console.error(`Collection not found: ${name}`);
|
|
@@ -2553,7 +2510,7 @@ if (isMain) {
|
|
|
2553
2510
|
const logFd = openSync(logPath, "w"); // truncate — fresh log per daemon run
|
|
2554
2511
|
const selfPath = fileURLToPath(import.meta.url);
|
|
2555
2512
|
const spawnArgs = selfPath.endsWith(".ts")
|
|
2556
|
-
? ["--import", pathJoin(dirname(selfPath), "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port)]
|
|
2513
|
+
? ["--import", pathJoin(dirname(selfPath), "..", "..", "node_modules", "tsx", "dist", "esm", "index.mjs"), selfPath, "mcp", "--http", "--port", String(port)]
|
|
2557
2514
|
: [selfPath, "mcp", "--http", "--port", String(port)];
|
|
2558
2515
|
const child = nodeSpawn(process.execPath, spawnArgs, {
|
|
2559
2516
|
stdio: ["ignore", logFd, logFd],
|
|
@@ -2570,7 +2527,7 @@ if (isMain) {
|
|
|
2570
2527
|
// async cleanup handlers in startMcpHttpServer actually run.
|
|
2571
2528
|
process.removeAllListeners("SIGTERM");
|
|
2572
2529
|
process.removeAllListeners("SIGINT");
|
|
2573
|
-
const { startMcpHttpServer } = await import("
|
|
2530
|
+
const { startMcpHttpServer } = await import("../mcp/server.js");
|
|
2574
2531
|
try {
|
|
2575
2532
|
await startMcpHttpServer(port);
|
|
2576
2533
|
}
|
|
@@ -2584,7 +2541,7 @@ if (isMain) {
|
|
|
2584
2541
|
}
|
|
2585
2542
|
else {
|
|
2586
2543
|
// Default: stdio transport
|
|
2587
|
-
const { startMcpServer } = await import("
|
|
2544
|
+
const { startMcpServer } = await import("../mcp/server.js");
|
|
2588
2545
|
await startMcpServer();
|
|
2589
2546
|
}
|
|
2590
2547
|
break;
|
package/dist/collections.d.ts
CHANGED
|
@@ -34,18 +34,32 @@ export interface CollectionConfig {
|
|
|
34
34
|
export interface NamedCollection extends Collection {
|
|
35
35
|
name: string;
|
|
36
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* Set the config source for SDK mode.
|
|
39
|
+
* - File path: load/save from a specific YAML file
|
|
40
|
+
* - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
|
|
41
|
+
* - undefined: reset to default file-based config
|
|
42
|
+
*/
|
|
43
|
+
export declare function setConfigSource(source?: {
|
|
44
|
+
configPath?: string;
|
|
45
|
+
config?: CollectionConfig;
|
|
46
|
+
}): void;
|
|
37
47
|
/**
|
|
38
48
|
* Set the current index name for config file lookup
|
|
39
49
|
* Config file will be ~/.config/qmd/{indexName}.yml
|
|
40
50
|
*/
|
|
41
51
|
export declare function setConfigIndexName(name: string): void;
|
|
42
52
|
/**
|
|
43
|
-
* Load configuration from
|
|
53
|
+
* Load configuration from the configured source.
|
|
54
|
+
* - Inline config: returns the in-memory object directly
|
|
55
|
+
* - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
|
|
44
56
|
* Returns empty config if file doesn't exist
|
|
45
57
|
*/
|
|
46
58
|
export declare function loadConfig(): CollectionConfig;
|
|
47
59
|
/**
|
|
48
|
-
* Save configuration to
|
|
60
|
+
* Save configuration to the configured source.
|
|
61
|
+
* - Inline config: updates the in-memory object (no file I/O)
|
|
62
|
+
* - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
|
|
49
63
|
*/
|
|
50
64
|
export declare function saveConfig(config: CollectionConfig): void;
|
|
51
65
|
/**
|
package/dist/collections.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Collections define which directories to index and their associated contexts.
|
|
6
6
|
*/
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
-
import { join } from "path";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import YAML from "yaml";
|
|
11
11
|
// ============================================================================
|
|
@@ -13,6 +13,33 @@ import YAML from "yaml";
|
|
|
13
13
|
// ============================================================================
|
|
14
14
|
// Current index name (default: "index")
|
|
15
15
|
let currentIndexName = "index";
|
|
16
|
+
// SDK mode: optional in-memory config or custom config path
|
|
17
|
+
let configSource = { type: 'file' };
|
|
18
|
+
/**
|
|
19
|
+
* Set the config source for SDK mode.
|
|
20
|
+
* - File path: load/save from a specific YAML file
|
|
21
|
+
* - Inline config: use an in-memory CollectionConfig (saveConfig updates in place, no file I/O)
|
|
22
|
+
* - undefined: reset to default file-based config
|
|
23
|
+
*/
|
|
24
|
+
export function setConfigSource(source) {
|
|
25
|
+
if (!source) {
|
|
26
|
+
configSource = { type: 'file' };
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (source.config) {
|
|
30
|
+
// Ensure collections object exists
|
|
31
|
+
if (!source.config.collections) {
|
|
32
|
+
source.config.collections = {};
|
|
33
|
+
}
|
|
34
|
+
configSource = { type: 'inline', config: source.config };
|
|
35
|
+
}
|
|
36
|
+
else if (source.configPath) {
|
|
37
|
+
configSource = { type: 'file', path: source.configPath };
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
configSource = { type: 'file' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
16
43
|
/**
|
|
17
44
|
* Set the current index name for config file lookup
|
|
18
45
|
* Config file will be ~/.config/qmd/{indexName}.yml
|
|
@@ -57,11 +84,18 @@ function ensureConfigDir() {
|
|
|
57
84
|
// Core functions
|
|
58
85
|
// ============================================================================
|
|
59
86
|
/**
|
|
60
|
-
* Load configuration from
|
|
87
|
+
* Load configuration from the configured source.
|
|
88
|
+
* - Inline config: returns the in-memory object directly
|
|
89
|
+
* - File-based: reads from YAML file (default ~/.config/qmd/index.yml)
|
|
61
90
|
* Returns empty config if file doesn't exist
|
|
62
91
|
*/
|
|
63
92
|
export function loadConfig() {
|
|
64
|
-
|
|
93
|
+
// SDK inline config mode
|
|
94
|
+
if (configSource.type === 'inline') {
|
|
95
|
+
return configSource.config;
|
|
96
|
+
}
|
|
97
|
+
// File-based config (SDK custom path or default)
|
|
98
|
+
const configPath = configSource.path || getConfigFilePath();
|
|
65
99
|
if (!existsSync(configPath)) {
|
|
66
100
|
return { collections: {} };
|
|
67
101
|
}
|
|
@@ -79,11 +113,21 @@ export function loadConfig() {
|
|
|
79
113
|
}
|
|
80
114
|
}
|
|
81
115
|
/**
|
|
82
|
-
* Save configuration to
|
|
116
|
+
* Save configuration to the configured source.
|
|
117
|
+
* - Inline config: updates the in-memory object (no file I/O)
|
|
118
|
+
* - File-based: writes to YAML file (default ~/.config/qmd/index.yml)
|
|
83
119
|
*/
|
|
84
120
|
export function saveConfig(config) {
|
|
85
|
-
|
|
86
|
-
|
|
121
|
+
// SDK inline config mode: update in place, no file I/O
|
|
122
|
+
if (configSource.type === 'inline') {
|
|
123
|
+
configSource.config = config;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
const configPath = configSource.path || getConfigFilePath();
|
|
127
|
+
const configDir = dirname(configPath);
|
|
128
|
+
if (!existsSync(configDir)) {
|
|
129
|
+
mkdirSync(configDir, { recursive: true });
|
|
130
|
+
}
|
|
87
131
|
try {
|
|
88
132
|
const yaml = YAML.stringify(config, {
|
|
89
133
|
indent: 2,
|
|
@@ -318,13 +362,18 @@ export function findContextForPath(collectionName, filePath) {
|
|
|
318
362
|
* Get the config file path (useful for error messages)
|
|
319
363
|
*/
|
|
320
364
|
export function getConfigPath() {
|
|
321
|
-
|
|
365
|
+
if (configSource.type === 'inline')
|
|
366
|
+
return '<inline>';
|
|
367
|
+
return configSource.path || getConfigFilePath();
|
|
322
368
|
}
|
|
323
369
|
/**
|
|
324
370
|
* Check if config file exists
|
|
325
371
|
*/
|
|
326
372
|
export function configExists() {
|
|
327
|
-
|
|
373
|
+
if (configSource.type === 'inline')
|
|
374
|
+
return true;
|
|
375
|
+
const path = configSource.path || getConfigFilePath();
|
|
376
|
+
return existsSync(path);
|
|
328
377
|
}
|
|
329
378
|
/**
|
|
330
379
|
* Validate a collection name
|