fossel 1.0.9 → 1.1.1
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 +125 -45
- package/dist/cli.js +2002 -257
- package/dist/index.js +1471 -102
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -17,6 +17,9 @@ function hasColumn(db, tableName, columnName) {
|
|
|
17
17
|
const columns = db.prepare(`PRAGMA table_info(${tableName})`).all();
|
|
18
18
|
return columns.some((column) => column.name === columnName);
|
|
19
19
|
}
|
|
20
|
+
function normalizeNoteForMigration(text) {
|
|
21
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
22
|
+
}
|
|
20
23
|
var migrations = [
|
|
21
24
|
{
|
|
22
25
|
name: "001_init_memories_schema",
|
|
@@ -82,6 +85,59 @@ var migrations = [
|
|
|
82
85
|
`);
|
|
83
86
|
}
|
|
84
87
|
}
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: "004_add_repo_aliases",
|
|
91
|
+
apply: (db) => {
|
|
92
|
+
db.exec(`
|
|
93
|
+
CREATE TABLE IF NOT EXISTS repo_aliases (
|
|
94
|
+
alias TEXT PRIMARY KEY,
|
|
95
|
+
canonical TEXT NOT NULL,
|
|
96
|
+
created_at INTEGER NOT NULL
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_repo_aliases_canonical
|
|
100
|
+
ON repo_aliases (canonical);
|
|
101
|
+
`);
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "005_add_memories_metadata_json",
|
|
106
|
+
apply: (db) => {
|
|
107
|
+
if (!hasColumn(db, "memories", "metadata_json")) {
|
|
108
|
+
db.exec(`
|
|
109
|
+
ALTER TABLE memories
|
|
110
|
+
ADD COLUMN metadata_json TEXT NOT NULL DEFAULT '{}';
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
name: "006_add_memories_note_normalized",
|
|
117
|
+
apply: (db) => {
|
|
118
|
+
if (!hasColumn(db, "memories", "note_normalized")) {
|
|
119
|
+
db.exec(`
|
|
120
|
+
ALTER TABLE memories
|
|
121
|
+
ADD COLUMN note_normalized TEXT NOT NULL DEFAULT '';
|
|
122
|
+
`);
|
|
123
|
+
}
|
|
124
|
+
db.exec(`
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_memories_note_normalized
|
|
126
|
+
ON memories (repo, note_normalized);
|
|
127
|
+
`);
|
|
128
|
+
const rows = db.prepare("SELECT rowid AS row_id, note FROM memories WHERE note_normalized = ''").all();
|
|
129
|
+
if (rows.length > 0) {
|
|
130
|
+
const update = db.prepare(
|
|
131
|
+
"UPDATE memories SET note_normalized = ? WHERE rowid = ?"
|
|
132
|
+
);
|
|
133
|
+
const tx = db.transaction((batch) => {
|
|
134
|
+
for (const row of batch) {
|
|
135
|
+
update.run(normalizeNoteForMigration(row.note), row.row_id);
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
tx(rows);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
85
141
|
}
|
|
86
142
|
];
|
|
87
143
|
function runMigrations(db) {
|
|
@@ -139,23 +195,615 @@ function getDb() {
|
|
|
139
195
|
return dbInstance;
|
|
140
196
|
}
|
|
141
197
|
|
|
142
|
-
// src/
|
|
198
|
+
// src/lib/context.ts
|
|
199
|
+
var SECTION_TITLES = {
|
|
200
|
+
convention: "Conventions",
|
|
201
|
+
bug_fix: "Bug Fixes",
|
|
202
|
+
reviewer_pattern: "Reviewer Patterns",
|
|
203
|
+
decision: "Decisions",
|
|
204
|
+
issue: "Issues",
|
|
205
|
+
general: "General"
|
|
206
|
+
};
|
|
207
|
+
function parseTags(raw) {
|
|
208
|
+
try {
|
|
209
|
+
const parsed = JSON.parse(raw);
|
|
210
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
211
|
+
} catch {
|
|
212
|
+
return [];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function normalizeNoteForReadDedupe(text) {
|
|
216
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
217
|
+
}
|
|
218
|
+
function buildFtsQuery(query) {
|
|
219
|
+
const terms = query.trim().split(/\s+/).map((term) => term.replace(/"/g, '""')).filter((term) => term.length > 0);
|
|
220
|
+
if (terms.length === 0) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
return terms.map((term) => `"${term}"`).join(" AND ");
|
|
224
|
+
}
|
|
225
|
+
function fetchRepoContext(db, repo, limit, query) {
|
|
226
|
+
const rows = [];
|
|
227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
228
|
+
const seenNormalized = /* @__PURE__ */ new Set();
|
|
229
|
+
const push = (memory, source, rank) => {
|
|
230
|
+
if (seen.has(memory.row_id)) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const normalized = normalizeNoteForReadDedupe(memory.note);
|
|
234
|
+
if (normalized && seenNormalized.has(normalized)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
seen.add(memory.row_id);
|
|
238
|
+
if (normalized) {
|
|
239
|
+
seenNormalized.add(normalized);
|
|
240
|
+
}
|
|
241
|
+
rows.push({ ...memory, source, rank });
|
|
242
|
+
};
|
|
243
|
+
const pinned = db.prepare(
|
|
244
|
+
`
|
|
245
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
246
|
+
FROM memories
|
|
247
|
+
WHERE repo = ? AND pinned = 1
|
|
248
|
+
ORDER BY updated_at DESC
|
|
249
|
+
LIMIT ?
|
|
250
|
+
`
|
|
251
|
+
).all(repo, limit);
|
|
252
|
+
for (const row of pinned) {
|
|
253
|
+
push(row, "pinned");
|
|
254
|
+
}
|
|
255
|
+
if (rows.length < limit) {
|
|
256
|
+
const recent = db.prepare(
|
|
257
|
+
`
|
|
258
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
259
|
+
FROM memories
|
|
260
|
+
WHERE repo = ? AND pinned = 0
|
|
261
|
+
ORDER BY updated_at DESC
|
|
262
|
+
LIMIT ?
|
|
263
|
+
`
|
|
264
|
+
).all(repo, limit - rows.length);
|
|
265
|
+
for (const row of recent) {
|
|
266
|
+
push(row, "recent");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (query && rows.length < limit) {
|
|
270
|
+
const ftsQuery = buildFtsQuery(query);
|
|
271
|
+
if (ftsQuery) {
|
|
272
|
+
try {
|
|
273
|
+
const matches = db.prepare(
|
|
274
|
+
`
|
|
275
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
276
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
277
|
+
FROM memories_fts
|
|
278
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
279
|
+
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
280
|
+
ORDER BY rank
|
|
281
|
+
LIMIT ?
|
|
282
|
+
`
|
|
283
|
+
).all(ftsQuery, repo, limit);
|
|
284
|
+
for (const row of matches) {
|
|
285
|
+
push(row, "search", row.rank);
|
|
286
|
+
if (rows.length >= limit) {
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return rows.slice(0, limit);
|
|
295
|
+
}
|
|
296
|
+
function formatContext(rows, options) {
|
|
297
|
+
const { repo, query, format = "text" } = options;
|
|
298
|
+
if (rows.length === 0) {
|
|
299
|
+
if (format === "markdown") {
|
|
300
|
+
return `# Fossel context: ${repo}
|
|
301
|
+
|
|
302
|
+
No memories found${query ? ` for "${query}"` : ""}.`;
|
|
303
|
+
}
|
|
304
|
+
return `No memories found for ${repo}${query ? ` matching "${query}"` : ""}.`;
|
|
305
|
+
}
|
|
306
|
+
if (format === "markdown") {
|
|
307
|
+
return formatMarkdown(rows, repo, query);
|
|
308
|
+
}
|
|
309
|
+
return formatText(rows, repo, query);
|
|
310
|
+
}
|
|
311
|
+
function formatMarkdown(rows, repo, query) {
|
|
312
|
+
const sections = [`# Fossel context: ${repo}`];
|
|
313
|
+
if (query) {
|
|
314
|
+
sections.push(`Query: \`${query}\``);
|
|
315
|
+
}
|
|
316
|
+
const pinned = rows.filter((row) => row.pinned === 1);
|
|
317
|
+
if (pinned.length > 0) {
|
|
318
|
+
sections.push(["## \u{1F4CC} Pinned", ...pinned.map(renderMarkdownRow)].join("\n"));
|
|
319
|
+
}
|
|
320
|
+
for (const type of MEMORY_TYPES) {
|
|
321
|
+
const entries = rows.filter((row) => row.pinned !== 1 && row.type === type);
|
|
322
|
+
if (entries.length === 0) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
sections.push(
|
|
326
|
+
[`## ${SECTION_TITLES[type]}`, ...entries.map(renderMarkdownRow)].join("\n")
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
return sections.join("\n\n");
|
|
330
|
+
}
|
|
331
|
+
function renderMarkdownRow(row) {
|
|
332
|
+
const tags = parseTags(row.tags);
|
|
333
|
+
const tagSuffix = tags.length > 0 ? ` _(${tags.join(", ")})_` : "";
|
|
334
|
+
return `- (${row.row_id}) ${row.note}${tagSuffix}`;
|
|
335
|
+
}
|
|
336
|
+
function formatText(rows, repo, query) {
|
|
337
|
+
const header = query ? `Repository context for ${repo} (query: "${query}")` : `Repository context for ${repo}`;
|
|
338
|
+
const lines = [header, `Total: ${rows.length}`, ""];
|
|
339
|
+
for (const row of rows) {
|
|
340
|
+
const tags = parseTags(row.tags);
|
|
341
|
+
const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
|
|
342
|
+
const pinPrefix = row.pinned ? "\u{1F4CC} " : "";
|
|
343
|
+
const sourceLabel = row.source === "search" ? " [match]" : "";
|
|
344
|
+
lines.push(
|
|
345
|
+
`- (${row.row_id} | ${row.type})${sourceLabel} ${pinPrefix}${row.note}${tagSuffix}`
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
return lines.join("\n");
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// src/lib/repo.ts
|
|
352
|
+
import { spawnSync } from "child_process";
|
|
353
|
+
import { basename } from "path";
|
|
354
|
+
var REMOTE_PATTERNS = [
|
|
355
|
+
// git@github.com:owner/repo.git, git@gitlab.com:group/sub/repo.git
|
|
356
|
+
/^[^@\s]+@([^:]+):([^\s]+?)(?:\.git)?$/,
|
|
357
|
+
// ssh://git@github.com/owner/repo.git
|
|
358
|
+
/^ssh:\/\/[^@/]+@([^/]+)\/([^\s]+?)(?:\.git)?$/,
|
|
359
|
+
// https://github.com/owner/repo.git, http://gitlab.com/group/sub/repo
|
|
360
|
+
/^https?:\/\/(?:[^@/]+@)?([^/]+)\/([^\s]+?)(?:\.git)?$/,
|
|
361
|
+
// git://github.com/owner/repo.git
|
|
362
|
+
/^git:\/\/([^/]+)\/([^\s]+?)(?:\.git)?$/
|
|
363
|
+
];
|
|
364
|
+
function normalizeGitRemote(remoteUrl) {
|
|
365
|
+
const trimmed = remoteUrl.trim();
|
|
366
|
+
if (!trimmed) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
for (const pattern of REMOTE_PATTERNS) {
|
|
370
|
+
const match = pattern.exec(trimmed);
|
|
371
|
+
if (!match) {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
const path = match[2]?.replace(/^\/+/, "").replace(/\\/g, "/").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
375
|
+
if (!path) {
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
return path;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
function readGitRemote(cwd) {
|
|
383
|
+
const result = spawnSync("git", ["remote", "get-url", "origin"], {
|
|
384
|
+
cwd,
|
|
385
|
+
encoding: "utf8"
|
|
386
|
+
});
|
|
387
|
+
if (result.status !== 0) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
const value = result.stdout.trim();
|
|
391
|
+
return value.length > 0 ? value : null;
|
|
392
|
+
}
|
|
393
|
+
function detectFolderName(cwd) {
|
|
394
|
+
const name = basename(cwd);
|
|
395
|
+
return name.length > 0 ? name : cwd;
|
|
396
|
+
}
|
|
397
|
+
function fetchAliases(db, canonical) {
|
|
398
|
+
const rows = db.prepare("SELECT alias FROM repo_aliases WHERE canonical = ? ORDER BY alias").all(canonical);
|
|
399
|
+
return rows.map((row) => row.alias);
|
|
400
|
+
}
|
|
401
|
+
function upsertAlias(db, alias, canonical) {
|
|
402
|
+
const trimmed = alias.trim();
|
|
403
|
+
const target = canonical.trim();
|
|
404
|
+
if (!trimmed || !target) {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
408
|
+
db.prepare(
|
|
409
|
+
`
|
|
410
|
+
INSERT INTO repo_aliases (alias, canonical, created_at)
|
|
411
|
+
VALUES (?, ?, ?)
|
|
412
|
+
ON CONFLICT(alias) DO UPDATE SET canonical = excluded.canonical
|
|
413
|
+
`
|
|
414
|
+
).run(trimmed, target, now);
|
|
415
|
+
}
|
|
416
|
+
function lookupAlias(db, alias) {
|
|
417
|
+
const row = db.prepare("SELECT alias, canonical FROM repo_aliases WHERE alias = ?").get(alias);
|
|
418
|
+
return row ?? null;
|
|
419
|
+
}
|
|
420
|
+
function resolveRepo(cwd, db) {
|
|
421
|
+
const gitRemote = readGitRemote(cwd);
|
|
422
|
+
const fromRemote = gitRemote ? normalizeGitRemote(gitRemote) : null;
|
|
423
|
+
const folder = detectFolderName(cwd);
|
|
424
|
+
let canonical;
|
|
425
|
+
let source;
|
|
426
|
+
if (fromRemote) {
|
|
427
|
+
canonical = fromRemote;
|
|
428
|
+
source = "git-remote";
|
|
429
|
+
} else {
|
|
430
|
+
canonical = folder;
|
|
431
|
+
source = "folder";
|
|
432
|
+
}
|
|
433
|
+
upsertAlias(db, canonical, canonical);
|
|
434
|
+
if (folder && folder !== canonical) {
|
|
435
|
+
const existing = lookupAlias(db, folder);
|
|
436
|
+
if (!existing) {
|
|
437
|
+
upsertAlias(db, folder, canonical);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
canonical,
|
|
442
|
+
cwd,
|
|
443
|
+
gitRemote,
|
|
444
|
+
source,
|
|
445
|
+
aliases: fetchAliases(db, canonical)
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
function resolveRepoArg(input, cwd, db) {
|
|
449
|
+
const trimmed = input?.trim();
|
|
450
|
+
if (!trimmed) {
|
|
451
|
+
return resolveRepo(cwd, db);
|
|
452
|
+
}
|
|
453
|
+
const aliasRow = lookupAlias(db, trimmed);
|
|
454
|
+
if (aliasRow) {
|
|
455
|
+
return {
|
|
456
|
+
canonical: aliasRow.canonical,
|
|
457
|
+
cwd,
|
|
458
|
+
gitRemote: null,
|
|
459
|
+
source: "alias",
|
|
460
|
+
aliases: fetchAliases(db, aliasRow.canonical)
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
const workspace = resolveRepo(cwd, db);
|
|
464
|
+
if (workspace.canonical && workspace.canonical !== trimmed) {
|
|
465
|
+
const tail = workspace.canonical.split("/").at(-1) ?? workspace.canonical;
|
|
466
|
+
const inputTail = trimmed.split("/").at(-1) ?? trimmed;
|
|
467
|
+
if (tail === inputTail || tail === trimmed || inputTail === workspace.canonical) {
|
|
468
|
+
upsertAlias(db, trimmed, workspace.canonical);
|
|
469
|
+
return {
|
|
470
|
+
...workspace,
|
|
471
|
+
source: "alias",
|
|
472
|
+
aliases: fetchAliases(db, workspace.canonical)
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
upsertAlias(db, trimmed, trimmed);
|
|
477
|
+
return {
|
|
478
|
+
canonical: trimmed,
|
|
479
|
+
cwd,
|
|
480
|
+
gitRemote: null,
|
|
481
|
+
source: "input",
|
|
482
|
+
aliases: fetchAliases(db, trimmed)
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// src/lib/workspace.ts
|
|
487
|
+
function getWorkspaceRoot() {
|
|
488
|
+
const fromEnv = process.env.FOSSEL_WORKSPACE?.trim();
|
|
489
|
+
if (fromEnv) {
|
|
490
|
+
return fromEnv;
|
|
491
|
+
}
|
|
492
|
+
return process.cwd();
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// src/tools/dedupe-repo.ts
|
|
143
496
|
import { z } from "zod";
|
|
497
|
+
|
|
498
|
+
// src/lib/dedupe.ts
|
|
499
|
+
var DEFAULT_THRESHOLD = 0.82;
|
|
500
|
+
var DEFAULT_CANDIDATE_LIMIT = 200;
|
|
501
|
+
function normalizeText(text) {
|
|
502
|
+
return text.toLowerCase().replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
|
|
503
|
+
}
|
|
504
|
+
function tokenize(text) {
|
|
505
|
+
return normalizeText(text).split(" ").filter((token) => token.length >= 2);
|
|
506
|
+
}
|
|
507
|
+
function trigrams(text) {
|
|
508
|
+
const padded = ` ${text} `;
|
|
509
|
+
const grams = /* @__PURE__ */ new Set();
|
|
510
|
+
for (let i = 0; i < padded.length - 2; i += 1) {
|
|
511
|
+
grams.add(padded.slice(i, i + 3));
|
|
512
|
+
}
|
|
513
|
+
return grams;
|
|
514
|
+
}
|
|
515
|
+
function jaccard(a, b) {
|
|
516
|
+
if (a.size === 0 && b.size === 0) {
|
|
517
|
+
return 1;
|
|
518
|
+
}
|
|
519
|
+
let intersection = 0;
|
|
520
|
+
for (const value of a) {
|
|
521
|
+
if (b.has(value)) {
|
|
522
|
+
intersection += 1;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const union = a.size + b.size - intersection;
|
|
526
|
+
return union === 0 ? 0 : intersection / union;
|
|
527
|
+
}
|
|
528
|
+
function similarity(a, b) {
|
|
529
|
+
const normalizedA = normalizeText(a);
|
|
530
|
+
const normalizedB = normalizeText(b);
|
|
531
|
+
if (!normalizedA && !normalizedB) {
|
|
532
|
+
return 1;
|
|
533
|
+
}
|
|
534
|
+
if (!normalizedA || !normalizedB) {
|
|
535
|
+
return 0;
|
|
536
|
+
}
|
|
537
|
+
if (normalizedA === normalizedB) {
|
|
538
|
+
return 1;
|
|
539
|
+
}
|
|
540
|
+
const wordScore = jaccard(new Set(tokenize(normalizedA)), new Set(tokenize(normalizedB)));
|
|
541
|
+
const triScore = jaccard(trigrams(normalizedA), trigrams(normalizedB));
|
|
542
|
+
return wordScore * 0.55 + triScore * 0.45;
|
|
543
|
+
}
|
|
544
|
+
function findDuplicate(db, repo, note, options = {}) {
|
|
545
|
+
const threshold = options.threshold ?? DEFAULT_THRESHOLD;
|
|
546
|
+
const limit = options.candidateLimit ?? DEFAULT_CANDIDATE_LIMIT;
|
|
547
|
+
const normalized = normalizeText(note);
|
|
548
|
+
if (!normalized) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
const exact = db.prepare(
|
|
552
|
+
`
|
|
553
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
554
|
+
FROM memories
|
|
555
|
+
WHERE repo = ? AND note_normalized = ?
|
|
556
|
+
ORDER BY updated_at DESC
|
|
557
|
+
LIMIT 1
|
|
558
|
+
`
|
|
559
|
+
).get(repo, normalized);
|
|
560
|
+
if (exact) {
|
|
561
|
+
return { memory: exact, similarity: 1 };
|
|
562
|
+
}
|
|
563
|
+
const candidates = db.prepare(
|
|
564
|
+
`
|
|
565
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
566
|
+
FROM memories
|
|
567
|
+
WHERE repo = ?
|
|
568
|
+
ORDER BY updated_at DESC
|
|
569
|
+
LIMIT ?
|
|
570
|
+
`
|
|
571
|
+
).all(repo, limit);
|
|
572
|
+
let best = null;
|
|
573
|
+
for (const candidate of candidates) {
|
|
574
|
+
const score = similarity(note, candidate.note);
|
|
575
|
+
if (score >= threshold && (!best || score > best.similarity)) {
|
|
576
|
+
best = { memory: candidate, similarity: score };
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
return best;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// src/tools/dedupe-repo.ts
|
|
583
|
+
var dedupeRepoInputSchema = {
|
|
584
|
+
repo: z.string().trim().min(1).optional(),
|
|
585
|
+
threshold: z.number().min(0.5).max(1).default(0.85),
|
|
586
|
+
apply: z.boolean().default(false)
|
|
587
|
+
};
|
|
588
|
+
function parseTags2(raw) {
|
|
589
|
+
try {
|
|
590
|
+
const parsed = JSON.parse(raw);
|
|
591
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
592
|
+
} catch {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function parseMetadata(raw) {
|
|
597
|
+
try {
|
|
598
|
+
const parsed = JSON.parse(raw);
|
|
599
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
600
|
+
return parsed;
|
|
601
|
+
}
|
|
602
|
+
} catch {
|
|
603
|
+
}
|
|
604
|
+
return {};
|
|
605
|
+
}
|
|
606
|
+
function mergeTagLists(...lists) {
|
|
607
|
+
const seen = /* @__PURE__ */ new Set();
|
|
608
|
+
const out = [];
|
|
609
|
+
for (const list of lists) {
|
|
610
|
+
for (const value of list) {
|
|
611
|
+
const trimmed = value.trim().toLowerCase();
|
|
612
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
613
|
+
seen.add(trimmed);
|
|
614
|
+
out.push(trimmed);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return out;
|
|
618
|
+
}
|
|
619
|
+
function registerDedupeRepoTool(server) {
|
|
620
|
+
server.registerTool(
|
|
621
|
+
"dedupe_repo",
|
|
622
|
+
{
|
|
623
|
+
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.",
|
|
624
|
+
inputSchema: dedupeRepoInputSchema
|
|
625
|
+
},
|
|
626
|
+
async ({ repo, threshold, apply }) => {
|
|
627
|
+
try {
|
|
628
|
+
const db = getDb();
|
|
629
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
630
|
+
const rows = db.prepare(
|
|
631
|
+
`
|
|
632
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json
|
|
633
|
+
FROM memories
|
|
634
|
+
WHERE repo = ?
|
|
635
|
+
ORDER BY updated_at DESC
|
|
636
|
+
`
|
|
637
|
+
).all(resolved.canonical);
|
|
638
|
+
if (rows.length < 2) {
|
|
639
|
+
return {
|
|
640
|
+
content: [
|
|
641
|
+
{
|
|
642
|
+
type: "text",
|
|
643
|
+
text: `No duplicates possible: only ${rows.length} memory in ${resolved.canonical}.`
|
|
644
|
+
}
|
|
645
|
+
]
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
const consumed = /* @__PURE__ */ new Set();
|
|
649
|
+
const plan = [];
|
|
650
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
651
|
+
const keep = rows[i];
|
|
652
|
+
if (!keep || consumed.has(keep.row_id)) continue;
|
|
653
|
+
for (let j = i + 1; j < rows.length; j += 1) {
|
|
654
|
+
const other = rows[j];
|
|
655
|
+
if (!other || consumed.has(other.row_id)) continue;
|
|
656
|
+
if (other.type !== keep.type) continue;
|
|
657
|
+
const score = similarity(keep.note, other.note);
|
|
658
|
+
if (score >= threshold) {
|
|
659
|
+
plan.push({ keep: keep.row_id, drop: other.row_id, similarity: score });
|
|
660
|
+
consumed.add(other.row_id);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (plan.length === 0) {
|
|
665
|
+
return {
|
|
666
|
+
content: [
|
|
667
|
+
{
|
|
668
|
+
type: "text",
|
|
669
|
+
text: `No duplicates \u2265 ${threshold} found in ${resolved.canonical} (${rows.length} memories scanned).`
|
|
670
|
+
}
|
|
671
|
+
]
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
if (!apply) {
|
|
675
|
+
const lines = plan.map(
|
|
676
|
+
(entry) => `- keep ${entry.keep}, drop ${entry.drop} (similarity ${entry.similarity.toFixed(2)})`
|
|
677
|
+
);
|
|
678
|
+
return {
|
|
679
|
+
content: [
|
|
680
|
+
{
|
|
681
|
+
type: "text",
|
|
682
|
+
text: `Dry run for ${resolved.canonical}. Found ${plan.length} duplicate pair(s):
|
|
683
|
+
${lines.join("\n")}
|
|
684
|
+
|
|
685
|
+
Re-run with apply=true to merge.`
|
|
686
|
+
}
|
|
687
|
+
]
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
const byId = new Map(rows.map((row) => [row.row_id, row]));
|
|
691
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
692
|
+
let merged = 0;
|
|
693
|
+
const tx = db.transaction((entries) => {
|
|
694
|
+
for (const entry of entries) {
|
|
695
|
+
const keep = byId.get(entry.keep);
|
|
696
|
+
const drop = byId.get(entry.drop);
|
|
697
|
+
if (!keep || !drop) continue;
|
|
698
|
+
const longerNote = keep.note.length >= drop.note.length ? keep.note : drop.note;
|
|
699
|
+
const mergedTags = mergeTagLists(parseTags2(keep.tags), parseTags2(drop.tags));
|
|
700
|
+
const metadata = parseMetadata(keep.metadata_json);
|
|
701
|
+
const changelog = metadata.changelog ?? [];
|
|
702
|
+
changelog.push({
|
|
703
|
+
at: now,
|
|
704
|
+
action: "deduped",
|
|
705
|
+
similarity: Number(entry.similarity.toFixed(3)),
|
|
706
|
+
merged_from: drop.row_id,
|
|
707
|
+
previous_note: drop.note
|
|
708
|
+
});
|
|
709
|
+
metadata.changelog = changelog;
|
|
710
|
+
db.prepare(
|
|
711
|
+
`
|
|
712
|
+
UPDATE memories
|
|
713
|
+
SET note = ?, note_normalized = ?, tags = ?, metadata_json = ?, updated_at = ?,
|
|
714
|
+
pinned = CASE WHEN pinned = 1 OR ? = 1 THEN 1 ELSE pinned END
|
|
715
|
+
WHERE rowid = ?
|
|
716
|
+
`
|
|
717
|
+
).run(
|
|
718
|
+
longerNote,
|
|
719
|
+
normalizeText(longerNote),
|
|
720
|
+
JSON.stringify(mergedTags),
|
|
721
|
+
JSON.stringify(metadata),
|
|
722
|
+
now,
|
|
723
|
+
drop.pinned,
|
|
724
|
+
keep.row_id
|
|
725
|
+
);
|
|
726
|
+
db.prepare("DELETE FROM memories WHERE rowid = ?").run(drop.row_id);
|
|
727
|
+
merged += 1;
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
tx(plan);
|
|
731
|
+
return {
|
|
732
|
+
content: [
|
|
733
|
+
{
|
|
734
|
+
type: "text",
|
|
735
|
+
text: `Merged ${merged} duplicate pair(s) in ${resolved.canonical}.`
|
|
736
|
+
}
|
|
737
|
+
]
|
|
738
|
+
};
|
|
739
|
+
} catch (error) {
|
|
740
|
+
const message = error instanceof Error ? error.message : "Unknown error while deduping repo.";
|
|
741
|
+
return {
|
|
742
|
+
isError: true,
|
|
743
|
+
content: [
|
|
744
|
+
{
|
|
745
|
+
type: "text",
|
|
746
|
+
text: `Failed to dedupe repo: ${message}`
|
|
747
|
+
}
|
|
748
|
+
]
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// src/tools/delete.ts
|
|
756
|
+
import { z as z2 } from "zod";
|
|
757
|
+
|
|
758
|
+
// src/lib/memory.ts
|
|
759
|
+
function findMemoryByAnyId(db, input) {
|
|
760
|
+
const numeric = typeof input === "number" ? input : Number(input);
|
|
761
|
+
const isNumericId = Number.isInteger(numeric) && numeric > 0 && String(numeric) === String(input).trim();
|
|
762
|
+
if (isNumericId) {
|
|
763
|
+
const row = db.prepare(
|
|
764
|
+
`
|
|
765
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
766
|
+
FROM memories
|
|
767
|
+
WHERE rowid = ?
|
|
768
|
+
`
|
|
769
|
+
).get(numeric);
|
|
770
|
+
if (row) {
|
|
771
|
+
return row;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const stringInput = String(input).trim();
|
|
775
|
+
if (stringInput.length === 0) {
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
const stringRow = db.prepare(
|
|
779
|
+
`
|
|
780
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
781
|
+
FROM memories
|
|
782
|
+
WHERE id = ?
|
|
783
|
+
`
|
|
784
|
+
).get(stringInput);
|
|
785
|
+
return stringRow ?? null;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/tools/delete.ts
|
|
144
789
|
var deleteMemoryInputSchema = {
|
|
145
|
-
|
|
790
|
+
// Accept either the numeric row_id or the legacy nanoid string. Tools used
|
|
791
|
+
// to disagree about which form to take; this unifies them so callers can
|
|
792
|
+
// paste whichever id they have in front of them.
|
|
793
|
+
id: z2.union([z2.number().int().positive(), z2.string().trim().min(1)])
|
|
146
794
|
};
|
|
147
795
|
function registerDeleteMemoryTool(server) {
|
|
148
796
|
server.registerTool(
|
|
149
797
|
"delete_memory",
|
|
150
798
|
{
|
|
151
|
-
description: "Delete a memory from storage by id.",
|
|
799
|
+
description: "Delete a memory from storage by id. Accepts either the numeric row id or the legacy string id.",
|
|
152
800
|
inputSchema: deleteMemoryInputSchema
|
|
153
801
|
},
|
|
154
802
|
async ({ id }) => {
|
|
155
803
|
try {
|
|
156
804
|
const db = getDb();
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
805
|
+
const memory = findMemoryByAnyId(db, id);
|
|
806
|
+
if (!memory) {
|
|
159
807
|
return {
|
|
160
808
|
isError: true,
|
|
161
809
|
content: [
|
|
@@ -166,26 +814,75 @@ function registerDeleteMemoryTool(server) {
|
|
|
166
814
|
]
|
|
167
815
|
};
|
|
168
816
|
}
|
|
169
|
-
const deleteTx = db.transaction((
|
|
170
|
-
db.prepare("DELETE FROM memories WHERE
|
|
817
|
+
const deleteTx = db.transaction((rowId) => {
|
|
818
|
+
db.prepare("DELETE FROM memories WHERE rowid = ?").run(rowId);
|
|
819
|
+
});
|
|
820
|
+
deleteTx(memory.row_id);
|
|
821
|
+
return {
|
|
822
|
+
content: [
|
|
823
|
+
{
|
|
824
|
+
type: "text",
|
|
825
|
+
text: `Deleted memory ${memory.row_id} (legacy: ${memory.id}).`
|
|
826
|
+
}
|
|
827
|
+
]
|
|
828
|
+
};
|
|
829
|
+
} catch (error) {
|
|
830
|
+
const message = error instanceof Error ? error.message : "Unknown error while deleting memory.";
|
|
831
|
+
return {
|
|
832
|
+
isError: true,
|
|
833
|
+
content: [
|
|
834
|
+
{
|
|
835
|
+
type: "text",
|
|
836
|
+
text: `Failed to delete memory: ${message}`
|
|
837
|
+
}
|
|
838
|
+
]
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// src/tools/get-context.ts
|
|
846
|
+
import { z as z3 } from "zod";
|
|
847
|
+
var getContextInputSchema = {
|
|
848
|
+
repo: z3.string().trim().min(1).optional(),
|
|
849
|
+
query: z3.string().trim().min(1).optional(),
|
|
850
|
+
limit: z3.number().int().positive().max(50).default(8),
|
|
851
|
+
format: z3.enum(["text", "markdown"]).default("text")
|
|
852
|
+
};
|
|
853
|
+
function registerGetContextTool(server) {
|
|
854
|
+
server.registerTool(
|
|
855
|
+
"get_context",
|
|
856
|
+
{
|
|
857
|
+
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.",
|
|
858
|
+
inputSchema: getContextInputSchema
|
|
859
|
+
},
|
|
860
|
+
async ({ repo, query, limit, format }) => {
|
|
861
|
+
try {
|
|
862
|
+
const db = getDb();
|
|
863
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
864
|
+
const rows = fetchRepoContext(db, resolved.canonical, limit, query);
|
|
865
|
+
const text = formatContext(rows, {
|
|
866
|
+
repo: resolved.canonical,
|
|
867
|
+
query,
|
|
868
|
+
format
|
|
171
869
|
});
|
|
172
|
-
deleteTx(id);
|
|
173
870
|
return {
|
|
174
871
|
content: [
|
|
175
872
|
{
|
|
176
873
|
type: "text",
|
|
177
|
-
text
|
|
874
|
+
text
|
|
178
875
|
}
|
|
179
876
|
]
|
|
180
877
|
};
|
|
181
878
|
} catch (error) {
|
|
182
|
-
const message = error instanceof Error ? error.message : "Unknown error while
|
|
879
|
+
const message = error instanceof Error ? error.message : "Unknown error while fetching context.";
|
|
183
880
|
return {
|
|
184
881
|
isError: true,
|
|
185
882
|
content: [
|
|
186
883
|
{
|
|
187
884
|
type: "text",
|
|
188
|
-
text: `Failed to
|
|
885
|
+
text: `Failed to fetch context: ${message}`
|
|
189
886
|
}
|
|
190
887
|
]
|
|
191
888
|
};
|
|
@@ -195,12 +892,12 @@ function registerDeleteMemoryTool(server) {
|
|
|
195
892
|
}
|
|
196
893
|
|
|
197
894
|
// src/tools/get-repo.ts
|
|
198
|
-
import { z as
|
|
895
|
+
import { z as z4 } from "zod";
|
|
199
896
|
var getRepoContextInputSchema = {
|
|
200
|
-
repo:
|
|
201
|
-
limit:
|
|
897
|
+
repo: z4.string().trim().min(1).optional(),
|
|
898
|
+
limit: z4.number().int().positive().max(100).default(10)
|
|
202
899
|
};
|
|
203
|
-
function
|
|
900
|
+
function parseTags3(raw) {
|
|
204
901
|
try {
|
|
205
902
|
const parsed = JSON.parse(raw);
|
|
206
903
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -215,12 +912,13 @@ function registerGetRepoContextTool(server) {
|
|
|
215
912
|
server.registerTool(
|
|
216
913
|
"get_repo_context",
|
|
217
914
|
{
|
|
218
|
-
description: "Get recent memories for a repository grouped by memory type.",
|
|
915
|
+
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.",
|
|
219
916
|
inputSchema: getRepoContextInputSchema
|
|
220
917
|
},
|
|
221
918
|
async ({ repo, limit }) => {
|
|
222
919
|
try {
|
|
223
920
|
const db = getDb();
|
|
921
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
224
922
|
const rows = db.prepare(
|
|
225
923
|
`
|
|
226
924
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
@@ -229,20 +927,20 @@ function registerGetRepoContextTool(server) {
|
|
|
229
927
|
ORDER BY pinned DESC, updated_at DESC
|
|
230
928
|
LIMIT ?
|
|
231
929
|
`
|
|
232
|
-
).all(
|
|
930
|
+
).all(resolved.canonical, limit);
|
|
233
931
|
if (rows.length === 0) {
|
|
234
932
|
return {
|
|
235
933
|
content: [
|
|
236
934
|
{
|
|
237
935
|
type: "text",
|
|
238
|
-
text: `No memories found for ${
|
|
936
|
+
text: `No memories found for ${resolved.canonical}.`
|
|
239
937
|
}
|
|
240
938
|
]
|
|
241
939
|
};
|
|
242
940
|
}
|
|
243
941
|
const grouped = /* @__PURE__ */ new Map();
|
|
244
942
|
for (const memory of rows) {
|
|
245
|
-
const tags =
|
|
943
|
+
const tags = parseTags3(memory.tags);
|
|
246
944
|
const tagSuffix = tags.length > 0 ? ` [tags: ${tags.join(", ")}]` : "";
|
|
247
945
|
const pinPrefix = memory.pinned ? "\u{1F4CC} Pinned " : "";
|
|
248
946
|
const item = `- (${memory.row_id} | legacy: ${memory.id}) ${pinPrefix}${memory.note}${tagSuffix}`;
|
|
@@ -263,7 +961,7 @@ ${entries.join("\n")}`);
|
|
|
263
961
|
content: [
|
|
264
962
|
{
|
|
265
963
|
type: "text",
|
|
266
|
-
text: `Repository context for ${
|
|
964
|
+
text: `Repository context for ${resolved.canonical}
|
|
267
965
|
Total memories: ${rows.length}
|
|
268
966
|
|
|
269
967
|
${sections.join("\n\n")}`
|
|
@@ -287,11 +985,12 @@ ${sections.join("\n\n")}`
|
|
|
287
985
|
}
|
|
288
986
|
|
|
289
987
|
// src/tools/pin.ts
|
|
290
|
-
import { z as
|
|
988
|
+
import { z as z5 } from "zod";
|
|
291
989
|
var pinInputSchema = {
|
|
292
|
-
id
|
|
990
|
+
// Accept numeric row_id or legacy string id for parity with the other tools.
|
|
991
|
+
id: z5.union([z5.number().int().positive(), z5.string().trim().min(1)])
|
|
293
992
|
};
|
|
294
|
-
function setPinnedState(
|
|
993
|
+
function setPinnedState(rowId, pinned) {
|
|
295
994
|
const db = getDb();
|
|
296
995
|
const now = Math.floor(Date.now() / 1e3);
|
|
297
996
|
const updateResult = db.prepare(
|
|
@@ -300,7 +999,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
300
999
|
SET pinned = ?, updated_at = ?
|
|
301
1000
|
WHERE rowid = ?
|
|
302
1001
|
`
|
|
303
|
-
).run(pinned, now,
|
|
1002
|
+
).run(pinned, now, rowId);
|
|
304
1003
|
if (updateResult.changes === 0) {
|
|
305
1004
|
return null;
|
|
306
1005
|
}
|
|
@@ -310,7 +1009,7 @@ function setPinnedState(memoryId, pinned) {
|
|
|
310
1009
|
FROM memories
|
|
311
1010
|
WHERE rowid = ?
|
|
312
1011
|
`
|
|
313
|
-
).get(
|
|
1012
|
+
).get(rowId);
|
|
314
1013
|
}
|
|
315
1014
|
function registerPinMemoryTool(server) {
|
|
316
1015
|
server.registerTool(
|
|
@@ -321,7 +1020,20 @@ function registerPinMemoryTool(server) {
|
|
|
321
1020
|
},
|
|
322
1021
|
async ({ id }) => {
|
|
323
1022
|
try {
|
|
324
|
-
const
|
|
1023
|
+
const db = getDb();
|
|
1024
|
+
const target = findMemoryByAnyId(db, id);
|
|
1025
|
+
if (!target) {
|
|
1026
|
+
return {
|
|
1027
|
+
isError: true,
|
|
1028
|
+
content: [
|
|
1029
|
+
{
|
|
1030
|
+
type: "text",
|
|
1031
|
+
text: `Memory ${id} not found.`
|
|
1032
|
+
}
|
|
1033
|
+
]
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
const memory = setPinnedState(target.row_id, 1);
|
|
325
1037
|
if (!memory) {
|
|
326
1038
|
return {
|
|
327
1039
|
isError: true,
|
|
@@ -365,7 +1077,20 @@ function registerUnpinMemoryTool(server) {
|
|
|
365
1077
|
},
|
|
366
1078
|
async ({ id }) => {
|
|
367
1079
|
try {
|
|
368
|
-
const
|
|
1080
|
+
const db = getDb();
|
|
1081
|
+
const target = findMemoryByAnyId(db, id);
|
|
1082
|
+
if (!target) {
|
|
1083
|
+
return {
|
|
1084
|
+
isError: true,
|
|
1085
|
+
content: [
|
|
1086
|
+
{
|
|
1087
|
+
type: "text",
|
|
1088
|
+
text: `Memory ${id} not found.`
|
|
1089
|
+
}
|
|
1090
|
+
]
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
const memory = setPinnedState(target.row_id, 0);
|
|
369
1094
|
if (!memory) {
|
|
370
1095
|
return {
|
|
371
1096
|
isError: true,
|
|
@@ -401,21 +1126,560 @@ function registerUnpinMemoryTool(server) {
|
|
|
401
1126
|
);
|
|
402
1127
|
}
|
|
403
1128
|
|
|
1129
|
+
// src/tools/remember.ts
|
|
1130
|
+
import { nanoid } from "nanoid";
|
|
1131
|
+
import { z as z6 } from "zod";
|
|
1132
|
+
|
|
1133
|
+
// src/lib/inference.ts
|
|
1134
|
+
var TYPE_RULES = [
|
|
1135
|
+
{
|
|
1136
|
+
type: "bug_fix",
|
|
1137
|
+
patterns: [
|
|
1138
|
+
{ pattern: /\broot cause\b/i, weight: 4 },
|
|
1139
|
+
{ pattern: /\bregression\b/i, weight: 4 },
|
|
1140
|
+
{ pattern: /\bhotfix\b/i, weight: 4 },
|
|
1141
|
+
{ pattern: /\bfix(?:ed|es|ing)?\b/i, weight: 3 },
|
|
1142
|
+
{ pattern: /\bbugs?\b/i, weight: 2 },
|
|
1143
|
+
{ pattern: /\bcrash(?:ed|es|ing)?\b/i, weight: 2 },
|
|
1144
|
+
{ pattern: /\bbroken\b/i, weight: 2 },
|
|
1145
|
+
{ pattern: /\bworkaround\b/i, weight: 2 }
|
|
1146
|
+
]
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
type: "issue",
|
|
1150
|
+
patterns: [
|
|
1151
|
+
{ pattern: /\bissue\s*#\d+/i, weight: 5 },
|
|
1152
|
+
{ pattern: /\bticket\s*#?\w+/i, weight: 4 },
|
|
1153
|
+
{ pattern: /\bjira[-\s]?\w+/i, weight: 4 },
|
|
1154
|
+
{ pattern: /\bgh[-\s]?\d+/i, weight: 3 },
|
|
1155
|
+
{ pattern: /#\d{2,}/i, weight: 2 }
|
|
1156
|
+
]
|
|
1157
|
+
},
|
|
1158
|
+
{
|
|
1159
|
+
type: "decision",
|
|
1160
|
+
patterns: [
|
|
1161
|
+
{ pattern: /\bdecided not to\b/i, weight: 5 },
|
|
1162
|
+
{ pattern: /\bdecided to\b/i, weight: 4 },
|
|
1163
|
+
{ pattern: /\bwe chose\b/i, weight: 4 },
|
|
1164
|
+
{ pattern: /\bchose\s+\w+\s+over\b/i, weight: 4 },
|
|
1165
|
+
{ pattern: /\barchitecture\b/i, weight: 3 },
|
|
1166
|
+
{ pattern: /\bdecision\b/i, weight: 3 },
|
|
1167
|
+
{ pattern: /\btrade[- ]?off\b/i, weight: 2 },
|
|
1168
|
+
{ pattern: /\brfc\b/i, weight: 2 },
|
|
1169
|
+
{ pattern: /\b(?:adopted|migrated to)\b/i, weight: 2 }
|
|
1170
|
+
]
|
|
1171
|
+
},
|
|
1172
|
+
{
|
|
1173
|
+
type: "reviewer_pattern",
|
|
1174
|
+
patterns: [
|
|
1175
|
+
{ pattern: /\breviewer(?:s)?\s+(?:prefer|want|expect|require)/i, weight: 5 },
|
|
1176
|
+
{ pattern: /\bpr\s+style\b/i, weight: 4 },
|
|
1177
|
+
{ pattern: /\bcode review\b/i, weight: 3 },
|
|
1178
|
+
{ pattern: /\bprefer(?:s|red)?\b/i, weight: 2 },
|
|
1179
|
+
{ pattern: /\breview comment\b/i, weight: 2 }
|
|
1180
|
+
]
|
|
1181
|
+
},
|
|
1182
|
+
{
|
|
1183
|
+
type: "convention",
|
|
1184
|
+
patterns: [
|
|
1185
|
+
{ pattern: /\bconvention\b/i, weight: 4 },
|
|
1186
|
+
{ pattern: /\balways\b/i, weight: 2 },
|
|
1187
|
+
{ pattern: /\bnever\b/i, weight: 2 },
|
|
1188
|
+
{ pattern: /\bstandard\b/i, weight: 2 },
|
|
1189
|
+
{ pattern: /\bstyle guide\b/i, weight: 3 },
|
|
1190
|
+
{ pattern: /\buse\b\s+\w+\s+\bfor\b/i, weight: 1 }
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
];
|
|
1194
|
+
var AUTH_KEYWORDS = /\b(?:auth|jwt|oauth|token|login|logout|session|sso|saml)\b/i;
|
|
1195
|
+
var CHOICE_KEYWORDS = /\b(?:chose|choose|decided|prefer|switched|migrated|adopted|over|instead of)\b/i;
|
|
1196
|
+
var TAG_KEYWORDS = [
|
|
1197
|
+
{ tag: "auth", pattern: /\b(?:auth|authentication|authorization)\b/i },
|
|
1198
|
+
{ tag: "jwt", pattern: /\bjwt\b/i },
|
|
1199
|
+
{ tag: "oauth", pattern: /\boauth\b/i },
|
|
1200
|
+
{ tag: "session", pattern: /\bsession(?:s)?\b/i },
|
|
1201
|
+
{ tag: "api", pattern: /\bapi\b/i },
|
|
1202
|
+
{ tag: "rest", pattern: /\brest(?:ful)?\b/i },
|
|
1203
|
+
{ tag: "graphql", pattern: /\bgraphql\b/i },
|
|
1204
|
+
{ tag: "websocket", pattern: /\bweb[- ]?socket(?:s)?\b/i },
|
|
1205
|
+
{ tag: "database", pattern: /\b(?:database|db|sqlite|postgres|mysql|mongo)\b/i },
|
|
1206
|
+
{ tag: "migration", pattern: /\bmigration(?:s)?\b/i },
|
|
1207
|
+
{ tag: "schema", pattern: /\bschema\b/i },
|
|
1208
|
+
{ tag: "frontend", pattern: /\b(?:frontend|ui|react|vue|svelte|next\.js|nextjs)\b/i },
|
|
1209
|
+
{ tag: "backend", pattern: /\b(?:backend|server|node\.js|nodejs|express|fastify)\b/i },
|
|
1210
|
+
{ tag: "testing", pattern: /\b(?:test|tests|testing|jest|vitest|pytest|rspec)\b/i },
|
|
1211
|
+
{ tag: "ci", pattern: /\b(?:ci|cd|pipeline|github actions|gitlab ci)\b/i },
|
|
1212
|
+
{ tag: "deployment", pattern: /\b(?:deploy|deployment|release|rollout)\b/i },
|
|
1213
|
+
{ tag: "performance", pattern: /\b(?:performance|perf|latency|throughput)\b/i },
|
|
1214
|
+
{ tag: "security", pattern: /\b(?:security|vuln|cve|xss|csrf|injection)\b/i },
|
|
1215
|
+
{ tag: "logging", pattern: /\b(?:log|logging|telemetry|tracing)\b/i },
|
|
1216
|
+
{ tag: "config", pattern: /\b(?:config|configuration|env|environment)\b/i },
|
|
1217
|
+
{ tag: "routing", pattern: /\b(?:route|routing|router|endpoint)\b/i },
|
|
1218
|
+
{ tag: "build", pattern: /\b(?:build|webpack|vite|tsup|rollup|esbuild)\b/i },
|
|
1219
|
+
{ tag: "docs", pattern: /\b(?:docs|documentation|readme)\b/i }
|
|
1220
|
+
];
|
|
1221
|
+
var STOP_WORDS = /* @__PURE__ */ new Set([
|
|
1222
|
+
"the",
|
|
1223
|
+
"a",
|
|
1224
|
+
"an",
|
|
1225
|
+
"and",
|
|
1226
|
+
"or",
|
|
1227
|
+
"but",
|
|
1228
|
+
"is",
|
|
1229
|
+
"are",
|
|
1230
|
+
"was",
|
|
1231
|
+
"were",
|
|
1232
|
+
"be",
|
|
1233
|
+
"been",
|
|
1234
|
+
"being",
|
|
1235
|
+
"to",
|
|
1236
|
+
"of",
|
|
1237
|
+
"in",
|
|
1238
|
+
"on",
|
|
1239
|
+
"for",
|
|
1240
|
+
"with",
|
|
1241
|
+
"by",
|
|
1242
|
+
"at",
|
|
1243
|
+
"from",
|
|
1244
|
+
"as",
|
|
1245
|
+
"that",
|
|
1246
|
+
"this",
|
|
1247
|
+
"it",
|
|
1248
|
+
"we",
|
|
1249
|
+
"our",
|
|
1250
|
+
"you",
|
|
1251
|
+
"your",
|
|
1252
|
+
"i",
|
|
1253
|
+
"my",
|
|
1254
|
+
"they",
|
|
1255
|
+
"their",
|
|
1256
|
+
"them",
|
|
1257
|
+
"he",
|
|
1258
|
+
"she",
|
|
1259
|
+
"his",
|
|
1260
|
+
"her",
|
|
1261
|
+
"if",
|
|
1262
|
+
"then",
|
|
1263
|
+
"than",
|
|
1264
|
+
"so",
|
|
1265
|
+
"do",
|
|
1266
|
+
"does",
|
|
1267
|
+
"did",
|
|
1268
|
+
"done",
|
|
1269
|
+
"not",
|
|
1270
|
+
"no",
|
|
1271
|
+
"yes",
|
|
1272
|
+
"can",
|
|
1273
|
+
"will",
|
|
1274
|
+
"would",
|
|
1275
|
+
"should",
|
|
1276
|
+
"could",
|
|
1277
|
+
"may",
|
|
1278
|
+
"might",
|
|
1279
|
+
"must",
|
|
1280
|
+
"have",
|
|
1281
|
+
"has",
|
|
1282
|
+
"had",
|
|
1283
|
+
"just",
|
|
1284
|
+
"also",
|
|
1285
|
+
"use",
|
|
1286
|
+
"used",
|
|
1287
|
+
"using",
|
|
1288
|
+
"want",
|
|
1289
|
+
"wants",
|
|
1290
|
+
"wanted",
|
|
1291
|
+
"need",
|
|
1292
|
+
"needs",
|
|
1293
|
+
"needed",
|
|
1294
|
+
"like",
|
|
1295
|
+
"now",
|
|
1296
|
+
"new",
|
|
1297
|
+
"old",
|
|
1298
|
+
"good",
|
|
1299
|
+
"bad",
|
|
1300
|
+
"make",
|
|
1301
|
+
"makes",
|
|
1302
|
+
"made",
|
|
1303
|
+
"get",
|
|
1304
|
+
"gets",
|
|
1305
|
+
"got",
|
|
1306
|
+
"set",
|
|
1307
|
+
"sets",
|
|
1308
|
+
"go",
|
|
1309
|
+
"going",
|
|
1310
|
+
"into",
|
|
1311
|
+
"over",
|
|
1312
|
+
"under",
|
|
1313
|
+
"through",
|
|
1314
|
+
"because",
|
|
1315
|
+
"when",
|
|
1316
|
+
"where",
|
|
1317
|
+
"while",
|
|
1318
|
+
"there",
|
|
1319
|
+
"here",
|
|
1320
|
+
"what",
|
|
1321
|
+
"which",
|
|
1322
|
+
"who",
|
|
1323
|
+
"why",
|
|
1324
|
+
"how",
|
|
1325
|
+
"live",
|
|
1326
|
+
"lives",
|
|
1327
|
+
"living",
|
|
1328
|
+
"keep",
|
|
1329
|
+
"kept",
|
|
1330
|
+
"keeps",
|
|
1331
|
+
"take",
|
|
1332
|
+
"takes",
|
|
1333
|
+
"took",
|
|
1334
|
+
"taken",
|
|
1335
|
+
"say",
|
|
1336
|
+
"says",
|
|
1337
|
+
"said",
|
|
1338
|
+
"tell",
|
|
1339
|
+
"tells",
|
|
1340
|
+
"told",
|
|
1341
|
+
"know",
|
|
1342
|
+
"knows",
|
|
1343
|
+
"known",
|
|
1344
|
+
"knew",
|
|
1345
|
+
"redirect",
|
|
1346
|
+
"redirects",
|
|
1347
|
+
"redirected",
|
|
1348
|
+
"redirecting",
|
|
1349
|
+
"user",
|
|
1350
|
+
"users",
|
|
1351
|
+
"page",
|
|
1352
|
+
"pages"
|
|
1353
|
+
]);
|
|
1354
|
+
function inferMemoryType(text) {
|
|
1355
|
+
const scores = /* @__PURE__ */ new Map();
|
|
1356
|
+
for (const rule of TYPE_RULES) {
|
|
1357
|
+
let score = 0;
|
|
1358
|
+
for (const { pattern, weight } of rule.patterns) {
|
|
1359
|
+
if (pattern.test(text)) {
|
|
1360
|
+
score += weight;
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
if (score > 0) {
|
|
1364
|
+
scores.set(rule.type, (scores.get(rule.type) ?? 0) + score);
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
if (AUTH_KEYWORDS.test(text)) {
|
|
1368
|
+
if (CHOICE_KEYWORDS.test(text)) {
|
|
1369
|
+
scores.set("decision", (scores.get("decision") ?? 0) + 3);
|
|
1370
|
+
} else {
|
|
1371
|
+
scores.set("convention", (scores.get("convention") ?? 0) + 2);
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
if (scores.size === 0) {
|
|
1375
|
+
return "convention";
|
|
1376
|
+
}
|
|
1377
|
+
let bestType = "convention";
|
|
1378
|
+
let bestScore = -1;
|
|
1379
|
+
for (const [type, score] of scores) {
|
|
1380
|
+
if (score > bestScore) {
|
|
1381
|
+
bestType = type;
|
|
1382
|
+
bestScore = score;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
return bestType;
|
|
1386
|
+
}
|
|
1387
|
+
function extractKeywordTags(text) {
|
|
1388
|
+
const found = [];
|
|
1389
|
+
for (const { tag, pattern } of TAG_KEYWORDS) {
|
|
1390
|
+
if (pattern.test(text)) {
|
|
1391
|
+
found.push(tag);
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
return found;
|
|
1395
|
+
}
|
|
1396
|
+
function extractIdentifierTags(text) {
|
|
1397
|
+
const tokens = /* @__PURE__ */ new Set();
|
|
1398
|
+
const pathLike = text.match(/\/(?:[a-z0-9_-]+\/?){1,4}/gi);
|
|
1399
|
+
if (pathLike) {
|
|
1400
|
+
for (const segment of pathLike) {
|
|
1401
|
+
for (const part of segment.split("/")) {
|
|
1402
|
+
if (part.length >= 3 && /^[a-z0-9_-]+$/i.test(part)) {
|
|
1403
|
+
tokens.add(part.toLowerCase());
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
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);
|
|
1409
|
+
if (fileLike) {
|
|
1410
|
+
for (const file of fileLike) {
|
|
1411
|
+
const base = file.split(".").slice(0, -1).join(".");
|
|
1412
|
+
if (base.length >= 3) {
|
|
1413
|
+
tokens.add(base.toLowerCase());
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
return Array.from(tokens);
|
|
1418
|
+
}
|
|
1419
|
+
function extractSalientWords(text, limit) {
|
|
1420
|
+
const words = text.toLowerCase().replace(/[^a-z0-9\s/_-]/g, " ").split(/\s+/).filter((word) => word.length >= 4 && !STOP_WORDS.has(word));
|
|
1421
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1422
|
+
for (const word of words) {
|
|
1423
|
+
counts.set(word, (counts.get(word) ?? 0) + 1);
|
|
1424
|
+
}
|
|
1425
|
+
return Array.from(counts.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, limit).map(([word]) => word);
|
|
1426
|
+
}
|
|
1427
|
+
function inferTags(text) {
|
|
1428
|
+
const ordered = [];
|
|
1429
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1430
|
+
const push = (value) => {
|
|
1431
|
+
const normalized = value.trim().toLowerCase();
|
|
1432
|
+
if (!normalized || seen.has(normalized)) {
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
seen.add(normalized);
|
|
1436
|
+
ordered.push(normalized);
|
|
1437
|
+
};
|
|
1438
|
+
for (const tag of extractKeywordTags(text)) {
|
|
1439
|
+
push(tag);
|
|
1440
|
+
}
|
|
1441
|
+
for (const tag of extractIdentifierTags(text)) {
|
|
1442
|
+
push(tag);
|
|
1443
|
+
}
|
|
1444
|
+
if (ordered.length < 5) {
|
|
1445
|
+
for (const word of extractSalientWords(text, 8)) {
|
|
1446
|
+
push(word);
|
|
1447
|
+
if (ordered.length >= 5) {
|
|
1448
|
+
break;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
return ordered.slice(0, 5);
|
|
1453
|
+
}
|
|
1454
|
+
function inferMemoryFromNote(text) {
|
|
1455
|
+
return {
|
|
1456
|
+
type: inferMemoryType(text),
|
|
1457
|
+
tags: inferTags(text)
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/tools/remember.ts
|
|
1462
|
+
var rememberInputSchema = {
|
|
1463
|
+
note: z6.string().trim().min(1, "note is required"),
|
|
1464
|
+
repo: z6.string().trim().min(1).optional(),
|
|
1465
|
+
type: z6.enum(MEMORY_TYPES).optional(),
|
|
1466
|
+
tags: z6.array(z6.string().trim().min(1)).optional()
|
|
1467
|
+
};
|
|
1468
|
+
function mergeTagLists2(...lists) {
|
|
1469
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1470
|
+
const out = [];
|
|
1471
|
+
for (const list of lists) {
|
|
1472
|
+
if (!list) continue;
|
|
1473
|
+
for (const raw of list) {
|
|
1474
|
+
const value = raw.trim().toLowerCase();
|
|
1475
|
+
if (!value || seen.has(value)) continue;
|
|
1476
|
+
seen.add(value);
|
|
1477
|
+
out.push(value);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
return out;
|
|
1481
|
+
}
|
|
1482
|
+
function parseStoredTags(raw) {
|
|
1483
|
+
try {
|
|
1484
|
+
const parsed = JSON.parse(raw);
|
|
1485
|
+
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
1486
|
+
} catch {
|
|
1487
|
+
return [];
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
function parseStoredMetadata(raw) {
|
|
1491
|
+
try {
|
|
1492
|
+
const parsed = JSON.parse(raw);
|
|
1493
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
1494
|
+
return parsed;
|
|
1495
|
+
}
|
|
1496
|
+
} catch {
|
|
1497
|
+
}
|
|
1498
|
+
return {};
|
|
1499
|
+
}
|
|
1500
|
+
function registerRememberTool(server) {
|
|
1501
|
+
server.registerTool(
|
|
1502
|
+
"remember",
|
|
1503
|
+
{
|
|
1504
|
+
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.",
|
|
1505
|
+
inputSchema: rememberInputSchema
|
|
1506
|
+
},
|
|
1507
|
+
async ({ note, repo, type, tags }) => {
|
|
1508
|
+
try {
|
|
1509
|
+
const db = getDb();
|
|
1510
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
1511
|
+
const inferred = inferMemoryFromNote(note);
|
|
1512
|
+
const finalType = type ?? inferred.type;
|
|
1513
|
+
const finalTags = mergeTagLists2(tags, inferred.tags).slice(0, 5);
|
|
1514
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
1515
|
+
const duplicate = findDuplicate(db, resolved.canonical, note);
|
|
1516
|
+
if (duplicate) {
|
|
1517
|
+
const existing = duplicate.memory;
|
|
1518
|
+
const existingTags = parseStoredTags(existing.tags);
|
|
1519
|
+
const mergedTags = mergeTagLists2(existingTags, finalTags);
|
|
1520
|
+
const metadata2 = parseStoredMetadata(
|
|
1521
|
+
existing.metadata_json ?? "{}"
|
|
1522
|
+
);
|
|
1523
|
+
const changelog = metadata2.changelog ?? [];
|
|
1524
|
+
changelog.push({
|
|
1525
|
+
at: now,
|
|
1526
|
+
action: "merged",
|
|
1527
|
+
similarity: Number(duplicate.similarity.toFixed(3)),
|
|
1528
|
+
previous_note: existing.note
|
|
1529
|
+
});
|
|
1530
|
+
metadata2.changelog = changelog;
|
|
1531
|
+
const longerNote = note.length > existing.note.length ? note : existing.note;
|
|
1532
|
+
const nextType = type ?? existing.type;
|
|
1533
|
+
db.prepare(
|
|
1534
|
+
`
|
|
1535
|
+
UPDATE memories
|
|
1536
|
+
SET note = ?, note_normalized = ?, tags = ?, type = ?, metadata_json = ?, updated_at = ?
|
|
1537
|
+
WHERE rowid = ?
|
|
1538
|
+
`
|
|
1539
|
+
).run(
|
|
1540
|
+
longerNote,
|
|
1541
|
+
normalizeText(longerNote),
|
|
1542
|
+
JSON.stringify(mergedTags),
|
|
1543
|
+
nextType,
|
|
1544
|
+
JSON.stringify(metadata2),
|
|
1545
|
+
now,
|
|
1546
|
+
existing.row_id
|
|
1547
|
+
);
|
|
1548
|
+
return {
|
|
1549
|
+
content: [
|
|
1550
|
+
{
|
|
1551
|
+
type: "text",
|
|
1552
|
+
text: `Merged into memory ${existing.row_id} for ${resolved.canonical} (similarity ${duplicate.similarity.toFixed(2)}, type ${nextType}, tags: ${mergedTags.join(", ") || "none"}).`
|
|
1553
|
+
}
|
|
1554
|
+
]
|
|
1555
|
+
};
|
|
1556
|
+
}
|
|
1557
|
+
const id = nanoid();
|
|
1558
|
+
const metadata = {
|
|
1559
|
+
changelog: [
|
|
1560
|
+
{
|
|
1561
|
+
at: now,
|
|
1562
|
+
action: "created"
|
|
1563
|
+
}
|
|
1564
|
+
],
|
|
1565
|
+
inferred: {
|
|
1566
|
+
type: inferred.type,
|
|
1567
|
+
tags: inferred.tags,
|
|
1568
|
+
type_overridden: type !== void 0
|
|
1569
|
+
}
|
|
1570
|
+
};
|
|
1571
|
+
db.prepare(
|
|
1572
|
+
`
|
|
1573
|
+
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json, note_normalized)
|
|
1574
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, ?, ?)
|
|
1575
|
+
`
|
|
1576
|
+
).run(
|
|
1577
|
+
id,
|
|
1578
|
+
resolved.canonical,
|
|
1579
|
+
finalType,
|
|
1580
|
+
note,
|
|
1581
|
+
JSON.stringify(finalTags),
|
|
1582
|
+
now,
|
|
1583
|
+
now,
|
|
1584
|
+
JSON.stringify(metadata),
|
|
1585
|
+
normalizeText(note)
|
|
1586
|
+
);
|
|
1587
|
+
const inserted = db.prepare("SELECT rowid AS row_id FROM memories WHERE id = ?").get(id);
|
|
1588
|
+
return {
|
|
1589
|
+
content: [
|
|
1590
|
+
{
|
|
1591
|
+
type: "text",
|
|
1592
|
+
text: `Stored memory ${inserted?.row_id ?? "?"} for ${resolved.canonical} (type ${finalType}, tags: ${finalTags.join(", ") || "none"}).`
|
|
1593
|
+
}
|
|
1594
|
+
]
|
|
1595
|
+
};
|
|
1596
|
+
} catch (error) {
|
|
1597
|
+
const message = error instanceof Error ? error.message : "Unknown error while remembering note.";
|
|
1598
|
+
return {
|
|
1599
|
+
isError: true,
|
|
1600
|
+
content: [
|
|
1601
|
+
{
|
|
1602
|
+
type: "text",
|
|
1603
|
+
text: `Failed to remember note: ${message}`
|
|
1604
|
+
}
|
|
1605
|
+
]
|
|
1606
|
+
};
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// src/tools/resolve-repo.ts
|
|
1613
|
+
import { z as z7 } from "zod";
|
|
1614
|
+
var resolveRepoInputSchema = {
|
|
1615
|
+
cwd: z7.string().trim().min(1).optional()
|
|
1616
|
+
};
|
|
1617
|
+
function registerResolveRepoTool(server) {
|
|
1618
|
+
server.registerTool(
|
|
1619
|
+
"resolve_repo",
|
|
1620
|
+
{
|
|
1621
|
+
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.",
|
|
1622
|
+
inputSchema: resolveRepoInputSchema
|
|
1623
|
+
},
|
|
1624
|
+
async ({ cwd }) => {
|
|
1625
|
+
try {
|
|
1626
|
+
const db = getDb();
|
|
1627
|
+
const target = cwd?.trim() || getWorkspaceRoot();
|
|
1628
|
+
const resolved = resolveRepo(target, db);
|
|
1629
|
+
const payload = {
|
|
1630
|
+
canonical: resolved.canonical,
|
|
1631
|
+
aliases: resolved.aliases,
|
|
1632
|
+
cwd: resolved.cwd,
|
|
1633
|
+
gitRemote: resolved.gitRemote,
|
|
1634
|
+
source: resolved.source
|
|
1635
|
+
};
|
|
1636
|
+
return {
|
|
1637
|
+
content: [
|
|
1638
|
+
{
|
|
1639
|
+
type: "text",
|
|
1640
|
+
text: JSON.stringify(payload, null, 2)
|
|
1641
|
+
}
|
|
1642
|
+
]
|
|
1643
|
+
};
|
|
1644
|
+
} catch (error) {
|
|
1645
|
+
const message = error instanceof Error ? error.message : "Unknown error while resolving repo.";
|
|
1646
|
+
return {
|
|
1647
|
+
isError: true,
|
|
1648
|
+
content: [
|
|
1649
|
+
{
|
|
1650
|
+
type: "text",
|
|
1651
|
+
text: `Failed to resolve repo: ${message}`
|
|
1652
|
+
}
|
|
1653
|
+
]
|
|
1654
|
+
};
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
);
|
|
1658
|
+
}
|
|
1659
|
+
|
|
404
1660
|
// src/tools/search.ts
|
|
405
|
-
import { z as
|
|
1661
|
+
import { z as z8 } from "zod";
|
|
406
1662
|
var searchMemoryInputSchema = {
|
|
407
|
-
query:
|
|
408
|
-
repo:
|
|
409
|
-
limit:
|
|
1663
|
+
query: z8.string().trim().min(1, "query is required"),
|
|
1664
|
+
repo: z8.string().trim().min(1).optional(),
|
|
1665
|
+
limit: z8.number().int().positive().max(50).default(5)
|
|
410
1666
|
};
|
|
411
|
-
function
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
1667
|
+
function tokenizeQuery(query) {
|
|
1668
|
+
return query.toLowerCase().replace(/["()]/g, " ").split(/[\s/_\-.,;:!?]+/).map((token) => token.replace(/[^a-z0-9*]/g, "")).filter((token) => token.length >= 2);
|
|
1669
|
+
}
|
|
1670
|
+
function buildFtsQuery2(tokens) {
|
|
1671
|
+
if (tokens.length === 0) {
|
|
1672
|
+
return null;
|
|
415
1673
|
}
|
|
416
|
-
return
|
|
1674
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" AND ");
|
|
417
1675
|
}
|
|
418
|
-
function
|
|
1676
|
+
function buildFtsQueryOr(tokens) {
|
|
1677
|
+
if (tokens.length === 0) {
|
|
1678
|
+
return null;
|
|
1679
|
+
}
|
|
1680
|
+
return tokens.map((token) => `"${token.replace(/"/g, '""')}"`).join(" OR ");
|
|
1681
|
+
}
|
|
1682
|
+
function parseTags4(raw) {
|
|
419
1683
|
try {
|
|
420
1684
|
const parsed = JSON.parse(raw);
|
|
421
1685
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -423,58 +1687,89 @@ function parseTags2(raw) {
|
|
|
423
1687
|
return [];
|
|
424
1688
|
}
|
|
425
1689
|
}
|
|
1690
|
+
function runFts(ftsQuery, resolvedRepo, limit) {
|
|
1691
|
+
const db = getDb();
|
|
1692
|
+
try {
|
|
1693
|
+
if (resolvedRepo) {
|
|
1694
|
+
return db.prepare(
|
|
1695
|
+
`
|
|
1696
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1697
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1698
|
+
FROM memories_fts
|
|
1699
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1700
|
+
WHERE memories_fts MATCH ? AND m.repo = ?
|
|
1701
|
+
ORDER BY rank
|
|
1702
|
+
LIMIT ?
|
|
1703
|
+
`
|
|
1704
|
+
).all(ftsQuery, resolvedRepo, limit);
|
|
1705
|
+
}
|
|
1706
|
+
return db.prepare(
|
|
1707
|
+
`
|
|
1708
|
+
SELECT m.rowid AS row_id, m.id, m.repo, m.type, m.note, m.tags,
|
|
1709
|
+
m.created_at, m.updated_at, m.pinned, bm25(memories_fts) AS rank
|
|
1710
|
+
FROM memories_fts
|
|
1711
|
+
JOIN memories AS m ON m.rowid = memories_fts.rowid
|
|
1712
|
+
WHERE memories_fts MATCH ?
|
|
1713
|
+
ORDER BY rank
|
|
1714
|
+
LIMIT ?
|
|
1715
|
+
`
|
|
1716
|
+
).all(ftsQuery, limit);
|
|
1717
|
+
} catch {
|
|
1718
|
+
return [];
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
426
1721
|
function registerSearchMemoryTool(server) {
|
|
427
1722
|
server.registerTool(
|
|
428
1723
|
"search_memory",
|
|
429
1724
|
{
|
|
430
|
-
description: "Search memories using full-text search with optional repository filtering.",
|
|
1725
|
+
description: "Search memories using full-text search with optional repository filtering. Falls back to recent + pinned context when the query has no exact matches.",
|
|
431
1726
|
inputSchema: searchMemoryInputSchema
|
|
432
1727
|
},
|
|
433
1728
|
async ({ query, repo, limit }) => {
|
|
434
1729
|
try {
|
|
435
1730
|
const db = getDb();
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
).all(ftsQuery, limit);
|
|
1731
|
+
const tokens = tokenizeQuery(query);
|
|
1732
|
+
const resolvedRepo = repo ? resolveRepoArg(repo, getWorkspaceRoot(), db).canonical : void 0;
|
|
1733
|
+
const andQuery = buildFtsQuery2(tokens);
|
|
1734
|
+
let rows = [];
|
|
1735
|
+
if (andQuery) {
|
|
1736
|
+
rows = runFts(andQuery, resolvedRepo, limit);
|
|
1737
|
+
}
|
|
1738
|
+
if (rows.length === 0 && tokens.length > 1) {
|
|
1739
|
+
const orQuery = buildFtsQueryOr(tokens);
|
|
1740
|
+
if (orQuery) {
|
|
1741
|
+
rows = runFts(orQuery, resolvedRepo, limit);
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
let usedFallback = false;
|
|
1745
|
+
if (rows.length === 0 && resolvedRepo) {
|
|
1746
|
+
const fallback = fetchRepoContext(db, resolvedRepo, limit);
|
|
1747
|
+
rows = fallback.map((row) => ({ ...row, rank: 0 }));
|
|
1748
|
+
usedFallback = fallback.length > 0;
|
|
1749
|
+
}
|
|
456
1750
|
if (rows.length === 0) {
|
|
457
1751
|
return {
|
|
458
1752
|
content: [
|
|
459
1753
|
{
|
|
460
1754
|
type: "text",
|
|
461
|
-
text:
|
|
1755
|
+
text: resolvedRepo ? `No memories matched "${query}" in ${resolvedRepo}.` : `No memories matched "${query}".`
|
|
462
1756
|
}
|
|
463
1757
|
]
|
|
464
1758
|
};
|
|
465
1759
|
}
|
|
466
1760
|
const formatted = rows.map((row, index) => {
|
|
467
|
-
const tags =
|
|
1761
|
+
const tags = parseTags4(row.tags);
|
|
468
1762
|
const tagsText = tags.length > 0 ? ` | tags: ${tags.join(", ")}` : "";
|
|
469
1763
|
const pinPrefix = row.pinned ? "\u{1F4CC} Pinned " : "";
|
|
470
1764
|
return `${index + 1}. [${row.repo}] ${row.type} (${row.row_id} | legacy: ${row.id})
|
|
471
1765
|
${pinPrefix}${row.note}${tagsText}`;
|
|
472
1766
|
}).join("\n\n");
|
|
1767
|
+
const header = usedFallback ? `No exact match for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}; showing recent + pinned context:` : `Search results for "${query}"${resolvedRepo ? ` in ${resolvedRepo}` : ""}:`;
|
|
473
1768
|
return {
|
|
474
1769
|
content: [
|
|
475
1770
|
{
|
|
476
1771
|
type: "text",
|
|
477
|
-
text:
|
|
1772
|
+
text: `${header}
|
|
478
1773
|
|
|
479
1774
|
${formatted}`
|
|
480
1775
|
}
|
|
@@ -497,35 +1792,45 @@ ${formatted}`
|
|
|
497
1792
|
}
|
|
498
1793
|
|
|
499
1794
|
// src/tools/store.ts
|
|
500
|
-
import { nanoid } from "nanoid";
|
|
501
|
-
import { z as
|
|
1795
|
+
import { nanoid as nanoid2 } from "nanoid";
|
|
1796
|
+
import { z as z9 } from "zod";
|
|
502
1797
|
var storeContextInputSchema = {
|
|
503
|
-
repo:
|
|
504
|
-
type:
|
|
505
|
-
note:
|
|
506
|
-
tags:
|
|
1798
|
+
repo: z9.string().trim().min(1).optional(),
|
|
1799
|
+
type: z9.enum(MEMORY_TYPES),
|
|
1800
|
+
note: z9.string().trim().min(1, "note is required"),
|
|
1801
|
+
tags: z9.array(z9.string().trim().min(1)).optional()
|
|
507
1802
|
};
|
|
508
1803
|
function registerStoreContextTool(server) {
|
|
509
1804
|
server.registerTool(
|
|
510
1805
|
"store_context",
|
|
511
1806
|
{
|
|
512
|
-
description: "Store repository-specific contributor context such as bug fixes, conventions, and decisions.",
|
|
1807
|
+
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.",
|
|
513
1808
|
inputSchema: storeContextInputSchema
|
|
514
1809
|
},
|
|
515
1810
|
async ({ repo, type, note, tags }) => {
|
|
516
1811
|
try {
|
|
517
1812
|
const db = getDb();
|
|
1813
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
518
1814
|
const now = Math.floor(Date.now() / 1e3);
|
|
519
|
-
const id =
|
|
1815
|
+
const id = nanoid2();
|
|
520
1816
|
const normalizedTags = Array.from(
|
|
521
1817
|
new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))
|
|
522
1818
|
);
|
|
523
1819
|
db.prepare(
|
|
524
1820
|
`
|
|
525
|
-
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at)
|
|
526
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1821
|
+
INSERT INTO memories (id, repo, type, note, tags, created_at, updated_at, pinned, metadata_json, note_normalized)
|
|
1822
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, 0, '{}', ?)
|
|
527
1823
|
`
|
|
528
|
-
).run(
|
|
1824
|
+
).run(
|
|
1825
|
+
id,
|
|
1826
|
+
resolved.canonical,
|
|
1827
|
+
type,
|
|
1828
|
+
note,
|
|
1829
|
+
JSON.stringify(normalizedTags),
|
|
1830
|
+
now,
|
|
1831
|
+
now,
|
|
1832
|
+
normalizeText(note)
|
|
1833
|
+
);
|
|
529
1834
|
const stored = db.prepare(
|
|
530
1835
|
`
|
|
531
1836
|
SELECT rowid AS row_id, id
|
|
@@ -537,7 +1842,7 @@ function registerStoreContextTool(server) {
|
|
|
537
1842
|
content: [
|
|
538
1843
|
{
|
|
539
1844
|
type: "text",
|
|
540
|
-
text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${
|
|
1845
|
+
text: `Stored memory ${id} (numeric id: ${stored?.row_id ?? "unknown"}) for ${resolved.canonical} (${type}).`
|
|
541
1846
|
}
|
|
542
1847
|
]
|
|
543
1848
|
};
|
|
@@ -558,9 +1863,9 @@ function registerStoreContextTool(server) {
|
|
|
558
1863
|
}
|
|
559
1864
|
|
|
560
1865
|
// src/tools/summarize.ts
|
|
561
|
-
import { z as
|
|
1866
|
+
import { z as z10 } from "zod";
|
|
562
1867
|
var summarizeRepoContextInputSchema = {
|
|
563
|
-
repo:
|
|
1868
|
+
repo: z10.string().trim().min(1).optional()
|
|
564
1869
|
};
|
|
565
1870
|
var sectionTitleByType = {
|
|
566
1871
|
convention: "Conventions",
|
|
@@ -580,6 +1885,7 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
580
1885
|
async ({ repo }) => {
|
|
581
1886
|
try {
|
|
582
1887
|
const db = getDb();
|
|
1888
|
+
const resolved = resolveRepoArg(repo, getWorkspaceRoot(), db);
|
|
583
1889
|
const rows = db.prepare(
|
|
584
1890
|
`
|
|
585
1891
|
SELECT rowid AS row_id, type, note, pinned
|
|
@@ -587,13 +1893,13 @@ function registerSummarizeRepoContextTool(server) {
|
|
|
587
1893
|
WHERE repo = ?
|
|
588
1894
|
ORDER BY pinned DESC, updated_at DESC
|
|
589
1895
|
`
|
|
590
|
-
).all(
|
|
1896
|
+
).all(resolved.canonical);
|
|
591
1897
|
if (rows.length === 0) {
|
|
592
1898
|
return {
|
|
593
1899
|
content: [
|
|
594
1900
|
{
|
|
595
1901
|
type: "text",
|
|
596
|
-
text: `Fossel Context Summary: ${
|
|
1902
|
+
text: `Fossel Context Summary: ${resolved.canonical}
|
|
597
1903
|
|
|
598
1904
|
No memories found.`
|
|
599
1905
|
}
|
|
@@ -601,7 +1907,7 @@ No memories found.`
|
|
|
601
1907
|
};
|
|
602
1908
|
}
|
|
603
1909
|
const pinnedLines = rows.filter((row) => row.pinned === 1).map((row) => `- (${row.row_id}) ${row.note}`);
|
|
604
|
-
const sections = [`Fossel Context Summary: ${
|
|
1910
|
+
const sections = [`Fossel Context Summary: ${resolved.canonical}`];
|
|
605
1911
|
if (pinnedLines.length > 0) {
|
|
606
1912
|
sections.push(`\u{1F4CC} Pinned
|
|
607
1913
|
${pinnedLines.join("\n")}`);
|
|
@@ -639,13 +1945,15 @@ ${entries.join("\n")}`);
|
|
|
639
1945
|
}
|
|
640
1946
|
|
|
641
1947
|
// src/tools/update.ts
|
|
642
|
-
import { z as
|
|
1948
|
+
import { z as z11 } from "zod";
|
|
643
1949
|
var updateMemoryInputSchema = {
|
|
644
|
-
id
|
|
645
|
-
|
|
646
|
-
|
|
1950
|
+
// Accept numeric row_id or legacy string id so callers can paste whichever
|
|
1951
|
+
// form they have.
|
|
1952
|
+
id: z11.union([z11.number().int().positive(), z11.string().trim().min(1)]),
|
|
1953
|
+
content: z11.string().trim().min(1).optional(),
|
|
1954
|
+
memory_type: z11.enum(MEMORY_TYPES).optional()
|
|
647
1955
|
};
|
|
648
|
-
function
|
|
1956
|
+
function parseTags5(raw) {
|
|
649
1957
|
try {
|
|
650
1958
|
const parsed = JSON.parse(raw);
|
|
651
1959
|
return Array.isArray(parsed) ? parsed.filter((value) => typeof value === "string") : [];
|
|
@@ -654,7 +1962,7 @@ function parseTags3(raw) {
|
|
|
654
1962
|
}
|
|
655
1963
|
}
|
|
656
1964
|
function formatMemory(memory) {
|
|
657
|
-
const tags =
|
|
1965
|
+
const tags = parseTags5(memory.tags);
|
|
658
1966
|
const tagsLine = tags.length > 0 ? tags.join(", ") : "(none)";
|
|
659
1967
|
return [
|
|
660
1968
|
`Memory ${memory.row_id} updated successfully.`,
|
|
@@ -673,7 +1981,7 @@ function registerUpdateMemoryTool(server) {
|
|
|
673
1981
|
server.registerTool(
|
|
674
1982
|
"update_memory",
|
|
675
1983
|
{
|
|
676
|
-
description: "Update an existing memory by numeric
|
|
1984
|
+
description: "Update an existing memory by id (numeric or legacy string) with partial fields.",
|
|
677
1985
|
inputSchema: updateMemoryInputSchema
|
|
678
1986
|
},
|
|
679
1987
|
async ({ id, content, memory_type }) => {
|
|
@@ -690,14 +1998,8 @@ function registerUpdateMemoryTool(server) {
|
|
|
690
1998
|
};
|
|
691
1999
|
}
|
|
692
2000
|
const db = getDb();
|
|
693
|
-
const
|
|
694
|
-
|
|
695
|
-
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
696
|
-
FROM memories
|
|
697
|
-
WHERE rowid = ?
|
|
698
|
-
`
|
|
699
|
-
).get(id);
|
|
700
|
-
if (!existing) {
|
|
2001
|
+
const target = findMemoryByAnyId(db, id);
|
|
2002
|
+
if (!target) {
|
|
701
2003
|
return {
|
|
702
2004
|
isError: true,
|
|
703
2005
|
content: [
|
|
@@ -708,23 +2010,41 @@ function registerUpdateMemoryTool(server) {
|
|
|
708
2010
|
]
|
|
709
2011
|
};
|
|
710
2012
|
}
|
|
2013
|
+
const existing = db.prepare(
|
|
2014
|
+
`
|
|
2015
|
+
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
2016
|
+
FROM memories
|
|
2017
|
+
WHERE rowid = ?
|
|
2018
|
+
`
|
|
2019
|
+
).get(target.row_id);
|
|
711
2020
|
const now = Math.floor(Date.now() / 1e3);
|
|
712
2021
|
const nextType = memory_type ?? existing.type;
|
|
713
2022
|
const nextNote = content ?? existing.note;
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
2023
|
+
const nextNormalized = content ? normalizeText(content) : null;
|
|
2024
|
+
if (nextNormalized !== null) {
|
|
2025
|
+
db.prepare(
|
|
2026
|
+
`
|
|
2027
|
+
UPDATE memories
|
|
2028
|
+
SET type = ?, note = ?, note_normalized = ?, updated_at = ?
|
|
2029
|
+
WHERE rowid = ?
|
|
2030
|
+
`
|
|
2031
|
+
).run(nextType, nextNote, nextNormalized, now, existing.row_id);
|
|
2032
|
+
} else {
|
|
2033
|
+
db.prepare(
|
|
2034
|
+
`
|
|
2035
|
+
UPDATE memories
|
|
2036
|
+
SET type = ?, note = ?, updated_at = ?
|
|
2037
|
+
WHERE rowid = ?
|
|
2038
|
+
`
|
|
2039
|
+
).run(nextType, nextNote, now, existing.row_id);
|
|
2040
|
+
}
|
|
721
2041
|
const updated = db.prepare(
|
|
722
2042
|
`
|
|
723
2043
|
SELECT rowid AS row_id, id, repo, type, note, tags, created_at, updated_at, pinned
|
|
724
2044
|
FROM memories
|
|
725
2045
|
WHERE rowid = ?
|
|
726
2046
|
`
|
|
727
|
-
).get(
|
|
2047
|
+
).get(existing.row_id);
|
|
728
2048
|
if (!updated) {
|
|
729
2049
|
return {
|
|
730
2050
|
isError: true,
|
|
@@ -764,13 +2084,61 @@ function registerUpdateMemoryTool(server) {
|
|
|
764
2084
|
function resolveDbPath() {
|
|
765
2085
|
return process.env.FOSSEL_DB_PATH?.trim() || join(homedir(), ".fossel", "memory.db");
|
|
766
2086
|
}
|
|
2087
|
+
function registerStartupContextResource(server) {
|
|
2088
|
+
server.registerResource(
|
|
2089
|
+
"fossel-startup-context",
|
|
2090
|
+
"fossel://context/current-repo",
|
|
2091
|
+
{
|
|
2092
|
+
title: "Fossel: current repo context",
|
|
2093
|
+
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.",
|
|
2094
|
+
mimeType: "text/markdown"
|
|
2095
|
+
},
|
|
2096
|
+
async (uri) => {
|
|
2097
|
+
try {
|
|
2098
|
+
const db = getDb();
|
|
2099
|
+
const resolved = resolveRepo(getWorkspaceRoot(), db);
|
|
2100
|
+
const rows = fetchRepoContext(db, resolved.canonical, 5);
|
|
2101
|
+
const text = formatContext(rows, {
|
|
2102
|
+
repo: resolved.canonical,
|
|
2103
|
+
format: "markdown"
|
|
2104
|
+
});
|
|
2105
|
+
return {
|
|
2106
|
+
contents: [
|
|
2107
|
+
{
|
|
2108
|
+
uri: uri.href,
|
|
2109
|
+
mimeType: "text/markdown",
|
|
2110
|
+
text
|
|
2111
|
+
}
|
|
2112
|
+
]
|
|
2113
|
+
};
|
|
2114
|
+
} catch (error) {
|
|
2115
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2116
|
+
return {
|
|
2117
|
+
contents: [
|
|
2118
|
+
{
|
|
2119
|
+
uri: uri.href,
|
|
2120
|
+
mimeType: "text/markdown",
|
|
2121
|
+
text: `# Fossel context unavailable
|
|
2122
|
+
|
|
2123
|
+
${message}`
|
|
2124
|
+
}
|
|
2125
|
+
]
|
|
2126
|
+
};
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
);
|
|
2130
|
+
}
|
|
767
2131
|
async function startServer() {
|
|
768
2132
|
const dbPath = resolveDbPath();
|
|
769
2133
|
initDb(dbPath);
|
|
770
2134
|
const server = new McpServer({
|
|
771
2135
|
name: "fossel",
|
|
772
|
-
version: "1.
|
|
2136
|
+
version: "1.1.1"
|
|
773
2137
|
});
|
|
2138
|
+
registerRememberTool(server);
|
|
2139
|
+
registerGetContextTool(server);
|
|
2140
|
+
registerResolveRepoTool(server);
|
|
2141
|
+
registerDedupeRepoTool(server);
|
|
774
2142
|
registerStoreContextTool(server);
|
|
775
2143
|
registerGetRepoContextTool(server);
|
|
776
2144
|
registerSearchMemoryTool(server);
|
|
@@ -779,6 +2147,7 @@ async function startServer() {
|
|
|
779
2147
|
registerPinMemoryTool(server);
|
|
780
2148
|
registerUnpinMemoryTool(server);
|
|
781
2149
|
registerSummarizeRepoContextTool(server);
|
|
2150
|
+
registerStartupContextResource(server);
|
|
782
2151
|
const transport = new StdioServerTransport();
|
|
783
2152
|
await server.connect(transport);
|
|
784
2153
|
}
|