@naveenadi/mnemonic 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 (80) hide show
  1. package/README.md +93 -0
  2. package/SKILL.md +158 -0
  3. package/bin/mne +8 -0
  4. package/dist/chunker/index.d.ts +8 -0
  5. package/dist/chunker/index.d.ts.map +1 -0
  6. package/dist/chunker/index.js +157 -0
  7. package/dist/chunker/index.js.map +1 -0
  8. package/dist/cli/commands.d.ts +2 -0
  9. package/dist/cli/commands.d.ts.map +1 -0
  10. package/dist/cli/commands.js +793 -0
  11. package/dist/cli/commands.js.map +1 -0
  12. package/dist/cli/index.d.ts +3 -0
  13. package/dist/cli/index.d.ts.map +1 -0
  14. package/dist/cli/index.js +7 -0
  15. package/dist/cli/index.js.map +1 -0
  16. package/dist/cli/mne.d.ts +3 -0
  17. package/dist/cli/mne.d.ts.map +1 -0
  18. package/dist/cli/mne.js +7 -0
  19. package/dist/cli/mne.js.map +1 -0
  20. package/dist/index.d.ts +13 -0
  21. package/dist/index.d.ts.map +1 -0
  22. package/dist/index.js +15 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/llm/factory.d.ts +15 -0
  25. package/dist/llm/factory.d.ts.map +1 -0
  26. package/dist/llm/factory.js +60 -0
  27. package/dist/llm/factory.js.map +1 -0
  28. package/dist/llm/index.d.ts +19 -0
  29. package/dist/llm/index.d.ts.map +1 -0
  30. package/dist/llm/index.js +18 -0
  31. package/dist/llm/index.js.map +1 -0
  32. package/dist/llm/node-llama-cpp.d.ts +30 -0
  33. package/dist/llm/node-llama-cpp.d.ts.map +1 -0
  34. package/dist/llm/node-llama-cpp.js +143 -0
  35. package/dist/llm/node-llama-cpp.js.map +1 -0
  36. package/dist/llm/ollama.d.ts +19 -0
  37. package/dist/llm/ollama.d.ts.map +1 -0
  38. package/dist/llm/ollama.js +99 -0
  39. package/dist/llm/ollama.js.map +1 -0
  40. package/dist/mcp/server.d.ts +2 -0
  41. package/dist/mcp/server.d.ts.map +1 -0
  42. package/dist/mcp/server.js +360 -0
  43. package/dist/mcp/server.js.map +1 -0
  44. package/dist/pi-extension/index.d.ts +3 -0
  45. package/dist/pi-extension/index.d.ts.map +1 -0
  46. package/dist/pi-extension/index.js +193 -0
  47. package/dist/pi-extension/index.js.map +1 -0
  48. package/dist/search/fts.d.ts +17 -0
  49. package/dist/search/fts.d.ts.map +1 -0
  50. package/dist/search/fts.js +110 -0
  51. package/dist/search/fts.js.map +1 -0
  52. package/dist/search/fusion.d.ts +33 -0
  53. package/dist/search/fusion.d.ts.map +1 -0
  54. package/dist/search/fusion.js +97 -0
  55. package/dist/search/fusion.js.map +1 -0
  56. package/dist/search/pipeline.d.ts +34 -0
  57. package/dist/search/pipeline.d.ts.map +1 -0
  58. package/dist/search/pipeline.js +216 -0
  59. package/dist/search/pipeline.js.map +1 -0
  60. package/dist/search/vector.d.ts +20 -0
  61. package/dist/search/vector.d.ts.map +1 -0
  62. package/dist/search/vector.js +70 -0
  63. package/dist/search/vector.js.map +1 -0
  64. package/dist/store/collections.d.ts +23 -0
  65. package/dist/store/collections.d.ts.map +1 -0
  66. package/dist/store/collections.js +99 -0
  67. package/dist/store/collections.js.map +1 -0
  68. package/dist/store/database.d.ts +22 -0
  69. package/dist/store/database.d.ts.map +1 -0
  70. package/dist/store/database.js +209 -0
  71. package/dist/store/database.js.map +1 -0
  72. package/dist/store/documents.d.ts +68 -0
  73. package/dist/store/documents.d.ts.map +1 -0
  74. package/dist/store/documents.js +273 -0
  75. package/dist/store/documents.js.map +1 -0
  76. package/dist/types.d.ts +193 -0
  77. package/dist/types.d.ts.map +1 -0
  78. package/dist/types.js +3 -0
  79. package/dist/types.js.map +1 -0
  80. package/package.json +58 -0
