@memextend/cursor 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.
@@ -0,0 +1,1029 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli/inject.ts
27
+ var import_crypto = require("crypto");
28
+ var import_fs2 = require("fs");
29
+ var import_path2 = require("path");
30
+ var import_os = require("os");
31
+ var import_child_process = require("child_process");
32
+
33
+ // ../../core/dist/storage/sqlite.js
34
+ var import_better_sqlite3 = __toESM(require("better-sqlite3"), 1);
35
+ var SQLiteStorage = class {
36
+ db;
37
+ constructor(dbPath) {
38
+ this.db = new import_better_sqlite3.default(dbPath);
39
+ this.db.pragma("journal_mode = WAL");
40
+ this.initialize();
41
+ }
42
+ initialize() {
43
+ this.db.exec(`
44
+ CREATE TABLE IF NOT EXISTS memories (
45
+ id TEXT PRIMARY KEY,
46
+ project_id TEXT,
47
+ content TEXT NOT NULL,
48
+ type TEXT NOT NULL,
49
+ source_tool TEXT,
50
+ created_at TEXT NOT NULL,
51
+ session_id TEXT,
52
+ metadata TEXT
53
+ );
54
+
55
+ CREATE TABLE IF NOT EXISTS projects (
56
+ id TEXT PRIMARY KEY,
57
+ name TEXT NOT NULL,
58
+ path TEXT NOT NULL,
59
+ created_at TEXT NOT NULL
60
+ );
61
+
62
+ CREATE TABLE IF NOT EXISTS global_profile (
63
+ id TEXT PRIMARY KEY,
64
+ key TEXT NOT NULL,
65
+ content TEXT NOT NULL,
66
+ created_at TEXT NOT NULL
67
+ );
68
+
69
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
70
+ content,
71
+ content='memories',
72
+ content_rowid='rowid'
73
+ );
74
+
75
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
76
+ INSERT INTO memories_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
77
+ END;
78
+
79
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
80
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
81
+ END;
82
+
83
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
84
+ INSERT INTO memories_fts(memories_fts, rowid, content) VALUES('delete', OLD.rowid, OLD.content);
85
+ INSERT INTO memories_fts(rowid, content) VALUES (NEW.rowid, NEW.content);
86
+ END;
87
+ `);
88
+ }
89
+ getTables() {
90
+ const rows = this.db.prepare(`
91
+ SELECT name FROM sqlite_master
92
+ WHERE type='table' OR type='virtual table'
93
+ `).all();
94
+ return rows.map((r) => r.name);
95
+ }
96
+ insertMemory(memory) {
97
+ const stmt = this.db.prepare(`
98
+ INSERT INTO memories (id, project_id, content, type, source_tool, created_at, session_id, metadata)
99
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
100
+ `);
101
+ stmt.run(memory.id, memory.projectId, memory.content, memory.type, memory.sourceTool, memory.createdAt, memory.sessionId, memory.metadata ? JSON.stringify(memory.metadata) : null);
102
+ }
103
+ getMemory(id) {
104
+ const row = this.db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
105
+ if (!row)
106
+ return null;
107
+ return this.rowToMemory(row);
108
+ }
109
+ getAllMemories(projectId, limit = 100) {
110
+ let query = "SELECT * FROM memories";
111
+ const params = [];
112
+ if (projectId) {
113
+ query += " WHERE project_id = ?";
114
+ params.push(projectId);
115
+ }
116
+ query += " ORDER BY created_at DESC LIMIT ?";
117
+ params.push(limit);
118
+ const rows = this.db.prepare(query).all(...params);
119
+ return rows.map((row) => this.rowToMemory(row));
120
+ }
121
+ searchFTS(query, limit = 10) {
122
+ const rows = this.db.prepare(`
123
+ SELECT m.*, bm25(memories_fts) as score
124
+ FROM memories_fts fts
125
+ JOIN memories m ON m.rowid = fts.rowid
126
+ WHERE memories_fts MATCH ?
127
+ ORDER BY score
128
+ LIMIT ?
129
+ `).all(query, limit);
130
+ return rows.map((row) => ({
131
+ memory: this.rowToMemory(row),
132
+ score: Math.abs(row.score),
133
+ source: "fts"
134
+ }));
135
+ }
136
+ getRecentMemories(projectId, limit = 0, days = 0) {
137
+ let query = "SELECT * FROM memories";
138
+ const params = [];
139
+ const conditions = [];
140
+ if (days > 0) {
141
+ const cutoff = /* @__PURE__ */ new Date();
142
+ cutoff.setDate(cutoff.getDate() - days);
143
+ conditions.push("created_at > ?");
144
+ params.push(cutoff.toISOString());
145
+ }
146
+ if (projectId !== null) {
147
+ conditions.push("project_id = ?");
148
+ params.push(projectId);
149
+ }
150
+ if (conditions.length > 0) {
151
+ query += " WHERE " + conditions.join(" AND ");
152
+ }
153
+ query += " ORDER BY created_at DESC";
154
+ if (limit > 0) {
155
+ query += " LIMIT ?";
156
+ params.push(limit);
157
+ }
158
+ const rows = this.db.prepare(query).all(...params);
159
+ return rows.map((row) => this.rowToMemory(row));
160
+ }
161
+ deleteMemory(id) {
162
+ const result = this.db.prepare("DELETE FROM memories WHERE id = ?").run(id);
163
+ return result.changes > 0;
164
+ }
165
+ updateMemory(id, content) {
166
+ const result = this.db.prepare("UPDATE memories SET content = ? WHERE id = ?").run(content, id);
167
+ return result.changes > 0;
168
+ }
169
+ deleteAllMemories(projectId) {
170
+ if (projectId) {
171
+ const result = this.db.prepare("DELETE FROM memories WHERE project_id = ?").run(projectId);
172
+ return result.changes;
173
+ } else {
174
+ const result = this.db.prepare("DELETE FROM memories").run();
175
+ return result.changes;
176
+ }
177
+ }
178
+ deleteMemoriesBefore(date, projectId) {
179
+ const timestamp = date.toISOString();
180
+ if (projectId) {
181
+ const result = this.db.prepare("DELETE FROM memories WHERE created_at < ? AND project_id = ?").run(timestamp, projectId);
182
+ return result.changes;
183
+ } else {
184
+ const result = this.db.prepare("DELETE FROM memories WHERE created_at < ?").run(timestamp);
185
+ return result.changes;
186
+ }
187
+ }
188
+ // Project methods
189
+ insertProject(project) {
190
+ const stmt = this.db.prepare(`
191
+ INSERT OR REPLACE INTO projects (id, name, path, created_at)
192
+ VALUES (?, ?, ?, ?)
193
+ `);
194
+ stmt.run(project.id, project.name, project.path, project.createdAt);
195
+ }
196
+ getProject(id) {
197
+ const row = this.db.prepare("SELECT * FROM projects WHERE id = ?").get(id);
198
+ if (!row)
199
+ return null;
200
+ return {
201
+ id: row.id,
202
+ name: row.name,
203
+ path: row.path,
204
+ createdAt: row.created_at
205
+ };
206
+ }
207
+ /**
208
+ * Find a project by name (case-insensitive).
209
+ * Returns the first matching project, preferring projects with a path set.
210
+ */
211
+ getProjectByName(name) {
212
+ const rowWithPath = this.db.prepare("SELECT * FROM projects WHERE LOWER(name) = LOWER(?) AND path != '' ORDER BY created_at ASC LIMIT 1").get(name);
213
+ if (rowWithPath) {
214
+ return {
215
+ id: rowWithPath.id,
216
+ name: rowWithPath.name,
217
+ path: rowWithPath.path,
218
+ createdAt: rowWithPath.created_at
219
+ };
220
+ }
221
+ const row = this.db.prepare("SELECT * FROM projects WHERE LOWER(name) = LOWER(?) ORDER BY created_at ASC LIMIT 1").get(name);
222
+ if (!row)
223
+ return null;
224
+ return {
225
+ id: row.id,
226
+ name: row.name,
227
+ path: row.path,
228
+ createdAt: row.created_at
229
+ };
230
+ }
231
+ /**
232
+ * Delete a project and all its memories.
233
+ * @returns Object with counts of deleted project and memories
234
+ */
235
+ deleteProject(projectId) {
236
+ const memoriesResult = this.db.prepare("DELETE FROM memories WHERE project_id = ?").run(projectId);
237
+ const projectResult = this.db.prepare("DELETE FROM projects WHERE id = ?").run(projectId);
238
+ return {
239
+ projectDeleted: projectResult.changes > 0,
240
+ memoriesDeleted: memoriesResult.changes
241
+ };
242
+ }
243
+ /**
244
+ * Get all projects.
245
+ */
246
+ getAllProjects() {
247
+ const rows = this.db.prepare("SELECT * FROM projects ORDER BY name ASC").all();
248
+ return rows.map((row) => ({
249
+ id: row.id,
250
+ name: row.name,
251
+ path: row.path,
252
+ createdAt: row.created_at
253
+ }));
254
+ }
255
+ // Global profile methods
256
+ insertGlobalProfile(profile) {
257
+ const stmt = this.db.prepare(`
258
+ INSERT INTO global_profile (id, key, content, created_at)
259
+ VALUES (?, ?, ?, ?)
260
+ `);
261
+ stmt.run(profile.id, profile.key, profile.content, profile.createdAt);
262
+ }
263
+ getGlobalProfiles(limit = 10) {
264
+ const rows = this.db.prepare(`
265
+ SELECT * FROM global_profile ORDER BY created_at DESC LIMIT ?
266
+ `).all(limit);
267
+ return rows.map((row) => ({
268
+ id: row.id,
269
+ key: row.key,
270
+ content: row.content,
271
+ createdAt: row.created_at
272
+ }));
273
+ }
274
+ /**
275
+ * Delete a specific global profile by ID.
276
+ */
277
+ deleteGlobalProfile(id) {
278
+ const result = this.db.prepare("DELETE FROM global_profile WHERE id = ?").run(id);
279
+ return result.changes > 0;
280
+ }
281
+ /**
282
+ * Delete all global profiles.
283
+ * @returns Number of profiles deleted
284
+ */
285
+ deleteAllGlobalProfiles() {
286
+ const result = this.db.prepare("DELETE FROM global_profile").run();
287
+ return result.changes;
288
+ }
289
+ getMemoryCount() {
290
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM memories").get();
291
+ return row.count;
292
+ }
293
+ getMemoryCountByProject(projectId) {
294
+ const row = this.db.prepare("SELECT COUNT(*) as count FROM memories WHERE project_id = ?").get(projectId);
295
+ return row.count;
296
+ }
297
+ /**
298
+ * Get IDs of oldest memories for a project, exceeding the limit
299
+ * @returns Array of memory IDs to delete (oldest first)
300
+ */
301
+ getOldestMemoryIds(projectId, limit) {
302
+ let query;
303
+ let params;
304
+ if (projectId) {
305
+ query = `
306
+ SELECT id FROM memories
307
+ WHERE project_id = ?
308
+ ORDER BY created_at ASC
309
+ LIMIT ?
310
+ `;
311
+ params = [projectId, limit];
312
+ } else {
313
+ query = `
314
+ SELECT id FROM memories
315
+ ORDER BY created_at ASC
316
+ LIMIT ?
317
+ `;
318
+ params = [limit];
319
+ }
320
+ const rows = this.db.prepare(query).all(...params);
321
+ return rows.map((row) => row.id);
322
+ }
323
+ /**
324
+ * Prune memories to stay within limits
325
+ * @returns Number of memories deleted
326
+ */
327
+ pruneMemories(options) {
328
+ const deletedIds = [];
329
+ if (options.maxPerProject && options.maxPerProject > 0 && options.projectId) {
330
+ const count = this.getMemoryCountByProject(options.projectId);
331
+ if (count > options.maxPerProject) {
332
+ const excess = count - options.maxPerProject;
333
+ const idsToDelete = this.getOldestMemoryIds(options.projectId, excess);
334
+ for (const id of idsToDelete) {
335
+ this.deleteMemory(id);
336
+ deletedIds.push(id);
337
+ }
338
+ }
339
+ }
340
+ if (options.maxTotal && options.maxTotal > 0) {
341
+ const count = this.getMemoryCount();
342
+ if (count > options.maxTotal) {
343
+ const excess = count - options.maxTotal;
344
+ const idsToDelete = this.getOldestMemoryIds(null, excess);
345
+ for (const id of idsToDelete) {
346
+ if (!deletedIds.includes(id)) {
347
+ this.deleteMemory(id);
348
+ deletedIds.push(id);
349
+ }
350
+ }
351
+ }
352
+ }
353
+ return { deleted: deletedIds.length, deletedIds };
354
+ }
355
+ rowToMemory(row) {
356
+ return {
357
+ id: row.id,
358
+ projectId: row.project_id,
359
+ content: row.content,
360
+ type: row.type,
361
+ sourceTool: row.source_tool,
362
+ createdAt: row.created_at,
363
+ sessionId: row.session_id,
364
+ metadata: row.metadata ? JSON.parse(row.metadata) : null
365
+ };
366
+ }
367
+ close() {
368
+ this.db.close();
369
+ }
370
+ };
371
+
372
+ // ../../core/dist/storage/lancedb.js
373
+ var lancedb = __toESM(require("@lancedb/lancedb"), 1);
374
+ var LanceDBStorage = class _LanceDBStorage {
375
+ db;
376
+ table = null;
377
+ tableName = "memories";
378
+ dimensions = 384;
379
+ constructor(db) {
380
+ this.db = db;
381
+ }
382
+ static async create(dbPath) {
383
+ const db = await lancedb.connect(dbPath);
384
+ const storage = new _LanceDBStorage(db);
385
+ await storage.initialize();
386
+ return storage;
387
+ }
388
+ async initialize() {
389
+ const tableNames = await this.db.tableNames();
390
+ if (tableNames.includes(this.tableName)) {
391
+ this.table = await this.db.openTable(this.tableName);
392
+ }
393
+ }
394
+ async insertVector(id, vector) {
395
+ if (vector.length !== this.dimensions) {
396
+ throw new Error(`Vector must have ${this.dimensions} dimensions, got ${vector.length}`);
397
+ }
398
+ const data = [{ id, vector }];
399
+ if (!this.table) {
400
+ this.table = await this.db.createTable(this.tableName, data);
401
+ } else {
402
+ await this.table.add(data);
403
+ }
404
+ }
405
+ async insertVectors(items) {
406
+ for (const item of items) {
407
+ if (item.vector.length !== this.dimensions) {
408
+ throw new Error(`Vector must have ${this.dimensions} dimensions, got ${item.vector.length}`);
409
+ }
410
+ }
411
+ if (!this.table) {
412
+ this.table = await this.db.createTable(this.tableName, items);
413
+ } else {
414
+ await this.table.add(items);
415
+ }
416
+ }
417
+ async search(vector, limit = 10) {
418
+ if (!this.table) {
419
+ return [];
420
+ }
421
+ const effectiveLimit = limit > 0 ? limit : 100;
422
+ const results = await this.table.vectorSearch(vector).limit(effectiveLimit).toArray();
423
+ return results.map((row) => ({
424
+ id: row.id,
425
+ score: 1 - row._distance
426
+ // Convert distance to similarity
427
+ }));
428
+ }
429
+ async deleteVector(id) {
430
+ if (!this.table)
431
+ return;
432
+ const sanitizedId = id.replace(/'/g, "''");
433
+ await this.table.delete(`id = '${sanitizedId}'`);
434
+ }
435
+ async getVectorCount() {
436
+ if (!this.table)
437
+ return 0;
438
+ return await this.table.countRows();
439
+ }
440
+ async getVectorsByIds(ids) {
441
+ const result = /* @__PURE__ */ new Map();
442
+ if (!this.table || ids.length === 0)
443
+ return result;
444
+ const BATCH_SIZE = 100;
445
+ try {
446
+ for (let i = 0; i < ids.length; i += BATCH_SIZE) {
447
+ const batch = ids.slice(i, i + BATCH_SIZE);
448
+ const sanitizedIds = batch.map((id) => id.replace(/'/g, "''"));
449
+ const filter = sanitizedIds.map((id) => `id = '${id}'`).join(" OR ");
450
+ const rows = await this.table.query().where(filter).toArray();
451
+ for (const row of rows) {
452
+ result.set(row.id, row.vector);
453
+ }
454
+ }
455
+ } catch {
456
+ }
457
+ return result;
458
+ }
459
+ async close() {
460
+ }
461
+ };
462
+
463
+ // ../../core/dist/embedding/local.js
464
+ var import_path = require("path");
465
+ var import_fs = require("fs");
466
+ var import_promises = require("fs/promises");
467
+ var import_promises2 = require("stream/promises");
468
+ var MODEL_URL = "https://huggingface.co/nomic-ai/nomic-embed-text-v1.5-GGUF/resolve/main/nomic-embed-text-v1.5.Q8_0.gguf";
469
+ var MODEL_FILENAME = "nomic-embed-text-v1.5.Q8_0.gguf";
470
+ var LocalEmbedding = class _LocalEmbedding {
471
+ // Using any to avoid ESM/CJS type issues with dynamic imports
472
+ model;
473
+ context;
474
+ dimensions;
475
+ constructor(model, context, dimensions) {
476
+ this.model = model;
477
+ this.context = context;
478
+ this.dimensions = dimensions;
479
+ }
480
+ static async create(modelsDir) {
481
+ const modelPath = (0, import_path.join)(modelsDir, MODEL_FILENAME);
482
+ if (!(0, import_fs.existsSync)(modelPath)) {
483
+ await _LocalEmbedding.downloadModel(modelsDir, modelPath);
484
+ }
485
+ const { getLlama, LlamaLogLevel } = await import("node-llama-cpp");
486
+ const llama = await getLlama({
487
+ logLevel: LlamaLogLevel.error
488
+ // Only show errors, not warnings
489
+ });
490
+ const model = await llama.loadModel({ modelPath });
491
+ const context = await model.createEmbeddingContext();
492
+ return new _LocalEmbedding(model, context, 384);
493
+ }
494
+ static async downloadModel(modelsDir, modelPath) {
495
+ await (0, import_promises.mkdir)(modelsDir, { recursive: true });
496
+ console.log(`Downloading embedding model to ${modelPath}...`);
497
+ console.log("This is a one-time download (~274MB)");
498
+ const response = await fetch(MODEL_URL);
499
+ if (!response.ok || !response.body) {
500
+ throw new Error(`Failed to download model: ${response.statusText}`);
501
+ }
502
+ const fileStream = (0, import_fs.createWriteStream)(modelPath);
503
+ await (0, import_promises2.pipeline)(response.body, fileStream);
504
+ console.log("Model downloaded successfully");
505
+ }
506
+ /**
507
+ * Generate embedding for a document/memory
508
+ */
509
+ async embed(text) {
510
+ const prefixedText = `search_document: ${text}`;
511
+ const embedding = await this.context.getEmbeddingFor(prefixedText);
512
+ const vector = Array.from(embedding.vector).slice(0, this.dimensions);
513
+ return this.normalize(vector);
514
+ }
515
+ /**
516
+ * Generate embedding for a search query
517
+ */
518
+ async embedQuery(text) {
519
+ const prefixedText = `search_query: ${text}`;
520
+ const embedding = await this.context.getEmbeddingFor(prefixedText);
521
+ const vector = Array.from(embedding.vector).slice(0, this.dimensions);
522
+ return this.normalize(vector);
523
+ }
524
+ /**
525
+ * Batch embed multiple texts (documents)
526
+ */
527
+ async embedBatch(texts) {
528
+ const results = [];
529
+ for (const text of texts) {
530
+ results.push(await this.embed(text));
531
+ }
532
+ return results;
533
+ }
534
+ /**
535
+ * Get the embedding dimensions
536
+ */
537
+ getDimensions() {
538
+ return this.dimensions;
539
+ }
540
+ /**
541
+ * Normalize vector to unit length (for cosine similarity)
542
+ */
543
+ normalize(vector) {
544
+ const norm = Math.sqrt(vector.reduce((sum, v) => sum + v * v, 0));
545
+ if (norm === 0)
546
+ return vector;
547
+ return vector.map((v) => v / norm);
548
+ }
549
+ async close() {
550
+ await this.context.dispose();
551
+ }
552
+ };
553
+ function isModelAvailable(modelsDir) {
554
+ const modelPath = (0, import_path.join)(modelsDir, MODEL_FILENAME);
555
+ return (0, import_fs.existsSync)(modelPath);
556
+ }
557
+ async function createEmbedFunction(modelsDir) {
558
+ if (isModelAvailable(modelsDir)) {
559
+ try {
560
+ const embedding = await LocalEmbedding.create(modelsDir);
561
+ return {
562
+ embed: (text) => embedding.embed(text),
563
+ embedQuery: (text) => embedding.embedQuery(text),
564
+ isReal: true,
565
+ close: () => embedding.close()
566
+ };
567
+ } catch (error) {
568
+ console.error("[memextend] Failed to load embedding model, using fallback:", error);
569
+ }
570
+ }
571
+ const { createHash: createHash2 } = await import("crypto");
572
+ const hashEmbed = (text, prefix) => {
573
+ const hash = createHash2("sha256").update(prefix + text).digest();
574
+ const vector = Array.from(hash).slice(0, 32);
575
+ const expanded = [];
576
+ for (let i = 0; i < 12; i++) {
577
+ const roundHash = createHash2("sha256").update(text + i.toString()).digest();
578
+ expanded.push(...Array.from(roundHash).slice(0, 32));
579
+ }
580
+ const norm = Math.sqrt(expanded.reduce((sum, v) => sum + v * v, 0));
581
+ return expanded.map((v) => v / norm / 255);
582
+ };
583
+ return {
584
+ embed: async (text) => hashEmbed(text, "doc:"),
585
+ embedQuery: async (text) => hashEmbed(text, "query:"),
586
+ isReal: false,
587
+ close: async () => {
588
+ }
589
+ };
590
+ }
591
+
592
+ // ../../core/dist/memory/retrieve.js
593
+ var MemoryRetriever = class {
594
+ sqlite;
595
+ lancedb;
596
+ embed;
597
+ options;
598
+ constructor(sqlite, lancedb2, embed, options = {}) {
599
+ this.sqlite = sqlite;
600
+ this.lancedb = lancedb2;
601
+ this.embed = embed;
602
+ this.options = {
603
+ defaultLimit: options.defaultLimit ?? 0,
604
+ // 0 = unlimited
605
+ defaultRecentDays: options.defaultRecentDays ?? 0,
606
+ // 0 = unlimited
607
+ rrfK: options.rrfK ?? 60
608
+ };
609
+ }
610
+ /**
611
+ * Full-text search using FTS5
612
+ */
613
+ async search(query, options = {}) {
614
+ const limit = options.limit ?? this.options.defaultLimit;
615
+ return this.sqlite.searchFTS(query, limit);
616
+ }
617
+ /**
618
+ * Vector similarity search using LanceDB
619
+ */
620
+ async vectorSearch(query, options = {}) {
621
+ const limit = options.limit ?? this.options.defaultLimit;
622
+ const queryVector = await this.embed(query);
623
+ const vectorResults = await this.lancedb.search(queryVector, limit * 2);
624
+ const results = [];
625
+ for (const vr of vectorResults) {
626
+ const memory = this.sqlite.getMemory(vr.id);
627
+ if (memory) {
628
+ if (options.projectId && memory.projectId !== options.projectId) {
629
+ continue;
630
+ }
631
+ results.push({
632
+ memory,
633
+ score: vr.score,
634
+ source: "vector"
635
+ });
636
+ }
637
+ }
638
+ return results.slice(0, limit);
639
+ }
640
+ /**
641
+ * Hybrid search combining FTS and vector search with RRF fusion
642
+ */
643
+ async hybridSearch(query, options = {}) {
644
+ const limit = options.limit ?? this.options.defaultLimit;
645
+ const k = this.options.rrfK;
646
+ const [ftsResults, vectorResults] = await Promise.all([
647
+ this.search(query, { ...options, limit: limit * 2 }),
648
+ this.vectorSearch(query, { ...options, limit: limit * 2 })
649
+ ]);
650
+ const scores = /* @__PURE__ */ new Map();
651
+ const memories = /* @__PURE__ */ new Map();
652
+ ftsResults.forEach((result, rank) => {
653
+ const rrfScore = 1 / (k + rank + 1);
654
+ scores.set(result.memory.id, (scores.get(result.memory.id) ?? 0) + rrfScore);
655
+ memories.set(result.memory.id, result.memory);
656
+ });
657
+ vectorResults.forEach((result, rank) => {
658
+ const rrfScore = 1 / (k + rank + 1);
659
+ scores.set(result.memory.id, (scores.get(result.memory.id) ?? 0) + rrfScore);
660
+ memories.set(result.memory.id, result.memory);
661
+ });
662
+ const combined = Array.from(scores.entries()).sort((a, b) => b[1] - a[1]).slice(0, limit).map(([id, score]) => ({
663
+ memory: memories.get(id),
664
+ score,
665
+ source: "hybrid"
666
+ }));
667
+ return combined;
668
+ }
669
+ /**
670
+ * Get recent memories for a project
671
+ */
672
+ getRecent(projectId, options = {}) {
673
+ const limit = options.limit ?? this.options.defaultLimit;
674
+ const days = options.recentDays ?? this.options.defaultRecentDays;
675
+ return this.sqlite.getRecentMemories(projectId, limit, days);
676
+ }
677
+ /**
678
+ * Get context for a new session - combines recent memories and global profile
679
+ */
680
+ async getContextForSession(projectId, options = {}) {
681
+ const limit = options.limit ?? this.options.defaultLimit;
682
+ const includeGlobal = options.includeGlobal ?? true;
683
+ const recentMemories = this.getRecent(projectId, {
684
+ limit: Math.floor(limit / 2),
685
+ recentDays: options.recentDays
686
+ });
687
+ const globalProfile = includeGlobal ? this.sqlite.getGlobalProfiles(5) : [];
688
+ const project = this.sqlite.getProject(projectId);
689
+ const projectName = project?.name ?? "project";
690
+ const relevantMemories = await this.vectorSearch(projectName, {
691
+ projectId,
692
+ limit: Math.floor(limit / 2)
693
+ });
694
+ return {
695
+ recentMemories,
696
+ globalProfile,
697
+ relevantMemories
698
+ };
699
+ }
700
+ };
701
+ function formatContextForInjection(context, options = {}) {
702
+ const maxChars = options.maxChars ?? 0;
703
+ const previewLength = options.previewLength ?? 200;
704
+ const lines = ["<memextend-context>", "## Your Memory for This Project", ""];
705
+ let currentLength = lines.join("\n").length;
706
+ const footer = [
707
+ "Use these memories naturally. Use `memextend_search` to find more details.",
708
+ "Use `memextend_save` to remember important decisions for future sessions.",
709
+ "</memextend-context>"
710
+ ].join("\n");
711
+ const footerLength = footer.length + 1;
712
+ const canAdd = (text) => {
713
+ if (maxChars === 0)
714
+ return true;
715
+ return currentLength + text.length + footerLength <= maxChars;
716
+ };
717
+ if (context.recentMemories.length > 0) {
718
+ const sectionHeader = "### Recent Work\n";
719
+ if (canAdd(sectionHeader)) {
720
+ lines.push("### Recent Work");
721
+ currentLength += sectionHeader.length;
722
+ for (const memory of context.recentMemories) {
723
+ const date = formatRelativeDate(memory.createdAt);
724
+ const preview = memory.content.split("\n")[0].slice(0, previewLength);
725
+ const line = `- [${date}] ${preview}
726
+ `;
727
+ if (!canAdd(line))
728
+ break;
729
+ lines.push(`- [${date}] ${preview}`);
730
+ currentLength += line.length;
731
+ }
732
+ lines.push("");
733
+ currentLength += 1;
734
+ }
735
+ }
736
+ if (context.globalProfile.length > 0) {
737
+ const sectionHeader = "### User Preferences (Global)\n";
738
+ if (canAdd(sectionHeader)) {
739
+ lines.push("### User Preferences (Global)");
740
+ currentLength += sectionHeader.length;
741
+ for (const profile of context.globalProfile) {
742
+ const line = `- ${profile.content}
743
+ `;
744
+ if (!canAdd(line))
745
+ break;
746
+ lines.push(`- ${profile.content}`);
747
+ currentLength += line.length;
748
+ }
749
+ lines.push("");
750
+ currentLength += 1;
751
+ }
752
+ }
753
+ if (context.relevantMemories && context.relevantMemories.length > 0) {
754
+ const sectionHeader = "### Relevant Past Work\n";
755
+ if (canAdd(sectionHeader)) {
756
+ lines.push("### Relevant Past Work");
757
+ currentLength += sectionHeader.length;
758
+ for (const result of context.relevantMemories) {
759
+ const preview = result.memory.content.split("\n")[0].slice(0, previewLength);
760
+ const line = `- ${preview}
761
+ `;
762
+ if (!canAdd(line))
763
+ break;
764
+ lines.push(`- ${preview}`);
765
+ currentLength += line.length;
766
+ }
767
+ lines.push("");
768
+ }
769
+ }
770
+ lines.push("Use these memories naturally. Use `memextend_search` to find more details.");
771
+ lines.push("Use `memextend_save` to remember important decisions for future sessions.");
772
+ lines.push("</memextend-context>");
773
+ return lines.join("\n");
774
+ }
775
+ function formatRelativeDate(isoDate) {
776
+ const date = new Date(isoDate);
777
+ const now = /* @__PURE__ */ new Date();
778
+ const diffMs = now.getTime() - date.getTime();
779
+ const diffDays = Math.floor(diffMs / (1e3 * 60 * 60 * 24));
780
+ if (diffDays === 0)
781
+ return "today";
782
+ if (diffDays === 1)
783
+ return "yesterday";
784
+ if (diffDays < 7)
785
+ return `${diffDays} days ago`;
786
+ if (diffDays < 30)
787
+ return `${Math.floor(diffDays / 7)} weeks ago`;
788
+ return `${Math.floor(diffDays / 30)} months ago`;
789
+ }
790
+
791
+ // src/cli/inject.ts
792
+ var MEMEXTEND_DIR = (0, import_path2.join)((0, import_os.homedir)(), ".memextend");
793
+ var DB_PATH = (0, import_path2.join)(MEMEXTEND_DIR, "memextend.db");
794
+ var VECTORS_PATH = (0, import_path2.join)(MEMEXTEND_DIR, "vectors");
795
+ var MODELS_PATH = (0, import_path2.join)(MEMEXTEND_DIR, "models");
796
+ function parseArgs() {
797
+ const args = process.argv.slice(2);
798
+ const options = {
799
+ includeGlobal: true,
800
+ format: "text"
801
+ };
802
+ for (let i = 0; i < args.length; i++) {
803
+ const arg = args[i];
804
+ const next = args[i + 1];
805
+ switch (arg) {
806
+ case "--workspace":
807
+ case "-w":
808
+ options.workspace = next;
809
+ i++;
810
+ break;
811
+ case "--days":
812
+ case "-d":
813
+ options.days = parseInt(next, 10);
814
+ i++;
815
+ break;
816
+ case "--limit":
817
+ case "-l":
818
+ options.limit = parseInt(next, 10);
819
+ i++;
820
+ break;
821
+ case "--no-global":
822
+ options.includeGlobal = false;
823
+ break;
824
+ case "--format":
825
+ case "-f":
826
+ options.format = next;
827
+ i++;
828
+ break;
829
+ case "--clipboard":
830
+ case "-c":
831
+ options.clipboard = true;
832
+ break;
833
+ case "--quiet":
834
+ case "-q":
835
+ options.quiet = true;
836
+ break;
837
+ case "--help":
838
+ case "-h":
839
+ printHelp();
840
+ process.exit(0);
841
+ }
842
+ }
843
+ return options;
844
+ }
845
+ function printHelp() {
846
+ console.log(`
847
+ memextend-cursor-inject - Retrieve context for session start
848
+
849
+ USAGE:
850
+ memextend-cursor-inject [OPTIONS]
851
+
852
+ OPTIONS:
853
+ -w, --workspace <dir> Workspace directory (default: cwd)
854
+ -d, --days <n> Look back N days (default: 7)
855
+ -l, --limit <n> Maximum memories to retrieve (default: 10)
856
+ --no-global Exclude global preferences
857
+ -f, --format <type> Output format: text, json, markdown (default: text)
858
+ -c, --clipboard Copy to clipboard (macOS only)
859
+ -q, --quiet Only output the context (no status messages)
860
+ -h, --help Show this help
861
+
862
+ EXAMPLES:
863
+ # Get context for current workspace
864
+ memextend-cursor-inject
865
+
866
+ # Get context for specific project
867
+ memextend-cursor-inject -w /path/to/project
868
+
869
+ # Get extended history
870
+ memextend-cursor-inject --days 30 --limit 20
871
+
872
+ # Copy to clipboard for pasting into Cursor
873
+ memextend-cursor-inject --clipboard
874
+
875
+ # Get JSON for programmatic use
876
+ memextend-cursor-inject --format json
877
+
878
+ INTEGRATION:
879
+ Run at session start or add to your shell profile.
880
+ Can be piped to Cursor's context via extensions or tasks.
881
+ `);
882
+ }
883
+ function getProjectId2(cwd) {
884
+ try {
885
+ const gitRoot = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
886
+ cwd,
887
+ encoding: "utf-8",
888
+ stdio: ["pipe", "pipe", "pipe"]
889
+ }).trim();
890
+ return (0, import_crypto.createHash)("sha256").update(gitRoot).digest("hex").slice(0, 16);
891
+ } catch {
892
+ return (0, import_crypto.createHash)("sha256").update(cwd).digest("hex").slice(0, 16);
893
+ }
894
+ }
895
+ function copyToClipboard(text) {
896
+ try {
897
+ if (process.platform === "darwin") {
898
+ (0, import_child_process.execSync)("pbcopy", { input: text });
899
+ return true;
900
+ }
901
+ if (process.platform === "linux") {
902
+ (0, import_child_process.execSync)("xclip -selection clipboard", { input: text });
903
+ return true;
904
+ }
905
+ if (process.platform === "win32") {
906
+ (0, import_child_process.execSync)("clip", { input: text });
907
+ return true;
908
+ }
909
+ return false;
910
+ } catch {
911
+ return false;
912
+ }
913
+ }
914
+ async function main() {
915
+ const options = parseArgs();
916
+ if (!(0, import_fs2.existsSync)(DB_PATH)) {
917
+ if (!options.quiet) {
918
+ console.error("memextend not initialized. Run `memextend init` first.");
919
+ }
920
+ console.log("<memextend-context>\nNo memories available.\n</memextend-context>");
921
+ process.exit(0);
922
+ }
923
+ const workspace = options.workspace ? (0, import_path2.resolve)(options.workspace) : process.cwd();
924
+ const projectId = getProjectId2(workspace);
925
+ const sqlite = new SQLiteStorage(DB_PATH);
926
+ const lancedb2 = await LanceDBStorage.create(VECTORS_PATH);
927
+ const embedder = await createEmbedFunction(MODELS_PATH);
928
+ try {
929
+ const retriever = new MemoryRetriever(sqlite, lancedb2, embedder.embedQuery, {
930
+ defaultLimit: options.limit ?? 0,
931
+ defaultRecentDays: options.days ?? 0
932
+ });
933
+ const project = sqlite.getProject(projectId);
934
+ if (!project) {
935
+ sqlite.insertProject({
936
+ id: projectId,
937
+ name: (0, import_path2.basename)(workspace),
938
+ path: workspace,
939
+ createdAt: (/* @__PURE__ */ new Date()).toISOString()
940
+ });
941
+ }
942
+ const context = await retriever.getContextForSession(projectId, {
943
+ includeGlobal: options.includeGlobal ?? true,
944
+ recentDays: options.days
945
+ });
946
+ const hasContent = context.recentMemories.length > 0 || context.globalProfile.length > 0 || context.relevantMemories && context.relevantMemories.length > 0;
947
+ let output;
948
+ if (options.format === "json") {
949
+ output = JSON.stringify({
950
+ projectId,
951
+ projectName: (0, import_path2.basename)(workspace),
952
+ workspace,
953
+ recentMemories: context.recentMemories,
954
+ globalProfile: context.globalProfile,
955
+ relevantMemories: context.relevantMemories,
956
+ hasContent
957
+ }, null, 2);
958
+ } else if (options.format === "markdown") {
959
+ if (!hasContent) {
960
+ output = `# Memory Context for ${(0, import_path2.basename)(workspace)}
961
+
962
+ No memories found. This might be a new project.
963
+ `;
964
+ } else {
965
+ output = formatContextAsMarkdown(context, (0, import_path2.basename)(workspace));
966
+ }
967
+ } else {
968
+ if (!hasContent) {
969
+ output = `<memextend-context>
970
+ ## Project: ${(0, import_path2.basename)(workspace)}
971
+
972
+ No memories found. This might be a new project.
973
+ </memextend-context>`;
974
+ } else {
975
+ output = formatContextForInjection(context);
976
+ }
977
+ }
978
+ if (options.clipboard) {
979
+ const copied = copyToClipboard(output);
980
+ if (!options.quiet) {
981
+ if (copied) {
982
+ console.error("Context copied to clipboard!");
983
+ } else {
984
+ console.error("Warning: Could not copy to clipboard");
985
+ }
986
+ }
987
+ }
988
+ console.log(output);
989
+ } finally {
990
+ sqlite.close();
991
+ await lancedb2.close();
992
+ await embedder.close();
993
+ }
994
+ }
995
+ function formatContextAsMarkdown(context, projectName) {
996
+ const lines = [`# Memory Context for ${projectName}`, ""];
997
+ if (context.recentMemories.length > 0) {
998
+ lines.push("## Recent Work");
999
+ lines.push("");
1000
+ for (const memory of context.recentMemories) {
1001
+ const date = new Date(memory.createdAt).toLocaleDateString();
1002
+ lines.push(`### ${date}`);
1003
+ lines.push("");
1004
+ lines.push(memory.content);
1005
+ lines.push("");
1006
+ }
1007
+ }
1008
+ if (context.globalProfile.length > 0) {
1009
+ lines.push("## User Preferences (Global)");
1010
+ lines.push("");
1011
+ for (const profile of context.globalProfile) {
1012
+ lines.push(`- **${profile.key}**: ${profile.content}`);
1013
+ }
1014
+ lines.push("");
1015
+ }
1016
+ if (context.relevantMemories && context.relevantMemories.length > 0) {
1017
+ lines.push("## Relevant Past Work");
1018
+ lines.push("");
1019
+ for (const result of context.relevantMemories) {
1020
+ lines.push(`- ${result.memory.content.split("\n")[0].slice(0, 100)}`);
1021
+ }
1022
+ lines.push("");
1023
+ }
1024
+ return lines.join("\n");
1025
+ }
1026
+ main().catch((error) => {
1027
+ console.error("Error:", error.message);
1028
+ process.exit(1);
1029
+ });