@memextend/claude-code 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.
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +9 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/logger.d.ts +3 -0
- package/dist/hooks/logger.d.ts.map +1 -0
- package/dist/hooks/logger.js +62 -0
- package/dist/hooks/logger.js.map +1 -0
- package/dist/hooks/pre-compact.cjs +1115 -0
- package/dist/hooks/pre-compact.d.ts +2 -0
- package/dist/hooks/pre-compact.d.ts.map +1 -0
- package/dist/hooks/pre-compact.js +303 -0
- package/dist/hooks/pre-compact.js.map +1 -0
- package/dist/hooks/session-start.cjs +1054 -0
- package/dist/hooks/session-start.d.ts +2 -0
- package/dist/hooks/session-start.d.ts.map +1 -0
- package/dist/hooks/session-start.js +205 -0
- package/dist/hooks/session-start.js.map +1 -0
- package/dist/hooks/stop.cjs +1059 -0
- package/dist/hooks/stop.d.ts +2 -0
- package/dist/hooks/stop.d.ts.map +1 -0
- package/dist/hooks/stop.js +234 -0
- package/dist/hooks/stop.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +2 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +6 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/server.cjs +14694 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/server.js +330 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +49 -0
- package/scripts/build-hooks.js +65 -0
|
@@ -0,0 +1,1059 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (let key of __getOwnPropNames(from))
|
|
11
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
12
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
13
|
+
}
|
|
14
|
+
return to;
|
|
15
|
+
};
|
|
16
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
17
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
18
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
19
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
20
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
21
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
22
|
+
mod
|
|
23
|
+
));
|
|
24
|
+
|
|
25
|
+
// src/hooks/stop.ts
|
|
26
|
+
var import_crypto = require("crypto");
|
|
27
|
+
var import_fs3 = require("fs");
|
|
28
|
+
var import_promises3 = require("fs/promises");
|
|
29
|
+
var import_path3 = require("path");
|
|
30
|
+
var import_os2 = 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 cosineSimilarity(a, b) {
|
|
554
|
+
if (a.length !== b.length) {
|
|
555
|
+
throw new Error(`Vector dimensions must match: ${a.length} vs ${b.length}`);
|
|
556
|
+
}
|
|
557
|
+
let dot = 0;
|
|
558
|
+
for (let i = 0; i < a.length; i++) {
|
|
559
|
+
dot += a[i] * b[i];
|
|
560
|
+
}
|
|
561
|
+
return dot;
|
|
562
|
+
}
|
|
563
|
+
function isModelAvailable(modelsDir) {
|
|
564
|
+
const modelPath = (0, import_path.join)(modelsDir, MODEL_FILENAME);
|
|
565
|
+
return (0, import_fs.existsSync)(modelPath);
|
|
566
|
+
}
|
|
567
|
+
async function createEmbedFunction(modelsDir) {
|
|
568
|
+
if (isModelAvailable(modelsDir)) {
|
|
569
|
+
try {
|
|
570
|
+
const embedding = await LocalEmbedding.create(modelsDir);
|
|
571
|
+
return {
|
|
572
|
+
embed: (text) => embedding.embed(text),
|
|
573
|
+
embedQuery: (text) => embedding.embedQuery(text),
|
|
574
|
+
isReal: true,
|
|
575
|
+
close: () => embedding.close()
|
|
576
|
+
};
|
|
577
|
+
} catch (error) {
|
|
578
|
+
console.error("[memextend] Failed to load embedding model, using fallback:", error);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
const { createHash: createHash2 } = await import("crypto");
|
|
582
|
+
const hashEmbed = (text, prefix) => {
|
|
583
|
+
const hash = createHash2("sha256").update(prefix + text).digest();
|
|
584
|
+
const vector = Array.from(hash).slice(0, 32);
|
|
585
|
+
const expanded = [];
|
|
586
|
+
for (let i = 0; i < 12; i++) {
|
|
587
|
+
const roundHash = createHash2("sha256").update(text + i.toString()).digest();
|
|
588
|
+
expanded.push(...Array.from(roundHash).slice(0, 32));
|
|
589
|
+
}
|
|
590
|
+
const norm = Math.sqrt(expanded.reduce((sum, v) => sum + v * v, 0));
|
|
591
|
+
return expanded.map((v) => v / norm / 255);
|
|
592
|
+
};
|
|
593
|
+
return {
|
|
594
|
+
embed: async (text) => hashEmbed(text, "doc:"),
|
|
595
|
+
embedQuery: async (text) => hashEmbed(text, "query:"),
|
|
596
|
+
isReal: false,
|
|
597
|
+
close: async () => {
|
|
598
|
+
}
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// ../../core/dist/memory/capture.js
|
|
603
|
+
var MIN_TEXT_LENGTH = 100;
|
|
604
|
+
var DEFAULT_MAX_REASONING_LENGTH = 1e4;
|
|
605
|
+
var DEFAULT_MAX_TOOL_OUTPUT_LENGTH = 2e3;
|
|
606
|
+
var CONFIGURABLE_TOOLS = ["Edit", "Write", "Bash", "Task"];
|
|
607
|
+
var DEFAULT_TOOL_CONFIG = {
|
|
608
|
+
Edit: false,
|
|
609
|
+
Write: false,
|
|
610
|
+
Bash: false,
|
|
611
|
+
Task: false
|
|
612
|
+
};
|
|
613
|
+
var TranscriptParser = class {
|
|
614
|
+
toolConfig;
|
|
615
|
+
maxReasoningLength;
|
|
616
|
+
maxToolOutputLength;
|
|
617
|
+
captureReasoning;
|
|
618
|
+
constructor(options = {}) {
|
|
619
|
+
if (options.toolConfig) {
|
|
620
|
+
this.toolConfig = { ...DEFAULT_TOOL_CONFIG, ...options.toolConfig };
|
|
621
|
+
} else if (options.captureTools) {
|
|
622
|
+
this.toolConfig = { ...DEFAULT_TOOL_CONFIG };
|
|
623
|
+
for (const tool of CONFIGURABLE_TOOLS) {
|
|
624
|
+
this.toolConfig[tool] = options.captureTools.has(tool);
|
|
625
|
+
}
|
|
626
|
+
} else {
|
|
627
|
+
this.toolConfig = { ...DEFAULT_TOOL_CONFIG };
|
|
628
|
+
}
|
|
629
|
+
if (options.maxContentLength !== void 0) {
|
|
630
|
+
this.maxReasoningLength = options.maxContentLength;
|
|
631
|
+
this.maxToolOutputLength = options.maxContentLength;
|
|
632
|
+
} else {
|
|
633
|
+
this.maxReasoningLength = options.maxReasoningLength ?? DEFAULT_MAX_REASONING_LENGTH;
|
|
634
|
+
this.maxToolOutputLength = options.maxToolOutputLength ?? DEFAULT_MAX_TOOL_OUTPUT_LENGTH;
|
|
635
|
+
}
|
|
636
|
+
this.captureReasoning = options.captureReasoning ?? true;
|
|
637
|
+
}
|
|
638
|
+
/**
|
|
639
|
+
* Parse a Claude Code JSONL transcript and extract captures
|
|
640
|
+
*
|
|
641
|
+
* Captures two types of content:
|
|
642
|
+
* 1. Claude's text responses (reasoning, explanations, decisions)
|
|
643
|
+
* 2. Tool calls for code changes (Edit, Write)
|
|
644
|
+
*
|
|
645
|
+
* Claude Code transcript format:
|
|
646
|
+
* - type: "assistant" with message.content containing text and tool_use objects
|
|
647
|
+
* - type: "user" with message.content containing tool_result objects
|
|
648
|
+
*/
|
|
649
|
+
parse(transcript) {
|
|
650
|
+
const lines = transcript.trim().split("\n");
|
|
651
|
+
const captures = [];
|
|
652
|
+
const pendingToolUses = /* @__PURE__ */ new Map();
|
|
653
|
+
for (const line of lines) {
|
|
654
|
+
if (!line.trim())
|
|
655
|
+
continue;
|
|
656
|
+
try {
|
|
657
|
+
const entry = JSON.parse(line);
|
|
658
|
+
if (!entry.message?.content || typeof entry.message.content === "string") {
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
const contentArray = entry.message.content;
|
|
662
|
+
if (entry.type === "assistant") {
|
|
663
|
+
if (this.captureReasoning) {
|
|
664
|
+
for (const block of contentArray) {
|
|
665
|
+
if (block.type === "text" && block.text) {
|
|
666
|
+
const text = block.text.trim();
|
|
667
|
+
if (text.length >= MIN_TEXT_LENGTH && this.isSubstantialContent(text)) {
|
|
668
|
+
captures.push({
|
|
669
|
+
type: "reasoning",
|
|
670
|
+
content: this.truncate(text, this.maxReasoningLength)
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
for (const block of contentArray) {
|
|
677
|
+
if (block.type === "tool_use" && block.name && block.id && block.input) {
|
|
678
|
+
if (this.shouldCapture(block.name)) {
|
|
679
|
+
pendingToolUses.set(block.id, {
|
|
680
|
+
tool: block.name,
|
|
681
|
+
input: block.input
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (entry.type === "user") {
|
|
688
|
+
for (const block of contentArray) {
|
|
689
|
+
if (block.type === "tool_result" && block.tool_use_id) {
|
|
690
|
+
const pending = pendingToolUses.get(block.tool_use_id);
|
|
691
|
+
if (pending) {
|
|
692
|
+
let output = "";
|
|
693
|
+
if (typeof block.content === "string") {
|
|
694
|
+
output = block.content;
|
|
695
|
+
} else if (Array.isArray(block.content)) {
|
|
696
|
+
output = block.content.filter((c) => c.type === "text" && c.text).map((c) => c.text).join("\n");
|
|
697
|
+
}
|
|
698
|
+
captures.push({
|
|
699
|
+
tool: pending.tool,
|
|
700
|
+
input: pending.input,
|
|
701
|
+
output: this.truncate(output, this.maxToolOutputLength)
|
|
702
|
+
});
|
|
703
|
+
pendingToolUses.delete(block.tool_use_id);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
} catch {
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return captures;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Check if text content is substantial (not just filler/transitional)
|
|
715
|
+
*/
|
|
716
|
+
isSubstantialContent(text) {
|
|
717
|
+
const skipPatterns = [
|
|
718
|
+
/^(let me|i'll|i will|now i|looking at|checking|searching)/i,
|
|
719
|
+
/^(here's|here is) (the|what|how)/i,
|
|
720
|
+
/^(done|completed|finished|success)/i,
|
|
721
|
+
/^(ok|okay|sure|yes|no|alright)/i
|
|
722
|
+
];
|
|
723
|
+
for (const pattern of skipPatterns) {
|
|
724
|
+
if (pattern.test(text.slice(0, 50))) {
|
|
725
|
+
if (text.length < 200) {
|
|
726
|
+
return false;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Legacy method for backward compatibility - returns only tool captures
|
|
734
|
+
*/
|
|
735
|
+
parseToolCaptures(transcript) {
|
|
736
|
+
return this.parse(transcript).filter((c) => "tool" in c);
|
|
737
|
+
}
|
|
738
|
+
shouldCapture(tool) {
|
|
739
|
+
if (CONFIGURABLE_TOOLS.includes(tool)) {
|
|
740
|
+
return this.toolConfig[tool] === true;
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
}
|
|
744
|
+
truncate(str, maxLen) {
|
|
745
|
+
if (str.length <= maxLen)
|
|
746
|
+
return str;
|
|
747
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
748
|
+
}
|
|
749
|
+
};
|
|
750
|
+
function isTextCapture(capture) {
|
|
751
|
+
return "type" in capture && capture.type === "reasoning";
|
|
752
|
+
}
|
|
753
|
+
function isToolCapture(capture) {
|
|
754
|
+
return "tool" in capture;
|
|
755
|
+
}
|
|
756
|
+
function formatCaptureContent(capture) {
|
|
757
|
+
if (isTextCapture(capture)) {
|
|
758
|
+
return capture.content;
|
|
759
|
+
}
|
|
760
|
+
return formatToolMemoryContent(capture.tool, capture.input, capture.output);
|
|
761
|
+
}
|
|
762
|
+
function formatToolMemoryContent(tool, input, output) {
|
|
763
|
+
const maxOutputLen = 200;
|
|
764
|
+
switch (tool) {
|
|
765
|
+
case "Edit": {
|
|
766
|
+
const filePath = input.file_path || "unknown file";
|
|
767
|
+
const oldStr = input.old_string || "";
|
|
768
|
+
const newStr = input.new_string || "";
|
|
769
|
+
let description = "";
|
|
770
|
+
if (!oldStr && newStr) {
|
|
771
|
+
description = "Added new content";
|
|
772
|
+
} else if (oldStr && !newStr) {
|
|
773
|
+
description = "Removed content";
|
|
774
|
+
} else {
|
|
775
|
+
description = "Modified content";
|
|
776
|
+
}
|
|
777
|
+
return `[Edit] ${filePath}
|
|
778
|
+
${description}. ${truncate(output, maxOutputLen)}`;
|
|
779
|
+
}
|
|
780
|
+
case "Write": {
|
|
781
|
+
const filePath = input.file_path || "unknown file";
|
|
782
|
+
const content = input.content || "";
|
|
783
|
+
const preview = content.slice(0, 100);
|
|
784
|
+
return `[Write] ${filePath}
|
|
785
|
+
Created new file. Preview: ${truncate(preview, maxOutputLen)}`;
|
|
786
|
+
}
|
|
787
|
+
case "Bash": {
|
|
788
|
+
const command = input.command || "unknown command";
|
|
789
|
+
return `[Bash] ${truncate(command, 100)}
|
|
790
|
+
Output: ${truncate(output, maxOutputLen)}`;
|
|
791
|
+
}
|
|
792
|
+
case "Task": {
|
|
793
|
+
const description = input.description || "Agent task";
|
|
794
|
+
const prompt = input.prompt || "";
|
|
795
|
+
return `[Task] ${description}
|
|
796
|
+
${truncate(prompt, 100)}
|
|
797
|
+
Result: ${truncate(output, 300)}`;
|
|
798
|
+
}
|
|
799
|
+
default:
|
|
800
|
+
return `[${tool}] ${truncate(output, maxOutputLen)}`;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
function truncate(str, maxLen) {
|
|
804
|
+
if (str.length <= maxLen)
|
|
805
|
+
return str;
|
|
806
|
+
return str.slice(0, maxLen - 3) + "...";
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// src/hooks/logger.ts
|
|
810
|
+
var import_fs2 = require("fs");
|
|
811
|
+
var import_path2 = require("path");
|
|
812
|
+
var import_os = require("os");
|
|
813
|
+
var MEMEXTEND_DIR = (0, import_path2.join)((0, import_os.homedir)(), ".memextend");
|
|
814
|
+
var LOGS_DIR = (0, import_path2.join)(MEMEXTEND_DIR, "logs");
|
|
815
|
+
var CONFIG_PATH = (0, import_path2.join)(MEMEXTEND_DIR, "config.json");
|
|
816
|
+
var debugEnabled = null;
|
|
817
|
+
function isDebugEnabled() {
|
|
818
|
+
if (debugEnabled !== null)
|
|
819
|
+
return debugEnabled;
|
|
820
|
+
try {
|
|
821
|
+
if ((0, import_fs2.existsSync)(CONFIG_PATH)) {
|
|
822
|
+
const config = JSON.parse((0, import_fs2.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
823
|
+
debugEnabled = config.debug === true;
|
|
824
|
+
} else {
|
|
825
|
+
debugEnabled = false;
|
|
826
|
+
}
|
|
827
|
+
} catch {
|
|
828
|
+
debugEnabled = false;
|
|
829
|
+
}
|
|
830
|
+
return debugEnabled;
|
|
831
|
+
}
|
|
832
|
+
function ensureLogsDir() {
|
|
833
|
+
if (!(0, import_fs2.existsSync)(LOGS_DIR)) {
|
|
834
|
+
(0, import_fs2.mkdirSync)(LOGS_DIR, { recursive: true });
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
function log(hook, message, data) {
|
|
838
|
+
if (!isDebugEnabled())
|
|
839
|
+
return;
|
|
840
|
+
try {
|
|
841
|
+
ensureLogsDir();
|
|
842
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
843
|
+
const logFile = (0, import_path2.join)(LOGS_DIR, "hooks.log");
|
|
844
|
+
const entry = data ? `[${timestamp}] [${hook}] ${message} ${JSON.stringify(data)}
|
|
845
|
+
` : `[${timestamp}] [${hook}] ${message}
|
|
846
|
+
`;
|
|
847
|
+
(0, import_fs2.appendFileSync)(logFile, entry);
|
|
848
|
+
} catch {
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
function logError(hook, error) {
|
|
852
|
+
try {
|
|
853
|
+
ensureLogsDir();
|
|
854
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
855
|
+
const logFile = (0, import_path2.join)(LOGS_DIR, "hooks.log");
|
|
856
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
857
|
+
const entry = `[${timestamp}] [${hook}] ERROR: ${errorMsg}
|
|
858
|
+
`;
|
|
859
|
+
(0, import_fs2.appendFileSync)(logFile, entry);
|
|
860
|
+
} catch {
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// src/hooks/stop.ts
|
|
865
|
+
var MEMEXTEND_DIR2 = (0, import_path3.join)((0, import_os2.homedir)(), ".memextend");
|
|
866
|
+
var CONFIG_PATH2 = (0, import_path3.join)(MEMEXTEND_DIR2, "config.json");
|
|
867
|
+
var DB_PATH = (0, import_path3.join)(MEMEXTEND_DIR2, "memextend.db");
|
|
868
|
+
var VECTORS_PATH = (0, import_path3.join)(MEMEXTEND_DIR2, "vectors");
|
|
869
|
+
var MODELS_PATH = (0, import_path3.join)(MEMEXTEND_DIR2, "models");
|
|
870
|
+
async function main() {
|
|
871
|
+
const chunks = [];
|
|
872
|
+
for await (const chunk of process.stdin) {
|
|
873
|
+
chunks.push(chunk);
|
|
874
|
+
}
|
|
875
|
+
const input = JSON.parse(Buffer.concat(chunks).toString());
|
|
876
|
+
log("Stop", "Hook fired", {
|
|
877
|
+
session_id: input.session_id,
|
|
878
|
+
cwd: input.cwd,
|
|
879
|
+
has_transcript: !!input.transcript_path
|
|
880
|
+
});
|
|
881
|
+
try {
|
|
882
|
+
if (!(0, import_fs3.existsSync)(DB_PATH)) {
|
|
883
|
+
log("Stop", "DB not found, skipping");
|
|
884
|
+
outputResult({ continue: true, suppressOutput: true });
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (!input.transcript_path || !(0, import_fs3.existsSync)(input.transcript_path)) {
|
|
888
|
+
log("Stop", "No transcript path, skipping");
|
|
889
|
+
outputResult({ continue: true, suppressOutput: true });
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
const config = await loadConfig();
|
|
893
|
+
const transcriptContent = await (0, import_promises3.readFile)(input.transcript_path, "utf-8");
|
|
894
|
+
const parser = new TranscriptParser({
|
|
895
|
+
toolConfig: config.capture?.tools ?? {},
|
|
896
|
+
maxReasoningLength: config.capture?.maxReasoningLength ?? 1e4,
|
|
897
|
+
maxToolOutputLength: config.capture?.maxToolOutputLength ?? 2e3,
|
|
898
|
+
captureReasoning: config.capture?.captureReasoning ?? true
|
|
899
|
+
});
|
|
900
|
+
const captures = parser.parse(transcriptContent);
|
|
901
|
+
if (captures.length === 0) {
|
|
902
|
+
log("Stop", "No captures found");
|
|
903
|
+
outputResult({ continue: true, suppressOutput: true });
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
const reasoningCount = captures.filter(isTextCapture).length;
|
|
907
|
+
const toolCount = captures.filter(isToolCapture).length;
|
|
908
|
+
log("Stop", `Found ${captures.length} captures to save`, { reasoning: reasoningCount, tools: toolCount });
|
|
909
|
+
const projectId = getProjectId2(input.cwd);
|
|
910
|
+
const sqlite = new SQLiteStorage(DB_PATH);
|
|
911
|
+
const lancedb2 = await LanceDBStorage.create(VECTORS_PATH);
|
|
912
|
+
const project = sqlite.getProject(projectId);
|
|
913
|
+
if (!project) {
|
|
914
|
+
sqlite.insertProject({
|
|
915
|
+
id: projectId,
|
|
916
|
+
name: (0, import_path3.basename)(input.cwd),
|
|
917
|
+
path: input.cwd,
|
|
918
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
const embedder = await createEmbedFunction(MODELS_PATH);
|
|
922
|
+
for (const capture of captures) {
|
|
923
|
+
const content = formatCaptureContent(capture);
|
|
924
|
+
const memoryId = (0, import_crypto.randomUUID)();
|
|
925
|
+
let memory;
|
|
926
|
+
if (isTextCapture(capture)) {
|
|
927
|
+
memory = {
|
|
928
|
+
id: memoryId,
|
|
929
|
+
projectId,
|
|
930
|
+
content,
|
|
931
|
+
type: "reasoning",
|
|
932
|
+
sourceTool: null,
|
|
933
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
934
|
+
sessionId: input.session_id,
|
|
935
|
+
metadata: null
|
|
936
|
+
};
|
|
937
|
+
} else {
|
|
938
|
+
memory = {
|
|
939
|
+
id: memoryId,
|
|
940
|
+
projectId,
|
|
941
|
+
content,
|
|
942
|
+
type: "tool_capture",
|
|
943
|
+
sourceTool: capture.tool,
|
|
944
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
945
|
+
sessionId: input.session_id,
|
|
946
|
+
metadata: {
|
|
947
|
+
input: capture.input,
|
|
948
|
+
outputPreview: capture.output.slice(0, 200)
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
sqlite.insertMemory(memory);
|
|
953
|
+
const vector = await embedder.embed(content);
|
|
954
|
+
await lancedb2.insertVector(memoryId, vector);
|
|
955
|
+
}
|
|
956
|
+
const dedupeOnPrune = config.storage?.deduplicateOnPrune ?? true;
|
|
957
|
+
const dedupeThreshold = config.retrieval?.deduplicationThreshold ?? 0.85;
|
|
958
|
+
if (dedupeOnPrune) {
|
|
959
|
+
const dedupedIds = await deduplicateStoredMemories(
|
|
960
|
+
sqlite,
|
|
961
|
+
lancedb2,
|
|
962
|
+
projectId,
|
|
963
|
+
dedupeThreshold
|
|
964
|
+
);
|
|
965
|
+
if (dedupedIds.length > 0) {
|
|
966
|
+
log("Stop", `Deduplicated ${dedupedIds.length} similar memories`);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
const maxPerProject = config.storage?.maxMemoriesPerProject ?? 500;
|
|
970
|
+
const maxTotal = config.storage?.maxTotalMemories ?? 5e3;
|
|
971
|
+
if (maxPerProject > 0 || maxTotal > 0) {
|
|
972
|
+
const pruneResult = sqlite.pruneMemories({
|
|
973
|
+
maxPerProject,
|
|
974
|
+
maxTotal,
|
|
975
|
+
projectId
|
|
976
|
+
});
|
|
977
|
+
if (pruneResult.deleted > 0) {
|
|
978
|
+
log("Stop", `Pruned ${pruneResult.deleted} old memories to stay within limits`);
|
|
979
|
+
for (const id of pruneResult.deletedIds) {
|
|
980
|
+
await lancedb2.deleteVector(id);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
sqlite.close();
|
|
985
|
+
await lancedb2.close();
|
|
986
|
+
await embedder.close();
|
|
987
|
+
log("Stop", `SUCCESS: Saved ${captures.length} memories`);
|
|
988
|
+
outputResult({ continue: true, suppressOutput: true });
|
|
989
|
+
} catch (error) {
|
|
990
|
+
logError("Stop", error);
|
|
991
|
+
console.error("[memextend] Stop hook error:", error);
|
|
992
|
+
outputResult({ continue: true, suppressOutput: true });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function getProjectId2(cwd) {
|
|
996
|
+
try {
|
|
997
|
+
const gitRoot = (0, import_child_process.execSync)("git rev-parse --show-toplevel", {
|
|
998
|
+
cwd,
|
|
999
|
+
encoding: "utf-8",
|
|
1000
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
1001
|
+
}).trim();
|
|
1002
|
+
return (0, import_crypto.createHash)("sha256").update(gitRoot).digest("hex").slice(0, 16);
|
|
1003
|
+
} catch {
|
|
1004
|
+
return (0, import_crypto.createHash)("sha256").update(cwd).digest("hex").slice(0, 16);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
async function loadConfig() {
|
|
1008
|
+
try {
|
|
1009
|
+
if ((0, import_fs3.existsSync)(CONFIG_PATH2)) {
|
|
1010
|
+
const content = await (0, import_promises3.readFile)(CONFIG_PATH2, "utf-8");
|
|
1011
|
+
return JSON.parse(content);
|
|
1012
|
+
}
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
return {};
|
|
1016
|
+
}
|
|
1017
|
+
function outputResult(result) {
|
|
1018
|
+
console.log(JSON.stringify(result));
|
|
1019
|
+
}
|
|
1020
|
+
async function deduplicateStoredMemories(sqlite, lancedb2, projectId, threshold) {
|
|
1021
|
+
const deletedIds = [];
|
|
1022
|
+
const memories = sqlite.getRecentMemories(projectId, 0, 0);
|
|
1023
|
+
if (memories.length < 2)
|
|
1024
|
+
return deletedIds;
|
|
1025
|
+
const memoryIds = memories.map((m) => m.id);
|
|
1026
|
+
const vectors = await lancedb2.getVectorsByIds(memoryIds);
|
|
1027
|
+
if (vectors.size < 2)
|
|
1028
|
+
return deletedIds;
|
|
1029
|
+
const keepIds = /* @__PURE__ */ new Set();
|
|
1030
|
+
const keptVectors = [];
|
|
1031
|
+
for (const memory of memories) {
|
|
1032
|
+
const vector = vectors.get(memory.id);
|
|
1033
|
+
if (!vector) {
|
|
1034
|
+
keepIds.add(memory.id);
|
|
1035
|
+
continue;
|
|
1036
|
+
}
|
|
1037
|
+
let isDuplicate = false;
|
|
1038
|
+
for (const kept of keptVectors) {
|
|
1039
|
+
const similarity = cosineSimilarity(vector, kept.vector);
|
|
1040
|
+
if (similarity > threshold) {
|
|
1041
|
+
isDuplicate = true;
|
|
1042
|
+
break;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (!isDuplicate) {
|
|
1046
|
+
keepIds.add(memory.id);
|
|
1047
|
+
keptVectors.push({ id: memory.id, vector });
|
|
1048
|
+
} else {
|
|
1049
|
+
sqlite.deleteMemory(memory.id);
|
|
1050
|
+
await lancedb2.deleteVector(memory.id);
|
|
1051
|
+
deletedIds.push(memory.id);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
return deletedIds;
|
|
1055
|
+
}
|
|
1056
|
+
main().catch((error) => {
|
|
1057
|
+
console.error("[memextend] Fatal error:", error);
|
|
1058
|
+
process.exit(0);
|
|
1059
|
+
});
|