fossel 1.0.9 → 1.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/README.md +110 -43
- package/dist/cli.js +1704 -275
- package/dist/index.js +1300 -54
- package/package.json +3 -2
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,9 @@ function hasColumn(db, tableName, columnName) {
|
|
|
14
14
|
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
15
15
|
return columns.some((column) => column.name === columnName);
|
|
16
16
|
}
|
|
17
|
+
function normalizeNoteForMigration(text) {
|
|
18
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
19
|
+
}
|
|
17
20
|
function runMigrations(db) {
|
|
18
21
|
db.exec(`
|
|
19
22
|
CREATE TABLE IF NOT EXISTS migrations (
|
|
@@ -108,6 +111,59 @@ var init_migrate = __esm({
|
|
|
108
111
|
`);
|
|
109
112
|
}
|
|
110
113
|
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "004_add_repo_aliases",
|
|
117
|
+
apply: (db) => {
|
|
118
|
+
db.exec(`
|
|
119
|
+
CREATE TABLE IF NOT EXISTS repo_aliases (
|
|
120
|
+
alias TEXT PRIMARY KEY,
|
|
121
|
+
canonical TEXT NOT NULL,
|
|
122
|
+
created_at INTEGER NOT NULL
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_repo_aliases_canonical
|
|
126
|
+
ON repo_aliases (canonical);
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
name: "005_add_memories_metadata_json",
|
|
132
|
+
apply: (db) => {
|
|
133
|
+
if (!hasColumn(db, "memories", "metadata_json")) {
|
|
134
|
+
db.exec(`
|
|
135
|
+
ALTER TABLE memories
|
|
136
|
+
ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}';
|
|
137
|
+
`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "006_add_memories_note_normalized",
|
|
143
|
+
apply: (db) => {
|
|
144
|
+
if (!hasColumn(db, "memories", "note_normalized")) {
|
|
145
|
+
db.exec(`
|
|
146
|
+
ALTER TABLE memories
|
|
147
|
+
ADD COLUMN note_normalized TEXT NOT NULL DEFAULT '';
|
|
148
|
+
`);
|
|
149
|
+
}
|
|
150
|
+
db.exec(`
|
|
151
|
+
CREATE INDEX IF NOT EXISTS idx_memories_note_normalized
|
|
152
|
+
ON memories (repo, note_normalized);
|
|
153
|
+
`);
|
|
154
|
+
const rows = db.prepare("SELECT rowid AS row_id, note FROM memories WHERE note_normalized = ''").all();
|
|
155
|
+
if (rows.length > 0) {
|
|
156
|
+
const update = db.prepare(
|
|
157
|
+
"UPDATE memories SET note_normalized = ? WHERE rowid = ?"
|
|
158
|
+
);
|
|
159
|
+
const tx = db.transaction((batch) => {
|
|
160
|
+
for (const row of batch) {
|
|
161
|
+
update.run(normalizeNoteForMigration(row.note), row.row_id);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
tx(rows);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
111
167
|
}
|
|
112
168
|
];
|
|
113
169
|
}
|
|
@@ -159,8 +215,599 @@ var init_client = __esm({
|
|
|
159
215
|
}
|
|
160
216
|
});
|
|
161
217
|
|
|
162
|
-
// src/
|
|
218
|
+
// src/lib/repo.ts
|
|
219
|
+
import { spawnSync } from "child_process";
|
|
220
|
+
import { basename } from "path";
|
|
221
|
+
function normalizeGitRemote(remoteUrl) {
|
|
222
|
+
const trimmed = remoteUrl.trim();
|
|
223
|
+
if (!trimmed) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
for (const pattern of REMOTE_PATTERNS) {
|
|
227
|
+
const match = pattern.exec(trimmed);
|
|
228
|
+
if (!match) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
const path = match[2]?.replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
232
|
+
if (!path) {
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
return path;
|
|
236
|
+
}
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
function readGitRemote(cwd) {
|
|
240
|
+
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
241
|
+
cwd,
|
|
242
|
+
encoding: "utf8"
|
|
243
|
+
});
|
|
244
|
+
if (result.status !== 0) {
|
|
245
|
+
return null;
|
|
246
|
+
}
|
|
247
|
+
const value = result.stdout.trim();
|
|
248
|
+
return value.length > 0 ? value : null;
|
|
249
|
+
}
|
|
250
|
+
function detectFolderName(cwd) {
|
|
251
|
+
const name = basename(cwd);
|
|
252
|
+
return name.length > 0 ? name : cwd;
|
|
253
|
+
}
|
|
254
|
+
function fetchAliases(db, canonical) {
|
|
255
|
+
const rows = db.prepare("SELECT alias FROM repo_aliases WHERE canonical = ? ORDER BY alias").all(canonical);
|
|
256
|
+
return rows.map((row) => row.alias);
|
|
257
|
+
}
|
|
258
|
+
function upsertAlias(db, alias, canonical) {
|
|
259
|
+
const trimmed = alias.trim();
|
|
260
|
+
const target = canonical.trim();
|
|
261
|
+
if (!trimmed || !target) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
265
|
+
db.prepare(
|
|
266
|
+
`
|
|
267
|
+
INSERT INTO repo_aliases (alias, canonical, created_at)
|
|
268
|
+
VALUES (?, ?, ?)
|
|
269
|
+
ON CONFLICT(alias) DO UPDATE SET canonical = excluded.canonical
|
|
270
|
+
`
|
|
271
|
+
).run(trimmed, target, now);
|
|
272
|
+
}
|
|
273
|
+
function lookupAlias(db, alias) {
|
|
274
|
+
const row = db.prepare("SELECT alias, canonical FROM repo_aliases WHERE alias = ?").get(alias);
|
|
275
|
+
return row ?? null;
|
|
276
|
+
}
|
|
277
|
+
function resolveRepo(cwd, db) {
|
|
278
|
+
const gitRemote = readGitRemote(cwd);
|
|
279
|
+
const fromRemote = gitRemote ? normalizeGitRemote(gitRemote) : null;
|
|
280
|
+
const folder = detectFolderName(cwd);
|
|
281
|
+
let canonical;
|
|
282
|
+
let source;
|
|
283
|
+
if (fromRemote) {
|
|
284
|
+
canonical = fromRemote;
|
|
285
|
+
source = "git-remote";
|
|
286
|
+
} else {
|
|
287
|
+
canonical = folder;
|
|
288
|
+
source = "folder";
|
|
289
|
+
}
|
|
290
|
+
upsertAlias(db, canonical, canonical);
|
|
291
|
+
if (folder && folder !== canonical) {
|
|
292
|
+
const existing = lookupAlias(db, folder);
|
|
293
|
+
if (!existing) {
|
|
294
|
+
upsertAlias(db, folder, canonical);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return {
|
|
298
|
+
canonical,
|
|
299
|
+
cwd,
|
|
300
|
+
gitRemote,
|
|
301
|
+
source,
|
|
302
|
+
aliases: fetchAliases(db, canonical)
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function resolveRepoArg(input, cwd, db) {
|
|
306
|
+
const trimmed = input?.trim();
|
|
307
|
+
if (!trimmed) {
|
|
308
|
+
return resolveRepo(cwd, db);
|
|
309
|
+
}
|
|
310
|
+
const aliasRow = lookupAlias(db, trimmed);
|
|
311
|
+
if (aliasRow) {
|
|
312
|
+
return {
|
|
313
|
+
canonical: aliasRow.canonical,
|
|
314
|
+
cwd,
|
|
315
|
+
gitRemote: null,
|
|
316
|
+
source: "alias",
|
|
317
|
+
aliases: fetchAliases(db, aliasRow.canonical)
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const workspace = resolveRepo(cwd, db);
|
|
321
|
+
if (workspace.canonical && workspace.canonical !== trimmed) {
|
|
322
|
+
const tail = workspace.canonical.split("/").at(-1) ?? workspace.canonical;
|
|
323
|
+
const inputTail = trimmed.split("/").at(-1) ?? trimmed;
|
|
324
|
+
if (tail === inputTail || tail === trimmed || inputTail === workspace.canonical) {
|
|
325
|
+
upsertAlias(db, trimmed, workspace.canonical);
|
|
326
|
+
return {
|
|
327
|
+
...workspace,
|
|
328
|
+
source: "alias",
|
|
329
|
+
aliases: fetchAliases(db, workspace.canonical)
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
upsertAlias(db, trimmed, trimmed);
|
|
334
|
+
return {
|
|
335
|
+
canonical: trimmed,
|
|
336
|
+
cwd,
|
|
337
|
+
gitRemote: null,
|
|
338
|
+
source: "input",
|
|
339
|
+
aliases: fetchAliases(db, trimmed)
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function mergeRepoKeys(db, from, to) {
|
|
343
|
+
if (from === to) {
|
|
344
|
+
return { movedAliases: 0, movedMemories: 0 };
|
|
345
|
+
}
|
|
346
|
+
const tx = db.transaction(() => {
|
|
347
|
+
const aliasResult = db.prepare("UPDATE repo_aliases SET canonical = ? WHERE canonical = ?").run(to, from);
|
|
348
|
+
const aliasesToReassign = db.prepare("SELECT alias FROM repo_aliases WHERE canonical = ?").all(to);
|
|
349
|
+
let movedMemories = 0;
|
|
350
|
+
const updateMemories = db.prepare(
|
|
351
|
+
"UPDATE memories SET repo = ? WHERE repo = ?"
|
|
352
|
+
);
|
|
353
|
+
for (const { alias } of aliasesToReassign) {
|
|
354
|
+
if (alias === to) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const result = updateMemories.run(to, alias);
|
|
358
|
+
movedMemories += result.changes;
|
|
359
|
+
}
|
|
360
|
+
movedMemories += updateMemories.run(to, from).changes;
|
|
361
|
+
upsertAlias(db, from, to);
|
|
362
|
+
upsertAlias(db, to, to);
|
|
363
|
+
return {
|
|
364
|
+
movedAliases: aliasResult.changes,
|
|
365
|
+
movedMemories
|
|
366
|
+
};
|
|
367
|
+
});
|
|
368
|
+
return tx();
|
|
369
|
+
}
|
|
370
|
+
var REMOTE_PATTERNS;
|
|
371
|
+
var init_repo = __esm({
|
|
372
|
+
"src/lib/repo.ts"() {
|
|
373
|
+
"use strict";
|
|
374
|
+
REMOTE_PATTERNS = [
|
|
375
|
+
// git@github.com:owner/repo.git, git@gitlab.com:group/sub/repo.git
|
|
376
|
+
/^[^@\s]+@([^:]+):([^\s]+?)(?:\.git)?$/,
|
|
377
|
+
// ssh://git@github.com/owner/repo.git
|
|
378
|
+
/^ssh:\/\/[^@/]+@([^/]+)\/([^\s]+?)(?:\.git)?$/,
|
|
379
|
+
// https://github.com/owner/repo.git, http://gitlab.com/group/sub/repo
|
|
380
|
+
/^https?:\/\/(?:[^@/]+@)?([^/]+)\/([^\s]+?)(?:\.git)?$/,
|
|
381
|
+
// git://github.com/owner/repo.git
|
|
382
|
+
/^git:\/\/([^/]+)\/([^\s]+?)(?:\.git)?$/
|
|
383
|
+
];
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
// src/lib/context.ts
|
|
388
|
+
function parseTags(raw) {
|
|
389
|
+
try {
|
|
390
|
+
const parsed = JSON.parse(raw);
|
|
391
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
392
|
+
} catch {
|
|
393
|
+
return [];
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function buildFtsQuery(query) {
|
|
397
|
+
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter((term) => term.length > 0);
|
|
398
|
+
if (terms.length === 0) {
|
|
399
|
+
return null;
|
|
400
|
+
}
|
|
401
|
+
return terms.map((term) => `"${term}"`).join(" AND ");
|
|
402
|
+
}
|
|
403
|
+
function fetchRepoContext(db, repo, limit, query) {
|
|
404
|
+
const rows = [];
|
|
405
|
+
const seen = /* @__PURE__ */ new Set();
|
|
406
|
+
const push = (memory, source, rank) => {
|
|
407
|
+
if (seen.has(memory.row_id)) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
seen.add(memory.row_id);
|
|
411
|
+
rows.push({ ...memory, source, rank });
|
|
412
|
+
};
|
|
413
|
+
const pinned = db.prepare(
|
|
414
|
+
`
|
|
415
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
416
|
+
FROM memories
|
|
417
|
+
WHERE repo = ? AND pinned = 1
|
|
418
|
+
ORDER BY updated_at DESC
|
|
419
|
+
LIMIT ?
|
|
420
|
+
`
|
|
421
|
+
).all(repo, limit);
|
|
422
|
+
for (const row of pinned) {
|
|
423
|
+
push(row, "pinned");
|
|
424
|
+
}
|
|
425
|
+
if (rows.length < limit) {
|
|
426
|
+
const recent = db.prepare(
|
|
427
|
+
`
|
|
428
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
429
|
+
FROM memories
|
|
430
|
+
WHERE repo = ? AND pinned = 0
|
|
431
|
+
ORDER BY updated_at DESC
|
|
432
|
+
LIMIT ?
|
|
433
|
+
`
|
|
434
|
+
).all(repo, limit - rows.length);
|
|
435
|
+
for (const row of recent) {
|
|
436
|
+
push(row, "recent");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (query && rows.length < limit) {
|
|
440
|
+
const ftsQuery = buildFtsQuery(query);
|
|
441
|
+
if (ftsQuery) {
|
|
442
|
+
try {
|
|
443
|
+
const matches = db.prepare(
|
|
444
|
+
`
|
|
445
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
446
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
447
|
+
FROM memories_fts
|
|
448
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
449
|
+
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
450
|
+
ORDER BY rank
|
|
451
|
+
LIMIT ?
|
|
452
|
+
`
|
|
453
|
+
).all(ftsQuery, repo, limit);
|
|
454
|
+
for (const row of matches) {
|
|
455
|
+
push(row, "search", row.rank);
|
|
456
|
+
if (rows.length >= limit) {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
} catch {
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return rows.slice(0, limit);
|
|
465
|
+
}
|
|
466
|
+
function formatContext(rows, options) {
|
|
467
|
+
const { repo, query, format = "text" } = options;
|
|
468
|
+
if (rows.length === 0) {
|
|
469
|
+
if (format === "markdown") {
|
|
470
|
+
return `# Fossel context: ${repo}
|
|
471
|
+
|
|
472
|
+
No memories found${query ? ` for "${query}"` : ""}.`;
|
|
473
|
+
}
|
|
474
|
+
return `No memories found for ${repo}${query ? ` matching "${query}"` : ""}.`;
|
|
475
|
+
}
|
|
476
|
+
if (format === "markdown") {
|
|
477
|
+
return formatMarkdown(rows, repo, query);
|
|
478
|
+
}
|
|
479
|
+
return formatText(rows, repo, query);
|
|
480
|
+
}
|
|
481
|
+
function formatMarkdown(rows, repo, query) {
|
|
482
|
+
const sections = [`# Fossel context: ${repo}`];
|
|
483
|
+
if (query) {
|
|
484
|
+
sections.push(`Query: \`${query}\``);
|
|
485
|
+
}
|
|
486
|
+
const pinned = rows.filter((row) => row.pinned === 1);
|
|
487
|
+
if (pinned.length > 0) {
|
|
488
|
+
sections.push(["## \u{1F4CC} Pinned", ...pinned.map(renderMarkdownRow)].join("\n"));
|
|
489
|
+
}
|
|
490
|
+
for (const type of MEMORY_TYPES) {
|
|
491
|
+
const entries = rows.filter((row) => row.pinned !== 1 && row.type === type);
|
|
492
|
+
if (entries.length === 0) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
sections.push(
|
|
496
|
+
[`## ${SECTION_TITLES[type]}`, ...entries.map(renderMarkdownRow)].join("\n")
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
return sections.join("\n\n");
|
|
500
|
+
}
|
|
501
|
+
function renderMarkdownRow(row) {
|
|
502
|
+
const tags = parseTags(row.tags);
|
|
503
|
+
const tagSuffix = tags.length > 0 ? ` _(${tags.join(", ")})_` : "";
|
|
504
|
+
return `- (${row.row_id}) ${row.note}${tagSuffix}`;
|
|
505
|
+
}
|
|
506
|
+
function formatText(rows, repo, query) {
|
|
507
|
+
const header = query ? `Repository context for ${repo} (query: "${query}")` : `Repository context for ${repo}`;
|
|
508
|
+
const lines = [header, `Total: ${rows.length}`, ""];
|
|
509
|
+
for (const row of rows) {
|
|
510
|
+
const tags = parseTags(row.tags);
|
|
511
|
+
const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
|
|
512
|
+
const pinPrefix = row.pinned ? "\u{1F4CC} " : "";
|
|
513
|
+
const sourceLabel = row.source === "search" ? " [match]" : "";
|
|
514
|
+
lines.push(
|
|
515
|
+
`- (${row.row_id} | ${row.type})${sourceLabel} ${pinPrefix}${row.note}${tagSuffix}`
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
return lines.join("\n");
|
|
519
|
+
}
|
|
520
|
+
var SECTION_TITLES;
|
|
521
|
+
var init_context = __esm({
|
|
522
|
+
"src/lib/context.ts"() {
|
|
523
|
+
"use strict";
|
|
524
|
+
init_client();
|
|
525
|
+
SECTION_TITLES = {
|
|
526
|
+
convention: "Conventions",
|
|
527
|
+
bug_fix: "Bug Fixes",
|
|
528
|
+
reviewer_pattern: "Reviewer Patterns",
|
|
529
|
+
decision: "Decisions",
|
|
530
|
+
issue: "Issues",
|
|
531
|
+
general: "General"
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
|
|
536
|
+
// src/lib/dedupe.ts
|
|
537
|
+
function normalizeText(text) {
|
|
538
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
539
|
+
}
|
|
540
|
+
function tokenize(text) {
|
|
541
|
+
return normalizeText(text).split(" ").filter((token) => token.length >= 2);
|
|
542
|
+
}
|
|
543
|
+
function trigrams(text) {
|
|
544
|
+
const padded = ` ${text} `;
|
|
545
|
+
const grams = /* @__PURE__ */ new Set();
|
|
546
|
+
for (let i = 0; i < padded.length - 2; i += 1) {
|
|
547
|
+
grams.add(padded.slice(i, i + 3));
|
|
548
|
+
}
|
|
549
|
+
return grams;
|
|
550
|
+
}
|
|
551
|
+
function jaccard(a, b) {
|
|
552
|
+
if (a.size === 0 && b.size === 0) {
|
|
553
|
+
return 1;
|
|
554
|
+
}
|
|
555
|
+
let intersection = 0;
|
|
556
|
+
for (const value of a) {
|
|
557
|
+
if (b.has(value)) {
|
|
558
|
+
intersection += 1;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
const union = a.size + b.size - intersection;
|
|
562
|
+
return union === 0 ? 0 : intersection / union;
|
|
563
|
+
}
|
|
564
|
+
function similarity(a, b) {
|
|
565
|
+
const normalizedA = normalizeText(a);
|
|
566
|
+
const normalizedB = normalizeText(b);
|
|
567
|
+
if (!normalizedA && !normalizedB) {
|
|
568
|
+
return 1;
|
|
569
|
+
}
|
|
570
|
+
if (!normalizedA || !normalizedB) {
|
|
571
|
+
return 0;
|
|
572
|
+
}
|
|
573
|
+
if (normalizedA === normalizedB) {
|
|
574
|
+
return 1;
|
|
575
|
+
}
|
|
576
|
+
const wordScore = jaccard(new Set(tokenize(normalizedA)), new Set(tokenize(normalizedB)));
|
|
577
|
+
const triScore = jaccard(trigrams(normalizedA), trigrams(normalizedB));
|
|
578
|
+
return wordScore * 0.55 + triScore * 0.45;
|
|
579
|
+
}
|
|
580
|
+
function findDuplicate(db, repo, note, options = {}) {
|
|
581
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
582
|
+
const limit = options.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT;
|
|
583
|
+
const normalized = normalizeText(note);
|
|
584
|
+
if (!normalized) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
const exact = db.prepare(
|
|
588
|
+
`
|
|
589
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
590
|
+
FROM memories
|
|
591
|
+
WHERE repo = ? AND note_normalized = ?
|
|
592
|
+
ORDER BY updated_at DESC
|
|
593
|
+
LIMIT 1
|
|
594
|
+
`
|
|
595
|
+
).get(repo, normalized);
|
|
596
|
+
if (exact) {
|
|
597
|
+
return { memory: exact, similarity: 1 };
|
|
598
|
+
}
|
|
599
|
+
const candidates = db.prepare(
|
|
600
|
+
`
|
|
601
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
602
|
+
FROM memories
|
|
603
|
+
WHERE repo = ?
|
|
604
|
+
ORDER BY updated_at DESC
|
|
605
|
+
LIMIT ?
|
|
606
|
+
`
|
|
607
|
+
).all(repo, limit);
|
|
608
|
+
let best = null;
|
|
609
|
+
for (const candidate of candidates) {
|
|
610
|
+
const score = similarity(note, candidate.note);
|
|
611
|
+
if (score >= threshold && (!best || score > best.similarity)) {
|
|
612
|
+
best = { memory: candidate, similarity: score };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
return best;
|
|
616
|
+
}
|
|
617
|
+
var DEFAULT_THRESHOLD, DEFAULT_CANDIDATE_LIMIT;
|
|
618
|
+
var init_dedupe = __esm({
|
|
619
|
+
"src/lib/dedupe.ts"() {
|
|
620
|
+
"use strict";
|
|
621
|
+
DEFAULT_THRESHOLD = 0.82;
|
|
622
|
+
DEFAULT_CANDIDATE_LIMIT = 200;
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// src/tools/dedupe-repo.ts
|
|
163
627
|
import { z } from "zod";
|
|
628
|
+
function parseTags2(raw) {
|
|
629
|
+
try {
|
|
630
|
+
const parsed = JSON.parse(raw);
|
|
631
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
632
|
+
} catch {
|
|
633
|
+
return [];
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
function parseMetadata(raw) {
|
|
637
|
+
try {
|
|
638
|
+
const parsed = JSON.parse(raw);
|
|
639
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
640
|
+
return parsed;
|
|
641
|
+
}
|
|
642
|
+
} catch {
|
|
643
|
+
}
|
|
644
|
+
return {};
|
|
645
|
+
}
|
|
646
|
+
function mergeTagLists(...lists) {
|
|
647
|
+
const seen = /* @__PURE__ */ new Set();
|
|
648
|
+
const out = [];
|
|
649
|
+
for (const list of lists) {
|
|
650
|
+
for (const value of list) {
|
|
651
|
+
const trimmed = value.trim().toLowerCase();
|
|
652
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
653
|
+
seen.add(trimmed);
|
|
654
|
+
out.push(trimmed);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return out;
|
|
658
|
+
}
|
|
659
|
+
function registerDedupeRepoTool(server) {
|
|
660
|
+
server.registerTool(
|
|
661
|
+
"dedupe_repo",
|
|
662
|
+
{
|
|
663
|
+
description: "Scan a repository for near-duplicate memories. Returns a plan by default; pass apply=true to merge duplicates into the most recently updated row, appending a changelog entry to metadata_json.",
|
|
664
|
+
inputSchema: dedupeRepoInputSchema
|
|
665
|
+
},
|
|
666
|
+
async ({ repo, threshold, apply }) => {
|
|
667
|
+
try {
|
|
668
|
+
const db = getDb();
|
|
669
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
670
|
+
const rows = db.prepare(
|
|
671
|
+
`
|
|
672
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json
|
|
673
|
+
FROM memories
|
|
674
|
+
WHERE repo = ?
|
|
675
|
+
ORDER BY updated_at DESC
|
|
676
|
+
`
|
|
677
|
+
).all(resolved.canonical);
|
|
678
|
+
if (rows.length < 2) {
|
|
679
|
+
return {
|
|
680
|
+
content: [
|
|
681
|
+
{
|
|
682
|
+
type: "text",
|
|
683
|
+
text: `No duplicates possible: only ${rows.length} memory in ${resolved.canonical}.`
|
|
684
|
+
}
|
|
685
|
+
]
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
689
|
+
const plan = [];
|
|
690
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
691
|
+
const keep = rows[i];
|
|
692
|
+
if (!keep || consumed.has(keep.row_id)) continue;
|
|
693
|
+
for (let j = i + 1; j < rows.length; j += 1) {
|
|
694
|
+
const other = rows[j];
|
|
695
|
+
if (!other || consumed.has(other.row_id)) continue;
|
|
696
|
+
if (other.type !== keep.type) continue;
|
|
697
|
+
const score = similarity(keep.note, other.note);
|
|
698
|
+
if (score >= threshold) {
|
|
699
|
+
plan.push({ keep: keep.row_id, drop: other.row_id, similarity: score });
|
|
700
|
+
consumed.add(other.row_id);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (plan.length === 0) {
|
|
705
|
+
return {
|
|
706
|
+
content: [
|
|
707
|
+
{
|
|
708
|
+
type: "text",
|
|
709
|
+
text: `No duplicates \u2265 ${threshold} found in ${resolved.canonical} (${rows.length} memories scanned).`
|
|
710
|
+
}
|
|
711
|
+
]
|
|
712
|
+
};
|
|
713
|
+
}
|
|
714
|
+
if (!apply) {
|
|
715
|
+
const lines = plan.map(
|
|
716
|
+
(entry) => `- keep ${entry.keep}, drop ${entry.drop} (similarity ${entry.similarity.toFixed(2)})`
|
|
717
|
+
);
|
|
718
|
+
return {
|
|
719
|
+
content: [
|
|
720
|
+
{
|
|
721
|
+
type: "text",
|
|
722
|
+
text: `Dry run for ${resolved.canonical}. Found ${plan.length} duplicate pair(s):
|
|
723
|
+
${lines.join("\n")}
|
|
724
|
+
|
|
725
|
+
Re-run with apply=true to merge.`
|
|
726
|
+
}
|
|
727
|
+
]
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
const byId = new Map(rows.map((row) => [row.row_id, row]));
|
|
731
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
732
|
+
let merged = 0;
|
|
733
|
+
const tx = db.transaction((entries) => {
|
|
734
|
+
for (const entry of entries) {
|
|
735
|
+
const keep = byId.get(entry.keep);
|
|
736
|
+
const drop = byId.get(entry.drop);
|
|
737
|
+
if (!keep || !drop) continue;
|
|
738
|
+
const longerNote = keep.note.length >= drop.note.length ? keep.note : drop.note;
|
|
739
|
+
const mergedTags = mergeTagLists(parseTags2(keep.tags), parseTags2(drop.tags));
|
|
740
|
+
const metadata = parseMetadata(keep.metadata_json);
|
|
741
|
+
const changelog = metadata.changelog ?? [];
|
|
742
|
+
changelog.push({
|
|
743
|
+
at: now,
|
|
744
|
+
action: "deduped",
|
|
745
|
+
similarity: Number(entry.similarity.toFixed(3)),
|
|
746
|
+
merged_from: drop.row_id,
|
|
747
|
+
previous_note: drop.note
|
|
748
|
+
});
|
|
749
|
+
metadata.changelog = changelog;
|
|
750
|
+
db.prepare(
|
|
751
|
+
`
|
|
752
|
+
UPDATE memories
|
|
753
|
+
SET note = ?, note_normalized = ?, tags = ?, metadata_json = ?, updated_at = ?,
|
|
754
|
+
pinned = CASE WHEN pinned = 1 OR ? = 1 THEN 1 ELSE pinned END
|
|
755
|
+
WHERE rowid = ?
|
|
756
|
+
`
|
|
757
|
+
).run(
|
|
758
|
+
longerNote,
|
|
759
|
+
normalizeText(longerNote),
|
|
760
|
+
JSON.stringify(mergedTags),
|
|
761
|
+
JSON.stringify(metadata),
|
|
762
|
+
now,
|
|
763
|
+
drop.pinned,
|
|
764
|
+
keep.row_id
|
|
765
|
+
);
|
|
766
|
+
db.prepare("DELETE FROM memories WHERE rowid = ?").run(drop.row_id);
|
|
767
|
+
merged += 1;
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
tx(plan);
|
|
771
|
+
return {
|
|
772
|
+
content: [
|
|
773
|
+
{
|
|
774
|
+
type: "text",
|
|
775
|
+
text: `Merged ${merged} duplicate pair(s) in ${resolved.canonical}.`
|
|
776
|
+
}
|
|
777
|
+
]
|
|
778
|
+
};
|
|
779
|
+
} catch (error) {
|
|
780
|
+
const message = error instanceof Error ? error.message : "Unknown error while deduping repo.";
|
|
781
|
+
return {
|
|
782
|
+
isError: true,
|
|
783
|
+
content: [
|
|
784
|
+
{
|
|
785
|
+
type: "text",
|
|
786
|
+
text: `Failed to dedupe repo: ${message}`
|
|
787
|
+
}
|
|
788
|
+
]
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
var dedupeRepoInputSchema;
|
|
795
|
+
var init_dedupe_repo = __esm({
|
|
796
|
+
"src/tools/dedupe-repo.ts"() {
|
|
797
|
+
"use strict";
|
|
798
|
+
init_client();
|
|
799
|
+
init_dedupe();
|
|
800
|
+
init_repo();
|
|
801
|
+
dedupeRepoInputSchema = {
|
|
802
|
+
repo: z.string().trim().min(1).optional(),
|
|
803
|
+
threshold: z.number().min(0.5).max(1).default(0.85),
|
|
804
|
+
apply: z.boolean().default(false)
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
// src/tools/delete.ts
|
|
810
|
+
import { z as z2 } from "zod";
|
|
164
811
|
function registerDeleteMemoryTool(server) {
|
|
165
812
|
server.registerTool(
|
|
166
813
|
"delete_memory",
|
|
@@ -216,14 +863,644 @@ var init_delete = __esm({
|
|
|
216
863
|
"use strict";
|
|
217
864
|
init_client();
|
|
218
865
|
deleteMemoryInputSchema = {
|
|
219
|
-
id:
|
|
866
|
+
id: z2.string().trim().min(1, "id is required")
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
});
|
|
870
|
+
|
|
871
|
+
// src/tools/get-context.ts
|
|
872
|
+
import { z as z3 } from "zod";
|
|
873
|
+
function registerGetContextTool(server) {
|
|
874
|
+
server.registerTool(
|
|
875
|
+
"get_context",
|
|
876
|
+
{
|
|
877
|
+
description: "Unified retrieval tool. Returns pinned memories first, then recent ones, then FTS matches when a query is provided. Default limit is tuned for direct injection into an LLM system prompt. Use format='markdown' for a PR-ready brief.",
|
|
878
|
+
inputSchema: getContextInputSchema
|
|
879
|
+
},
|
|
880
|
+
async ({ repo, query, limit, format }) => {
|
|
881
|
+
try {
|
|
882
|
+
const db = getDb();
|
|
883
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
884
|
+
const rows = fetchRepoContext(db, resolved.canonical, limit, query);
|
|
885
|
+
const text = formatContext(rows, {
|
|
886
|
+
repo: resolved.canonical,
|
|
887
|
+
query,
|
|
888
|
+
format
|
|
889
|
+
});
|
|
890
|
+
return {
|
|
891
|
+
content: [
|
|
892
|
+
{
|
|
893
|
+
type: "text",
|
|
894
|
+
text
|
|
895
|
+
}
|
|
896
|
+
]
|
|
897
|
+
};
|
|
898
|
+
} catch (error) {
|
|
899
|
+
const message = error instanceof Error ? error.message : "Unknown error while fetching context.";
|
|
900
|
+
return {
|
|
901
|
+
isError: true,
|
|
902
|
+
content: [
|
|
903
|
+
{
|
|
904
|
+
type: "text",
|
|
905
|
+
text: `Failed to fetch context: ${message}`
|
|
906
|
+
}
|
|
907
|
+
]
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
);
|
|
912
|
+
}
|
|
913
|
+
var getContextInputSchema;
|
|
914
|
+
var init_get_context = __esm({
|
|
915
|
+
"src/tools/get-context.ts"() {
|
|
916
|
+
"use strict";
|
|
917
|
+
init_client();
|
|
918
|
+
init_context();
|
|
919
|
+
init_repo();
|
|
920
|
+
getContextInputSchema = {
|
|
921
|
+
repo: z3.string().trim().min(1).optional(),
|
|
922
|
+
query: z3.string().trim().min(1).optional(),
|
|
923
|
+
limit: z3.number().int().positive().max(50).default(8),
|
|
924
|
+
format: z3.enum(["text", "markdown"]).default("text")
|
|
220
925
|
};
|
|
221
926
|
}
|
|
222
927
|
});
|
|
223
928
|
|
|
224
929
|
// src/tools/get-repo.ts
|
|
225
|
-
import { z as
|
|
226
|
-
function
|
|
930
|
+
import { z as z4 } from "zod";
|
|
931
|
+
function parseTags3(raw) {
|
|
932
|
+
try {
|
|
933
|
+
const parsed = JSON.parse(raw);
|
|
934
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
935
|
+
} catch {
|
|
936
|
+
return [];
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
function formatTypeHeading(type) {
|
|
940
|
+
return type.split("_").map((part) => part[0]?.toUpperCase() + part.slice(1)).join(" ");
|
|
941
|
+
}
|
|
942
|
+
function registerGetRepoContextTool(server) {
|
|
943
|
+
server.registerTool(
|
|
944
|
+
"get_repo_context",
|
|
945
|
+
{
|
|
946
|
+
description: "Get recent memories for a repository grouped by memory type. The repo argument is resolved to a canonical key automatically; omit it to use the current workspace.",
|
|
947
|
+
inputSchema: getRepoContextInputSchema
|
|
948
|
+
},
|
|
949
|
+
async ({ repo, limit }) => {
|
|
950
|
+
try {
|
|
951
|
+
const db = getDb();
|
|
952
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
953
|
+
const rows = db.prepare(
|
|
954
|
+
`
|
|
955
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
956
|
+
FROM memories
|
|
957
|
+
WHERE repo = ?
|
|
958
|
+
ORDER BY pinned DESC, updated_at DESC
|
|
959
|
+
LIMIT ?
|
|
960
|
+
`
|
|
961
|
+
).all(resolved.canonical, limit);
|
|
962
|
+
if (rows.length === 0) {
|
|
963
|
+
return {
|
|
964
|
+
content: [
|
|
965
|
+
{
|
|
966
|
+
type: "text",
|
|
967
|
+
text: `No memories found for ${resolved.canonical}.`
|
|
968
|
+
}
|
|
969
|
+
]
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
973
|
+
for (const memory of rows) {
|
|
974
|
+
const tags = parseTags3(memory.tags);
|
|
975
|
+
const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
|
|
976
|
+
const pinPrefix = memory.pinned ? "\u{1F4CC} Pinned " : "";
|
|
977
|
+
const item = `- (${memory.row_id} | legacy: ${memory.id}) ${pinPrefix}${memory.note}${tagSuffix}`;
|
|
978
|
+
const existing = grouped.get(memory.type) ?? [];
|
|
979
|
+
existing.push(item);
|
|
980
|
+
grouped.set(memory.type, existing);
|
|
981
|
+
}
|
|
982
|
+
const sections = [];
|
|
983
|
+
for (const type of MEMORY_TYPES) {
|
|
984
|
+
const entries = grouped.get(type);
|
|
985
|
+
if (!entries || entries.length === 0) {
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
sections.push(`${formatTypeHeading(type)}
|
|
989
|
+
${entries.join("\n")}`);
|
|
990
|
+
}
|
|
991
|
+
return {
|
|
992
|
+
content: [
|
|
993
|
+
{
|
|
994
|
+
type: "text",
|
|
995
|
+
text: `Repository context for ${resolved.canonical}
|
|
996
|
+
Total memories: ${rows.length}
|
|
997
|
+
|
|
998
|
+
${sections.join("\n\n")}`
|
|
999
|
+
}
|
|
1000
|
+
]
|
|
1001
|
+
};
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
const message = error instanceof Error ? error.message : "Unknown error while retrieving repository context.";
|
|
1004
|
+
return {
|
|
1005
|
+
isError: true,
|
|
1006
|
+
content: [
|
|
1007
|
+
{
|
|
1008
|
+
type: "text",
|
|
1009
|
+
text: `Failed to fetch repository context: ${message}`
|
|
1010
|
+
}
|
|
1011
|
+
]
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
var getRepoContextInputSchema;
|
|
1018
|
+
var init_get_repo = __esm({
|
|
1019
|
+
"src/tools/get-repo.ts"() {
|
|
1020
|
+
"use strict";
|
|
1021
|
+
init_client();
|
|
1022
|
+
init_repo();
|
|
1023
|
+
getRepoContextInputSchema = {
|
|
1024
|
+
repo: z4.string().trim().min(1).optional(),
|
|
1025
|
+
limit: z4.number().int().positive().max(100).default(10)
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
// src/tools/pin.ts
|
|
1031
|
+
import { z as z5 } from "zod";
|
|
1032
|
+
function setPinnedState(memoryId, pinned) {
|
|
1033
|
+
const db = getDb();
|
|
1034
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1035
|
+
const updateResult = db.prepare(
|
|
1036
|
+
`
|
|
1037
|
+
UPDATE memories
|
|
1038
|
+
SET pinned = ?, updated_at = ?
|
|
1039
|
+
WHERE rowid = ?
|
|
1040
|
+
`
|
|
1041
|
+
).run(pinned, now, memoryId);
|
|
1042
|
+
if (updateResult.changes === 0) {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
return db.prepare(
|
|
1046
|
+
`
|
|
1047
|
+
SELECT rowid AS row_id, note, pinned
|
|
1048
|
+
FROM memories
|
|
1049
|
+
WHERE rowid = ?
|
|
1050
|
+
`
|
|
1051
|
+
).get(memoryId);
|
|
1052
|
+
}
|
|
1053
|
+
function registerPinMemoryTool(server) {
|
|
1054
|
+
server.registerTool(
|
|
1055
|
+
"pin_memory",
|
|
1056
|
+
{
|
|
1057
|
+
description: "Pin a memory to keep it at the top of repository context.",
|
|
1058
|
+
inputSchema: pinInputSchema
|
|
1059
|
+
},
|
|
1060
|
+
async ({ id }) => {
|
|
1061
|
+
try {
|
|
1062
|
+
const memory = setPinnedState(id, 1);
|
|
1063
|
+
if (!memory) {
|
|
1064
|
+
return {
|
|
1065
|
+
isError: true,
|
|
1066
|
+
content: [
|
|
1067
|
+
{
|
|
1068
|
+
type: "text",
|
|
1069
|
+
text: `Memory ${id} not found.`
|
|
1070
|
+
}
|
|
1071
|
+
]
|
|
1072
|
+
};
|
|
1073
|
+
}
|
|
1074
|
+
return {
|
|
1075
|
+
content: [
|
|
1076
|
+
{
|
|
1077
|
+
type: "text",
|
|
1078
|
+
text: `Pinned memory ${memory.row_id}: ${memory.note}`
|
|
1079
|
+
}
|
|
1080
|
+
]
|
|
1081
|
+
};
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
const message = error instanceof Error ? error.message : "Unknown error while pinning memory.";
|
|
1084
|
+
return {
|
|
1085
|
+
isError: true,
|
|
1086
|
+
content: [
|
|
1087
|
+
{
|
|
1088
|
+
type: "text",
|
|
1089
|
+
text: `Failed to pin memory: ${message}`
|
|
1090
|
+
}
|
|
1091
|
+
]
|
|
1092
|
+
};
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
function registerUnpinMemoryTool(server) {
|
|
1098
|
+
server.registerTool(
|
|
1099
|
+
"unpin_memory",
|
|
1100
|
+
{
|
|
1101
|
+
description: "Unpin a previously pinned memory.",
|
|
1102
|
+
inputSchema: pinInputSchema
|
|
1103
|
+
},
|
|
1104
|
+
async ({ id }) => {
|
|
1105
|
+
try {
|
|
1106
|
+
const memory = setPinnedState(id, 0);
|
|
1107
|
+
if (!memory) {
|
|
1108
|
+
return {
|
|
1109
|
+
isError: true,
|
|
1110
|
+
content: [
|
|
1111
|
+
{
|
|
1112
|
+
type: "text",
|
|
1113
|
+
text: `Memory ${id} not found.`
|
|
1114
|
+
}
|
|
1115
|
+
]
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
content: [
|
|
1120
|
+
{
|
|
1121
|
+
type: "text",
|
|
1122
|
+
text: `Unpinned memory ${memory.row_id}.`
|
|
1123
|
+
}
|
|
1124
|
+
]
|
|
1125
|
+
};
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
const message = error instanceof Error ? error.message : "Unknown error while unpinning memory.";
|
|
1128
|
+
return {
|
|
1129
|
+
isError: true,
|
|
1130
|
+
content: [
|
|
1131
|
+
{
|
|
1132
|
+
type: "text",
|
|
1133
|
+
text: `Failed to unpin memory: ${message}`
|
|
1134
|
+
}
|
|
1135
|
+
]
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
var pinInputSchema;
|
|
1142
|
+
var init_pin = __esm({
|
|
1143
|
+
"src/tools/pin.ts"() {
|
|
1144
|
+
"use strict";
|
|
1145
|
+
init_client();
|
|
1146
|
+
pinInputSchema = {
|
|
1147
|
+
id: z5.number().int().positive()
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
});
|
|
1151
|
+
|
|
1152
|
+
// src/lib/inference.ts
|
|
1153
|
+
function inferMemoryType(text) {
|
|
1154
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1155
|
+
for (const rule of TYPE_RULES) {
|
|
1156
|
+
let score = 0;
|
|
1157
|
+
for (const { pattern, weight } of rule.patterns) {
|
|
1158
|
+
if (pattern.test(text)) {
|
|
1159
|
+
score += weight;
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (score > 0) {
|
|
1163
|
+
scores.set(rule.type, (scores.get(rule.type) ?? 0) + score);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (AUTH_KEYWORDS.test(text)) {
|
|
1167
|
+
if (CHOICE_KEYWORDS.test(text)) {
|
|
1168
|
+
scores.set("decision", (scores.get("decision") ?? 0) + 3);
|
|
1169
|
+
} else {
|
|
1170
|
+
scores.set("convention", (scores.get("convention") ?? 0) + 2);
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
if (scores.size === 0) {
|
|
1174
|
+
return "convention";
|
|
1175
|
+
}
|
|
1176
|
+
let bestType = "convention";
|
|
1177
|
+
let bestScore = -1;
|
|
1178
|
+
for (const [type, score] of scores) {
|
|
1179
|
+
if (score > bestScore) {
|
|
1180
|
+
bestType = type;
|
|
1181
|
+
bestScore = score;
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return bestType;
|
|
1185
|
+
}
|
|
1186
|
+
function extractKeywordTags(text) {
|
|
1187
|
+
const found = [];
|
|
1188
|
+
for (const { tag, pattern } of TAG_KEYWORDS) {
|
|
1189
|
+
if (pattern.test(text)) {
|
|
1190
|
+
found.push(tag);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
return found;
|
|
1194
|
+
}
|
|
1195
|
+
function extractIdentifierTags(text) {
|
|
1196
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1197
|
+
const pathLike = text.match(/\/(?:[a-z0-9_-]+\/?){1,4}/gi);
|
|
1198
|
+
if (pathLike) {
|
|
1199
|
+
for (const segment of pathLike) {
|
|
1200
|
+
for (const part of segment.split("/")) {
|
|
1201
|
+
if (part.length >= 3 && /^[a-z0-9_-]+$/i.test(part)) {
|
|
1202
|
+
tokens.add(part.toLowerCase());
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
const fileLike = text.match(/\b[a-z0-9_.-]+\.(?:ts|tsx|js|jsx|py|go|rb|rs|java|kt|sql|md|json|yml|yaml)\b/gi);
|
|
1208
|
+
if (fileLike) {
|
|
1209
|
+
for (const file of fileLike) {
|
|
1210
|
+
const base = file.split(".").slice(0, -1).join(".");
|
|
1211
|
+
if (base.length >= 3) {
|
|
1212
|
+
tokens.add(base.toLowerCase());
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
return Array.from(tokens);
|
|
1217
|
+
}
|
|
1218
|
+
function extractSalientWords(text, limit) {
|
|
1219
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s/_-]/g, " ").split(/\s+/).filter((word) => word.length >= 4 && !STOP_WORDS.has(word));
|
|
1220
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1221
|
+
for (const word of words) {
|
|
1222
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
1223
|
+
}
|
|
1224
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([word]) => word);
|
|
1225
|
+
}
|
|
1226
|
+
function inferTags(text) {
|
|
1227
|
+
const ordered = [];
|
|
1228
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1229
|
+
const push = (value) => {
|
|
1230
|
+
const normalized = value.trim().toLowerCase();
|
|
1231
|
+
if (!normalized || seen.has(normalized)) {
|
|
1232
|
+
return;
|
|
1233
|
+
}
|
|
1234
|
+
seen.add(normalized);
|
|
1235
|
+
ordered.push(normalized);
|
|
1236
|
+
};
|
|
1237
|
+
for (const tag of extractKeywordTags(text)) {
|
|
1238
|
+
push(tag);
|
|
1239
|
+
}
|
|
1240
|
+
for (const tag of extractIdentifierTags(text)) {
|
|
1241
|
+
push(tag);
|
|
1242
|
+
}
|
|
1243
|
+
if (ordered.length < 5) {
|
|
1244
|
+
for (const word of extractSalientWords(text, 8)) {
|
|
1245
|
+
push(word);
|
|
1246
|
+
if (ordered.length >= 5) {
|
|
1247
|
+
break;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
return ordered.slice(0, 5);
|
|
1252
|
+
}
|
|
1253
|
+
function inferMemoryFromNote(text) {
|
|
1254
|
+
return {
|
|
1255
|
+
type: inferMemoryType(text),
|
|
1256
|
+
tags: inferTags(text)
|
|
1257
|
+
};
|
|
1258
|
+
}
|
|
1259
|
+
var TYPE_RULES, AUTH_KEYWORDS, CHOICE_KEYWORDS, TAG_KEYWORDS, STOP_WORDS;
|
|
1260
|
+
var init_inference = __esm({
|
|
1261
|
+
"src/lib/inference.ts"() {
|
|
1262
|
+
"use strict";
|
|
1263
|
+
TYPE_RULES = [
|
|
1264
|
+
{
|
|
1265
|
+
type: "bug_fix",
|
|
1266
|
+
patterns: [
|
|
1267
|
+
{ pattern: /\broot cause\b/i, weight: 4 },
|
|
1268
|
+
{ pattern: /\bregression\b/i, weight: 4 },
|
|
1269
|
+
{ pattern: /\bhotfix\b/i, weight: 4 },
|
|
1270
|
+
{ pattern: /\bfix(?:ed|es|ing)?\b/i, weight: 3 },
|
|
1271
|
+
{ pattern: /\bbugs?\b/i, weight: 2 },
|
|
1272
|
+
{ pattern: /\bcrash(?:ed|es|ing)?\b/i, weight: 2 },
|
|
1273
|
+
{ pattern: /\bbroken\b/i, weight: 2 },
|
|
1274
|
+
{ pattern: /\bworkaround\b/i, weight: 2 }
|
|
1275
|
+
]
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
type: "issue",
|
|
1279
|
+
patterns: [
|
|
1280
|
+
{ pattern: /\bissue\s*#\d+/i, weight: 5 },
|
|
1281
|
+
{ pattern: /\bticket\s*#?\w+/i, weight: 4 },
|
|
1282
|
+
{ pattern: /\bjira[-\s]?\w+/i, weight: 4 },
|
|
1283
|
+
{ pattern: /\bgh[-\s]?\d+/i, weight: 3 },
|
|
1284
|
+
{ pattern: /#\d{2,}/i, weight: 2 }
|
|
1285
|
+
]
|
|
1286
|
+
},
|
|
1287
|
+
{
|
|
1288
|
+
type: "decision",
|
|
1289
|
+
patterns: [
|
|
1290
|
+
{ pattern: /\bdecided not to\b/i, weight: 5 },
|
|
1291
|
+
{ pattern: /\bdecided to\b/i, weight: 4 },
|
|
1292
|
+
{ pattern: /\bwe chose\b/i, weight: 4 },
|
|
1293
|
+
{ pattern: /\bchose\s+\w+\s+over\b/i, weight: 4 },
|
|
1294
|
+
{ pattern: /\barchitecture\b/i, weight: 3 },
|
|
1295
|
+
{ pattern: /\bdecision\b/i, weight: 3 },
|
|
1296
|
+
{ pattern: /\btrade[- ]?off\b/i, weight: 2 },
|
|
1297
|
+
{ pattern: /\brfc\b/i, weight: 2 },
|
|
1298
|
+
{ pattern: /\b(?:adopted|migrated to)\b/i, weight: 2 }
|
|
1299
|
+
]
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
type: "reviewer_pattern",
|
|
1303
|
+
patterns: [
|
|
1304
|
+
{ pattern: /\breviewer(?:s)?\s+(?:prefer|want|expect|require)/i, weight: 5 },
|
|
1305
|
+
{ pattern: /\bpr\s+style\b/i, weight: 4 },
|
|
1306
|
+
{ pattern: /\bcode review\b/i, weight: 3 },
|
|
1307
|
+
{ pattern: /\bprefer(?:s|red)?\b/i, weight: 2 },
|
|
1308
|
+
{ pattern: /\breview comment\b/i, weight: 2 }
|
|
1309
|
+
]
|
|
1310
|
+
},
|
|
1311
|
+
{
|
|
1312
|
+
type: "convention",
|
|
1313
|
+
patterns: [
|
|
1314
|
+
{ pattern: /\bconvention\b/i, weight: 4 },
|
|
1315
|
+
{ pattern: /\balways\b/i, weight: 2 },
|
|
1316
|
+
{ pattern: /\bnever\b/i, weight: 2 },
|
|
1317
|
+
{ pattern: /\bstandard\b/i, weight: 2 },
|
|
1318
|
+
{ pattern: /\bstyle guide\b/i, weight: 3 },
|
|
1319
|
+
{ pattern: /\buse\b\s+\w+\s+\bfor\b/i, weight: 1 }
|
|
1320
|
+
]
|
|
1321
|
+
}
|
|
1322
|
+
];
|
|
1323
|
+
AUTH_KEYWORDS = /\b(?:auth|jwt|oauth|token|login|logout|session|sso|saml)\b/i;
|
|
1324
|
+
CHOICE_KEYWORDS = /\b(?:chose|choose|decided|prefer|switched|migrated|adopted|over|instead of)\b/i;
|
|
1325
|
+
TAG_KEYWORDS = [
|
|
1326
|
+
{ tag: "auth", pattern: /\b(?:auth|authentication|authorization)\b/i },
|
|
1327
|
+
{ tag: "jwt", pattern: /\bjwt\b/i },
|
|
1328
|
+
{ tag: "oauth", pattern: /\boauth\b/i },
|
|
1329
|
+
{ tag: "session", pattern: /\bsession(?:s)?\b/i },
|
|
1330
|
+
{ tag: "api", pattern: /\bapi\b/i },
|
|
1331
|
+
{ tag: "rest", pattern: /\brest(?:ful)?\b/i },
|
|
1332
|
+
{ tag: "graphql", pattern: /\bgraphql\b/i },
|
|
1333
|
+
{ tag: "websocket", pattern: /\bweb[- ]?socket(?:s)?\b/i },
|
|
1334
|
+
{ tag: "database", pattern: /\b(?:database|db|sqlite|postgres|mysql|mongo)\b/i },
|
|
1335
|
+
{ tag: "migration", pattern: /\bmigration(?:s)?\b/i },
|
|
1336
|
+
{ tag: "schema", pattern: /\bschema\b/i },
|
|
1337
|
+
{ tag: "frontend", pattern: /\b(?:frontend|ui|react|vue|svelte|next\.js|nextjs)\b/i },
|
|
1338
|
+
{ tag: "backend", pattern: /\b(?:backend|server|node\.js|nodejs|express|fastify)\b/i },
|
|
1339
|
+
{ tag: "testing", pattern: /\b(?:test|tests|testing|jest|vitest|pytest|rspec)\b/i },
|
|
1340
|
+
{ tag: "ci", pattern: /\b(?:ci|cd|pipeline|github actions|gitlab ci)\b/i },
|
|
1341
|
+
{ tag: "deployment", pattern: /\b(?:deploy|deployment|release|rollout)\b/i },
|
|
1342
|
+
{ tag: "performance", pattern: /\b(?:performance|perf|latency|throughput)\b/i },
|
|
1343
|
+
{ tag: "security", pattern: /\b(?:security|vuln|cve|xss|csrf|injection)\b/i },
|
|
1344
|
+
{ tag: "logging", pattern: /\b(?:log|logging|telemetry|tracing)\b/i },
|
|
1345
|
+
{ tag: "config", pattern: /\b(?:config|configuration|env|environment)\b/i },
|
|
1346
|
+
{ tag: "routing", pattern: /\b(?:route|routing|router|endpoint)\b/i },
|
|
1347
|
+
{ tag: "build", pattern: /\b(?:build|webpack|vite|tsup|rollup|esbuild)\b/i },
|
|
1348
|
+
{ tag: "docs", pattern: /\b(?:docs|documentation|readme)\b/i }
|
|
1349
|
+
];
|
|
1350
|
+
STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1351
|
+
"the",
|
|
1352
|
+
"a",
|
|
1353
|
+
"an",
|
|
1354
|
+
"and",
|
|
1355
|
+
"or",
|
|
1356
|
+
"but",
|
|
1357
|
+
"is",
|
|
1358
|
+
"are",
|
|
1359
|
+
"was",
|
|
1360
|
+
"were",
|
|
1361
|
+
"be",
|
|
1362
|
+
"been",
|
|
1363
|
+
"being",
|
|
1364
|
+
"to",
|
|
1365
|
+
"of",
|
|
1366
|
+
"in",
|
|
1367
|
+
"on",
|
|
1368
|
+
"for",
|
|
1369
|
+
"with",
|
|
1370
|
+
"by",
|
|
1371
|
+
"at",
|
|
1372
|
+
"from",
|
|
1373
|
+
"as",
|
|
1374
|
+
"that",
|
|
1375
|
+
"this",
|
|
1376
|
+
"it",
|
|
1377
|
+
"we",
|
|
1378
|
+
"our",
|
|
1379
|
+
"you",
|
|
1380
|
+
"your",
|
|
1381
|
+
"i",
|
|
1382
|
+
"my",
|
|
1383
|
+
"they",
|
|
1384
|
+
"their",
|
|
1385
|
+
"them",
|
|
1386
|
+
"he",
|
|
1387
|
+
"she",
|
|
1388
|
+
"his",
|
|
1389
|
+
"her",
|
|
1390
|
+
"if",
|
|
1391
|
+
"then",
|
|
1392
|
+
"than",
|
|
1393
|
+
"so",
|
|
1394
|
+
"do",
|
|
1395
|
+
"does",
|
|
1396
|
+
"did",
|
|
1397
|
+
"done",
|
|
1398
|
+
"not",
|
|
1399
|
+
"no",
|
|
1400
|
+
"yes",
|
|
1401
|
+
"can",
|
|
1402
|
+
"will",
|
|
1403
|
+
"would",
|
|
1404
|
+
"should",
|
|
1405
|
+
"could",
|
|
1406
|
+
"may",
|
|
1407
|
+
"might",
|
|
1408
|
+
"must",
|
|
1409
|
+
"have",
|
|
1410
|
+
"has",
|
|
1411
|
+
"had",
|
|
1412
|
+
"just",
|
|
1413
|
+
"also",
|
|
1414
|
+
"use",
|
|
1415
|
+
"used",
|
|
1416
|
+
"using",
|
|
1417
|
+
"want",
|
|
1418
|
+
"wants",
|
|
1419
|
+
"wanted",
|
|
1420
|
+
"need",
|
|
1421
|
+
"needs",
|
|
1422
|
+
"needed",
|
|
1423
|
+
"like",
|
|
1424
|
+
"now",
|
|
1425
|
+
"new",
|
|
1426
|
+
"old",
|
|
1427
|
+
"good",
|
|
1428
|
+
"bad",
|
|
1429
|
+
"make",
|
|
1430
|
+
"makes",
|
|
1431
|
+
"made",
|
|
1432
|
+
"get",
|
|
1433
|
+
"gets",
|
|
1434
|
+
"got",
|
|
1435
|
+
"set",
|
|
1436
|
+
"sets",
|
|
1437
|
+
"go",
|
|
1438
|
+
"going",
|
|
1439
|
+
"into",
|
|
1440
|
+
"over",
|
|
1441
|
+
"under",
|
|
1442
|
+
"through",
|
|
1443
|
+
"because",
|
|
1444
|
+
"when",
|
|
1445
|
+
"where",
|
|
1446
|
+
"while",
|
|
1447
|
+
"there",
|
|
1448
|
+
"here",
|
|
1449
|
+
"what",
|
|
1450
|
+
"which",
|
|
1451
|
+
"who",
|
|
1452
|
+
"why",
|
|
1453
|
+
"how",
|
|
1454
|
+
"live",
|
|
1455
|
+
"lives",
|
|
1456
|
+
"living",
|
|
1457
|
+
"keep",
|
|
1458
|
+
"kept",
|
|
1459
|
+
"keeps",
|
|
1460
|
+
"take",
|
|
1461
|
+
"takes",
|
|
1462
|
+
"took",
|
|
1463
|
+
"taken",
|
|
1464
|
+
"say",
|
|
1465
|
+
"says",
|
|
1466
|
+
"said",
|
|
1467
|
+
"tell",
|
|
1468
|
+
"tells",
|
|
1469
|
+
"told",
|
|
1470
|
+
"know",
|
|
1471
|
+
"knows",
|
|
1472
|
+
"known",
|
|
1473
|
+
"knew",
|
|
1474
|
+
"redirect",
|
|
1475
|
+
"redirects",
|
|
1476
|
+
"redirected",
|
|
1477
|
+
"redirecting",
|
|
1478
|
+
"user",
|
|
1479
|
+
"users",
|
|
1480
|
+
"page",
|
|
1481
|
+
"pages"
|
|
1482
|
+
]);
|
|
1483
|
+
}
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
// src/tools/remember.ts
|
|
1487
|
+
import { nanoid } from "nanoid";
|
|
1488
|
+
import { z as z6 } from "zod";
|
|
1489
|
+
function mergeTagLists2(...lists) {
|
|
1490
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1491
|
+
const out = [];
|
|
1492
|
+
for (const list of lists) {
|
|
1493
|
+
if (!list) continue;
|
|
1494
|
+
for (const raw of list) {
|
|
1495
|
+
const value = raw.trim().toLowerCase();
|
|
1496
|
+
if (!value || seen.has(value)) continue;
|
|
1497
|
+
seen.add(value);
|
|
1498
|
+
out.push(value);
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
return out;
|
|
1502
|
+
}
|
|
1503
|
+
function parseStoredTags(raw) {
|
|
227
1504
|
try {
|
|
228
1505
|
const parsed = JSON.parse(raw);
|
|
229
1506
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -231,76 +1508,120 @@ function parseTags(raw) {
|
|
|
231
1508
|
return [];
|
|
232
1509
|
}
|
|
233
1510
|
}
|
|
234
|
-
function
|
|
235
|
-
|
|
1511
|
+
function parseStoredMetadata(raw) {
|
|
1512
|
+
try {
|
|
1513
|
+
const parsed = JSON.parse(raw);
|
|
1514
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1515
|
+
return parsed;
|
|
1516
|
+
}
|
|
1517
|
+
} catch {
|
|
1518
|
+
}
|
|
1519
|
+
return {};
|
|
236
1520
|
}
|
|
237
|
-
function
|
|
1521
|
+
function registerRememberTool(server) {
|
|
238
1522
|
server.registerTool(
|
|
239
|
-
"
|
|
1523
|
+
"remember",
|
|
240
1524
|
{
|
|
241
|
-
description: "
|
|
242
|
-
inputSchema:
|
|
1525
|
+
description: "Save a memory using only a natural-language note. Fossel infers memory_type, generates tags, resolves the repo, and merges into an existing memory when the note is a near-duplicate. Prefer this tool over store_context for everyday use.",
|
|
1526
|
+
inputSchema: rememberInputSchema
|
|
243
1527
|
},
|
|
244
|
-
async ({ repo,
|
|
1528
|
+
async ({ note, repo, type, tags }) => {
|
|
245
1529
|
try {
|
|
246
1530
|
const db = getDb();
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
1531
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
1532
|
+
const inferred = inferMemoryFromNote(note);
|
|
1533
|
+
const finalType = type ?? inferred.type;
|
|
1534
|
+
const finalTags = mergeTagLists2(tags, inferred.tags).slice(0, 5);
|
|
1535
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1536
|
+
const duplicate = findDuplicate(db, resolved.canonical, note);
|
|
1537
|
+
if (duplicate) {
|
|
1538
|
+
const existing = duplicate.memory;
|
|
1539
|
+
const existingTags = parseStoredTags(existing.tags);
|
|
1540
|
+
const mergedTags = mergeTagLists2(existingTags, finalTags);
|
|
1541
|
+
const metadata2 = parseStoredMetadata(
|
|
1542
|
+
existing.metadata_json ?? "{}"
|
|
1543
|
+
);
|
|
1544
|
+
const changelog = metadata2.changelog ?? [];
|
|
1545
|
+
changelog.push({
|
|
1546
|
+
at: now,
|
|
1547
|
+
action: "merged",
|
|
1548
|
+
similarity: Number(duplicate.similarity.toFixed(3)),
|
|
1549
|
+
previous_note: existing.note
|
|
1550
|
+
});
|
|
1551
|
+
metadata2.changelog = changelog;
|
|
1552
|
+
const longerNote = note.length > existing.note.length ? note : existing.note;
|
|
1553
|
+
const nextType = type ?? existing.type;
|
|
1554
|
+
db.prepare(
|
|
254
1555
|
`
|
|
255
|
-
|
|
256
|
-
|
|
1556
|
+
UPDATE memories
|
|
1557
|
+
SET note = ?, note_normalized = ?, tags = ?, type = ?, metadata_json = ?, updated_at = ?
|
|
1558
|
+
WHERE rowid = ?
|
|
1559
|
+
`
|
|
1560
|
+
).run(
|
|
1561
|
+
longerNote,
|
|
1562
|
+
normalizeText(longerNote),
|
|
1563
|
+
JSON.stringify(mergedTags),
|
|
1564
|
+
nextType,
|
|
1565
|
+
JSON.stringify(metadata2),
|
|
1566
|
+
now,
|
|
1567
|
+
existing.row_id
|
|
1568
|
+
);
|
|
257
1569
|
return {
|
|
258
1570
|
content: [
|
|
259
1571
|
{
|
|
260
1572
|
type: "text",
|
|
261
|
-
text: `
|
|
1573
|
+
text: `Merged into memory ${existing.row_id} for ${resolved.canonical} (similarity ${duplicate.similarity.toFixed(2)}, type ${nextType}, tags: ${mergedTags.join(", ") || "none"}).`
|
|
262
1574
|
}
|
|
263
1575
|
]
|
|
264
1576
|
};
|
|
265
1577
|
}
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
const entries = grouped.get(type);
|
|
279
|
-
if (!entries || entries.length === 0) {
|
|
280
|
-
continue;
|
|
1578
|
+
const id = nanoid();
|
|
1579
|
+
const metadata = {
|
|
1580
|
+
changelog: [
|
|
1581
|
+
{
|
|
1582
|
+
at: now,
|
|
1583
|
+
action: "created"
|
|
1584
|
+
}
|
|
1585
|
+
],
|
|
1586
|
+
inferred: {
|
|
1587
|
+
type: inferred.type,
|
|
1588
|
+
tags: inferred.tags,
|
|
1589
|
+
type_overridden: type !== void 0
|
|
281
1590
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
1591
|
+
};
|
|
1592
|
+
db.prepare(
|
|
1593
|
+
`
|
|
1594
|
+
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json, note_normalized)
|
|
1595
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
1596
|
+
`
|
|
1597
|
+
).run(
|
|
1598
|
+
id,
|
|
1599
|
+
resolved.canonical,
|
|
1600
|
+
finalType,
|
|
1601
|
+
note,
|
|
1602
|
+
JSON.stringify(finalTags),
|
|
1603
|
+
now,
|
|
1604
|
+
now,
|
|
1605
|
+
JSON.stringify(metadata),
|
|
1606
|
+
normalizeText(note)
|
|
1607
|
+
);
|
|
1608
|
+
const inserted = db.prepare("SELECT rowid AS row_id FROM memories WHERE id = ?").get(id);
|
|
285
1609
|
return {
|
|
286
1610
|
content: [
|
|
287
1611
|
{
|
|
288
1612
|
type: "text",
|
|
289
|
-
text: `
|
|
290
|
-
Total memories: ${rows.length}
|
|
291
|
-
|
|
292
|
-
${sections.join("\n\n")}`
|
|
1613
|
+
text: `Stored memory ${inserted?.row_id ?? "?"} for ${resolved.canonical} (type ${finalType}, tags: ${finalTags.join(", ") || "none"}).`
|
|
293
1614
|
}
|
|
294
1615
|
]
|
|
295
1616
|
};
|
|
296
1617
|
} catch (error) {
|
|
297
|
-
const message = error instanceof Error ? error.message : "Unknown error while
|
|
1618
|
+
const message = error instanceof Error ? error.message : "Unknown error while remembering note.";
|
|
298
1619
|
return {
|
|
299
1620
|
isError: true,
|
|
300
1621
|
content: [
|
|
301
1622
|
{
|
|
302
1623
|
type: "text",
|
|
303
|
-
text: `Failed to
|
|
1624
|
+
text: `Failed to remember note: ${message}`
|
|
304
1625
|
}
|
|
305
1626
|
]
|
|
306
1627
|
};
|
|
@@ -308,122 +1629,60 @@ ${sections.join("\n\n")}`
|
|
|
308
1629
|
}
|
|
309
1630
|
);
|
|
310
1631
|
}
|
|
311
|
-
var
|
|
312
|
-
var
|
|
313
|
-
"src/tools/
|
|
1632
|
+
var rememberInputSchema;
|
|
1633
|
+
var init_remember = __esm({
|
|
1634
|
+
"src/tools/remember.ts"() {
|
|
314
1635
|
"use strict";
|
|
315
1636
|
init_client();
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
1637
|
+
init_dedupe();
|
|
1638
|
+
init_inference();
|
|
1639
|
+
init_repo();
|
|
1640
|
+
rememberInputSchema = {
|
|
1641
|
+
note: z6.string().trim().min(1, "note is required"),
|
|
1642
|
+
repo: z6.string().trim().min(1).optional(),
|
|
1643
|
+
type: z6.enum(MEMORY_TYPES).optional(),
|
|
1644
|
+
tags: z6.array(z6.string().trim().min(1)).optional()
|
|
319
1645
|
};
|
|
320
1646
|
}
|
|
321
1647
|
});
|
|
322
1648
|
|
|
323
|
-
// src/tools/
|
|
324
|
-
import { z as
|
|
325
|
-
function
|
|
326
|
-
const db = getDb();
|
|
327
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
328
|
-
const updateResult = db.prepare(
|
|
329
|
-
`
|
|
330
|
-
UPDATE memories
|
|
331
|
-
SET pinned = ?, updated_at = ?
|
|
332
|
-
WHERE rowid = ?
|
|
333
|
-
`
|
|
334
|
-
).run(pinned, now, memoryId);
|
|
335
|
-
if (updateResult.changes === 0) {
|
|
336
|
-
return null;
|
|
337
|
-
}
|
|
338
|
-
return db.prepare(
|
|
339
|
-
`
|
|
340
|
-
SELECT rowid AS row_id, note, pinned
|
|
341
|
-
FROM memories
|
|
342
|
-
WHERE rowid = ?
|
|
343
|
-
`
|
|
344
|
-
).get(memoryId);
|
|
345
|
-
}
|
|
346
|
-
function registerPinMemoryTool(server) {
|
|
1649
|
+
// src/tools/resolve-repo.ts
|
|
1650
|
+
import { z as z7 } from "zod";
|
|
1651
|
+
function registerResolveRepoTool(server) {
|
|
347
1652
|
server.registerTool(
|
|
348
|
-
"
|
|
1653
|
+
"resolve_repo",
|
|
349
1654
|
{
|
|
350
|
-
description: "
|
|
351
|
-
inputSchema:
|
|
1655
|
+
description: "Return the canonical repo key for a working directory along with any aliases and the detected git remote. Useful for clients that want to display which repo Fossel is targeting before making other tool calls.",
|
|
1656
|
+
inputSchema: resolveRepoInputSchema
|
|
352
1657
|
},
|
|
353
|
-
async ({
|
|
1658
|
+
async ({ cwd }) => {
|
|
354
1659
|
try {
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
]
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
return {
|
|
368
|
-
content: [
|
|
369
|
-
{
|
|
370
|
-
type: "text",
|
|
371
|
-
text: `Pinned memory ${memory.row_id}: ${memory.note}`
|
|
372
|
-
}
|
|
373
|
-
]
|
|
374
|
-
};
|
|
375
|
-
} catch (error) {
|
|
376
|
-
const message = error instanceof Error ? error.message : "Unknown error while pinning memory.";
|
|
377
|
-
return {
|
|
378
|
-
isError: true,
|
|
379
|
-
content: [
|
|
380
|
-
{
|
|
381
|
-
type: "text",
|
|
382
|
-
text: `Failed to pin memory: ${message}`
|
|
383
|
-
}
|
|
384
|
-
]
|
|
1660
|
+
const db = getDb();
|
|
1661
|
+
const target = cwd?.trim() || process.cwd();
|
|
1662
|
+
const resolved = resolveRepo(target, db);
|
|
1663
|
+
const payload = {
|
|
1664
|
+
canonical: resolved.canonical,
|
|
1665
|
+
aliases: resolved.aliases,
|
|
1666
|
+
cwd: resolved.cwd,
|
|
1667
|
+
gitRemote: resolved.gitRemote,
|
|
1668
|
+
source: resolved.source
|
|
385
1669
|
};
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
function registerUnpinMemoryTool(server) {
|
|
391
|
-
server.registerTool(
|
|
392
|
-
"unpin_memory",
|
|
393
|
-
{
|
|
394
|
-
description: "Unpin a previously pinned memory.",
|
|
395
|
-
inputSchema: pinInputSchema
|
|
396
|
-
},
|
|
397
|
-
async ({ id }) => {
|
|
398
|
-
try {
|
|
399
|
-
const memory = setPinnedState(id, 0);
|
|
400
|
-
if (!memory) {
|
|
401
|
-
return {
|
|
402
|
-
isError: true,
|
|
403
|
-
content: [
|
|
404
|
-
{
|
|
405
|
-
type: "text",
|
|
406
|
-
text: `Memory ${id} not found.`
|
|
407
|
-
}
|
|
408
|
-
]
|
|
409
|
-
};
|
|
410
|
-
}
|
|
411
1670
|
return {
|
|
412
1671
|
content: [
|
|
413
1672
|
{
|
|
414
1673
|
type: "text",
|
|
415
|
-
text:
|
|
1674
|
+
text: JSON.stringify(payload, null, 2)
|
|
416
1675
|
}
|
|
417
1676
|
]
|
|
418
1677
|
};
|
|
419
1678
|
} catch (error) {
|
|
420
|
-
const message = error instanceof Error ? error.message : "Unknown error while
|
|
1679
|
+
const message = error instanceof Error ? error.message : "Unknown error while resolving repo.";
|
|
421
1680
|
return {
|
|
422
1681
|
isError: true,
|
|
423
1682
|
content: [
|
|
424
1683
|
{
|
|
425
1684
|
type: "text",
|
|
426
|
-
text: `Failed to
|
|
1685
|
+
text: `Failed to resolve repo: ${message}`
|
|
427
1686
|
}
|
|
428
1687
|
]
|
|
429
1688
|
};
|
|
@@ -431,19 +1690,20 @@ function registerUnpinMemoryTool(server) {
|
|
|
431
1690
|
}
|
|
432
1691
|
);
|
|
433
1692
|
}
|
|
434
|
-
var
|
|
435
|
-
var
|
|
436
|
-
"src/tools/
|
|
1693
|
+
var resolveRepoInputSchema;
|
|
1694
|
+
var init_resolve_repo = __esm({
|
|
1695
|
+
"src/tools/resolve-repo.ts"() {
|
|
437
1696
|
"use strict";
|
|
438
1697
|
init_client();
|
|
439
|
-
|
|
440
|
-
|
|
1698
|
+
init_repo();
|
|
1699
|
+
resolveRepoInputSchema = {
|
|
1700
|
+
cwd: z7.string().trim().min(1).optional()
|
|
441
1701
|
};
|
|
442
1702
|
}
|
|
443
1703
|
});
|
|
444
1704
|
|
|
445
1705
|
// src/tools/search.ts
|
|
446
|
-
import { z as
|
|
1706
|
+
import { z as z8 } from "zod";
|
|
447
1707
|
function normalizeFtsQuery(query) {
|
|
448
1708
|
const terms = query.trim().split(/\s+/).map((term) => term.replaceAll('"', '""')).filter(Boolean);
|
|
449
1709
|
if (terms.length === 0) {
|
|
@@ -451,7 +1711,7 @@ function normalizeFtsQuery(query) {
|
|
|
451
1711
|
}
|
|
452
1712
|
return terms.map((term) => `"${term}"`).join(" AND ");
|
|
453
1713
|
}
|
|
454
|
-
function
|
|
1714
|
+
function parseTags4(raw) {
|
|
455
1715
|
try {
|
|
456
1716
|
const parsed = JSON.parse(raw);
|
|
457
1717
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -470,7 +1730,8 @@ function registerSearchMemoryTool(server) {
|
|
|
470
1730
|
try {
|
|
471
1731
|
const db = getDb();
|
|
472
1732
|
const ftsQuery = normalizeFtsQuery(query);
|
|
473
|
-
const
|
|
1733
|
+
const resolvedRepo = repo ? resolveRepoArg(repo, process.cwd(), db).canonical : void 0;
|
|
1734
|
+
const rows = resolvedRepo ? db.prepare(
|
|
474
1735
|
`
|
|
475
1736
|
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
476
1737
|
FROM memories_fts
|
|
@@ -479,7 +1740,7 @@ function registerSearchMemoryTool(server) {
|
|
|
479
1740
|
ORDER BY rank
|
|
480
1741
|
LIMIT ?
|
|
481
1742
|
`
|
|
482
|
-
).all(ftsQuery,
|
|
1743
|
+
).all(ftsQuery, resolvedRepo, limit) : db.prepare(
|
|
483
1744
|
`
|
|
484
1745
|
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags, m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
485
1746
|
FROM memories_fts
|
|
@@ -494,13 +1755,13 @@ function registerSearchMemoryTool(server) {
|
|
|
494
1755
|
content: [
|
|
495
1756
|
{
|
|
496
1757
|
type: "text",
|
|
497
|
-
text:
|
|
1758
|
+
text: resolvedRepo ? `No memories matched "${query}" in ${resolvedRepo}.` : `No memories matched "${query}".`
|
|
498
1759
|
}
|
|
499
1760
|
]
|
|
500
1761
|
};
|
|
501
1762
|
}
|
|
502
1763
|
const formatted = rows.map((row, index) => {
|
|
503
|
-
const tags =
|
|
1764
|
+
const tags = parseTags4(row.tags);
|
|
504
1765
|
const tagsText = tags.length > 0 ? ` | tags: ${tags.join(", ")}` : "";
|
|
505
1766
|
const pinPrefix = row.pinned ? "\u{1F4CC} Pinned " : "";
|
|
506
1767
|
return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
|
|
@@ -510,7 +1771,7 @@ ${pinPrefix}${row.note}${tagsText}`;
|
|
|
510
1771
|
content: [
|
|
511
1772
|
{
|
|
512
1773
|
type: "text",
|
|
513
|
-
text: `Search results for "${query}"${
|
|
1774
|
+
text: `Search results for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}:
|
|
514
1775
|
|
|
515
1776
|
${formatted}`
|
|
516
1777
|
}
|
|
@@ -536,38 +1797,49 @@ var init_search = __esm({
|
|
|
536
1797
|
"src/tools/search.ts"() {
|
|
537
1798
|
"use strict";
|
|
538
1799
|
init_client();
|
|
1800
|
+
init_repo();
|
|
539
1801
|
searchMemoryInputSchema = {
|
|
540
|
-
query:
|
|
541
|
-
repo:
|
|
542
|
-
limit:
|
|
1802
|
+
query: z8.string().trim().min(1, "query is required"),
|
|
1803
|
+
repo: z8.string().trim().min(1).optional(),
|
|
1804
|
+
limit: z8.number().int().positive().max(50).default(5)
|
|
543
1805
|
};
|
|
544
1806
|
}
|
|
545
1807
|
});
|
|
546
1808
|
|
|
547
1809
|
// src/tools/store.ts
|
|
548
|
-
import { nanoid } from "nanoid";
|
|
549
|
-
import { z as
|
|
1810
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1811
|
+
import { z as z9 } from "zod";
|
|
550
1812
|
function registerStoreContextTool(server) {
|
|
551
1813
|
server.registerTool(
|
|
552
1814
|
"store_context",
|
|
553
1815
|
{
|
|
554
|
-
description: "Store repository-specific contributor context such as bug fixes, conventions, and decisions.",
|
|
1816
|
+
description: "Store repository-specific contributor context such as bug fixes, conventions, and decisions. The repo argument is resolved to a canonical key automatically; pass it explicitly only when targeting a different repo than the current workspace.",
|
|
555
1817
|
inputSchema: storeContextInputSchema
|
|
556
1818
|
},
|
|
557
1819
|
async ({ repo, type, note, tags }) => {
|
|
558
1820
|
try {
|
|
559
1821
|
const db = getDb();
|
|
1822
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
560
1823
|
const now = Math.floor(Date.now() / 1e3);
|
|
561
|
-
const id =
|
|
1824
|
+
const id = nanoid2();
|
|
562
1825
|
const normalizedTags = Array.from(
|
|
563
1826
|
new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))
|
|
564
1827
|
);
|
|
565
1828
|
db.prepare(
|
|
566
1829
|
`
|
|
567
|
-
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at)
|
|
568
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1830
|
+
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json, note_normalized)
|
|
1831
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, '{}', ?)
|
|
569
1832
|
`
|
|
570
|
-
).run(
|
|
1833
|
+
).run(
|
|
1834
|
+
id,
|
|
1835
|
+
resolved.canonical,
|
|
1836
|
+
type,
|
|
1837
|
+
note,
|
|
1838
|
+
JSON.stringify(normalizedTags),
|
|
1839
|
+
now,
|
|
1840
|
+
now,
|
|
1841
|
+
normalizeText(note)
|
|
1842
|
+
);
|
|
571
1843
|
const stored = db.prepare(
|
|
572
1844
|
`
|
|
573
1845
|
SELECT rowid AS row_id, id
|
|
@@ -579,7 +1851,7 @@ function registerStoreContextTool(server) {
|
|
|
579
1851
|
content: [
|
|
580
1852
|
{
|
|
581
1853
|
type: "text",
|
|
582
|
-
text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${
|
|
1854
|
+
text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${resolved.canonical} (${type}).`
|
|
583
1855
|
}
|
|
584
1856
|
]
|
|
585
1857
|
};
|
|
@@ -603,17 +1875,19 @@ var init_store = __esm({
|
|
|
603
1875
|
"src/tools/store.ts"() {
|
|
604
1876
|
"use strict";
|
|
605
1877
|
init_client();
|
|
1878
|
+
init_dedupe();
|
|
1879
|
+
init_repo();
|
|
606
1880
|
storeContextInputSchema = {
|
|
607
|
-
repo:
|
|
608
|
-
type:
|
|
609
|
-
note:
|
|
610
|
-
tags:
|
|
1881
|
+
repo: z9.string().trim().min(1).optional(),
|
|
1882
|
+
type: z9.enum(MEMORY_TYPES),
|
|
1883
|
+
note: z9.string().trim().min(1, "note is required"),
|
|
1884
|
+
tags: z9.array(z9.string().trim().min(1)).optional()
|
|
611
1885
|
};
|
|
612
1886
|
}
|
|
613
1887
|
});
|
|
614
1888
|
|
|
615
1889
|
// src/tools/summarize.ts
|
|
616
|
-
import { z as
|
|
1890
|
+
import { z as z10 } from "zod";
|
|
617
1891
|
function registerSummarizeRepoContextTool(server) {
|
|
618
1892
|
server.registerTool(
|
|
619
1893
|
"summarize_repo_context",
|
|
@@ -624,6 +1898,7 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
624
1898
|
async ({ repo }) => {
|
|
625
1899
|
try {
|
|
626
1900
|
const db = getDb();
|
|
1901
|
+
const resolved = resolveRepoArg(repo, process.cwd(), db);
|
|
627
1902
|
const rows = db.prepare(
|
|
628
1903
|
`
|
|
629
1904
|
SELECT rowid AS row_id, type, note, pinned
|
|
@@ -631,13 +1906,13 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
631
1906
|
WHERE repo = ?
|
|
632
1907
|
ORDER BY pinned DESC, updated_at DESC
|
|
633
1908
|
`
|
|
634
|
-
).all(
|
|
1909
|
+
).all(resolved.canonical);
|
|
635
1910
|
if (rows.length === 0) {
|
|
636
1911
|
return {
|
|
637
1912
|
content: [
|
|
638
1913
|
{
|
|
639
1914
|
type: "text",
|
|
640
|
-
text: `Fossel Context Summary: ${
|
|
1915
|
+
text: `Fossel Context Summary: ${resolved.canonical}
|
|
641
1916
|
|
|
642
1917
|
No memories found.`
|
|
643
1918
|
}
|
|
@@ -645,7 +1920,7 @@ No memories found.`
|
|
|
645
1920
|
};
|
|
646
1921
|
}
|
|
647
1922
|
const pinnedLines = rows.filter((row) => row.pinned === 1).map((row) => `- (${row.row_id}) ${row.note}`);
|
|
648
|
-
const sections = [`Fossel Context Summary: ${
|
|
1923
|
+
const sections = [`Fossel Context Summary: ${resolved.canonical}`];
|
|
649
1924
|
if (pinnedLines.length > 0) {
|
|
650
1925
|
sections.push(`\u{1F4CC} Pinned
|
|
651
1926
|
${pinnedLines.join("\n")}`);
|
|
@@ -686,8 +1961,9 @@ var init_summarize = __esm({
|
|
|
686
1961
|
"src/tools/summarize.ts"() {
|
|
687
1962
|
"use strict";
|
|
688
1963
|
init_client();
|
|
1964
|
+
init_repo();
|
|
689
1965
|
summarizeRepoContextInputSchema = {
|
|
690
|
-
repo:
|
|
1966
|
+
repo: z10.string().trim().min(1).optional()
|
|
691
1967
|
};
|
|
692
1968
|
sectionTitleByType = {
|
|
693
1969
|
convention: "Conventions",
|
|
@@ -701,8 +1977,8 @@ var init_summarize = __esm({
|
|
|
701
1977
|
});
|
|
702
1978
|
|
|
703
1979
|
// src/tools/update.ts
|
|
704
|
-
import { z as
|
|
705
|
-
function
|
|
1980
|
+
import { z as z11 } from "zod";
|
|
1981
|
+
function parseTags5(raw) {
|
|
706
1982
|
try {
|
|
707
1983
|
const parsed = JSON.parse(raw);
|
|
708
1984
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -711,7 +1987,7 @@ function parseTags3(raw) {
|
|
|
711
1987
|
}
|
|
712
1988
|
}
|
|
713
1989
|
function formatMemory(memory) {
|
|
714
|
-
const tags =
|
|
1990
|
+
const tags = parseTags5(memory.tags);
|
|
715
1991
|
const tagsLine = tags.length > 0 ? tags.join(", ") : "(none)";
|
|
716
1992
|
return [
|
|
717
1993
|
`Memory ${memory.row_id} updated successfully.`,
|
|
@@ -768,13 +2044,24 @@ function registerUpdateMemoryTool(server) {
|
|
|
768
2044
|
const now = Math.floor(Date.now() / 1e3);
|
|
769
2045
|
const nextType = memory_type ?? existing.type;
|
|
770
2046
|
const nextNote = content ?? existing.note;
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
2047
|
+
const nextNormalized = content ? normalizeText(content) : null;
|
|
2048
|
+
if (nextNormalized !== null) {
|
|
2049
|
+
db.prepare(
|
|
2050
|
+
`
|
|
2051
|
+
UPDATE memories
|
|
2052
|
+
SET type = ?, note = ?, note_normalized = ?, updated_at = ?
|
|
2053
|
+
WHERE rowid = ?
|
|
2054
|
+
`
|
|
2055
|
+
).run(nextType, nextNote, nextNormalized, now, id);
|
|
2056
|
+
} else {
|
|
2057
|
+
db.prepare(
|
|
2058
|
+
`
|
|
2059
|
+
UPDATE memories
|
|
2060
|
+
SET type = ?, note = ?, updated_at = ?
|
|
2061
|
+
WHERE rowid = ?
|
|
2062
|
+
`
|
|
2063
|
+
).run(nextType, nextNote, now, id);
|
|
2064
|
+
}
|
|
778
2065
|
const updated = db.prepare(
|
|
779
2066
|
`
|
|
780
2067
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
@@ -821,10 +2108,11 @@ var init_update = __esm({
|
|
|
821
2108
|
"src/tools/update.ts"() {
|
|
822
2109
|
"use strict";
|
|
823
2110
|
init_client();
|
|
2111
|
+
init_dedupe();
|
|
824
2112
|
updateMemoryInputSchema = {
|
|
825
|
-
id:
|
|
826
|
-
content:
|
|
827
|
-
memory_type:
|
|
2113
|
+
id: z11.number().int().positive(),
|
|
2114
|
+
content: z11.string().trim().min(1).optional(),
|
|
2115
|
+
memory_type: z11.enum(MEMORY_TYPES).optional()
|
|
828
2116
|
};
|
|
829
2117
|
}
|
|
830
2118
|
});
|
|
@@ -843,13 +2131,61 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
843
2131
|
function resolveDbPath() {
|
|
844
2132
|
return process.env.FOSSEL_DB_PATH?.trim() || join(homedir(), ".fossel", "memory.db");
|
|
845
2133
|
}
|
|
2134
|
+
function registerStartupContextResource(server) {
|
|
2135
|
+
server.registerResource(
|
|
2136
|
+
"fossel-startup-context",
|
|
2137
|
+
"fossel://context/current-repo",
|
|
2138
|
+
{
|
|
2139
|
+
title: "Fossel: current repo context",
|
|
2140
|
+
description: "Top pinned and recent memories for the current workspace. Read this at the start of a session to ground the conversation in prior context.",
|
|
2141
|
+
mimeType: "text/markdown"
|
|
2142
|
+
},
|
|
2143
|
+
async (uri) => {
|
|
2144
|
+
try {
|
|
2145
|
+
const db = getDb();
|
|
2146
|
+
const resolved = resolveRepo(process.cwd(), db);
|
|
2147
|
+
const rows = fetchRepoContext(db, resolved.canonical, 5);
|
|
2148
|
+
const text = formatContext(rows, {
|
|
2149
|
+
repo: resolved.canonical,
|
|
2150
|
+
format: "markdown"
|
|
2151
|
+
});
|
|
2152
|
+
return {
|
|
2153
|
+
contents: [
|
|
2154
|
+
{
|
|
2155
|
+
uri: uri.href,
|
|
2156
|
+
mimeType: "text/markdown",
|
|
2157
|
+
text
|
|
2158
|
+
}
|
|
2159
|
+
]
|
|
2160
|
+
};
|
|
2161
|
+
} catch (error) {
|
|
2162
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2163
|
+
return {
|
|
2164
|
+
contents: [
|
|
2165
|
+
{
|
|
2166
|
+
uri: uri.href,
|
|
2167
|
+
mimeType: "text/markdown",
|
|
2168
|
+
text: `# Fossel context unavailable
|
|
2169
|
+
|
|
2170
|
+
${message}`
|
|
2171
|
+
}
|
|
2172
|
+
]
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
846
2178
|
async function startServer() {
|
|
847
2179
|
const dbPath = resolveDbPath();
|
|
848
2180
|
initDb(dbPath);
|
|
849
2181
|
const server = new McpServer({
|
|
850
2182
|
name: "fossel",
|
|
851
|
-
version: "1.
|
|
2183
|
+
version: "1.1.0"
|
|
852
2184
|
});
|
|
2185
|
+
registerRememberTool(server);
|
|
2186
|
+
registerGetContextTool(server);
|
|
2187
|
+
registerResolveRepoTool(server);
|
|
2188
|
+
registerDedupeRepoTool(server);
|
|
853
2189
|
registerStoreContextTool(server);
|
|
854
2190
|
registerGetRepoContextTool(server);
|
|
855
2191
|
registerSearchMemoryTool(server);
|
|
@@ -858,6 +2194,7 @@ async function startServer() {
|
|
|
858
2194
|
registerPinMemoryTool(server);
|
|
859
2195
|
registerUnpinMemoryTool(server);
|
|
860
2196
|
registerSummarizeRepoContextTool(server);
|
|
2197
|
+
registerStartupContextResource(server);
|
|
861
2198
|
const transport = new StdioServerTransport();
|
|
862
2199
|
await server.connect(transport);
|
|
863
2200
|
}
|
|
@@ -866,9 +2203,15 @@ var init_index = __esm({
|
|
|
866
2203
|
"src/index.ts"() {
|
|
867
2204
|
"use strict";
|
|
868
2205
|
init_client();
|
|
2206
|
+
init_context();
|
|
2207
|
+
init_repo();
|
|
2208
|
+
init_dedupe_repo();
|
|
869
2209
|
init_delete();
|
|
2210
|
+
init_get_context();
|
|
870
2211
|
init_get_repo();
|
|
871
2212
|
init_pin();
|
|
2213
|
+
init_remember();
|
|
2214
|
+
init_resolve_repo();
|
|
872
2215
|
init_search();
|
|
873
2216
|
init_store();
|
|
874
2217
|
init_summarize();
|
|
@@ -887,111 +2230,194 @@ var init_index = __esm({
|
|
|
887
2230
|
|
|
888
2231
|
// src/cli.ts
|
|
889
2232
|
init_client();
|
|
2233
|
+
init_repo();
|
|
890
2234
|
import { homedir as homedir2 } from "os";
|
|
891
|
-
import {
|
|
892
|
-
import {
|
|
893
|
-
import { nanoid as
|
|
2235
|
+
import { join as join2 } from "path";
|
|
2236
|
+
import { statSync } from "fs";
|
|
2237
|
+
import { nanoid as nanoid3 } from "nanoid";
|
|
894
2238
|
var DEFAULT_DB_PATH = join2(homedir2(), ".fossel", "memory.db");
|
|
895
2239
|
var INIT_MEMORY_TEXT = "Fossel is active for this repo. Use store_context to save context.";
|
|
896
2240
|
function resolveDbPath2() {
|
|
897
2241
|
return process.env.FOSSEL_DB_PATH?.trim() || DEFAULT_DB_PATH;
|
|
898
2242
|
}
|
|
899
|
-
function
|
|
900
|
-
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
901
|
-
cwd,
|
|
902
|
-
encoding: "utf8"
|
|
903
|
-
});
|
|
904
|
-
if (result.status !== 0) {
|
|
905
|
-
return null;
|
|
906
|
-
}
|
|
907
|
-
const remote = result.stdout.trim();
|
|
908
|
-
if (!remote) {
|
|
909
|
-
return null;
|
|
910
|
-
}
|
|
911
|
-
const normalized = remote.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
912
|
-
const lastSegment = normalized.split("/").at(-1);
|
|
913
|
-
if (!lastSegment) {
|
|
914
|
-
return null;
|
|
915
|
-
}
|
|
916
|
-
return lastSegment.replace(/\.git$/i, "");
|
|
917
|
-
}
|
|
918
|
-
function detectRepoName(cwd) {
|
|
919
|
-
return detectRepoFromRemote(cwd) ?? basename(cwd);
|
|
920
|
-
}
|
|
921
|
-
function ensureSampleMemory(repo) {
|
|
2243
|
+
function ensureSampleMemoryIfEmpty(repo) {
|
|
922
2244
|
const db = getDb();
|
|
923
|
-
const
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
FROM memories
|
|
927
|
-
WHERE repo = ? AND type = 'convention' AND note = ?
|
|
928
|
-
LIMIT 1
|
|
929
|
-
`
|
|
930
|
-
).get(repo, INIT_MEMORY_TEXT);
|
|
931
|
-
if (existing) {
|
|
932
|
-
return;
|
|
2245
|
+
const totalRow = db.prepare("SELECT COUNT(*) AS count FROM memories").get();
|
|
2246
|
+
if (totalRow.count > 0) {
|
|
2247
|
+
return false;
|
|
933
2248
|
}
|
|
934
2249
|
const now = Math.floor(Date.now() / 1e3);
|
|
935
2250
|
db.prepare(
|
|
936
2251
|
`
|
|
937
|
-
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned)
|
|
938
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
2252
|
+
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json, note_normalized)
|
|
2253
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, '{}', ?)
|
|
939
2254
|
`
|
|
940
|
-
).run(
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
},
|
|
952
|
-
null,
|
|
953
|
-
2
|
|
2255
|
+
).run(
|
|
2256
|
+
nanoid3(),
|
|
2257
|
+
repo,
|
|
2258
|
+
"convention",
|
|
2259
|
+
INIT_MEMORY_TEXT,
|
|
2260
|
+
"[]",
|
|
2261
|
+
now,
|
|
2262
|
+
now,
|
|
2263
|
+
INIT_MEMORY_TEXT.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim()
|
|
954
2264
|
);
|
|
2265
|
+
return true;
|
|
955
2266
|
}
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
{
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
args: ["-y", "fossel"]
|
|
963
|
-
}
|
|
2267
|
+
var MCP_CONFIG_SNIPPET = JSON.stringify(
|
|
2268
|
+
{
|
|
2269
|
+
mcpServers: {
|
|
2270
|
+
fossel: {
|
|
2271
|
+
command: "npx",
|
|
2272
|
+
args: ["-y", "fossel"]
|
|
964
2273
|
}
|
|
965
|
-
}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
2274
|
+
}
|
|
2275
|
+
},
|
|
2276
|
+
null,
|
|
2277
|
+
2
|
|
2278
|
+
);
|
|
2279
|
+
function findMergeCandidates(canonical) {
|
|
2280
|
+
const db = getDb();
|
|
2281
|
+
const tail = canonical.split("/").at(-1) ?? canonical;
|
|
2282
|
+
const rows = db.prepare(
|
|
2283
|
+
`
|
|
2284
|
+
SELECT repo, COUNT(*) AS count
|
|
2285
|
+
FROM memories
|
|
2286
|
+
WHERE repo != ?
|
|
2287
|
+
GROUP BY repo
|
|
2288
|
+
`
|
|
2289
|
+
).all(canonical);
|
|
2290
|
+
return rows.filter((row) => {
|
|
2291
|
+
if (!row.repo) return false;
|
|
2292
|
+
const otherTail = row.repo.split("/").at(-1) ?? row.repo;
|
|
2293
|
+
return otherTail === tail || otherTail === canonical || row.repo === tail;
|
|
2294
|
+
});
|
|
969
2295
|
}
|
|
970
|
-
function
|
|
2296
|
+
function runInit() {
|
|
2297
|
+
const dbPath = resolveDbPath2();
|
|
2298
|
+
initDb(dbPath);
|
|
971
2299
|
const db = getDb();
|
|
972
|
-
const
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
2300
|
+
const resolved = resolveRepo(process.cwd(), db);
|
|
2301
|
+
const candidates = findMergeCandidates(resolved.canonical);
|
|
2302
|
+
let mergedAliases = 0;
|
|
2303
|
+
let mergedMemories = 0;
|
|
2304
|
+
for (const candidate of candidates) {
|
|
2305
|
+
const result = mergeRepoKeys(db, candidate.repo, resolved.canonical);
|
|
2306
|
+
mergedAliases += result.movedAliases;
|
|
2307
|
+
mergedMemories += result.movedMemories;
|
|
2308
|
+
}
|
|
2309
|
+
const sampleAdded = ensureSampleMemoryIfEmpty(resolved.canonical);
|
|
2310
|
+
const countRow = db.prepare("SELECT COUNT(*) AS count FROM memories WHERE repo = ?").get(resolved.canonical);
|
|
2311
|
+
console.log("Fossel \u2014 local-first MCP memory for your repos.\n");
|
|
2312
|
+
console.log(`Canonical repo key: ${resolved.canonical}`);
|
|
2313
|
+
console.log(` source: ${resolved.source}`);
|
|
2314
|
+
if (resolved.gitRemote) {
|
|
2315
|
+
console.log(` git remote: ${resolved.gitRemote}`);
|
|
2316
|
+
}
|
|
2317
|
+
if (resolved.aliases.length > 0) {
|
|
2318
|
+
console.log(` aliases: ${resolved.aliases.join(", ")}`);
|
|
2319
|
+
}
|
|
2320
|
+
console.log("");
|
|
2321
|
+
if (mergedAliases > 0 || mergedMemories > 0) {
|
|
2322
|
+
console.log(
|
|
2323
|
+
`Merged ${mergedMemories} memory row(s) and ${mergedAliases} alias row(s) into ${resolved.canonical}.`
|
|
2324
|
+
);
|
|
2325
|
+
console.log("");
|
|
2326
|
+
}
|
|
2327
|
+
console.log("MCP config (Cursor: ~/.cursor/mcp.json, Claude Desktop: settings):");
|
|
2328
|
+
console.log(MCP_CONFIG_SNIPPET);
|
|
980
2329
|
console.log("");
|
|
981
|
-
console.log(
|
|
982
|
-
console.log(
|
|
2330
|
+
console.log(`DB path: ${dbPath}`);
|
|
2331
|
+
console.log(`Memories for ${resolved.canonical}: ${countRow.count}`);
|
|
2332
|
+
if (sampleAdded) {
|
|
2333
|
+
console.log("Inserted one starter memory because the database was empty.");
|
|
2334
|
+
}
|
|
983
2335
|
console.log("");
|
|
984
|
-
console.log(
|
|
985
|
-
console.log(
|
|
2336
|
+
console.log("Quick usage in chat:");
|
|
2337
|
+
console.log(" remember \u2014 natural-language save (no type/tags needed)");
|
|
2338
|
+
console.log(" get_context \u2014 pinned + recent + matching memories");
|
|
2339
|
+
console.log(" resolve_repo \u2014 show which repo key Fossel will use");
|
|
2340
|
+
console.log(" store_context \u2014 explicit save (advanced)");
|
|
2341
|
+
console.log(" dedupe_repo \u2014 merge near-duplicate memories");
|
|
2342
|
+
closeDb();
|
|
2343
|
+
}
|
|
2344
|
+
function runDoctor() {
|
|
2345
|
+
const dbPath = resolveDbPath2();
|
|
2346
|
+
const lines = [];
|
|
2347
|
+
let ok = true;
|
|
2348
|
+
initDb(dbPath);
|
|
2349
|
+
const db = getDb();
|
|
2350
|
+
lines.push(`DB path: ${dbPath}`);
|
|
2351
|
+
const resolved = resolveRepo(process.cwd(), db);
|
|
2352
|
+
lines.push(`Canonical repo key: ${resolved.canonical} (source: ${resolved.source})`);
|
|
2353
|
+
if (resolved.gitRemote) {
|
|
2354
|
+
lines.push(`Git remote: ${resolved.gitRemote}`);
|
|
2355
|
+
} else {
|
|
2356
|
+
lines.push("Git remote: not detected (using folder name).");
|
|
2357
|
+
}
|
|
2358
|
+
if (resolved.aliases.length > 0) {
|
|
2359
|
+
lines.push(`Aliases: ${resolved.aliases.join(", ")}`);
|
|
2360
|
+
}
|
|
2361
|
+
const siblings = findMergeCandidates(resolved.canonical);
|
|
2362
|
+
if (siblings.length > 0) {
|
|
2363
|
+
ok = false;
|
|
2364
|
+
const summary = siblings.map((row) => `${row.repo} (${row.count})`).join(", ");
|
|
2365
|
+
lines.push(`\u26A0 Sibling repo keys detected: ${summary}. Run \`npx fossel init\` to merge.`);
|
|
2366
|
+
} else {
|
|
2367
|
+
lines.push("No sibling repo keys.");
|
|
2368
|
+
}
|
|
2369
|
+
const duplicateRows = db.prepare(
|
|
2370
|
+
`
|
|
2371
|
+
SELECT note_normalized, COUNT(*) AS count
|
|
2372
|
+
FROM memories
|
|
2373
|
+
WHERE repo = ? AND note_normalized != ''
|
|
2374
|
+
GROUP BY note_normalized
|
|
2375
|
+
HAVING COUNT(*) > 1
|
|
2376
|
+
`
|
|
2377
|
+
).all(resolved.canonical);
|
|
2378
|
+
if (duplicateRows.length > 0) {
|
|
2379
|
+
ok = false;
|
|
2380
|
+
const total = duplicateRows.reduce((sum, row) => sum + row.count - 1, 0);
|
|
2381
|
+
lines.push(
|
|
2382
|
+
`\u26A0 ${duplicateRows.length} duplicate clusters covering ${total} extra row(s). Run \`dedupe_repo\` with apply=true.`
|
|
2383
|
+
);
|
|
2384
|
+
} else {
|
|
2385
|
+
lines.push("No exact-duplicate memory clusters.");
|
|
2386
|
+
}
|
|
2387
|
+
const mcpConfigCandidates = [
|
|
2388
|
+
join2(homedir2(), ".cursor", "mcp.json"),
|
|
2389
|
+
join2(
|
|
2390
|
+
homedir2(),
|
|
2391
|
+
"AppData",
|
|
2392
|
+
"Roaming",
|
|
2393
|
+
"Claude",
|
|
2394
|
+
"claude_desktop_config.json"
|
|
2395
|
+
),
|
|
2396
|
+
join2(homedir2(), "Library", "Application Support", "Claude", "claude_desktop_config.json")
|
|
2397
|
+
];
|
|
2398
|
+
const found = mcpConfigCandidates.filter((path) => {
|
|
2399
|
+
try {
|
|
2400
|
+
return statSync(path).isFile();
|
|
2401
|
+
} catch {
|
|
2402
|
+
return false;
|
|
2403
|
+
}
|
|
2404
|
+
});
|
|
2405
|
+
if (found.length === 0) {
|
|
2406
|
+
lines.push(
|
|
2407
|
+
"\u26A0 Could not find Cursor or Claude Desktop MCP config. Run `npx fossel init` and paste the snippet."
|
|
2408
|
+
);
|
|
2409
|
+
} else {
|
|
2410
|
+
lines.push(`Detected MCP config(s): ${found.join(", ")}`);
|
|
2411
|
+
}
|
|
2412
|
+
const totalRow = db.prepare("SELECT COUNT(*) AS count FROM memories").get();
|
|
2413
|
+
lines.push(`Total memories across all repos: ${totalRow.count}`);
|
|
2414
|
+
closeDb();
|
|
2415
|
+
console.log(lines.join("\n"));
|
|
986
2416
|
console.log("");
|
|
987
|
-
console.log("
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
console.log("npx -y fossel init Initialize Fossel for current repository");
|
|
992
|
-
console.log("store_context Save context memory");
|
|
993
|
-
console.log("get_repo_context Retrieve recent repo memories");
|
|
994
|
-
console.log("summarize_repo_context Generate markdown context summary");
|
|
2417
|
+
console.log(ok ? "Status: OK" : "Status: needs attention (see \u26A0 lines above)");
|
|
2418
|
+
if (!ok) {
|
|
2419
|
+
process.exitCode = 1;
|
|
2420
|
+
}
|
|
995
2421
|
}
|
|
996
2422
|
async function main() {
|
|
997
2423
|
const command = process.argv[2];
|
|
@@ -1001,16 +2427,15 @@ async function main() {
|
|
|
1001
2427
|
return;
|
|
1002
2428
|
}
|
|
1003
2429
|
if (command === "init") {
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
closeDb();
|
|
2430
|
+
runInit();
|
|
2431
|
+
return;
|
|
2432
|
+
}
|
|
2433
|
+
if (command === "doctor") {
|
|
2434
|
+
runDoctor();
|
|
1010
2435
|
return;
|
|
1011
2436
|
}
|
|
1012
2437
|
console.error(`Unknown command: ${command}`);
|
|
1013
|
-
console.error("Usage: fossel [init]");
|
|
2438
|
+
console.error("Usage: fossel [init | doctor]");
|
|
1014
2439
|
process.exit(1);
|
|
1015
2440
|
}
|
|
1016
2441
|
main().catch((error) => {
|
|
@@ -1018,3 +2443,7 @@ main().catch((error) => {
|
|
|
1018
2443
|
console.error(`Fossel command failed: ${message}`);
|
|
1019
2444
|
process.exit(1);
|
|
1020
2445
|
});
|
|
2446
|
+
export {
|
|
2447
|
+
runDoctor,
|
|
2448
|
+
runInit
|
|
2449
|
+
};
|