fossel 1.1.0 → 1.2.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/README.md +91 -17
- package/dist/cli.js +739 -174
- package/dist/index.js +426 -73
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -138,6 +138,25 @@ var migrations = [
|
|
|
138
138
|
tx(rows);
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
name: "007_add_memory_embeddings",
|
|
144
|
+
apply: (db) => {
|
|
145
|
+
db.exec(`
|
|
146
|
+
CREATE TABLE IF NOT EXISTS memory_embeddings (
|
|
147
|
+
memory_rowid INTEGER PRIMARY KEY,
|
|
148
|
+
dim INTEGER NOT NULL,
|
|
149
|
+
version INTEGER NOT NULL,
|
|
150
|
+
vector BLOB NOT NULL,
|
|
151
|
+
updated_at INTEGER NOT NULL
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
CREATE TRIGGER IF NOT EXISTS memories_embeddings_ad
|
|
155
|
+
AFTER DELETE ON memories BEGIN
|
|
156
|
+
DELETE FROM memory_embeddings WHERE memory_rowid = old.rowid;
|
|
157
|
+
END;
|
|
158
|
+
`);
|
|
159
|
+
}
|
|
141
160
|
}
|
|
142
161
|
];
|
|
143
162
|
function runMigrations(db) {
|
|
@@ -195,6 +214,158 @@ function getDb() {
|
|
|
195
214
|
return dbInstance;
|
|
196
215
|
}
|
|
197
216
|
|
|
217
|
+
// src/lib/embeddings.ts
|
|
218
|
+
var EMBEDDING_DIM = 256;
|
|
219
|
+
var EMBEDDING_VERSION = 1;
|
|
220
|
+
function embeddingsEnabled() {
|
|
221
|
+
const value = process.env.FOSSEL_EMBEDDINGS?.trim().toLowerCase();
|
|
222
|
+
return value === "1" || value === "true" || value === "on" || value === "yes";
|
|
223
|
+
}
|
|
224
|
+
function fnv1a(str) {
|
|
225
|
+
let hash = 2166136261;
|
|
226
|
+
for (let i = 0; i < str.length; i += 1) {
|
|
227
|
+
hash ^= str.charCodeAt(i);
|
|
228
|
+
hash = Math.imul(hash, 16777619);
|
|
229
|
+
}
|
|
230
|
+
return hash >>> 0;
|
|
231
|
+
}
|
|
232
|
+
function tokenize(text) {
|
|
233
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim().split(" ").filter((token) => token.length >= 2);
|
|
234
|
+
}
|
|
235
|
+
function embedText(text) {
|
|
236
|
+
const vector = new Float32Array(EMBEDDING_DIM);
|
|
237
|
+
const tokens = tokenize(text);
|
|
238
|
+
if (tokens.length === 0) {
|
|
239
|
+
return vector;
|
|
240
|
+
}
|
|
241
|
+
const addFeature = (feature, weight) => {
|
|
242
|
+
const h = fnv1a(feature);
|
|
243
|
+
const index = h % EMBEDDING_DIM;
|
|
244
|
+
const sign = (fnv1a(`#${feature}`) & 1) === 0 ? 1 : -1;
|
|
245
|
+
vector[index] += sign * weight;
|
|
246
|
+
};
|
|
247
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
248
|
+
addFeature(tokens[i], 1);
|
|
249
|
+
if (i + 1 < tokens.length) {
|
|
250
|
+
addFeature(`${tokens[i]} ${tokens[i + 1]}`, 0.6);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
let norm = 0;
|
|
254
|
+
for (let i = 0; i < EMBEDDING_DIM; i += 1) {
|
|
255
|
+
norm += vector[i] * vector[i];
|
|
256
|
+
}
|
|
257
|
+
norm = Math.sqrt(norm);
|
|
258
|
+
if (norm > 0) {
|
|
259
|
+
for (let i = 0; i < EMBEDDING_DIM; i += 1) {
|
|
260
|
+
vector[i] /= norm;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return vector;
|
|
264
|
+
}
|
|
265
|
+
function cosineSimilarity(a, b) {
|
|
266
|
+
if (a.length !== b.length) {
|
|
267
|
+
return 0;
|
|
268
|
+
}
|
|
269
|
+
let dot = 0;
|
|
270
|
+
for (let i = 0; i < a.length; i += 1) {
|
|
271
|
+
dot += a[i] * b[i];
|
|
272
|
+
}
|
|
273
|
+
return dot;
|
|
274
|
+
}
|
|
275
|
+
function vectorToBuffer(vector) {
|
|
276
|
+
return Buffer.from(vector.buffer, vector.byteOffset, vector.byteLength);
|
|
277
|
+
}
|
|
278
|
+
function bufferToVector(buffer) {
|
|
279
|
+
const copy = Buffer.from(buffer);
|
|
280
|
+
return new Float32Array(
|
|
281
|
+
copy.buffer,
|
|
282
|
+
copy.byteOffset,
|
|
283
|
+
Math.floor(copy.byteLength / 4)
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/lib/vector-index.ts
|
|
288
|
+
function indexMemoryEmbedding(db, rowId, note) {
|
|
289
|
+
if (!embeddingsEnabled()) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
const vector = embedText(note);
|
|
293
|
+
db.prepare(
|
|
294
|
+
`
|
|
295
|
+
INSERT INTO memory_embeddings (memory_rowid, dim, version, vector, updated_at)
|
|
296
|
+
VALUES (?, ?, ?, ?, ?)
|
|
297
|
+
ON CONFLICT(memory_rowid) DO UPDATE SET
|
|
298
|
+
dim = excluded.dim,
|
|
299
|
+
version = excluded.version,
|
|
300
|
+
vector = excluded.vector,
|
|
301
|
+
updated_at = excluded.updated_at
|
|
302
|
+
`
|
|
303
|
+
).run(
|
|
304
|
+
rowId,
|
|
305
|
+
EMBEDDING_DIM,
|
|
306
|
+
EMBEDDING_VERSION,
|
|
307
|
+
vectorToBuffer(vector),
|
|
308
|
+
Math.floor(Date.now() / 1e3)
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
function backfillRepoEmbeddings(db, repo) {
|
|
312
|
+
if (!embeddingsEnabled()) {
|
|
313
|
+
return 0;
|
|
314
|
+
}
|
|
315
|
+
const rows = db.prepare(
|
|
316
|
+
`
|
|
317
|
+
SELECT m.rowid AS row_id, m.note
|
|
318
|
+
FROM memories AS m
|
|
319
|
+
LEFT JOIN memory_embeddings AS e ON e.memory_rowid = m.rowid
|
|
320
|
+
WHERE m.repo = ?
|
|
321
|
+
AND (e.memory_rowid IS NULL OR e.version != ? OR e.dim != ?)
|
|
322
|
+
`
|
|
323
|
+
).all(repo, EMBEDDING_VERSION, EMBEDDING_DIM);
|
|
324
|
+
if (rows.length === 0) {
|
|
325
|
+
return 0;
|
|
326
|
+
}
|
|
327
|
+
const tx = db.transaction((batch) => {
|
|
328
|
+
for (const row of batch) {
|
|
329
|
+
indexMemoryEmbedding(db, row.row_id, row.note);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
tx(rows);
|
|
333
|
+
return rows.length;
|
|
334
|
+
}
|
|
335
|
+
function vectorSearch(db, repo, query, limit) {
|
|
336
|
+
if (!embeddingsEnabled()) {
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
backfillRepoEmbeddings(db, repo);
|
|
340
|
+
const queryVector = embedText(query);
|
|
341
|
+
let queryNorm = 0;
|
|
342
|
+
for (let i = 0; i < queryVector.length; i += 1) {
|
|
343
|
+
queryNorm += queryVector[i] * queryVector[i];
|
|
344
|
+
}
|
|
345
|
+
if (queryNorm === 0) {
|
|
346
|
+
return [];
|
|
347
|
+
}
|
|
348
|
+
const rows = db.prepare(
|
|
349
|
+
`
|
|
350
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
351
|
+
m.created_at, m.updated_at, m.pinned, e.vector AS vector
|
|
352
|
+
FROM memory_embeddings AS e
|
|
353
|
+
JOIN memories AS m ON m.rowid = e.memory_rowid
|
|
354
|
+
WHERE m.repo = ? AND e.dim = ? AND e.version = ?
|
|
355
|
+
`
|
|
356
|
+
).all(repo, EMBEDDING_DIM, EMBEDDING_VERSION);
|
|
357
|
+
const scored = [];
|
|
358
|
+
for (const row of rows) {
|
|
359
|
+
const { vector, ...memory } = row;
|
|
360
|
+
const score = cosineSimilarity(queryVector, bufferToVector(vector));
|
|
361
|
+
if (score > 0) {
|
|
362
|
+
scored.push({ ...memory, score });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
scored.sort((a, b) => b.score - a.score);
|
|
366
|
+
return scored.slice(0, limit);
|
|
367
|
+
}
|
|
368
|
+
|
|
198
369
|
// src/lib/context.ts
|
|
199
370
|
var SECTION_TITLES = {
|
|
200
371
|
convention: "Conventions",
|
|
@@ -212,6 +383,9 @@ function parseTags(raw) {
|
|
|
212
383
|
return [];
|
|
213
384
|
}
|
|
214
385
|
}
|
|
386
|
+
function normalizeNoteForReadDedupe(text) {
|
|
387
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
388
|
+
}
|
|
215
389
|
function buildFtsQuery(query) {
|
|
216
390
|
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter((term) => term.length > 0);
|
|
217
391
|
if (terms.length === 0) {
|
|
@@ -222,11 +396,19 @@ function buildFtsQuery(query) {
|
|
|
222
396
|
function fetchRepoContext(db, repo, limit, query) {
|
|
223
397
|
const rows = [];
|
|
224
398
|
const seen = /* @__PURE__ */ new Set();
|
|
399
|
+
const seenNormalized = /* @__PURE__ */ new Set();
|
|
225
400
|
const push = (memory, source, rank) => {
|
|
226
401
|
if (seen.has(memory.row_id)) {
|
|
227
402
|
return;
|
|
228
403
|
}
|
|
404
|
+
const normalized = normalizeNoteForReadDedupe(memory.note);
|
|
405
|
+
if (normalized && seenNormalized.has(normalized)) {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
229
408
|
seen.add(memory.row_id);
|
|
409
|
+
if (normalized) {
|
|
410
|
+
seenNormalized.add(normalized);
|
|
411
|
+
}
|
|
230
412
|
rows.push({ ...memory, source, rank });
|
|
231
413
|
};
|
|
232
414
|
const pinned = db.prepare(
|
|
@@ -241,7 +423,10 @@ function fetchRepoContext(db, repo, limit, query) {
|
|
|
241
423
|
for (const row of pinned) {
|
|
242
424
|
push(row, "pinned");
|
|
243
425
|
}
|
|
244
|
-
|
|
426
|
+
const pushRecent = () => {
|
|
427
|
+
if (rows.length >= limit) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
245
430
|
const recent = db.prepare(
|
|
246
431
|
`
|
|
247
432
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
@@ -250,13 +435,20 @@ function fetchRepoContext(db, repo, limit, query) {
|
|
|
250
435
|
ORDER BY updated_at DESC
|
|
251
436
|
LIMIT ?
|
|
252
437
|
`
|
|
253
|
-
).all(repo, limit
|
|
438
|
+
).all(repo, limit);
|
|
254
439
|
for (const row of recent) {
|
|
255
440
|
push(row, "recent");
|
|
441
|
+
if (rows.length >= limit) {
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
256
444
|
}
|
|
445
|
+
};
|
|
446
|
+
if (!query) {
|
|
447
|
+
pushRecent();
|
|
257
448
|
}
|
|
258
449
|
if (query && rows.length < limit) {
|
|
259
450
|
const ftsQuery = buildFtsQuery(query);
|
|
451
|
+
const ftsRows = [];
|
|
260
452
|
if (ftsQuery) {
|
|
261
453
|
try {
|
|
262
454
|
const matches = db.prepare(
|
|
@@ -270,15 +462,49 @@ function fetchRepoContext(db, repo, limit, query) {
|
|
|
270
462
|
LIMIT ?
|
|
271
463
|
`
|
|
272
464
|
).all(ftsQuery, repo, limit);
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
465
|
+
ftsRows.push(...matches);
|
|
466
|
+
} catch {
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const vectorRows = embeddingsEnabled() ? vectorSearch(db, repo, query, limit) : [];
|
|
470
|
+
if (vectorRows.length > 0) {
|
|
471
|
+
const RRF_K = 60;
|
|
472
|
+
const fused = /* @__PURE__ */ new Map();
|
|
473
|
+
const accumulate = (list, rankOf) => {
|
|
474
|
+
list.forEach((memory, index) => {
|
|
475
|
+
const contribution = 1 / (RRF_K + index + 1);
|
|
476
|
+
const prior = fused.get(memory.row_id);
|
|
477
|
+
if (prior) {
|
|
478
|
+
prior.score += contribution;
|
|
479
|
+
} else {
|
|
480
|
+
fused.set(memory.row_id, {
|
|
481
|
+
memory,
|
|
482
|
+
score: contribution,
|
|
483
|
+
rank: rankOf?.(memory, index)
|
|
484
|
+
});
|
|
277
485
|
}
|
|
486
|
+
});
|
|
487
|
+
};
|
|
488
|
+
accumulate(ftsRows, (m) => m.rank);
|
|
489
|
+
accumulate(vectorRows);
|
|
490
|
+
const ordered = Array.from(fused.values()).sort(
|
|
491
|
+
(a, b) => b.score - a.score
|
|
492
|
+
);
|
|
493
|
+
for (const { memory, rank } of ordered) {
|
|
494
|
+
push(memory, "search", rank);
|
|
495
|
+
if (rows.length >= limit) {
|
|
496
|
+
break;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
} else {
|
|
500
|
+
for (const row of ftsRows) {
|
|
501
|
+
push(row, "search", row.rank);
|
|
502
|
+
if (rows.length >= limit) {
|
|
503
|
+
break;
|
|
278
504
|
}
|
|
279
|
-
} catch {
|
|
280
505
|
}
|
|
281
506
|
}
|
|
507
|
+
pushRecent();
|
|
282
508
|
}
|
|
283
509
|
return rows.slice(0, limit);
|
|
284
510
|
}
|
|
@@ -472,6 +698,15 @@ function resolveRepoArg(input, cwd, db) {
|
|
|
472
698
|
};
|
|
473
699
|
}
|
|
474
700
|
|
|
701
|
+
// src/lib/workspace.ts
|
|
702
|
+
function getWorkspaceRoot() {
|
|
703
|
+
const fromEnv = process.env.FOSSEL_WORKSPACE?.trim();
|
|
704
|
+
if (fromEnv) {
|
|
705
|
+
return fromEnv;
|
|
706
|
+
}
|
|
707
|
+
return process.cwd();
|
|
708
|
+
}
|
|
709
|
+
|
|
475
710
|
// src/tools/dedupe-repo.ts
|
|
476
711
|
import { z } from "zod";
|
|
477
712
|
|
|
@@ -481,7 +716,7 @@ var DEFAULT_CANDIDATE_LIMIT = 200;
|
|
|
481
716
|
function normalizeText(text) {
|
|
482
717
|
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
483
718
|
}
|
|
484
|
-
function
|
|
719
|
+
function tokenize2(text) {
|
|
485
720
|
return normalizeText(text).split(" ").filter((token) => token.length >= 2);
|
|
486
721
|
}
|
|
487
722
|
function trigrams(text) {
|
|
@@ -517,7 +752,7 @@ function similarity(a, b) {
|
|
|
517
752
|
if (normalizedA === normalizedB) {
|
|
518
753
|
return 1;
|
|
519
754
|
}
|
|
520
|
-
const wordScore = jaccard(new Set(
|
|
755
|
+
const wordScore = jaccard(new Set(tokenize2(normalizedA)), new Set(tokenize2(normalizedB)));
|
|
521
756
|
const triScore = jaccard(trigrams(normalizedA), trigrams(normalizedB));
|
|
522
757
|
return wordScore * 0.55 + triScore * 0.45;
|
|
523
758
|
}
|
|
@@ -606,7 +841,7 @@ function registerDedupeRepoTool(server) {
|
|
|
606
841
|
async ({ repo, threshold, apply }) => {
|
|
607
842
|
try {
|
|
608
843
|
const db = getDb();
|
|
609
|
-
const resolved = resolveRepoArg(repo,
|
|
844
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
610
845
|
const rows = db.prepare(
|
|
611
846
|
`
|
|
612
847
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json
|
|
@@ -734,21 +969,56 @@ Re-run with apply=true to merge.`
|
|
|
734
969
|
|
|
735
970
|
// src/tools/delete.ts
|
|
736
971
|
import { z as z2 } from "zod";
|
|
972
|
+
|
|
973
|
+
// src/lib/memory.ts
|
|
974
|
+
function findMemoryByAnyId(db, input) {
|
|
975
|
+
const numeric = typeof input === "number" ? input : Number(input);
|
|
976
|
+
const isNumericId = Number.isInteger(numeric) && numeric > 0 && String(numeric) === String(input).trim();
|
|
977
|
+
if (isNumericId) {
|
|
978
|
+
const row = db.prepare(
|
|
979
|
+
`
|
|
980
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
981
|
+
FROM memories
|
|
982
|
+
WHERE rowid = ?
|
|
983
|
+
`
|
|
984
|
+
).get(numeric);
|
|
985
|
+
if (row) {
|
|
986
|
+
return row;
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
const stringInput = String(input).trim();
|
|
990
|
+
if (stringInput.length === 0) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
const stringRow = db.prepare(
|
|
994
|
+
`
|
|
995
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
996
|
+
FROM memories
|
|
997
|
+
WHERE id = ?
|
|
998
|
+
`
|
|
999
|
+
).get(stringInput);
|
|
1000
|
+
return stringRow ?? null;
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
// src/tools/delete.ts
|
|
737
1004
|
var deleteMemoryInputSchema = {
|
|
738
|
-
|
|
1005
|
+
// Accept either the numeric row_id or the legacy nanoid string. Tools used
|
|
1006
|
+
// to disagree about which form to take; this unifies them so callers can
|
|
1007
|
+
// paste whichever id they have in front of them.
|
|
1008
|
+
id: z2.union([z2.number().int().positive(), z2.string().trim().min(1)])
|
|
739
1009
|
};
|
|
740
1010
|
function registerDeleteMemoryTool(server) {
|
|
741
1011
|
server.registerTool(
|
|
742
1012
|
"delete_memory",
|
|
743
1013
|
{
|
|
744
|
-
description: "Delete a memory from storage by id.",
|
|
1014
|
+
description: "Delete a memory from storage by id. Accepts either the numeric row id or the legacy string id.",
|
|
745
1015
|
inputSchema: deleteMemoryInputSchema
|
|
746
1016
|
},
|
|
747
1017
|
async ({ id }) => {
|
|
748
1018
|
try {
|
|
749
1019
|
const db = getDb();
|
|
750
|
-
const
|
|
751
|
-
if (!
|
|
1020
|
+
const memory = findMemoryByAnyId(db, id);
|
|
1021
|
+
if (!memory) {
|
|
752
1022
|
return {
|
|
753
1023
|
isError: true,
|
|
754
1024
|
content: [
|
|
@@ -759,15 +1029,15 @@ function registerDeleteMemoryTool(server) {
|
|
|
759
1029
|
]
|
|
760
1030
|
};
|
|
761
1031
|
}
|
|
762
|
-
const deleteTx = db.transaction((
|
|
763
|
-
db.prepare("DELETE FROM memories WHERE
|
|
1032
|
+
const deleteTx = db.transaction((rowId) => {
|
|
1033
|
+
db.prepare("DELETE FROM memories WHERE rowid = ?").run(rowId);
|
|
764
1034
|
});
|
|
765
|
-
deleteTx(
|
|
1035
|
+
deleteTx(memory.row_id);
|
|
766
1036
|
return {
|
|
767
1037
|
content: [
|
|
768
1038
|
{
|
|
769
1039
|
type: "text",
|
|
770
|
-
text: `Deleted memory ${id}.`
|
|
1040
|
+
text: `Deleted memory ${memory.row_id} (legacy: ${memory.id}).`
|
|
771
1041
|
}
|
|
772
1042
|
]
|
|
773
1043
|
};
|
|
@@ -805,7 +1075,7 @@ function registerGetContextTool(server) {
|
|
|
805
1075
|
async ({ repo, query, limit, format }) => {
|
|
806
1076
|
try {
|
|
807
1077
|
const db = getDb();
|
|
808
|
-
const resolved = resolveRepoArg(repo,
|
|
1078
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
809
1079
|
const rows = fetchRepoContext(db, resolved.canonical, limit, query);
|
|
810
1080
|
const text = formatContext(rows, {
|
|
811
1081
|
repo: resolved.canonical,
|
|
@@ -863,7 +1133,7 @@ function registerGetRepoContextTool(server) {
|
|
|
863
1133
|
async ({ repo, limit }) => {
|
|
864
1134
|
try {
|
|
865
1135
|
const db = getDb();
|
|
866
|
-
const resolved = resolveRepoArg(repo,
|
|
1136
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
867
1137
|
const rows = db.prepare(
|
|
868
1138
|
`
|
|
869
1139
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
@@ -932,9 +1202,10 @@ ${sections.join("\n\n")}`
|
|
|
932
1202
|
// src/tools/pin.ts
|
|
933
1203
|
import { z as z5 } from "zod";
|
|
934
1204
|
var pinInputSchema = {
|
|
935
|
-
id
|
|
1205
|
+
// Accept numeric row_id or legacy string id for parity with the other tools.
|
|
1206
|
+
id: z5.union([z5.number().int().positive(), z5.string().trim().min(1)])
|
|
936
1207
|
};
|
|
937
|
-
function setPinnedState(
|
|
1208
|
+
function setPinnedState(rowId, pinned) {
|
|
938
1209
|
const db = getDb();
|
|
939
1210
|
const now = Math.floor(Date.now() / 1e3);
|
|
940
1211
|
const updateResult = db.prepare(
|
|
@@ -943,7 +1214,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
943
1214
|
SET pinned = ?, updated_at = ?
|
|
944
1215
|
WHERE rowid = ?
|
|
945
1216
|
`
|
|
946
|
-
).run(pinned, now,
|
|
1217
|
+
).run(pinned, now, rowId);
|
|
947
1218
|
if (updateResult.changes === 0) {
|
|
948
1219
|
return null;
|
|
949
1220
|
}
|
|
@@ -953,7 +1224,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
953
1224
|
FROM memories
|
|
954
1225
|
WHERE rowid = ?
|
|
955
1226
|
`
|
|
956
|
-
).get(
|
|
1227
|
+
).get(rowId);
|
|
957
1228
|
}
|
|
958
1229
|
function registerPinMemoryTool(server) {
|
|
959
1230
|
server.registerTool(
|
|
@@ -964,7 +1235,20 @@ function registerPinMemoryTool(server) {
|
|
|
964
1235
|
},
|
|
965
1236
|
async ({ id }) => {
|
|
966
1237
|
try {
|
|
967
|
-
const
|
|
1238
|
+
const db = getDb();
|
|
1239
|
+
const target = findMemoryByAnyId(db, id);
|
|
1240
|
+
if (!target) {
|
|
1241
|
+
return {
|
|
1242
|
+
isError: true,
|
|
1243
|
+
content: [
|
|
1244
|
+
{
|
|
1245
|
+
type: "text",
|
|
1246
|
+
text: `Memory ${id} not found.`
|
|
1247
|
+
}
|
|
1248
|
+
]
|
|
1249
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const memory = setPinnedState(target.row_id, 1);
|
|
968
1252
|
if (!memory) {
|
|
969
1253
|
return {
|
|
970
1254
|
isError: true,
|
|
@@ -1008,7 +1292,20 @@ function registerUnpinMemoryTool(server) {
|
|
|
1008
1292
|
},
|
|
1009
1293
|
async ({ id }) => {
|
|
1010
1294
|
try {
|
|
1011
|
-
const
|
|
1295
|
+
const db = getDb();
|
|
1296
|
+
const target = findMemoryByAnyId(db, id);
|
|
1297
|
+
if (!target) {
|
|
1298
|
+
return {
|
|
1299
|
+
isError: true,
|
|
1300
|
+
content: [
|
|
1301
|
+
{
|
|
1302
|
+
type: "text",
|
|
1303
|
+
text: `Memory ${id} not found.`
|
|
1304
|
+
}
|
|
1305
|
+
]
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
const memory = setPinnedState(target.row_id, 0);
|
|
1012
1309
|
if (!memory) {
|
|
1013
1310
|
return {
|
|
1014
1311
|
isError: true,
|
|
@@ -1425,7 +1722,7 @@ function registerRememberTool(server) {
|
|
|
1425
1722
|
async ({ note, repo, type, tags }) => {
|
|
1426
1723
|
try {
|
|
1427
1724
|
const db = getDb();
|
|
1428
|
-
const resolved = resolveRepoArg(repo,
|
|
1725
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1429
1726
|
const inferred = inferMemoryFromNote(note);
|
|
1430
1727
|
const finalType = type ?? inferred.type;
|
|
1431
1728
|
const finalTags = mergeTagLists2(tags, inferred.tags).slice(0, 5);
|
|
@@ -1463,6 +1760,7 @@ function registerRememberTool(server) {
|
|
|
1463
1760
|
now,
|
|
1464
1761
|
existing.row_id
|
|
1465
1762
|
);
|
|
1763
|
+
indexMemoryEmbedding(db, existing.row_id, longerNote);
|
|
1466
1764
|
return {
|
|
1467
1765
|
content: [
|
|
1468
1766
|
{
|
|
@@ -1503,6 +1801,9 @@ function registerRememberTool(server) {
|
|
|
1503
1801
|
normalizeText(note)
|
|
1504
1802
|
);
|
|
1505
1803
|
const inserted = db.prepare("SELECT rowid AS row_id FROM memories WHERE id = ?").get(id);
|
|
1804
|
+
if (inserted) {
|
|
1805
|
+
indexMemoryEmbedding(db, inserted.row_id, note);
|
|
1806
|
+
}
|
|
1506
1807
|
return {
|
|
1507
1808
|
content: [
|
|
1508
1809
|
{
|
|
@@ -1542,7 +1843,7 @@ function registerResolveRepoTool(server) {
|
|
|
1542
1843
|
async ({ cwd }) => {
|
|
1543
1844
|
try {
|
|
1544
1845
|
const db = getDb();
|
|
1545
|
-
const target = cwd?.trim() ||
|
|
1846
|
+
const target = cwd?.trim() || getWorkspaceRoot();
|
|
1546
1847
|
const resolved = resolveRepo(target, db);
|
|
1547
1848
|
const payload = {
|
|
1548
1849
|
canonical: resolved.canonical,
|
|
@@ -1582,12 +1883,20 @@ var searchMemoryInputSchema = {
|
|
|
1582
1883
|
repo: z8.string().trim().min(1).optional(),
|
|
1583
1884
|
limit: z8.number().int().positive().max(50).default(5)
|
|
1584
1885
|
};
|
|
1585
|
-
function
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1886
|
+
function tokenizeQuery(query) {
|
|
1887
|
+
return query.toLowerCase().replace(/["()]/g, " ").split(/[\s/_\-.,;:!?]+/).map((token) => token.replace(/[^a-z0-9*]/g, "")).filter((token) => token.length >= 2);
|
|
1888
|
+
}
|
|
1889
|
+
function buildFtsQuery2(tokens) {
|
|
1890
|
+
if (tokens.length === 0) {
|
|
1891
|
+
return null;
|
|
1589
1892
|
}
|
|
1590
|
-
return
|
|
1893
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" AND ");
|
|
1894
|
+
}
|
|
1895
|
+
function buildFtsQueryOr(tokens) {
|
|
1896
|
+
if (tokens.length === 0) {
|
|
1897
|
+
return null;
|
|
1898
|
+
}
|
|
1899
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR ");
|
|
1591
1900
|
}
|
|
1592
1901
|
function parseTags4(raw) {
|
|
1593
1902
|
try {
|
|
@@ -1597,37 +1906,73 @@ function parseTags4(raw) {
|
|
|
1597
1906
|
return [];
|
|
1598
1907
|
}
|
|
1599
1908
|
}
|
|
1909
|
+
function runFts(ftsQuery, resolvedRepo, limit) {
|
|
1910
|
+
const db = getDb();
|
|
1911
|
+
try {
|
|
1912
|
+
if (resolvedRepo) {
|
|
1913
|
+
return db.prepare(
|
|
1914
|
+
`
|
|
1915
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1916
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1917
|
+
FROM memories_fts
|
|
1918
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1919
|
+
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
1920
|
+
ORDER BY rank
|
|
1921
|
+
LIMIT ?
|
|
1922
|
+
`
|
|
1923
|
+
).all(ftsQuery, resolvedRepo, limit);
|
|
1924
|
+
}
|
|
1925
|
+
return db.prepare(
|
|
1926
|
+
`
|
|
1927
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1928
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1929
|
+
FROM memories_fts
|
|
1930
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1931
|
+
WHERE memories_fts MATCH ?
|
|
1932
|
+
ORDER BY rank
|
|
1933
|
+
LIMIT ?
|
|
1934
|
+
`
|
|
1935
|
+
).all(ftsQuery, limit);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return [];
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1600
1940
|
function registerSearchMemoryTool(server) {
|
|
1601
1941
|
server.registerTool(
|
|
1602
1942
|
"search_memory",
|
|
1603
1943
|
{
|
|
1604
|
-
description: "Search memories using full-text search with optional repository filtering.",
|
|
1944
|
+
description: "Search memories using full-text search with optional repository filtering. Falls back to recent + pinned context when the query has no exact matches.",
|
|
1605
1945
|
inputSchema: searchMemoryInputSchema
|
|
1606
1946
|
},
|
|
1607
1947
|
async ({ query, repo, limit }) => {
|
|
1608
1948
|
try {
|
|
1609
1949
|
const db = getDb();
|
|
1610
|
-
const
|
|
1611
|
-
const resolvedRepo = repo ? resolveRepoArg(repo,
|
|
1612
|
-
const
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1950
|
+
const tokens = tokenizeQuery(query);
|
|
1951
|
+
const resolvedRepo = repo ? resolveRepoArg(repo, getWorkspaceRoot(), db).canonical : void 0;
|
|
1952
|
+
const andQuery = buildFtsQuery2(tokens);
|
|
1953
|
+
let rows = [];
|
|
1954
|
+
if (andQuery) {
|
|
1955
|
+
rows = runFts(andQuery, resolvedRepo, limit);
|
|
1956
|
+
}
|
|
1957
|
+
if (rows.length === 0 && tokens.length > 1) {
|
|
1958
|
+
const orQuery = buildFtsQueryOr(tokens);
|
|
1959
|
+
if (orQuery) {
|
|
1960
|
+
rows = runFts(orQuery, resolvedRepo, limit);
|
|
1961
|
+
}
|
|
1962
|
+
}
|
|
1963
|
+
if (rows.length === 0 && resolvedRepo && embeddingsEnabled()) {
|
|
1964
|
+
const semantic = vectorSearch(db, resolvedRepo, query, limit);
|
|
1965
|
+
rows = semantic.map(({ score, ...row }) => ({
|
|
1966
|
+
...row,
|
|
1967
|
+
rank: score
|
|
1968
|
+
}));
|
|
1969
|
+
}
|
|
1970
|
+
let usedFallback = false;
|
|
1971
|
+
if (rows.length === 0 && resolvedRepo) {
|
|
1972
|
+
const fallback = fetchRepoContext(db, resolvedRepo, limit);
|
|
1973
|
+
rows = fallback.map((row) => ({ ...row, rank: 0 }));
|
|
1974
|
+
usedFallback = fallback.length > 0;
|
|
1975
|
+
}
|
|
1631
1976
|
if (rows.length === 0) {
|
|
1632
1977
|
return {
|
|
1633
1978
|
content: [
|
|
@@ -1645,11 +1990,12 @@ function registerSearchMemoryTool(server) {
|
|
|
1645
1990
|
return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
|
|
1646
1991
|
${pinPrefix}${row.note}${tagsText}`;
|
|
1647
1992
|
}).join("\n\n");
|
|
1993
|
+
const header = usedFallback ? `No exact match for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}; showing recent + pinned context:` : `Search results for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}:`;
|
|
1648
1994
|
return {
|
|
1649
1995
|
content: [
|
|
1650
1996
|
{
|
|
1651
1997
|
type: "text",
|
|
1652
|
-
text:
|
|
1998
|
+
text: `${header}
|
|
1653
1999
|
|
|
1654
2000
|
${formatted}`
|
|
1655
2001
|
}
|
|
@@ -1690,7 +2036,7 @@ function registerStoreContextTool(server) {
|
|
|
1690
2036
|
async ({ repo, type, note, tags }) => {
|
|
1691
2037
|
try {
|
|
1692
2038
|
const db = getDb();
|
|
1693
|
-
const resolved = resolveRepoArg(repo,
|
|
2039
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1694
2040
|
const now = Math.floor(Date.now() / 1e3);
|
|
1695
2041
|
const id = nanoid2();
|
|
1696
2042
|
const normalizedTags = Array.from(
|
|
@@ -1718,6 +2064,9 @@ function registerStoreContextTool(server) {
|
|
|
1718
2064
|
WHERE id = ?
|
|
1719
2065
|
`
|
|
1720
2066
|
).get(id);
|
|
2067
|
+
if (stored) {
|
|
2068
|
+
indexMemoryEmbedding(db, stored.row_id, note);
|
|
2069
|
+
}
|
|
1721
2070
|
return {
|
|
1722
2071
|
content: [
|
|
1723
2072
|
{
|
|
@@ -1765,7 +2114,7 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
1765
2114
|
async ({ repo }) => {
|
|
1766
2115
|
try {
|
|
1767
2116
|
const db = getDb();
|
|
1768
|
-
const resolved = resolveRepoArg(repo,
|
|
2117
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1769
2118
|
const rows = db.prepare(
|
|
1770
2119
|
`
|
|
1771
2120
|
SELECT rowid AS row_id, type, note, pinned
|
|
@@ -1827,7 +2176,9 @@ ${entries.join("\n")}`);
|
|
|
1827
2176
|
// src/tools/update.ts
|
|
1828
2177
|
import { z as z11 } from "zod";
|
|
1829
2178
|
var updateMemoryInputSchema = {
|
|
1830
|
-
id
|
|
2179
|
+
// Accept numeric row_id or legacy string id so callers can paste whichever
|
|
2180
|
+
// form they have.
|
|
2181
|
+
id: z11.union([z11.number().int().positive(), z11.string().trim().min(1)]),
|
|
1831
2182
|
content: z11.string().trim().min(1).optional(),
|
|
1832
2183
|
memory_type: z11.enum(MEMORY_TYPES).optional()
|
|
1833
2184
|
};
|
|
@@ -1859,7 +2210,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
1859
2210
|
server.registerTool(
|
|
1860
2211
|
"update_memory",
|
|
1861
2212
|
{
|
|
1862
|
-
description: "Update an existing memory by numeric
|
|
2213
|
+
description: "Update an existing memory by id (numeric or legacy string) with partial fields.",
|
|
1863
2214
|
inputSchema: updateMemoryInputSchema
|
|
1864
2215
|
},
|
|
1865
2216
|
async ({ id, content, memory_type }) => {
|
|
@@ -1876,14 +2227,8 @@ function registerUpdateMemoryTool(server) {
|
|
|
1876
2227
|
};
|
|
1877
2228
|
}
|
|
1878
2229
|
const db = getDb();
|
|
1879
|
-
const
|
|
1880
|
-
|
|
1881
|
-
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
1882
|
-
FROM memories
|
|
1883
|
-
WHERE rowid = ?
|
|
1884
|
-
`
|
|
1885
|
-
).get(id);
|
|
1886
|
-
if (!existing) {
|
|
2230
|
+
const target = findMemoryByAnyId(db, id);
|
|
2231
|
+
if (!target) {
|
|
1887
2232
|
return {
|
|
1888
2233
|
isError: true,
|
|
1889
2234
|
content: [
|
|
@@ -1894,6 +2239,13 @@ function registerUpdateMemoryTool(server) {
|
|
|
1894
2239
|
]
|
|
1895
2240
|
};
|
|
1896
2241
|
}
|
|
2242
|
+
const existing = db.prepare(
|
|
2243
|
+
`
|
|
2244
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
2245
|
+
FROM memories
|
|
2246
|
+
WHERE rowid = ?
|
|
2247
|
+
`
|
|
2248
|
+
).get(target.row_id);
|
|
1897
2249
|
const now = Math.floor(Date.now() / 1e3);
|
|
1898
2250
|
const nextType = memory_type ?? existing.type;
|
|
1899
2251
|
const nextNote = content ?? existing.note;
|
|
@@ -1905,7 +2257,8 @@ function registerUpdateMemoryTool(server) {
|
|
|
1905
2257
|
SET type = ?, note = ?, note_normalized = ?, updated_at = ?
|
|
1906
2258
|
WHERE rowid = ?
|
|
1907
2259
|
`
|
|
1908
|
-
).run(nextType, nextNote, nextNormalized, now,
|
|
2260
|
+
).run(nextType, nextNote, nextNormalized, now, existing.row_id);
|
|
2261
|
+
indexMemoryEmbedding(db, existing.row_id, nextNote);
|
|
1909
2262
|
} else {
|
|
1910
2263
|
db.prepare(
|
|
1911
2264
|
`
|
|
@@ -1913,7 +2266,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
1913
2266
|
SET type = ?, note = ?, updated_at = ?
|
|
1914
2267
|
WHERE rowid = ?
|
|
1915
2268
|
`
|
|
1916
|
-
).run(nextType, nextNote, now,
|
|
2269
|
+
).run(nextType, nextNote, now, existing.row_id);
|
|
1917
2270
|
}
|
|
1918
2271
|
const updated = db.prepare(
|
|
1919
2272
|
`
|
|
@@ -1921,7 +2274,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
1921
2274
|
FROM memories
|
|
1922
2275
|
WHERE rowid = ?
|
|
1923
2276
|
`
|
|
1924
|
-
).get(
|
|
2277
|
+
).get(existing.row_id);
|
|
1925
2278
|
if (!updated) {
|
|
1926
2279
|
return {
|
|
1927
2280
|
isError: true,
|
|
@@ -1973,7 +2326,7 @@ function registerStartupContextResource(server) {
|
|
|
1973
2326
|
async (uri) => {
|
|
1974
2327
|
try {
|
|
1975
2328
|
const db = getDb();
|
|
1976
|
-
const resolved = resolveRepo(
|
|
2329
|
+
const resolved = resolveRepo(getWorkspaceRoot(), db);
|
|
1977
2330
|
const rows = fetchRepoContext(db, resolved.canonical, 5);
|
|
1978
2331
|
const text = formatContext(rows, {
|
|
1979
2332
|
repo: resolved.canonical,
|
|
@@ -2010,7 +2363,7 @@ async function startServer() {
|
|
|
2010
2363
|
initDb(dbPath);
|
|
2011
2364
|
const server = new McpServer({
|
|
2012
2365
|
name: "fossel",
|
|
2013
|
-
version: "1.
|
|
2366
|
+
version: "1.2.0"
|
|
2014
2367
|
});
|
|
2015
2368
|
registerRememberTool(server);
|
|
2016
2369
|
registerGetContextTool(server);
|