@tobilu/qmd 1.0.0 → 1.0.6
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 +287 -42
- package/README.md +3 -1
- package/dist/collections.d.ts +115 -0
- package/dist/collections.js +282 -0
- package/dist/db.d.ts +33 -0
- package/dist/db.js +34 -0
- package/dist/formatter.d.ts +119 -0
- package/dist/formatter.js +350 -0
- package/dist/llm.d.ts +375 -0
- package/dist/llm.js +1036 -0
- package/dist/mcp.d.ts +21 -0
- package/dist/mcp.js +545 -0
- package/dist/qmd.d.ts +1 -0
- package/dist/qmd.js +2231 -0
- package/dist/store.d.ts +696 -0
- package/dist/store.js +2374 -0
- package/package.json +4 -4
- package/qmd +1 -1
- package/src/bench-rerank.ts +0 -327
- package/src/collections.ts +0 -390
- package/src/db.ts +0 -52
- package/src/formatter.ts +0 -429
- package/src/llm.ts +0 -1397
- package/src/mcp.ts +0 -687
- package/src/qmd.ts +0 -2568
- package/src/store.ts +0 -3057
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collections configuration management
|
|
3
|
+
*
|
|
4
|
+
* This module manages the YAML-based collection configuration at ~/.config/qmd/index.yml.
|
|
5
|
+
* Collections define which directories to index and their associated contexts.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import YAML from "yaml";
|
|
11
|
+
// ============================================================================
|
|
12
|
+
// Configuration paths
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Current index name (default: "index")
|
|
15
|
+
let currentIndexName = "index";
|
|
16
|
+
/**
|
|
17
|
+
* Set the current index name for config file lookup
|
|
18
|
+
* Config file will be ~/.config/qmd/{indexName}.yml
|
|
19
|
+
*/
|
|
20
|
+
export function setConfigIndexName(name) {
|
|
21
|
+
currentIndexName = name;
|
|
22
|
+
}
|
|
23
|
+
function getConfigDir() {
|
|
24
|
+
// Allow override via QMD_CONFIG_DIR for testing
|
|
25
|
+
if (process.env.QMD_CONFIG_DIR) {
|
|
26
|
+
return process.env.QMD_CONFIG_DIR;
|
|
27
|
+
}
|
|
28
|
+
return join(homedir(), ".config", "qmd");
|
|
29
|
+
}
|
|
30
|
+
function getConfigFilePath() {
|
|
31
|
+
return join(getConfigDir(), `${currentIndexName}.yml`);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Ensure config directory exists
|
|
35
|
+
*/
|
|
36
|
+
function ensureConfigDir() {
|
|
37
|
+
const configDir = getConfigDir();
|
|
38
|
+
if (!existsSync(configDir)) {
|
|
39
|
+
mkdirSync(configDir, { recursive: true });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Core functions
|
|
44
|
+
// ============================================================================
|
|
45
|
+
/**
|
|
46
|
+
* Load configuration from ~/.config/qmd/index.yml
|
|
47
|
+
* Returns empty config if file doesn't exist
|
|
48
|
+
*/
|
|
49
|
+
export function loadConfig() {
|
|
50
|
+
const configPath = getConfigFilePath();
|
|
51
|
+
if (!existsSync(configPath)) {
|
|
52
|
+
return { collections: {} };
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
const content = readFileSync(configPath, "utf-8");
|
|
56
|
+
const config = YAML.parse(content);
|
|
57
|
+
// Ensure collections object exists
|
|
58
|
+
if (!config.collections) {
|
|
59
|
+
config.collections = {};
|
|
60
|
+
}
|
|
61
|
+
return config;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
throw new Error(`Failed to parse ${configPath}: ${error}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Save configuration to ~/.config/qmd/index.yml
|
|
69
|
+
*/
|
|
70
|
+
export function saveConfig(config) {
|
|
71
|
+
ensureConfigDir();
|
|
72
|
+
const configPath = getConfigFilePath();
|
|
73
|
+
try {
|
|
74
|
+
const yaml = YAML.stringify(config, {
|
|
75
|
+
indent: 2,
|
|
76
|
+
lineWidth: 0, // Don't wrap lines
|
|
77
|
+
});
|
|
78
|
+
writeFileSync(configPath, yaml, "utf-8");
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw new Error(`Failed to write ${configPath}: ${error}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get a specific collection by name
|
|
86
|
+
* Returns null if not found
|
|
87
|
+
*/
|
|
88
|
+
export function getCollection(name) {
|
|
89
|
+
const config = loadConfig();
|
|
90
|
+
const collection = config.collections[name];
|
|
91
|
+
if (!collection) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
return { name, ...collection };
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* List all collections
|
|
98
|
+
*/
|
|
99
|
+
export function listCollections() {
|
|
100
|
+
const config = loadConfig();
|
|
101
|
+
return Object.entries(config.collections).map(([name, collection]) => ({
|
|
102
|
+
name,
|
|
103
|
+
...collection,
|
|
104
|
+
}));
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Add or update a collection
|
|
108
|
+
*/
|
|
109
|
+
export function addCollection(name, path, pattern = "**/*.md") {
|
|
110
|
+
const config = loadConfig();
|
|
111
|
+
config.collections[name] = {
|
|
112
|
+
path,
|
|
113
|
+
pattern,
|
|
114
|
+
context: config.collections[name]?.context, // Preserve existing context
|
|
115
|
+
};
|
|
116
|
+
saveConfig(config);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Remove a collection
|
|
120
|
+
*/
|
|
121
|
+
export function removeCollection(name) {
|
|
122
|
+
const config = loadConfig();
|
|
123
|
+
if (!config.collections[name]) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
delete config.collections[name];
|
|
127
|
+
saveConfig(config);
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Rename a collection
|
|
132
|
+
*/
|
|
133
|
+
export function renameCollection(oldName, newName) {
|
|
134
|
+
const config = loadConfig();
|
|
135
|
+
if (!config.collections[oldName]) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
if (config.collections[newName]) {
|
|
139
|
+
throw new Error(`Collection '${newName}' already exists`);
|
|
140
|
+
}
|
|
141
|
+
config.collections[newName] = config.collections[oldName];
|
|
142
|
+
delete config.collections[oldName];
|
|
143
|
+
saveConfig(config);
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
// ============================================================================
|
|
147
|
+
// Context management
|
|
148
|
+
// ============================================================================
|
|
149
|
+
/**
|
|
150
|
+
* Get global context
|
|
151
|
+
*/
|
|
152
|
+
export function getGlobalContext() {
|
|
153
|
+
const config = loadConfig();
|
|
154
|
+
return config.global_context;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Set global context
|
|
158
|
+
*/
|
|
159
|
+
export function setGlobalContext(context) {
|
|
160
|
+
const config = loadConfig();
|
|
161
|
+
config.global_context = context;
|
|
162
|
+
saveConfig(config);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get all contexts for a collection
|
|
166
|
+
*/
|
|
167
|
+
export function getContexts(collectionName) {
|
|
168
|
+
const collection = getCollection(collectionName);
|
|
169
|
+
return collection?.context;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Add or update a context for a specific path in a collection
|
|
173
|
+
*/
|
|
174
|
+
export function addContext(collectionName, pathPrefix, contextText) {
|
|
175
|
+
const config = loadConfig();
|
|
176
|
+
const collection = config.collections[collectionName];
|
|
177
|
+
if (!collection) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
if (!collection.context) {
|
|
181
|
+
collection.context = {};
|
|
182
|
+
}
|
|
183
|
+
collection.context[pathPrefix] = contextText;
|
|
184
|
+
saveConfig(config);
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Remove a context from a collection
|
|
189
|
+
*/
|
|
190
|
+
export function removeContext(collectionName, pathPrefix) {
|
|
191
|
+
const config = loadConfig();
|
|
192
|
+
const collection = config.collections[collectionName];
|
|
193
|
+
if (!collection?.context?.[pathPrefix]) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
delete collection.context[pathPrefix];
|
|
197
|
+
// Remove empty context object
|
|
198
|
+
if (Object.keys(collection.context).length === 0) {
|
|
199
|
+
delete collection.context;
|
|
200
|
+
}
|
|
201
|
+
saveConfig(config);
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* List all contexts across all collections
|
|
206
|
+
*/
|
|
207
|
+
export function listAllContexts() {
|
|
208
|
+
const config = loadConfig();
|
|
209
|
+
const results = [];
|
|
210
|
+
// Add global context if present
|
|
211
|
+
if (config.global_context) {
|
|
212
|
+
results.push({
|
|
213
|
+
collection: "*",
|
|
214
|
+
path: "/",
|
|
215
|
+
context: config.global_context,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
// Add collection contexts
|
|
219
|
+
for (const [name, collection] of Object.entries(config.collections)) {
|
|
220
|
+
if (collection.context) {
|
|
221
|
+
for (const [path, context] of Object.entries(collection.context)) {
|
|
222
|
+
results.push({
|
|
223
|
+
collection: name,
|
|
224
|
+
path,
|
|
225
|
+
context,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return results;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Find best matching context for a given collection and path
|
|
234
|
+
* Returns the most specific matching context (longest path prefix match)
|
|
235
|
+
*/
|
|
236
|
+
export function findContextForPath(collectionName, filePath) {
|
|
237
|
+
const config = loadConfig();
|
|
238
|
+
const collection = config.collections[collectionName];
|
|
239
|
+
if (!collection?.context) {
|
|
240
|
+
return config.global_context;
|
|
241
|
+
}
|
|
242
|
+
// Find all matching prefixes
|
|
243
|
+
const matches = [];
|
|
244
|
+
for (const [prefix, context] of Object.entries(collection.context)) {
|
|
245
|
+
// Normalize paths for comparison
|
|
246
|
+
const normalizedPath = filePath.startsWith("/") ? filePath : `/${filePath}`;
|
|
247
|
+
const normalizedPrefix = prefix.startsWith("/") ? prefix : `/${prefix}`;
|
|
248
|
+
if (normalizedPath.startsWith(normalizedPrefix)) {
|
|
249
|
+
matches.push({ prefix: normalizedPrefix, context });
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
// Return most specific match (longest prefix)
|
|
253
|
+
if (matches.length > 0) {
|
|
254
|
+
matches.sort((a, b) => b.prefix.length - a.prefix.length);
|
|
255
|
+
return matches[0].context;
|
|
256
|
+
}
|
|
257
|
+
// Fallback to global context
|
|
258
|
+
return config.global_context;
|
|
259
|
+
}
|
|
260
|
+
// ============================================================================
|
|
261
|
+
// Utility functions
|
|
262
|
+
// ============================================================================
|
|
263
|
+
/**
|
|
264
|
+
* Get the config file path (useful for error messages)
|
|
265
|
+
*/
|
|
266
|
+
export function getConfigPath() {
|
|
267
|
+
return getConfigFilePath();
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Check if config file exists
|
|
271
|
+
*/
|
|
272
|
+
export function configExists() {
|
|
273
|
+
return existsSync(getConfigFilePath());
|
|
274
|
+
}
|
|
275
|
+
/**
|
|
276
|
+
* Validate a collection name
|
|
277
|
+
* Collection names must be valid and not contain special characters
|
|
278
|
+
*/
|
|
279
|
+
export function isValidCollectionName(name) {
|
|
280
|
+
// Allow alphanumeric, hyphens, underscores
|
|
281
|
+
return /^[a-zA-Z0-9_-]+$/.test(name);
|
|
282
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts - Cross-runtime SQLite compatibility layer
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified Database export that works under both Bun (bun:sqlite)
|
|
5
|
+
* and Node.js (better-sqlite3). The APIs are nearly identical — the main
|
|
6
|
+
* difference is the import path.
|
|
7
|
+
*/
|
|
8
|
+
export declare const isBun: boolean;
|
|
9
|
+
/**
|
|
10
|
+
* Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
|
|
11
|
+
*/
|
|
12
|
+
export declare function openDatabase(path: string): Database;
|
|
13
|
+
/**
|
|
14
|
+
* Common subset of the Database interface used throughout QMD.
|
|
15
|
+
*/
|
|
16
|
+
export interface Database {
|
|
17
|
+
exec(sql: string): void;
|
|
18
|
+
prepare(sql: string): Statement;
|
|
19
|
+
loadExtension(path: string): void;
|
|
20
|
+
close(): void;
|
|
21
|
+
}
|
|
22
|
+
export interface Statement {
|
|
23
|
+
run(...params: any[]): {
|
|
24
|
+
changes: number;
|
|
25
|
+
lastInsertRowid: number | bigint;
|
|
26
|
+
};
|
|
27
|
+
get(...params: any[]): any;
|
|
28
|
+
all(...params: any[]): any[];
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Load the sqlite-vec extension into a database.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadSqliteVec(db: Database): void;
|
package/dist/db.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* db.ts - Cross-runtime SQLite compatibility layer
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified Database export that works under both Bun (bun:sqlite)
|
|
5
|
+
* and Node.js (better-sqlite3). The APIs are nearly identical — the main
|
|
6
|
+
* difference is the import path.
|
|
7
|
+
*/
|
|
8
|
+
export const isBun = typeof globalThis.Bun !== "undefined";
|
|
9
|
+
let _Database;
|
|
10
|
+
let _sqliteVecLoad;
|
|
11
|
+
if (isBun) {
|
|
12
|
+
// Dynamic string prevents tsc from resolving bun:sqlite on Node.js builds
|
|
13
|
+
const bunSqlite = "bun:" + "sqlite";
|
|
14
|
+
_Database = (await import(/* @vite-ignore */ bunSqlite)).Database;
|
|
15
|
+
const { getLoadablePath } = await import("sqlite-vec");
|
|
16
|
+
_sqliteVecLoad = (db) => db.loadExtension(getLoadablePath());
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
_Database = (await import("better-sqlite3")).default;
|
|
20
|
+
const sqliteVec = await import("sqlite-vec");
|
|
21
|
+
_sqliteVecLoad = (db) => sqliteVec.load(db);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Open a SQLite database. Works with both bun:sqlite and better-sqlite3.
|
|
25
|
+
*/
|
|
26
|
+
export function openDatabase(path) {
|
|
27
|
+
return new _Database(path);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Load the sqlite-vec extension into a database.
|
|
31
|
+
*/
|
|
32
|
+
export function loadSqliteVec(db) {
|
|
33
|
+
_sqliteVecLoad(db);
|
|
34
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* formatter.ts - Output formatting utilities for QMD
|
|
3
|
+
*
|
|
4
|
+
* Provides methods to format search results and documents into various output formats:
|
|
5
|
+
* JSON, CSV, XML, Markdown, files list, and CLI (colored terminal output).
|
|
6
|
+
*/
|
|
7
|
+
import type { SearchResult, MultiGetResult, DocumentResult } from "./store.js";
|
|
8
|
+
export type { SearchResult, MultiGetResult, DocumentResult };
|
|
9
|
+
export type MultiGetFile = {
|
|
10
|
+
filepath: string;
|
|
11
|
+
displayPath: string;
|
|
12
|
+
title: string;
|
|
13
|
+
body: string;
|
|
14
|
+
context?: string | null;
|
|
15
|
+
skipped: false;
|
|
16
|
+
} | {
|
|
17
|
+
filepath: string;
|
|
18
|
+
displayPath: string;
|
|
19
|
+
title: string;
|
|
20
|
+
body: string;
|
|
21
|
+
context?: string | null;
|
|
22
|
+
skipped: true;
|
|
23
|
+
skipReason: string;
|
|
24
|
+
};
|
|
25
|
+
export type OutputFormat = "cli" | "csv" | "md" | "xml" | "files" | "json";
|
|
26
|
+
export type FormatOptions = {
|
|
27
|
+
full?: boolean;
|
|
28
|
+
query?: string;
|
|
29
|
+
useColor?: boolean;
|
|
30
|
+
lineNumbers?: boolean;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Add line numbers to text content.
|
|
34
|
+
* Each line becomes: "{lineNum}: {content}"
|
|
35
|
+
* @param text The text to add line numbers to
|
|
36
|
+
* @param startLine Optional starting line number (default: 1)
|
|
37
|
+
*/
|
|
38
|
+
export declare function addLineNumbers(text: string, startLine?: number): string;
|
|
39
|
+
/**
|
|
40
|
+
* Extract short docid from a full hash (first 6 characters).
|
|
41
|
+
*/
|
|
42
|
+
export declare function getDocid(hash: string): string;
|
|
43
|
+
export declare function escapeCSV(value: string | null | number): string;
|
|
44
|
+
export declare function escapeXml(str: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Format search results as JSON
|
|
47
|
+
*/
|
|
48
|
+
export declare function searchResultsToJson(results: SearchResult[], opts?: FormatOptions): string;
|
|
49
|
+
/**
|
|
50
|
+
* Format search results as CSV
|
|
51
|
+
*/
|
|
52
|
+
export declare function searchResultsToCsv(results: SearchResult[], opts?: FormatOptions): string;
|
|
53
|
+
/**
|
|
54
|
+
* Format search results as simple files list (docid,score,filepath,context)
|
|
55
|
+
*/
|
|
56
|
+
export declare function searchResultsToFiles(results: SearchResult[]): string;
|
|
57
|
+
/**
|
|
58
|
+
* Format search results as Markdown
|
|
59
|
+
*/
|
|
60
|
+
export declare function searchResultsToMarkdown(results: SearchResult[], opts?: FormatOptions): string;
|
|
61
|
+
/**
|
|
62
|
+
* Format search results as XML
|
|
63
|
+
*/
|
|
64
|
+
export declare function searchResultsToXml(results: SearchResult[], opts?: FormatOptions): string;
|
|
65
|
+
/**
|
|
66
|
+
* Format search results for MCP (simpler CSV format with pre-extracted snippets)
|
|
67
|
+
*/
|
|
68
|
+
export declare function searchResultsToMcpCsv(results: {
|
|
69
|
+
docid: string;
|
|
70
|
+
file: string;
|
|
71
|
+
title: string;
|
|
72
|
+
score: number;
|
|
73
|
+
context: string | null;
|
|
74
|
+
snippet: string;
|
|
75
|
+
}[]): string;
|
|
76
|
+
/**
|
|
77
|
+
* Format documents as JSON
|
|
78
|
+
*/
|
|
79
|
+
export declare function documentsToJson(results: MultiGetFile[]): string;
|
|
80
|
+
/**
|
|
81
|
+
* Format documents as CSV
|
|
82
|
+
*/
|
|
83
|
+
export declare function documentsToCsv(results: MultiGetFile[]): string;
|
|
84
|
+
/**
|
|
85
|
+
* Format documents as files list
|
|
86
|
+
*/
|
|
87
|
+
export declare function documentsToFiles(results: MultiGetFile[]): string;
|
|
88
|
+
/**
|
|
89
|
+
* Format documents as Markdown
|
|
90
|
+
*/
|
|
91
|
+
export declare function documentsToMarkdown(results: MultiGetFile[]): string;
|
|
92
|
+
/**
|
|
93
|
+
* Format documents as XML
|
|
94
|
+
*/
|
|
95
|
+
export declare function documentsToXml(results: MultiGetFile[]): string;
|
|
96
|
+
/**
|
|
97
|
+
* Format a single DocumentResult as JSON
|
|
98
|
+
*/
|
|
99
|
+
export declare function documentToJson(doc: DocumentResult): string;
|
|
100
|
+
/**
|
|
101
|
+
* Format a single DocumentResult as Markdown
|
|
102
|
+
*/
|
|
103
|
+
export declare function documentToMarkdown(doc: DocumentResult): string;
|
|
104
|
+
/**
|
|
105
|
+
* Format a single DocumentResult as XML
|
|
106
|
+
*/
|
|
107
|
+
export declare function documentToXml(doc: DocumentResult): string;
|
|
108
|
+
/**
|
|
109
|
+
* Format a single document to the specified format
|
|
110
|
+
*/
|
|
111
|
+
export declare function formatDocument(doc: DocumentResult, format: OutputFormat): string;
|
|
112
|
+
/**
|
|
113
|
+
* Format search results to the specified output format
|
|
114
|
+
*/
|
|
115
|
+
export declare function formatSearchResults(results: SearchResult[], format: OutputFormat, opts?: FormatOptions): string;
|
|
116
|
+
/**
|
|
117
|
+
* Format documents to the specified output format
|
|
118
|
+
*/
|
|
119
|
+
export declare function formatDocuments(results: MultiGetFile[], format: OutputFormat): string;
|