@tobilu/qmd 2.0.0 → 2.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.
- package/CHANGELOG.md +110 -0
- package/README.md +62 -2
- package/bin/qmd +24 -4
- package/dist/ast.d.ts +64 -0
- package/dist/ast.js +324 -0
- package/dist/bench/bench.d.ts +21 -0
- package/dist/bench/bench.js +185 -0
- package/dist/bench/score.d.ts +26 -0
- package/dist/bench/score.js +67 -0
- package/dist/bench/types.d.ts +67 -0
- package/dist/bench/types.js +8 -0
- package/dist/cli/formatter.js +5 -1
- package/dist/cli/qmd.d.ts +2 -1
- package/dist/cli/qmd.js +338 -21
- package/dist/collections.d.ts +11 -0
- package/dist/db.d.ts +8 -0
- package/dist/db.js +44 -3
- package/dist/embedded-skills.d.ts +6 -0
- package/dist/embedded-skills.js +14 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +13 -3
- package/dist/llm.d.ts +12 -3
- package/dist/llm.js +95 -25
- package/dist/mcp/server.js +29 -5
- package/dist/store.d.ts +56 -6
- package/dist/store.js +401 -138
- package/package.json +34 -17
package/dist/cli/qmd.js
CHANGED
|
@@ -3,13 +3,15 @@ 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
|
-
import { dirname, join as pathJoin } from "path";
|
|
6
|
+
import { dirname, join as pathJoin, relative as relativePath } from "path";
|
|
7
7
|
import { parseArgs } from "util";
|
|
8
|
-
import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync } from "fs";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
8
|
+
import { readFileSync, realpathSync, statSync, existsSync, unlinkSync, writeFileSync, openSync, closeSync, mkdirSync, lstatSync, rmSync, symlinkSync, readlinkSync } from "fs";
|
|
9
|
+
import { createInterface } from "readline/promises";
|
|
10
|
+
import { getPwd, getRealPath, homedir, resolve, enableProductionMode, searchFTS, extractSnippet, getContextForFile, getContextForPath, listCollections, removeCollection, renameCollection, findSimilarFiles, findDocumentByDocid, isDocid, matchFilesByGlob, getHashesNeedingEmbedding, 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_EMBED_MAX_BATCH_BYTES, DEFAULT_EMBED_MAX_DOCS_PER_BATCH, DEFAULT_RERANK_MODEL, DEFAULT_GLOB, DEFAULT_MULTI_GET_MAX_BYTES, createStore, getDefaultDbPath, reindexCollection, generateEmbeddings, syncConfigToDb, } from "../store.js";
|
|
11
|
+
import { disposeDefaultLlamaCpp, getDefaultLlamaCpp, setDefaultLlamaCpp, LlamaCpp, withLLMSession, pullModels, DEFAULT_EMBED_MODEL_URI, DEFAULT_GENERATE_MODEL_URI, DEFAULT_RERANK_MODEL_URI, DEFAULT_MODEL_CACHE_DIR } from "../llm.js";
|
|
11
12
|
import { formatSearchResults, formatDocuments, escapeXml, escapeCSV, } from "./formatter.js";
|
|
12
13
|
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";
|
|
14
|
+
import { getEmbeddedQmdSkillContent, getEmbeddedQmdSkillFiles } from "../embedded-skills.js";
|
|
13
15
|
// Enable production mode - allows using default database path
|
|
14
16
|
// Tests must set INDEX_PATH or use createStore() with explicit path
|
|
15
17
|
enableProductionMode();
|
|
@@ -25,6 +27,13 @@ function getStore() {
|
|
|
25
27
|
try {
|
|
26
28
|
const config = loadConfig();
|
|
27
29
|
syncConfigToDb(store.db, config);
|
|
30
|
+
if (config.models) {
|
|
31
|
+
setDefaultLlamaCpp(new LlamaCpp({
|
|
32
|
+
embedModel: config.models.embed,
|
|
33
|
+
generateModel: config.models.generate,
|
|
34
|
+
rerankModel: config.models.rerank,
|
|
35
|
+
}));
|
|
36
|
+
}
|
|
28
37
|
}
|
|
29
38
|
catch {
|
|
30
39
|
// Config may not exist yet — that's fine, DB works without it
|
|
@@ -259,6 +268,34 @@ async function showStatus() {
|
|
|
259
268
|
context: ctx.context
|
|
260
269
|
});
|
|
261
270
|
}
|
|
271
|
+
// AST chunking status
|
|
272
|
+
try {
|
|
273
|
+
const { getASTStatus } = await import("../ast.js");
|
|
274
|
+
const ast = await getASTStatus();
|
|
275
|
+
console.log(`\n${c.bold}AST Chunking${c.reset}`);
|
|
276
|
+
if (ast.available) {
|
|
277
|
+
const ok = ast.languages.filter(l => l.available).map(l => l.language);
|
|
278
|
+
const fail = ast.languages.filter(l => !l.available);
|
|
279
|
+
console.log(` Status: ${c.green}active${c.reset}`);
|
|
280
|
+
console.log(` Languages: ${ok.join(", ")}`);
|
|
281
|
+
if (fail.length > 0) {
|
|
282
|
+
for (const f of fail) {
|
|
283
|
+
console.log(` ${c.yellow}Unavailable: ${f.language} (${f.error})${c.reset}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
else {
|
|
288
|
+
console.log(` Status: ${c.yellow}unavailable${c.reset} (falling back to regex chunking)`);
|
|
289
|
+
for (const l of ast.languages) {
|
|
290
|
+
if (l.error)
|
|
291
|
+
console.log(` ${c.dim}${l.language}: ${l.error}${c.reset}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
console.log(`\n${c.bold}AST Chunking${c.reset}`);
|
|
297
|
+
console.log(` Status: ${c.dim}not available${c.reset}`);
|
|
298
|
+
}
|
|
262
299
|
if (collections.length > 0) {
|
|
263
300
|
console.log(`\n${c.bold}Collections${c.reset}`);
|
|
264
301
|
for (const col of collections) {
|
|
@@ -785,7 +822,7 @@ function getDocument(filename, fromLine, maxLines, lineNumbers) {
|
|
|
785
822
|
function multiGet(pattern, maxLines, maxBytes = DEFAULT_MULTI_GET_MAX_BYTES, format = "cli") {
|
|
786
823
|
const db = getDb();
|
|
787
824
|
// Check if it's a comma-separated list or a glob pattern
|
|
788
|
-
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?');
|
|
825
|
+
const isCommaSeparated = pattern.includes(',') && !pattern.includes('*') && !pattern.includes('?') && !pattern.includes('{');
|
|
789
826
|
let files;
|
|
790
827
|
if (isCommaSeparated) {
|
|
791
828
|
// Comma-separated list of files (can be virtual paths or relative paths)
|
|
@@ -1365,26 +1402,51 @@ function renderProgressBar(percent, width = 30) {
|
|
|
1365
1402
|
const bar = "█".repeat(filled) + "░".repeat(empty);
|
|
1366
1403
|
return bar;
|
|
1367
1404
|
}
|
|
1368
|
-
|
|
1405
|
+
function parseEmbedBatchOption(name, value) {
|
|
1406
|
+
if (value === undefined)
|
|
1407
|
+
return undefined;
|
|
1408
|
+
const parsed = Number(value);
|
|
1409
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
1410
|
+
throw new Error(`${name} must be a positive integer`);
|
|
1411
|
+
}
|
|
1412
|
+
return parsed;
|
|
1413
|
+
}
|
|
1414
|
+
function parseChunkStrategy(value) {
|
|
1415
|
+
if (value === undefined)
|
|
1416
|
+
return undefined;
|
|
1417
|
+
const s = String(value);
|
|
1418
|
+
if (s === "auto" || s === "regex")
|
|
1419
|
+
return s;
|
|
1420
|
+
throw new Error(`--chunk-strategy must be "auto" or "regex" (got "${s}")`);
|
|
1421
|
+
}
|
|
1422
|
+
async function vectorIndex(model = DEFAULT_EMBED_MODEL_URI, force = false, batchOptions) {
|
|
1369
1423
|
const storeInstance = getStore();
|
|
1370
1424
|
const db = storeInstance.db;
|
|
1371
1425
|
if (force) {
|
|
1372
1426
|
console.log(`${c.yellow}Force re-indexing: clearing all vectors...${c.reset}`);
|
|
1373
1427
|
}
|
|
1374
1428
|
// Check if there's work to do before starting
|
|
1375
|
-
const hashesToEmbed =
|
|
1376
|
-
if (hashesToEmbed
|
|
1429
|
+
const hashesToEmbed = getHashesNeedingEmbedding(db);
|
|
1430
|
+
if (hashesToEmbed === 0 && !force) {
|
|
1377
1431
|
console.log(`${c.green}✓ All content hashes already have embeddings.${c.reset}`);
|
|
1378
1432
|
closeDb();
|
|
1379
1433
|
return;
|
|
1380
1434
|
}
|
|
1381
1435
|
console.log(`${c.dim}Model: ${model}${c.reset}\n`);
|
|
1436
|
+
if (batchOptions?.maxDocsPerBatch !== undefined || batchOptions?.maxBatchBytes !== undefined) {
|
|
1437
|
+
const maxDocsPerBatch = batchOptions.maxDocsPerBatch ?? DEFAULT_EMBED_MAX_DOCS_PER_BATCH;
|
|
1438
|
+
const maxBatchBytes = batchOptions.maxBatchBytes ?? DEFAULT_EMBED_MAX_BATCH_BYTES;
|
|
1439
|
+
console.log(`${c.dim}Batch: ${maxDocsPerBatch} docs / ${formatBytes(maxBatchBytes)}${c.reset}\n`);
|
|
1440
|
+
}
|
|
1382
1441
|
cursor.hide();
|
|
1383
1442
|
progress.indeterminate();
|
|
1384
1443
|
const startTime = Date.now();
|
|
1385
1444
|
const result = await generateEmbeddings(storeInstance, {
|
|
1386
1445
|
force,
|
|
1387
1446
|
model,
|
|
1447
|
+
maxDocsPerBatch: batchOptions?.maxDocsPerBatch,
|
|
1448
|
+
maxBatchBytes: batchOptions?.maxBatchBytes,
|
|
1449
|
+
chunkStrategy: batchOptions?.chunkStrategy,
|
|
1388
1450
|
onProgress: (info) => {
|
|
1389
1451
|
if (info.totalBytes === 0)
|
|
1390
1452
|
return;
|
|
@@ -1511,6 +1573,45 @@ function printEmptySearchResults(format, reason = "no_results") {
|
|
|
1511
1573
|
}
|
|
1512
1574
|
console.log("No results found.");
|
|
1513
1575
|
}
|
|
1576
|
+
const DEFAULT_EDITOR_URI_TEMPLATE = "vscode://file/{path}:{line}:{col}";
|
|
1577
|
+
function encodePathForEditorUri(absolutePath) {
|
|
1578
|
+
return encodeURI(absolutePath)
|
|
1579
|
+
.replace(/\?/g, "%3F")
|
|
1580
|
+
.replace(/#/g, "%23");
|
|
1581
|
+
}
|
|
1582
|
+
function getEditorUriTemplate() {
|
|
1583
|
+
const envTemplate = process.env.QMD_EDITOR_URI?.trim();
|
|
1584
|
+
if (envTemplate)
|
|
1585
|
+
return envTemplate;
|
|
1586
|
+
try {
|
|
1587
|
+
const config = loadConfig();
|
|
1588
|
+
const configTemplate = (config.editor_uri
|
|
1589
|
+
|| config.editor_uri_template
|
|
1590
|
+
|| config.editorUri
|
|
1591
|
+
|| (typeof config["editor-uri"] === "string" ? config["editor-uri"] : undefined))?.trim();
|
|
1592
|
+
if (configTemplate)
|
|
1593
|
+
return configTemplate;
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
// Ignore config parsing issues and use default template.
|
|
1597
|
+
}
|
|
1598
|
+
return DEFAULT_EDITOR_URI_TEMPLATE;
|
|
1599
|
+
}
|
|
1600
|
+
export function buildEditorUri(template, absolutePath, line, col) {
|
|
1601
|
+
const safeLine = Number.isFinite(line) && line > 0 ? Math.floor(line) : 1;
|
|
1602
|
+
const safeCol = Number.isFinite(col) && col > 0 ? Math.floor(col) : 1;
|
|
1603
|
+
const encodedPath = encodePathForEditorUri(absolutePath);
|
|
1604
|
+
return template
|
|
1605
|
+
.replace(/\{path\}/g, encodedPath)
|
|
1606
|
+
.replace(/\{line\}/g, String(safeLine))
|
|
1607
|
+
.replace(/\{col\}/g, String(safeCol))
|
|
1608
|
+
.replace(/\{column\}/g, String(safeCol));
|
|
1609
|
+
}
|
|
1610
|
+
export function termLink(text, url, isTTY = !!process.stdout.isTTY) {
|
|
1611
|
+
if (!isTTY)
|
|
1612
|
+
return text;
|
|
1613
|
+
return `\x1b]8;;${url}\x07${text}\x1b]8;;\x07`;
|
|
1614
|
+
}
|
|
1514
1615
|
function outputResults(results, query, opts) {
|
|
1515
1616
|
const filtered = results.filter(r => r.score >= opts.minScore).slice(0, opts.limit);
|
|
1516
1617
|
if (filtered.length === 0) {
|
|
@@ -1553,6 +1654,8 @@ function outputResults(results, query, opts) {
|
|
|
1553
1654
|
}
|
|
1554
1655
|
}
|
|
1555
1656
|
else if (opts.format === "cli") {
|
|
1657
|
+
const editorUriTemplate = getEditorUriTemplate();
|
|
1658
|
+
const linkDb = getDb();
|
|
1556
1659
|
for (let i = 0; i < filtered.length; i++) {
|
|
1557
1660
|
const row = filtered[i];
|
|
1558
1661
|
if (!row)
|
|
@@ -1560,13 +1663,25 @@ function outputResults(results, query, opts) {
|
|
|
1560
1663
|
const { line, snippet } = extractSnippet(row.body, query, 500, row.chunkPos, undefined, opts.intent);
|
|
1561
1664
|
const docid = row.docid || (row.hash ? row.hash.slice(0, 6) : undefined);
|
|
1562
1665
|
// Line 1: filepath with docid
|
|
1563
|
-
const
|
|
1666
|
+
const virtualPath = row.file.startsWith("qmd://") ? row.file : toQmdPath(row.displayPath);
|
|
1667
|
+
const parsed = parseVirtualPath(virtualPath);
|
|
1668
|
+
const absolutePath = resolveVirtualPath(linkDb, virtualPath);
|
|
1669
|
+
const legacyPath = toQmdPath(row.displayPath);
|
|
1670
|
+
const displayPath = parsed?.path || row.displayPath;
|
|
1564
1671
|
// Only show :line if we actually found a term match in the snippet body (exclude header line).
|
|
1565
1672
|
const snippetBody = snippet.split("\n").slice(1).join("\n").toLowerCase();
|
|
1566
1673
|
const hasMatch = query.toLowerCase().split(/\s+/).some(t => t.length > 0 && snippetBody.includes(t));
|
|
1567
1674
|
const lineInfo = hasMatch ? `:${line}` : "";
|
|
1568
1675
|
const docidStr = docid ? ` ${c.dim}#${docid}${c.reset}` : "";
|
|
1569
|
-
|
|
1676
|
+
if (process.stdout.isTTY && absolutePath && parsed?.path) {
|
|
1677
|
+
const linkLine = hasMatch ? line : 1;
|
|
1678
|
+
const linkTarget = buildEditorUri(editorUriTemplate, absolutePath, linkLine, 1);
|
|
1679
|
+
const clickable = termLink(`${displayPath}${lineInfo}`, linkTarget);
|
|
1680
|
+
console.log(`${c.cyan}${clickable}${c.reset}${docidStr}`);
|
|
1681
|
+
}
|
|
1682
|
+
else {
|
|
1683
|
+
console.log(`${c.cyan}${legacyPath}${c.dim}${lineInfo}${c.reset}${docidStr}`);
|
|
1684
|
+
}
|
|
1570
1685
|
// Line 2: Title (if available)
|
|
1571
1686
|
if (row.title) {
|
|
1572
1687
|
console.log(`${c.bold}Title: ${row.title}${c.reset}`);
|
|
@@ -1865,8 +1980,10 @@ async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rera
|
|
|
1865
1980
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1866
1981
|
minScore: opts.minScore || 0,
|
|
1867
1982
|
candidateLimit: opts.candidateLimit,
|
|
1983
|
+
skipRerank: opts.skipRerank,
|
|
1868
1984
|
explain: !!opts.explain,
|
|
1869
1985
|
intent,
|
|
1986
|
+
chunkStrategy: opts.chunkStrategy,
|
|
1870
1987
|
hooks: {
|
|
1871
1988
|
onEmbedStart: (count) => {
|
|
1872
1989
|
process.stderr.write(`${c.dim}Embedding ${count} ${count === 1 ? 'query' : 'queries'}...${c.reset}`);
|
|
@@ -1892,8 +2009,10 @@ async function querySearch(query, opts, _embedModel = DEFAULT_EMBED_MODEL, _rera
|
|
|
1892
2009
|
limit: opts.all ? 500 : (opts.limit || 10),
|
|
1893
2010
|
minScore: opts.minScore || 0,
|
|
1894
2011
|
candidateLimit: opts.candidateLimit,
|
|
2012
|
+
skipRerank: opts.skipRerank,
|
|
1895
2013
|
explain: !!opts.explain,
|
|
1896
2014
|
intent,
|
|
2015
|
+
chunkStrategy: opts.chunkStrategy,
|
|
1897
2016
|
hooks: {
|
|
1898
2017
|
onStrongSignal: (score) => {
|
|
1899
2018
|
process.stderr.write(`${c.dim}Strong BM25 signal (${score.toFixed(2)}) — skipping expansion${c.reset}\n`);
|
|
@@ -1969,6 +2088,8 @@ function parseCLI() {
|
|
|
1969
2088
|
help: { type: "boolean", short: "h" },
|
|
1970
2089
|
version: { type: "boolean", short: "v" },
|
|
1971
2090
|
skill: { type: "boolean" },
|
|
2091
|
+
global: { type: "boolean" },
|
|
2092
|
+
yes: { type: "boolean" },
|
|
1972
2093
|
// Search options
|
|
1973
2094
|
n: { type: "string" },
|
|
1974
2095
|
"min-score": { type: "string" },
|
|
@@ -1986,6 +2107,8 @@ function parseCLI() {
|
|
|
1986
2107
|
mask: { type: "string" }, // glob pattern
|
|
1987
2108
|
// Embed options
|
|
1988
2109
|
force: { type: "boolean", short: "f" },
|
|
2110
|
+
"max-docs-per-batch": { type: "string" },
|
|
2111
|
+
"max-batch-mb": { type: "string" },
|
|
1989
2112
|
// Update options
|
|
1990
2113
|
pull: { type: "boolean" }, // git pull before update
|
|
1991
2114
|
refresh: { type: "boolean" },
|
|
@@ -1996,7 +2119,10 @@ function parseCLI() {
|
|
|
1996
2119
|
"line-numbers": { type: "boolean" }, // add line numbers to output
|
|
1997
2120
|
// Query options
|
|
1998
2121
|
"candidate-limit": { type: "string", short: "C" },
|
|
2122
|
+
"no-rerank": { type: "boolean", default: false },
|
|
1999
2123
|
intent: { type: "string" },
|
|
2124
|
+
// Chunking options
|
|
2125
|
+
"chunk-strategy": { type: "string" }, // "regex" (default) or "auto" (AST for code files)
|
|
2000
2126
|
// MCP HTTP transport options
|
|
2001
2127
|
http: { type: "boolean" },
|
|
2002
2128
|
daemon: { type: "boolean" },
|
|
@@ -2036,8 +2162,10 @@ function parseCLI() {
|
|
|
2036
2162
|
collection: values.collection,
|
|
2037
2163
|
lineNumbers: !!values["line-numbers"],
|
|
2038
2164
|
candidateLimit: values["candidate-limit"] ? parseInt(String(values["candidate-limit"]), 10) : undefined,
|
|
2165
|
+
skipRerank: !!values["no-rerank"],
|
|
2039
2166
|
explain: !!values.explain,
|
|
2040
2167
|
intent: values.intent,
|
|
2168
|
+
chunkStrategy: parseChunkStrategy(values["chunk-strategy"]),
|
|
2041
2169
|
};
|
|
2042
2170
|
return {
|
|
2043
2171
|
command: positionals[0] || "",
|
|
@@ -2047,19 +2175,116 @@ function parseCLI() {
|
|
|
2047
2175
|
values,
|
|
2048
2176
|
};
|
|
2049
2177
|
}
|
|
2178
|
+
function getSkillInstallDir(globalInstall) {
|
|
2179
|
+
return globalInstall
|
|
2180
|
+
? resolve(homedir(), ".agents", "skills", "qmd")
|
|
2181
|
+
: resolve(getPwd(), ".agents", "skills", "qmd");
|
|
2182
|
+
}
|
|
2183
|
+
function getClaudeSkillLinkPath(globalInstall) {
|
|
2184
|
+
return globalInstall
|
|
2185
|
+
? resolve(homedir(), ".claude", "skills", "qmd")
|
|
2186
|
+
: resolve(getPwd(), ".claude", "skills", "qmd");
|
|
2187
|
+
}
|
|
2188
|
+
function pathExists(path) {
|
|
2189
|
+
try {
|
|
2190
|
+
lstatSync(path);
|
|
2191
|
+
return true;
|
|
2192
|
+
}
|
|
2193
|
+
catch {
|
|
2194
|
+
return false;
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function removePath(path) {
|
|
2198
|
+
const stat = lstatSync(path);
|
|
2199
|
+
if (stat.isDirectory() && !stat.isSymbolicLink()) {
|
|
2200
|
+
rmSync(path, { recursive: true, force: true });
|
|
2201
|
+
}
|
|
2202
|
+
else {
|
|
2203
|
+
unlinkSync(path);
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2050
2206
|
function showSkill() {
|
|
2051
|
-
|
|
2052
|
-
const relativePath = pathJoin("skills", "qmd", "SKILL.md");
|
|
2053
|
-
const skillPath = pathJoin(scriptDir, "..", "..", relativePath);
|
|
2054
|
-
console.log(`QMD Skill (${relativePath})`);
|
|
2055
|
-
console.log(`Location: ${skillPath}`);
|
|
2207
|
+
console.log("QMD Skill (embedded)");
|
|
2056
2208
|
console.log("");
|
|
2057
|
-
|
|
2058
|
-
|
|
2209
|
+
const content = getEmbeddedQmdSkillContent();
|
|
2210
|
+
process.stdout.write(content.endsWith("\n") ? content : content + "\n");
|
|
2211
|
+
}
|
|
2212
|
+
function writeEmbeddedSkill(targetDir, force) {
|
|
2213
|
+
if (pathExists(targetDir)) {
|
|
2214
|
+
if (!force) {
|
|
2215
|
+
throw new Error(`Skill already exists: ${targetDir} (use --force to replace it)`);
|
|
2216
|
+
}
|
|
2217
|
+
removePath(targetDir);
|
|
2218
|
+
}
|
|
2219
|
+
mkdirSync(targetDir, { recursive: true });
|
|
2220
|
+
for (const file of getEmbeddedQmdSkillFiles()) {
|
|
2221
|
+
const destination = resolve(targetDir, file.relativePath);
|
|
2222
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
2223
|
+
writeFileSync(destination, file.content, "utf-8");
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
function ensureClaudeSymlink(linkPath, targetDir, force) {
|
|
2227
|
+
const parentDir = dirname(linkPath);
|
|
2228
|
+
if (pathExists(parentDir)) {
|
|
2229
|
+
const resolvedTargetDir = realpathSync(dirname(targetDir));
|
|
2230
|
+
const resolvedLinkParent = realpathSync(parentDir);
|
|
2231
|
+
// If .claude/skills already resolves to the same directory as .agents/skills,
|
|
2232
|
+
// the skill is already visible to Claude and creating qmd -> qmd would loop.
|
|
2233
|
+
if (resolvedTargetDir === resolvedLinkParent) {
|
|
2234
|
+
return false;
|
|
2235
|
+
}
|
|
2236
|
+
}
|
|
2237
|
+
const linkTarget = relativePath(parentDir, targetDir) || ".";
|
|
2238
|
+
mkdirSync(parentDir, { recursive: true });
|
|
2239
|
+
if (pathExists(linkPath)) {
|
|
2240
|
+
const stat = lstatSync(linkPath);
|
|
2241
|
+
if (stat.isSymbolicLink() && readlinkSync(linkPath) === linkTarget) {
|
|
2242
|
+
return true;
|
|
2243
|
+
}
|
|
2244
|
+
if (!force) {
|
|
2245
|
+
throw new Error(`Claude skill path already exists: ${linkPath} (use --force to replace it)`);
|
|
2246
|
+
}
|
|
2247
|
+
removePath(linkPath);
|
|
2248
|
+
}
|
|
2249
|
+
symlinkSync(linkTarget, linkPath, "dir");
|
|
2250
|
+
return true;
|
|
2251
|
+
}
|
|
2252
|
+
async function shouldCreateClaudeSymlink(linkPath, autoYes) {
|
|
2253
|
+
if (autoYes) {
|
|
2254
|
+
return true;
|
|
2255
|
+
}
|
|
2256
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
2257
|
+
console.log(`Tip: create a Claude symlink manually at ${linkPath}`);
|
|
2258
|
+
return false;
|
|
2259
|
+
}
|
|
2260
|
+
const rl = createInterface({
|
|
2261
|
+
input: process.stdin,
|
|
2262
|
+
output: process.stdout,
|
|
2263
|
+
});
|
|
2264
|
+
try {
|
|
2265
|
+
const answer = await rl.question(`Create a symlink in ${linkPath}? [y/N] `);
|
|
2266
|
+
const normalized = answer.trim().toLowerCase();
|
|
2267
|
+
return normalized === "y" || normalized === "yes";
|
|
2268
|
+
}
|
|
2269
|
+
finally {
|
|
2270
|
+
rl.close();
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
async function installSkill(globalInstall, force, autoYes) {
|
|
2274
|
+
const installDir = getSkillInstallDir(globalInstall);
|
|
2275
|
+
writeEmbeddedSkill(installDir, force);
|
|
2276
|
+
console.log(`✓ Installed QMD skill to ${installDir}`);
|
|
2277
|
+
const claudeLinkPath = getClaudeSkillLinkPath(globalInstall);
|
|
2278
|
+
if (!(await shouldCreateClaudeSymlink(claudeLinkPath, autoYes))) {
|
|
2059
2279
|
return;
|
|
2060
2280
|
}
|
|
2061
|
-
const
|
|
2062
|
-
|
|
2281
|
+
const linked = ensureClaudeSymlink(claudeLinkPath, installDir, force);
|
|
2282
|
+
if (linked) {
|
|
2283
|
+
console.log(`✓ Linked Claude skill at ${claudeLinkPath}`);
|
|
2284
|
+
}
|
|
2285
|
+
else {
|
|
2286
|
+
console.log(`✓ Claude already sees the skill via ${dirname(claudeLinkPath)}`);
|
|
2287
|
+
}
|
|
2063
2288
|
}
|
|
2064
2289
|
function showHelp() {
|
|
2065
2290
|
console.log("qmd — Quick Markdown Search");
|
|
@@ -2074,7 +2299,9 @@ function showHelp() {
|
|
|
2074
2299
|
console.log(" qmd vsearch <query> - Vector similarity only");
|
|
2075
2300
|
console.log(" qmd get <file>[:line] [-l N] - Show a single document, optional line slice");
|
|
2076
2301
|
console.log(" qmd multi-get <pattern> - Batch fetch via glob or comma-separated list");
|
|
2302
|
+
console.log(" qmd skill show/install - Show or install the packaged QMD skill");
|
|
2077
2303
|
console.log(" qmd mcp - Start the MCP server (stdio transport for AI agents)");
|
|
2304
|
+
console.log(" qmd bench <fixture.json> - Run search quality benchmarks against a fixture file");
|
|
2078
2305
|
console.log("");
|
|
2079
2306
|
console.log("Collections & context:");
|
|
2080
2307
|
console.log(" qmd collection add/list/remove/rename/show - Manage indexed folders");
|
|
@@ -2085,6 +2312,8 @@ function showHelp() {
|
|
|
2085
2312
|
console.log(" qmd status - View index + collection health");
|
|
2086
2313
|
console.log(" qmd update [--pull] - Re-index collections (optionally git pull first)");
|
|
2087
2314
|
console.log(" qmd embed [-f] - Generate/refresh vector embeddings");
|
|
2315
|
+
console.log(" --max-docs-per-batch <n> - Cap docs loaded into memory per embedding batch");
|
|
2316
|
+
console.log(" --max-batch-mb <n> - Cap UTF-8 MB loaded into memory per embedding batch");
|
|
2088
2317
|
console.log(" qmd cleanup - Clear caches, vacuum DB");
|
|
2089
2318
|
console.log("");
|
|
2090
2319
|
console.log("Query syntax (qmd query):");
|
|
@@ -2123,11 +2352,14 @@ function showHelp() {
|
|
|
2123
2352
|
console.log("");
|
|
2124
2353
|
console.log("AI agents & integrations:");
|
|
2125
2354
|
console.log(" - Run `qmd mcp` to expose the MCP server (stdio) to agents/IDEs.");
|
|
2126
|
-
console.log(" - `qmd
|
|
2355
|
+
console.log(" - `qmd skill install` installs the QMD skill into ./.agents/skills/qmd.");
|
|
2356
|
+
console.log(" - Use `qmd skill install --global` for ~/.agents/skills/qmd.");
|
|
2357
|
+
console.log(" - `qmd --skill` is kept as an alias for `qmd skill show`.");
|
|
2127
2358
|
console.log(" - Advanced: `qmd mcp --http ...` and `qmd mcp --http --daemon` are optional for custom transports.");
|
|
2128
2359
|
console.log("");
|
|
2129
2360
|
console.log("Global options:");
|
|
2130
2361
|
console.log(" --index <name> - Use a named index (default: index)");
|
|
2362
|
+
console.log(" QMD_EDITOR_URI - Editor link template for clickable TTY search output");
|
|
2131
2363
|
console.log("");
|
|
2132
2364
|
console.log("Search options:");
|
|
2133
2365
|
console.log(" -n <num> - Max results (default 5, or 20 for --files/--json)");
|
|
@@ -2135,11 +2367,15 @@ function showHelp() {
|
|
|
2135
2367
|
console.log(" --min-score <num> - Minimum similarity score");
|
|
2136
2368
|
console.log(" --full - Output full document instead of snippet");
|
|
2137
2369
|
console.log(" -C, --candidate-limit <n> - Max candidates to rerank (default 40, lower = faster)");
|
|
2370
|
+
console.log(" --no-rerank - Skip LLM reranking (use RRF scores only, much faster on CPU)");
|
|
2138
2371
|
console.log(" --line-numbers - Include line numbers in output");
|
|
2139
2372
|
console.log(" --explain - Include retrieval score traces (query --json/CLI)");
|
|
2140
2373
|
console.log(" --files | --json | --csv | --md | --xml - Output format");
|
|
2141
2374
|
console.log(" -c, --collection <name> - Filter by one or more collections");
|
|
2142
2375
|
console.log("");
|
|
2376
|
+
console.log("Embed/query options:");
|
|
2377
|
+
console.log(" --chunk-strategy <auto|regex> - Chunking mode (default: regex; auto uses AST for code files)");
|
|
2378
|
+
console.log("");
|
|
2143
2379
|
console.log("Multi-get options:");
|
|
2144
2380
|
console.log(" -l <num> - Maximum lines per file");
|
|
2145
2381
|
console.log(" --max-bytes <num> - Skip files larger than N bytes (default 10240)");
|
|
@@ -2178,6 +2414,19 @@ if (isMain) {
|
|
|
2178
2414
|
showSkill();
|
|
2179
2415
|
process.exit(0);
|
|
2180
2416
|
}
|
|
2417
|
+
if (cli.values.help && cli.command === "skill") {
|
|
2418
|
+
console.log("Usage: qmd skill <show|install> [options]");
|
|
2419
|
+
console.log("");
|
|
2420
|
+
console.log("Commands:");
|
|
2421
|
+
console.log(" show Print the packaged QMD skill");
|
|
2422
|
+
console.log(" install Install into ./.agents/skills/qmd");
|
|
2423
|
+
console.log("");
|
|
2424
|
+
console.log("Options:");
|
|
2425
|
+
console.log(" --global Install into ~/.agents/skills/qmd");
|
|
2426
|
+
console.log(" --yes Also create the .claude/skills/qmd symlink");
|
|
2427
|
+
console.log(" -f, --force Replace existing install or symlink");
|
|
2428
|
+
process.exit(0);
|
|
2429
|
+
}
|
|
2181
2430
|
if (!cli.command || cli.values.help) {
|
|
2182
2431
|
showHelp();
|
|
2183
2432
|
process.exit(cli.values.help ? 0 : 1);
|
|
@@ -2416,7 +2665,20 @@ if (isMain) {
|
|
|
2416
2665
|
await updateCollections();
|
|
2417
2666
|
break;
|
|
2418
2667
|
case "embed":
|
|
2419
|
-
|
|
2668
|
+
try {
|
|
2669
|
+
const maxDocsPerBatch = parseEmbedBatchOption("maxDocsPerBatch", cli.values["max-docs-per-batch"]);
|
|
2670
|
+
const maxBatchMb = parseEmbedBatchOption("maxBatchBytes", cli.values["max-batch-mb"]);
|
|
2671
|
+
const embedChunkStrategy = parseChunkStrategy(cli.values["chunk-strategy"]);
|
|
2672
|
+
await vectorIndex(DEFAULT_EMBED_MODEL_URI, !!cli.values.force, {
|
|
2673
|
+
maxDocsPerBatch,
|
|
2674
|
+
maxBatchBytes: maxBatchMb === undefined ? undefined : maxBatchMb * 1024 * 1024,
|
|
2675
|
+
chunkStrategy: embedChunkStrategy,
|
|
2676
|
+
});
|
|
2677
|
+
}
|
|
2678
|
+
catch (error) {
|
|
2679
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2680
|
+
process.exit(1);
|
|
2681
|
+
}
|
|
2420
2682
|
break;
|
|
2421
2683
|
case "pull": {
|
|
2422
2684
|
const refresh = cli.values.refresh === undefined ? false : Boolean(cli.values.refresh);
|
|
@@ -2464,6 +2726,23 @@ if (isMain) {
|
|
|
2464
2726
|
}
|
|
2465
2727
|
await querySearch(cli.query, cli.opts);
|
|
2466
2728
|
break;
|
|
2729
|
+
case "bench": {
|
|
2730
|
+
const fixturePath = cli.args[0];
|
|
2731
|
+
if (!fixturePath) {
|
|
2732
|
+
console.error("Usage: qmd bench <fixture.json> [--json] [-c collection]");
|
|
2733
|
+
console.error("");
|
|
2734
|
+
console.error("Run search quality benchmarks against a fixture file.");
|
|
2735
|
+
console.error("See src/bench/fixtures/example.json for the fixture format.");
|
|
2736
|
+
process.exit(1);
|
|
2737
|
+
}
|
|
2738
|
+
const { runBenchmark } = await import("../bench/bench.js");
|
|
2739
|
+
const benchCollection = cli.opts.collection;
|
|
2740
|
+
await runBenchmark(fixturePath, {
|
|
2741
|
+
json: !!cli.opts.json,
|
|
2742
|
+
collection: Array.isArray(benchCollection) ? benchCollection[0] : benchCollection,
|
|
2743
|
+
});
|
|
2744
|
+
break;
|
|
2745
|
+
}
|
|
2467
2746
|
case "mcp": {
|
|
2468
2747
|
const sub = cli.args[0]; // stop | status | undefined
|
|
2469
2748
|
// Cache dir for PID/log files — same dir as the index
|
|
@@ -2546,6 +2825,44 @@ if (isMain) {
|
|
|
2546
2825
|
}
|
|
2547
2826
|
break;
|
|
2548
2827
|
}
|
|
2828
|
+
case "skill": {
|
|
2829
|
+
const subcommand = cli.args[0];
|
|
2830
|
+
switch (subcommand) {
|
|
2831
|
+
case "show": {
|
|
2832
|
+
showSkill();
|
|
2833
|
+
break;
|
|
2834
|
+
}
|
|
2835
|
+
case "install": {
|
|
2836
|
+
try {
|
|
2837
|
+
await installSkill(Boolean(cli.values.global), Boolean(cli.values.force), Boolean(cli.values.yes));
|
|
2838
|
+
}
|
|
2839
|
+
catch (error) {
|
|
2840
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
2841
|
+
process.exit(1);
|
|
2842
|
+
}
|
|
2843
|
+
break;
|
|
2844
|
+
}
|
|
2845
|
+
case "help":
|
|
2846
|
+
case undefined: {
|
|
2847
|
+
console.log("Usage: qmd skill <show|install> [options]");
|
|
2848
|
+
console.log("");
|
|
2849
|
+
console.log("Commands:");
|
|
2850
|
+
console.log(" show Print the packaged QMD skill");
|
|
2851
|
+
console.log(" install Install into ./.agents/skills/qmd");
|
|
2852
|
+
console.log("");
|
|
2853
|
+
console.log("Options:");
|
|
2854
|
+
console.log(" --global Install into ~/.agents/skills/qmd");
|
|
2855
|
+
console.log(" --yes Also create the .claude/skills/qmd symlink");
|
|
2856
|
+
console.log(" -f, --force Replace existing install or symlink");
|
|
2857
|
+
process.exit(0);
|
|
2858
|
+
}
|
|
2859
|
+
default:
|
|
2860
|
+
console.error(`Unknown subcommand: ${subcommand}`);
|
|
2861
|
+
console.error("Run 'qmd skill help' for usage");
|
|
2862
|
+
process.exit(1);
|
|
2863
|
+
}
|
|
2864
|
+
break;
|
|
2865
|
+
}
|
|
2549
2866
|
case "cleanup": {
|
|
2550
2867
|
const db = getDb();
|
|
2551
2868
|
// 1. Clear llm_cache
|
package/dist/collections.d.ts
CHANGED
|
@@ -21,12 +21,23 @@ export interface Collection {
|
|
|
21
21
|
update?: string;
|
|
22
22
|
includeByDefault?: boolean;
|
|
23
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Model configuration for embedding, reranking, and generation
|
|
26
|
+
*/
|
|
27
|
+
export interface ModelsConfig {
|
|
28
|
+
embed?: string;
|
|
29
|
+
rerank?: string;
|
|
30
|
+
generate?: string;
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* The complete configuration file structure
|
|
26
34
|
*/
|
|
27
35
|
export interface CollectionConfig {
|
|
28
36
|
global_context?: string;
|
|
37
|
+
editor_uri?: string;
|
|
38
|
+
editor_uri_template?: string;
|
|
29
39
|
collections: Record<string, Collection>;
|
|
40
|
+
models?: ModelsConfig;
|
|
30
41
|
}
|
|
31
42
|
/**
|
|
32
43
|
* Collection with its name (for return values)
|
package/dist/db.d.ts
CHANGED
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Provides a unified Database export that works under both Bun (bun:sqlite)
|
|
5
5
|
* and Node.js (better-sqlite3). The APIs are nearly identical — the main
|
|
6
6
|
* difference is the import path.
|
|
7
|
+
*
|
|
8
|
+
* On macOS, Apple's system SQLite is compiled with SQLITE_OMIT_LOAD_EXTENSION,
|
|
9
|
+
* which prevents loading native extensions like sqlite-vec. When running under
|
|
10
|
+
* Bun we call Database.setCustomSQLite() to swap in Homebrew's full-featured
|
|
11
|
+
* SQLite build before creating any database instances.
|
|
7
12
|
*/
|
|
8
13
|
export declare const isBun: boolean;
|
|
9
14
|
/**
|
|
@@ -29,5 +34,8 @@ export interface Statement {
|
|
|
29
34
|
}
|
|
30
35
|
/**
|
|
31
36
|
* Load the sqlite-vec extension into a database.
|
|
37
|
+
*
|
|
38
|
+
* Throws with platform-specific fix instructions when the extension is
|
|
39
|
+
* unavailable.
|
|
32
40
|
*/
|
|
33
41
|
export declare function loadSqliteVec(db: Database): void;
|