@@ -0,0 +1,793 @@
1
+ import { existsSync, readFileSync, mkdirSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { resolve, join, relative } from 'node:path';
4
+ import { MnemonicDB } from '../store/database.js';
5
+ import { DocumentStore } from '../store/documents.js';
6
+ import { CollectionStore } from '../store/collections.js';
7
+ import { SearchPipeline } from '../search/pipeline.js';
8
+ import { chunkMarkdown } from '../chunker/index.js';
9
+ import { detectLLMBackend, checkOllama } from '../llm/factory.js';
10
+ const DEFAULT_DB = join(homedir(), '.cache', 'mnemonic', 'index.sqlite');
11
+ const DEFAULT_CONFIG = join(homedir(), '.config', 'mnemonic', 'config.yml');
12
+ export async function main(args) {
13
+ const [command, ...rest] = args;
14
+ if (!command || command === 'help' || command === '--help') {
15
+ printHelp();
16
+ return;
17
+ }
18
+ const verbose = args.includes('--verbose') || args.includes('-v');
19
+ const dbPath = resolve(getArg(args, '--db') ?? DEFAULT_DB);
20
+ try {
21
+ switch (command) {
22
+ case 'init':
23
+ return cmdInit(rest, dbPath);
24
+ case 'collection':
25
+ return cmdCollection(rest, dbPath, verbose);
26
+ case 'add':
27
+ return cmdAdd(rest, dbPath, verbose);
28
+ case 'index':
29
+ case 'update':
30
+ return cmdIndex(rest, dbPath, verbose);
31
+ case 'embed':
32
+ return cmdEmbed(rest, dbPath, verbose);
33
+ case 'search':
34
+ return cmdSearch(rest, dbPath, verbose);
35
+ case 'vsearch':
36
+ return cmdVectorSearch(rest, dbPath, verbose);
37
+ case 'query':
38
+ return cmdQuery(rest, dbPath, verbose);
39
+ case 'get':
40
+ return cmdGet(rest, dbPath);
41
+ case 'multi-get':
42
+ return cmdMultiGet(rest, dbPath);
43
+ case 'ls':
44
+ return cmdLs(rest, dbPath);
45
+ case 'status':
46
+ return cmdStatus(dbPath);
47
+ case 'doctor':
48
+ return cmdDoctor();
49
+ case 'context':
50
+ return cmdContext(rest, dbPath);
51
+ case 'tag':
52
+ return cmdTag(rest, dbPath);
53
+ case 'links':
54
+ return cmdLinks(rest, dbPath);
55
+ case 'backlinks':
56
+ return cmdBacklinks(rest, dbPath);
57
+ case 'orphans':
58
+ return cmdOrphans(dbPath);
59
+ case 'mcp':
60
+ return cmdMcp(rest, dbPath);
61
+ default:
62
+ console.error(`Unknown command: ${command}`);
63
+ printHelp();
64
+ process.exit(1);
65
+ }
66
+ }
67
+ finally {
68
+ // LLM cleanup happens in process exit
69
+ }
70
+ }
71
+ function printHelp() {
72
+ console.log(`
73
+ mnemonic — on-device hybrid search for markdown knowledge bases
74
+
75
+ Usage: mne <command> [options]
76
+
77
+ Commands:
78
+ init Initialize a new index
79
+ collection add <dir> Add a collection
80
+ collection list List collections
81
+ collection remove <n> Remove a collection
82
+ add <dir> Quick-add a collection from directory
83
+ index Index all collections
84
+ embed Generate vector embeddings
85
+ search <query> BM25 full-text search
86
+ vsearch <query> Vector semantic search
87
+ query <query> Hybrid search (BM25 + vector + reranking)
88
+ get <path|#docid> Retrieve a document
89
+ multi-get <pattern> Batch retrieve documents
90
+ ls [collection] List files in a collection
91
+ status Show index status
92
+ doctor Diagnostic checks
93
+ context add <path> <txt> Add context metadata
94
+ context list List contexts
95
+ tag <#docid> <tag> Add a tag
96
+ links <#docid> Show outgoing links
97
+ backlinks <#docid> Show incoming links
98
+ orphans Find orphan documents
99
+ mcp Start MCP server
100
+
101
+ Options:
102
+ --db <path> Database path (default: ~/.cache/mnemonic/index.sqlite)
103
+ -c, --collection <name> Filter by collection
104
+ -n <num> Number of results (default: 10)
105
+ --full Show full document content
106
+ --json JSON output
107
+ --no-rerank Skip LLM reranking
108
+ -v, --verbose Verbose output
109
+ `);
110
+ }
111
+ // ─── Command Implementations ────────────────────────────────────────
112
+ function getArg(args, name) {
113
+ const idx = args.indexOf(name);
114
+ if (idx >= 0 && idx < args.length - 1)
115
+ return args[idx + 1];
116
+ return undefined;
117
+ }
118
+ function hasArg(args, ...names) {
119
+ return names.some((n) => args.includes(n));
120
+ }
121
+ function createContext(dbPath, verbose) {
122
+ const db = new MnemonicDB(dbPath);
123
+ db.init();
124
+ const docs = new DocumentStore(db);
125
+ const collections = new CollectionStore(db);
126
+ const pipeline = new SearchPipeline(db);
127
+ return { db, docs, collections, pipeline, verbose };
128
+ }
129
+ async function getLLM(verbose) {
130
+ try {
131
+ const { backend } = await detectLLMBackend();
132
+ if (verbose)
133
+ console.error('Using LLM backend');
134
+ return backend;
135
+ }
136
+ catch {
137
+ if (verbose)
138
+ console.error('No LLM backend available');
139
+ return undefined;
140
+ }
141
+ }
142
+ function cmdInit(_args, dbPath) {
143
+ const dir = resolve(homedir(), '.cache', 'mnemonic');
144
+ mkdirSync(dir, { recursive: true });
145
+ const db = new MnemonicDB(dbPath);
146
+ db.init();
147
+ console.log(`Initialized index at: ${dbPath}`);
148
+ db.close();
149
+ }
150
+ async function cmdCollection(args, dbPath, verbose) {
151
+ const ctx = createContext(dbPath, verbose);
152
+ const sub = args[0];
153
+ switch (sub) {
154
+ case 'add': {
155
+ const path = args[1];
156
+ const name = getArg(args, '--name') ?? args[2];
157
+ if (!path || !name) {
158
+ console.error('Usage: mne collection add <path> --name <name>');
159
+ process.exit(1);
160
+ }
161
+ ctx.collections.upsert(name, {
162
+ path: resolve(path),
163
+ name,
164
+ pattern: getArg(args, '--mask') ?? '**/*.md',
165
+ includeByDefault: !hasArg(args, '--excluded'),
166
+ });
167
+ console.log(`Added collection: ${name}`);
168
+ break;
169
+ }
170
+ case 'list': {
171
+ const cols = ctx.collections.list();
172
+ if (cols.length === 0) {
173
+ console.log('No collections. Use "mne collection add <path> --name <name>"');
174
+ return;
175
+ }
176
+ for (const c of cols) {
177
+ console.log(`${c.name} (${c.docCount} docs, ${c.activeCount} embedded) ${c.includeByDefault ? '[default]' : '[excluded]'}`);
178
+ console.log(` path: ${c.path}`);
179
+ console.log(` pattern: ${c.globPattern}`);
180
+ }
181
+ break;
182
+ }
183
+ case 'remove': {
184
+ const name = args[1];
185
+ if (!name) {
186
+ console.error('Usage: mne collection remove <name>');
187
+ process.exit(1);
188
+ }
189
+ ctx.collections.remove(name);
190
+ console.log(`Removed collection: ${name}`);
191
+ break;
192
+ }
193
+ case 'show': {
194
+ const name = args[1];
195
+ if (!name) {
196
+ console.error('Usage: mne collection show <name>');
197
+ process.exit(1);
198
+ }
199
+ const c = ctx.collections.get(name);
200
+ if (!c) {
201
+ console.error(`Collection not found: ${name}`);
202
+ process.exit(1);
203
+ }
204
+ console.log(`Name: ${c.name}`);
205
+ console.log(`Path: ${c.path}`);
206
+ console.log(`Pattern: ${c.globPattern}`);
207
+ console.log(`Docs: ${c.docCount}`);
208
+ console.log(`Embedded: ${c.activeCount}`);
209
+ console.log(`Included: ${c.includeByDefault ? 'yes' : 'no'}`);
210
+ console.log(`Contexts: ${c.contextCount}`);
211
+ break;
212
+ }
213
+ case 'rename': {
214
+ const [oldName, newName] = args.slice(1);
215
+ if (!oldName || !newName) {
216
+ console.error('Usage: mne collection rename <old> <new>');
217
+ process.exit(1);
218
+ }
219
+ ctx.collections.rename(oldName, newName);
220
+ console.log(`Renamed: ${oldName} → ${newName}`);
221
+ break;
222
+ }
223
+ case 'include': {
224
+ const name = args[1];
225
+ if (!name) {
226
+ console.error('Usage: mne collection include <name>');
227
+ process.exit(1);
228
+ }
229
+ ctx.collections.setInclude(name, true);
230
+ console.log(`Collection ${name} is now included by default`);
231
+ break;
232
+ }
233
+ case 'exclude': {
234
+ const name = args[1];
235
+ if (!name) {
236
+ console.error('Usage: mne collection exclude <name>');
237
+ process.exit(1);
238
+ }
239
+ ctx.collections.setInclude(name, false);
240
+ console.log(`Collection ${name} is now excluded by default`);
241
+ break;
242
+ }
243
+ default:
244
+ console.error('Usage: mne collection <add|list|remove|show|rename|include|exclude> ...');
245
+ }
246
+ ctx.db.close();
247
+ }
248
+ async function cmdAdd(args, dbPath, verbose) {
249
+ const ctx = createContext(dbPath, verbose);
250
+ const path = args[0];
251
+ const name = getArg(args, '--name') ?? (path ? relative(process.cwd(), path) || 'default' : 'default');
252
+ if (!path) {
253
+ console.error('Usage: mne add <path> [--name <name>]');
254
+ process.exit(1);
255
+ }
256
+ ctx.collections.upsert(name, {
257
+ path: resolve(path),
258
+ name,
259
+ pattern: getArg(args, '--mask') ?? '**/*.md',
260
+ });
261
+ console.log(`Added collection: ${name} → ${resolve(path)}`);
262
+ ctx.db.close();
263
+ }
264
+ async function cmdIndex(args, dbPath, verbose) {
265
+ const ctx = createContext(dbPath, verbose);
266
+ const fastglob = (await import('fast-glob')).default;
267
+ const colFilter = getArg(args, '--collection') ?? getArg(args, '-c');
268
+ const collections = colFilter
269
+ ? ctx.collections.list().filter((c) => c.name === colFilter)
270
+ : ctx.collections.list();
271
+ if (collections.length === 0) {
272
+ console.error('No collections to index. Use "mne collection add" first.');
273
+ process.exit(1);
274
+ }
275
+ for (const col of collections) {
276
+ console.log(`Indexing ${col.name}...`);
277
+ const storedIgnore = ctx.db.db.prepare('SELECT ignore_pattern FROM collections WHERE name = ?').get(col.name);
278
+ const collectionIgnores = storedIgnore?.ignore_pattern ? storedIgnore.ignore_pattern.split('\n').filter(Boolean) : [];
279
+ const ignorePatterns = ['.git/**', 'node_modules/**', '**/node_modules/**', ...collectionIgnores];
280
+ const files = await fastglob(col.globPattern || '**/*.md', {
281
+ cwd: col.path,
282
+ absolute: true,
283
+ ignore: ignorePatterns,
284
+ });
285
+ let indexed = 0, updated = 0, unchanged = 0;
286
+ for (const file of files) {
287
+ try {
288
+ const content = readFileSync(file, 'utf-8');
289
+ const relPath = relative(col.path, file);
290
+ const result = ctx.docs.upsert(col.name, relPath, file, content);
291
+ if (result.isNew)
292
+ indexed++;
293
+ else if (result.needsEmbed)
294
+ updated++;
295
+ else
296
+ unchanged++;
297
+ if (verbose && (indexed + updated) % 50 === 0) {
298
+ console.error(` ${indexed + updated} files processed...`);
299
+ }
300
+ }
301
+ catch (err) {
302
+ if (verbose)
303
+ console.error(` Error indexing ${file}:`, err.message);
304
+ }
305
+ }
306
+ console.log(` ${indexed} new, ${updated} updated, ${unchanged} unchanged`);
307
+ }
308
+ ctx.db.close();
309
+ }
310
+ async function cmdEmbed(args, dbPath, verbose) {
311
+ const llm = await getLLM(verbose);
312
+ if (!llm) {
313
+ console.error('No LLM backend available. Install node-llama-cpp or start Ollama.');
314
+ process.exit(1);
315
+ }
316
+ const ctx = createContext(dbPath, verbose);
317
+ await ctx.db.loadVectors();
318
+ const dim = llm.embeddingDim();
319
+ ctx.db.createVectorTable(dim);
320
+ const colFilter = getArg(args, '--collection') ?? getArg(args, '-c');
321
+ const force = hasArg(args, '--force', '-f');
322
+ const docs = colFilter
323
+ ? ctx.db.db.prepare('SELECT docid, collection, path, content, checksum, title FROM documents WHERE collection = ?').all(colFilter)
324
+ : ctx.db.db.prepare('SELECT docid, collection, path, content, checksum, title FROM documents').all();
325
+ let embedded = 0, skipped = 0, failed = 0;
326
+ for (let i = 0; i < docs.length; i++) {
327
+ const doc = docs[i];
328
+ // Check if already embedded
329
+ if (!force) {
330
+ const existing = ctx.db.db.prepare('SELECT 1 FROM vectors WHERE hash = ?').get(doc.docid);
331
+ if (existing) {
332
+ skipped++;
333
+ continue;
334
+ }
335
+ }
336
+ const chunks = chunkMarkdown(doc.content, doc.docid);
337
+ const texts = chunks.map((c) => `${doc.title} | ${c.content}`);
338
+ try {
339
+ const embeddings = await llm.embed(texts);
340
+ ctx.db.db.transaction(() => {
341
+ // Store chunks
342
+ for (const chunk of chunks) {
343
+ ctx.db.db.prepare('INSERT OR REPLACE INTO chunks (docid, seq, pos, content, heading, checksum) VALUES (?, ?, ?, ?, ?, ?)').run(chunk.hash, chunk.seq, chunk.pos, chunk.content, chunk.heading, doc.checksum);
344
+ }
345
+ })();
346
+ // Store vectors
347
+ const vectors = embeddings.map((emb, idx) => ({
348
+ hash: chunks[idx].hash,
349
+ embedding: emb,
350
+ }));
351
+ const vs = new (await import('../search/vector.js')).VectorSearch(ctx.db);
352
+ vs.storeVectors(vectors);
353
+ embedded++;
354
+ }
355
+ catch (err) {
356
+ failed++;
357
+ if (verbose)
358
+ console.error(` Error embedding ${doc.docid}:`, err.message);
359
+ }
360
+ if (verbose && (i + 1) % 10 === 0) {
361
+ console.error(` Embedding ${i + 1}/${docs.length}...`);
362
+ }
363
+ }
364
+ console.log(`Embedded: ${embedded}, skipped: ${skipped}, failed: ${failed}`);
365
+ await llm.close();
366
+ ctx.db.close();
367
+ }
368
+ async function cmdSearch(args, dbPath, verbose) {
369
+ const ctx = createContext(dbPath, verbose);
370
+ const query = args.join(' ').replace(/ --\S+/g, '').trim();
371
+ const collection = getArg(args, '-c') ?? getArg(args, '--collection');
372
+ const limit = parseInt(getArg(args, '-n') ?? '10', 10);
373
+ const jsonOut = hasArg(args, '--json');
374
+ if (!query) {
375
+ console.error('Usage: mne search <query> [-n <num>] [-c <collection>] [--json]');
376
+ process.exit(1);
377
+ }
378
+ const results = ctx.pipeline.searchLex(query, {
379
+ collection: collection ? [collection] : undefined,
380
+ limit,
381
+ });
382
+ printResults(results, { json: jsonOut });
383
+ ctx.db.close();
384
+ }
385
+ async function cmdVectorSearch(args, dbPath, verbose) {
386
+ const llm = await getLLM(verbose);
387
+ if (!llm) {
388
+ console.error('No LLM backend available.');
389
+ process.exit(1);
390
+ }
391
+ const ctx = createContext(dbPath, verbose);
392
+ const query = args.join(' ').replace(/ --\S+/g, '').trim();
393
+ const collection = getArg(args, '-c') ?? getArg(args, '--collection');
394
+ const limit = parseInt(getArg(args, '-n') ?? '10', 10);
395
+ const jsonOut = hasArg(args, '--json');
396
+ if (!query) {
397
+ console.error('Usage: mne vsearch <query> [-n <num>] [-c <collection>] [--json]');
398
+ process.exit(1);
399
+ }
400
+ await ctx.db.loadVectors();
401
+ ctx.pipeline.setLLM(llm);
402
+ const results = await ctx.pipeline.searchVector(query, {
403
+ collection: collection ? [collection] : undefined,
404
+ limit,
405
+ });
406
+ printResults(results, { json: jsonOut });
407
+ await llm.close();
408
+ ctx.db.close();
409
+ }
410
+ async function cmdQuery(args, dbPath, verbose) {
411
+ const llm = await getLLM(verbose);
412
+ const ctx = createContext(dbPath, verbose);
413
+ // Parse structured query fields: intent:, lex:, vec:, hyde:
414
+ let query = args.join(' ');
415
+ const collection = getArg(args, '-c') ?? getArg(args, '--collection');
416
+ const limit = parseInt(getArg(args, '-n') ?? '10', 10);
417
+ const jsonOut = hasArg(args, '--json');
418
+ const noRerank = hasArg(args, '--no-rerank');
419
+ const explain = hasArg(args, '--explain');
420
+ let intent;
421
+ let parsedQueries;
422
+ // Parse structured fields if present
423
+ const intentMatch = query.match(/intent:\s*((?:.|\n)*?)(?=\n\w+:|$)/);
424
+ const lexMatch = query.match(/\blex:\s*(.+?)(?=\n\s*(?:vec|hyde|intent):|$)/);
425
+ const vecMatch = query.match(/\bvec:\s*(.+?)(?=\n\s*(?:lex|hyde|intent):|$)/);
426
+ const hydeMatch = query.match(/\bhyde:\s*(.+?)(?=\n\s*(?:lex|vec|intent):|$)/);
427
+ if (lexMatch || vecMatch || hydeMatch) {
428
+ intent = intentMatch?.[1]?.trim();
429
+ parsedQueries = [];
430
+ if (lexMatch)
431
+ parsedQueries.push({ type: 'lex', query: lexMatch[1].trim() });
432
+ if (vecMatch)
433
+ parsedQueries.push({ type: 'vec', query: vecMatch[1].trim() });
434
+ if (hydeMatch)
435
+ parsedQueries.push({ type: 'hyde', query: hydeMatch[1].trim() });
436
+ query = query.replace(/^(?:intent|lex|vec|hyde):.*$/gm, '').trim();
437
+ }
438
+ if (!query && !parsedQueries) {
439
+ console.error('Usage: mne query <query> [-n <num>] [-c <collection>] [--json] [--no-rerank]');
440
+ console.error(" Or: mne query with structured fields: intent: ..., lex: ..., vec: ..., hyde: ...");
441
+ process.exit(1);
442
+ }
443
+ if (llm)
444
+ ctx.pipeline.setLLM(llm);
445
+ await ctx.db.loadVectors();
446
+ const results = await ctx.pipeline.search({
447
+ query: parsedQueries ? undefined : query,
448
+ queries: parsedQueries,
449
+ intent,
450
+ collection: collection ? [collection] : undefined,
451
+ limit,
452
+ rerank: !noRerank && !!llm,
453
+ expand: true,
454
+ hyde: true,
455
+ explain,
456
+ });
457
+ printResults(results, { json: jsonOut, explain });
458
+ if (llm)
459
+ await llm.close();
460
+ ctx.db.close();
461
+ }
462
+ function cmdGet(args, dbPath) {
463
+ const ctx = createContext(dbPath, false);
464
+ const identifier = args[0];
465
+ const jsonOut = hasArg(args, '--json');
466
+ const noLineNumbers = hasArg(args, '--no-line-numbers');
467
+ const fullPath = hasArg(args, '--full-path');
468
+ if (!identifier) {
469
+ console.error('Usage: mne get <path|#docid> [--no-line-numbers] [--full-path] [--json]');
470
+ console.error(' Supports :from:count suffix: mne get #abc123:50:40');
471
+ process.exit(1);
472
+ }
473
+ // Parse :from:count suffix
474
+ let fromLine;
475
+ let maxLines;
476
+ const suffixMatch = identifier.match(/^(.+?):(\d+)(?::(\d+))?$/);
477
+ let cleanId = identifier;
478
+ if (suffixMatch) {
479
+ cleanId = suffixMatch[1];
480
+ fromLine = parseInt(suffixMatch[2], 10);
481
+ maxLines = suffixMatch[3] ? parseInt(suffixMatch[3], 10) : undefined;
482
+ }
483
+ // Override from flags
484
+ const flagFrom = getArg(args, '--from');
485
+ const flagLines = getArg(args, '-l');
486
+ if (flagFrom)
487
+ fromLine = parseInt(flagFrom, 10);
488
+ if (flagLines)
489
+ maxLines = parseInt(flagLines, 10);
490
+ const doc = ctx.docs.getBody(cleanId, { fromLine, maxLines });
491
+ if ('error' in doc) {
492
+ console.error(doc.error);
493
+ if (doc.similarFiles.length > 0) {
494
+ console.error('Similar files:', doc.similarFiles.join(', '));
495
+ }
496
+ process.exit(1);
497
+ }
498
+ const meta = ctx.docs.get(cleanId);
499
+ if ('error' in meta) {
500
+ // Already handled
501
+ }
502
+ if (jsonOut) {
503
+ console.log(JSON.stringify({ ...meta, content: doc.content, totalLines: doc.totalLines }, null, 2));
504
+ }
505
+ else {
506
+ console.log(`mne://${meta?.collection}/${meta?.path} #${meta?.docid}`);
507
+ console.log('---');
508
+ if (noLineNumbers) {
509
+ console.log(doc.content);
510
+ }
511
+ else {
512
+ const startLine = fromLine ?? 1;
513
+ doc.content.split('\n').forEach((line, i) => {
514
+ console.log(`${startLine + i}: ${line}`);
515
+ });
516
+ }
517
+ }
518
+ ctx.db.close();
519
+ }
520
+ function cmdMultiGet(args, dbPath) {
521
+ // Simplified: just runs get for comma-separated or glob patterns
522
+ const ctx = createContext(dbPath, false);
523
+ const pattern = args[0];
524
+ const jsonOut = hasArg(args, '--json');
525
+ const maxLines = parseInt(getArg(args, '-l') ?? '50', 10);
526
+ const noLineNumbers = hasArg(args, '--no-line-numbers');
527
+ if (!pattern) {
528
+ console.error('Usage: mne multi-get <pattern|#docid1,#docid2> [-l <lines>] [--json]');
529
+ process.exit(1);
530
+ }
531
+ // Comma-separated or glob?
532
+ const ids = pattern.includes(',') ? pattern.split(',').map((s) => s.trim()) : undefined;
533
+ if (ids) {
534
+ for (const id of ids) {
535
+ const doc = ctx.docs.getBody(id, { maxLines });
536
+ if ('error' in doc) {
537
+ console.error(`# ${id}: ${doc.error}`);
538
+ continue;
539
+ }
540
+ const meta = ctx.docs.get(id);
541
+ if (!('error' in meta)) {
542
+ console.log(`# ${meta.path} #${meta.docid}`);
543
+ console.log('---');
544
+ console.log(noLineNumbers ? doc.content : doc.content.split('\n').map((l, i) => `${i + 1}: ${l}`).join('\n'));
545
+ console.log();
546
+ }
547
+ }
548
+ }
549
+ ctx.db.close();
550
+ }
551
+ function cmdLs(args, dbPath) {
552
+ const ctx = createContext(dbPath, false);
553
+ const colName = args[0];
554
+ if (!colName) {
555
+ const cols = ctx.collections.list();
556
+ for (const c of cols) {
557
+ console.log(`${c.name}/ (${c.docCount} files)`);
558
+ }
559
+ ctx.db.close();
560
+ return;
561
+ }
562
+ // Remove mne:// prefix if present
563
+ const cleanName = colName.replace(/^mne:\/\//, '');
564
+ const [baseCol, ...subPath] = cleanName.split('/');
565
+ const filterPath = subPath.join('/');
566
+ const docs = ctx.db.db
567
+ .prepare('SELECT path FROM documents WHERE collection = ? AND path LIKE ? ORDER BY path')
568
+ .all(baseCol, `${filterPath}%`);
569
+ for (const d of docs) {
570
+ console.log(` ${d.path}`);
571
+ }
572
+ ctx.db.close();
573
+ }
574
+ async function cmdStatus(dbPath) {
575
+ const ctx = createContext(dbPath, false);
576
+ await ctx.db.loadVectors();
577
+ const docCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM documents').get().c;
578
+ const collectionCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM collections').get().c;
579
+ const chunkCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM chunks').get().c;
580
+ const hasVectors = ctx.db.hasVectorIndex();
581
+ const linkCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM links').get().c;
582
+ const tagCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM tags').get().c;
583
+ const ctxCount = ctx.db.db.prepare('SELECT COUNT(*) as c FROM contexts').get().c;
584
+ const dbSize = existsSync(dbPath) ? Math.round(readFileSync(dbPath).length / 1024) : 0;
585
+ console.log(`Index: ${dbPath}`);
586
+ console.log(`Collections: ${collectionCount}`);
587
+ console.log(`Documents: ${docCount}`);
588
+ console.log(`Chunks: ${chunkCount}`);
589
+ console.log(`Vectors: ${hasVectors ? 'yes' : 'no'}`);
590
+ console.log(`Links: ${linkCount}`);
591
+ console.log(`Tags: ${tagCount}`);
592
+ console.log(`Contexts: ${ctxCount}`);
593
+ console.log(`DB Size: ${dbSize} KB`);
594
+ console.log('\nCollections:');
595
+ const cols = ctx.collections.list();
596
+ for (const c of cols) {
597
+ console.log(` ${c.name} (${c.docCount} docs, ${c.activeCount} embedded)`);
598
+ }
599
+ ctx.db.close();
600
+ }
601
+ async function cmdDoctor() {
602
+ console.log('mnemonic diagnostics\n');
603
+ // Check DB
604
+ const dbPath = DEFAULT_DB;
605
+ if (existsSync(dbPath)) {
606
+ const size = Math.round(readFileSync(dbPath).length / 1024);
607
+ console.log(`✓ Database: ${dbPath} (${size} KB)`);
608
+ }
609
+ else {
610
+ console.log('✗ Database: not found. Run "mne init" first.');
611
+ }
612
+ // Check config
613
+ if (existsSync(DEFAULT_CONFIG)) {
614
+ console.log(`✓ Config: ${DEFAULT_CONFIG}`);
615
+ }
616
+ else {
617
+ console.log("○ Config: not found (using defaults)");
618
+ }
619
+ // Check Ollama
620
+ const ollamaOk = await checkOllama();
621
+ if (ollamaOk) {
622
+ console.log('✓ Ollama: running');
623
+ }
624
+ else {
625
+ console.log('○ Ollama: not detected. Install or start Ollama for local embeddings.');
626
+ }
627
+ // Check node-llama-cpp
628
+ try {
629
+ await import('node-llama-cpp');
630
+ console.log('✓ node-llama-cpp: available');
631
+ }
632
+ catch {
633
+ console.log('○ node-llama-cpp: not installed. Models will use Ollama.');
634
+ }
635
+ // Check models
636
+ const cacheDir = join(homedir(), '.cache', 'mnemonic', 'models');
637
+ if (existsSync(cacheDir)) {
638
+ const models = readdirSyncSafe(cacheDir);
639
+ console.log(`\nCached models: ${models.length > 0 ? models.join(', ') : 'none'}`);
640
+ }
641
+ }
642
+ function readdirSyncSafe(dir) {
643
+ try {
644
+ return readdirSync(dir);
645
+ }
646
+ catch {
647
+ return [];
648
+ }
649
+ }
650
+ function cmdContext(args, dbPath) {
651
+ const ctx = createContext(dbPath, false);
652
+ const sub = args[0];
653
+ switch (sub) {
654
+ case 'add': {
655
+ const path = args[1];
656
+ const text = args.slice(2).join(' ');
657
+ if (!path || !text) {
658
+ console.error('Usage: mne context add <mne://path|/> <text>');
659
+ process.exit(1);
660
+ }
661
+ // Parse mne://path
662
+ const mneMatch = path.match(/^mne:\/\/([^/]+)\/(.*)/);
663
+ if (mneMatch) {
664
+ ctx.docs.addContext(mneMatch[1], mneMatch[2], text);
665
+ }
666
+ else if (path === '/') {
667
+ ctx.docs.addContext(null, '/', text);
668
+ }
669
+ else {
670
+ // Try to detect collection from cwd
671
+ ctx.docs.addContext(null, path, text);
672
+ }
673
+ console.log(`Context added: ${path}`);
674
+ break;
675
+ }
676
+ case 'list': {
677
+ const contexts = ctx.docs.listContexts();
678
+ for (const c of contexts) {
679
+ const path = c.collection ? `mne://${c.collection}/${c.path}` : c.path;
680
+ console.log(`${path}: ${c.context}`);
681
+ }
682
+ break;
683
+ }
684
+ case 'rm': {
685
+ const path = args[1];
686
+ if (!path) {
687
+ console.error('Usage: mne context rm <path>');
688
+ process.exit(1);
689
+ }
690
+ const mneMatch = path.match(/^mne:\/\/([^/]+)\/(.*)/);
691
+ if (mneMatch) {
692
+ ctx.docs.removeContext(mneMatch[1], mneMatch[2]);
693
+ }
694
+ else {
695
+ ctx.docs.removeContext(null, path === '/' ? '/' : path);
696
+ }
697
+ console.log(`Context removed: ${path}`);
698
+ break;
699
+ }
700
+ default:
701
+ console.error('Usage: mne context <add|list|rm> ...');
702
+ }
703
+ ctx.db.close();
704
+ }
705
+ function cmdTag(args, dbPath) {
706
+ const ctx = createContext(dbPath, false);
707
+ const docid = args[0]?.replace(/^#/, '');
708
+ const tag = args[1];
709
+ if (!docid || !tag) {
710
+ console.error('Usage: mne tag <#docid> <tag>');
711
+ process.exit(1);
712
+ }
713
+ ctx.docs.addTag(docid, tag);
714
+ console.log(`Tag "${tag}" added to #${docid}`);
715
+ ctx.db.close();
716
+ }
717
+ function cmdLinks(args, dbPath) {
718
+ const ctx = createContext(dbPath, false);
719
+ const docid = args[0]?.replace(/^#/, '');
720
+ if (!docid) {
721
+ console.error('Usage: mne links <#docid>');
722
+ process.exit(1);
723
+ }
724
+ const links = ctx.docs.getLinks(docid);
725
+ for (const l of links) {
726
+ console.log(` → ${l.targetPath} [${l.linkType}]`);
727
+ }
728
+ ctx.db.close();
729
+ }
730
+ function cmdBacklinks(args, dbPath) {
731
+ const ctx = createContext(dbPath, false);
732
+ const docid = args[0]?.replace(/^#/, '');
733
+ if (!docid) {
734
+ console.error('Usage: mne backlinks <#docid>');
735
+ process.exit(1);
736
+ }
737
+ const links = ctx.docs.getBacklinks(docid);
738
+ for (const l of links) {
739
+ console.log(` ← ${l.sourcePath} [${l.linkType}]`);
740
+ }
741
+ ctx.db.close();
742
+ }
743
+ function cmdOrphans(dbPath) {
744
+ const ctx = createContext(dbPath, false);
745
+ const orphans = ctx.docs.getOrphans();
746
+ if (orphans.length === 0) {
747
+ console.log('No orphan documents found.');
748
+ }
749
+ else {
750
+ console.log(`Orphan documents (${orphans.length}):`);
751
+ for (const o of orphans) {
752
+ console.log(` #${o.docid} ${o.path}`);
753
+ }
754
+ }
755
+ ctx.db.close();
756
+ }
757
+ async function cmdMcp(args, dbPath) {
758
+ const { startMcpServer } = await import('../mcp/server.js');
759
+ await startMcpServer(dbPath, args);
760
+ }
761
+ // ─── Output Formatting ──────────────────────────────────────────────
762
+ function printResults(results, options) {
763
+ if (results.length === 0) {
764
+ console.log('No results found.');
765
+ return;
766
+ }
767
+ if (options?.json) {
768
+ console.log(JSON.stringify(results, null, 2));
769
+ return;
770
+ }
771
+ for (const r of results) {
772
+ const scorePct = Math.round(r.score * 100);
773
+ const color = scorePct > 70 ? '' : scorePct > 40 ? '' : '';
774
+ const reset = '';
775
+ console.log(`${r.path} #${r.docid}`);
776
+ console.log(`Title: ${r.title}`);
777
+ if (r.context.length > 0) {
778
+ console.log(`Context: ${r.context.join(' > ')}`);
779
+ }
780
+ if (r.tags.length > 0) {
781
+ console.log(`Tags: ${r.tags.join(', ')}`);
782
+ }
783
+ console.log(`Score: ${scorePct}%`);
784
+ if (r.snippet) {
785
+ console.log(`\n${r.snippet}\n`);
786
+ }
787
+ if (r.modifiedAt) {
788
+ console.log(`Modified: ${r.modifiedAt}`);
789
+ }
790
+ console.log();
791
+ }
792
+ }
793
+ //# sourceMappingURL=commands.js.map