@danielmarbach/mnemonic-mcp 0.15.0 → 0.16.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/CHANGELOG.md +16 -0
- package/README.md +2 -2
- package/build/index.js +248 -56
- package/build/index.js.map +1 -1
- package/build/relationships.d.ts +47 -0
- package/build/relationships.d.ts.map +1 -0
- package/build/relationships.js +153 -0
- package/build/relationships.js.map +1 -0
- package/build/structured-content.d.ts +238 -3
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +27 -1
- package/build/structured-content.js.map +1 -1
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,22 @@ All notable changes to `mnemonic` will be documented in this file.
|
|
|
4
4
|
|
|
5
5
|
The format is loosely based on Keep a Changelog and uses semver-style version headings.
|
|
6
6
|
|
|
7
|
+
## [0.16.0] - Unreleased
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Bounded 1-hop relationship expansion layer (`src/relationships.ts`): `recall`, `project_memory_summary`, and `get` now surface direct related notes as compact previews, scored by same-project priority, anchor status, recency, and confidence.
|
|
12
|
+
- `recall` automatically attaches relationship previews to top results (top 1 by default, top 3 when result count is small). Previews appear in both text and structured output.
|
|
13
|
+
- `project_memory_summary` orientation entries (`primaryEntry` and `suggestedNext`) include bounded relationship previews, including the fallback primaryEntry when no anchor notes exist.
|
|
14
|
+
- `get` accepts an optional `includeRelationships` parameter; when true, each returned note includes a bounded 1-hop relationship preview in both text and structured output.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- `RecallResult.results` entries now include an optional `relationships` field (`RelationshipPreview`).
|
|
19
|
+
- `GetResult.notes` entries now include an optional `relationships` field (`RelationshipPreview`).
|
|
20
|
+
- `OrientationNote` now includes an optional `relationships` field (`RelationshipPreview`).
|
|
21
|
+
- `discover_tags` now defaults to note-oriented tag suggestions using title/content/query context, returning bounded `recommendedTags`; broader inventory output is now explicit via `mode: "browse"`.
|
|
22
|
+
|
|
7
23
|
## [0.15.0] - 2026-03-23
|
|
8
24
|
|
|
9
25
|
### Added
|
package/README.md
CHANGED
|
@@ -429,10 +429,10 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
|
|
|
429
429
|
|-----------------------------|--------------------------------------------------------------------------|
|
|
430
430
|
| `consolidate` | Merge multiple notes into one with relationship to sources |
|
|
431
431
|
| `detect_project` | Resolve `cwd` to stable project id via git remote URL |
|
|
432
|
-
| `discover_tags` |
|
|
432
|
+
| `discover_tags` | Suggest canonical tags for a note using title/content/query context; `mode: "browse"` opts into broader inventory output |
|
|
433
433
|
| `execute_migration` | Execute a named migration (supports dry-run) |
|
|
434
434
|
| `forget` | Delete note + embedding, git commit + push, cleanup relationships |
|
|
435
|
-
| `get` | Fetch one or more notes by exact id
|
|
435
|
+
| `get` | Fetch one or more notes by exact id; `includeRelationships: true` adds bounded 1-hop previews |
|
|
436
436
|
| `get_project_identity` | Show effective project identity and remote override |
|
|
437
437
|
| `get_project_memory_policy` | Show saved write scope, consolidation mode, and protected-branch settings |
|
|
438
438
|
| `list` | List notes filtered by scope/tags/storage |
|
package/build/index.js
CHANGED
|
@@ -11,6 +11,7 @@ import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from
|
|
|
11
11
|
import { getOrBuildProjection } from "./projections.js";
|
|
12
12
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
13
13
|
import { selectRecallResults } from "./recall.js";
|
|
14
|
+
import { getRelationshipPreview } from "./relationships.js";
|
|
14
15
|
import { cleanMarkdown } from "./markdown.js";
|
|
15
16
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
16
17
|
import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
|
|
@@ -382,10 +383,10 @@ function formatProjectIdentityText(identity) {
|
|
|
382
383
|
function describeLifecycle(lifecycle) {
|
|
383
384
|
return `lifecycle: ${lifecycle}`;
|
|
384
385
|
}
|
|
385
|
-
function formatNote(note, score) {
|
|
386
|
+
function formatNote(note, score, showRawRelated = true) {
|
|
386
387
|
const scoreStr = score !== undefined ? ` | similarity: ${score.toFixed(3)}` : "";
|
|
387
388
|
const projectStr = note.project ? ` | project: ${note.projectName ?? note.project}` : " | global";
|
|
388
|
-
const relStr = note.relatedTo && note.relatedTo.length > 0
|
|
389
|
+
const relStr = showRawRelated && note.relatedTo && note.relatedTo.length > 0
|
|
389
390
|
? `\n**related:** ${note.relatedTo.map((r) => `\`${r.id}\` (${r.type})`).join(", ")}`
|
|
390
391
|
: "";
|
|
391
392
|
return (`## ${note.title}\n` +
|
|
@@ -404,6 +405,15 @@ function formatTemporalHistory(history) {
|
|
|
404
405
|
}
|
|
405
406
|
return lines.join("\n");
|
|
406
407
|
}
|
|
408
|
+
function formatRelationshipPreview(preview) {
|
|
409
|
+
const shown = preview.shown
|
|
410
|
+
.map(r => `${r.title} (\`${r.id}\`) [${r.relationType ?? "related-to"}]`)
|
|
411
|
+
.join(", ");
|
|
412
|
+
const more = preview.truncated
|
|
413
|
+
? ` [+${preview.totalDirectRelations - preview.shown.length} more]`
|
|
414
|
+
: "";
|
|
415
|
+
return `**related (${preview.totalDirectRelations}):** ${shown}${more}`;
|
|
416
|
+
}
|
|
407
417
|
// ── Git commit message helpers ────────────────────────────────────────────────
|
|
408
418
|
/**
|
|
409
419
|
* Extract a short human-readable summary from note content.
|
|
@@ -1636,6 +1646,7 @@ server.registerTool("recall", {
|
|
|
1636
1646
|
"- You just want to browse by tags or scope; use `list`\n\n" +
|
|
1637
1647
|
"Returns:\n" +
|
|
1638
1648
|
"- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n" +
|
|
1649
|
+
"- Bounded 1-hop relationship previews automatically attached to top results\n" +
|
|
1639
1650
|
"- In temporal mode, optional compact history entries for top matches\n\n" +
|
|
1640
1651
|
"Read-only.\n\n" +
|
|
1641
1652
|
"Typical next step:\n" +
|
|
@@ -1723,6 +1734,9 @@ server.registerTool("recall", {
|
|
|
1723
1734
|
: `Recall results (global):`;
|
|
1724
1735
|
const sections = [];
|
|
1725
1736
|
const structuredResults = [];
|
|
1737
|
+
// Determine how many top results get relationship expansion
|
|
1738
|
+
// Top 1 by default, top 3 if result count is small
|
|
1739
|
+
const recallRelationshipLimit = top.length <= 3 ? 3 : 1;
|
|
1726
1740
|
for (const [index, { id, score, vault, boosted }] of top.entries()) {
|
|
1727
1741
|
const note = await readCachedNote(vault, id);
|
|
1728
1742
|
if (note) {
|
|
@@ -1740,10 +1754,19 @@ server.registerTool("recall", {
|
|
|
1740
1754
|
}));
|
|
1741
1755
|
}
|
|
1742
1756
|
}
|
|
1757
|
+
// Add relationship preview for top N results (fail-soft)
|
|
1758
|
+
let relationships;
|
|
1759
|
+
if (index < recallRelationshipLimit) {
|
|
1760
|
+
relationships = await getRelationshipPreview(note, vaultManager.allKnownVaults(), { activeProjectId: project?.id, limit: 3 });
|
|
1761
|
+
}
|
|
1743
1762
|
const formattedHistory = mode === "temporal" && history !== undefined
|
|
1744
1763
|
? `\n\n${formatTemporalHistory(history)}`
|
|
1745
1764
|
: "";
|
|
1746
|
-
|
|
1765
|
+
const formattedRelationships = relationships !== undefined
|
|
1766
|
+
? `\n\n${formatRelationshipPreview(relationships)}`
|
|
1767
|
+
: "";
|
|
1768
|
+
// Suppress raw related IDs when enriched preview is shown to avoid duplication
|
|
1769
|
+
sections.push(`${formatNote(note, score, relationships === undefined)}${formattedHistory}${formattedRelationships}`);
|
|
1747
1770
|
structuredResults.push({
|
|
1748
1771
|
id,
|
|
1749
1772
|
title: note.title,
|
|
@@ -1758,6 +1781,7 @@ server.registerTool("recall", {
|
|
|
1758
1781
|
provenance,
|
|
1759
1782
|
confidence,
|
|
1760
1783
|
history,
|
|
1784
|
+
relationships,
|
|
1761
1785
|
});
|
|
1762
1786
|
}
|
|
1763
1787
|
}
|
|
@@ -2044,7 +2068,8 @@ server.registerTool("get", {
|
|
|
2044
2068
|
"- You are still searching by topic; use `recall`\n" +
|
|
2045
2069
|
"- You want to browse many notes; use `list`\n\n" +
|
|
2046
2070
|
"Returns:\n" +
|
|
2047
|
-
"- Full note content and metadata for the requested ids, including storage label\n
|
|
2071
|
+
"- Full note content and metadata for the requested ids, including storage label\n" +
|
|
2072
|
+
"- Bounded 1-hop relationship previews when `includeRelationships` is true (max 3 shown)\n\n" +
|
|
2048
2073
|
"Read-only.\n\n" +
|
|
2049
2074
|
"Typical next step:\n" +
|
|
2050
2075
|
"- Use `update`, `forget`, `move_memory`, or `relate` after inspection.",
|
|
@@ -2056,10 +2081,12 @@ server.registerTool("get", {
|
|
|
2056
2081
|
inputSchema: z.object({
|
|
2057
2082
|
ids: z.array(z.string()).min(1).describe("One or more memory ids to fetch"),
|
|
2058
2083
|
cwd: projectParam,
|
|
2084
|
+
includeRelationships: z.boolean().optional().default(false).describe("Include bounded direct relationship previews (1-hop expansion, max 3 shown)"),
|
|
2059
2085
|
}),
|
|
2060
2086
|
outputSchema: GetResultSchema,
|
|
2061
|
-
}, async ({ ids, cwd }) => {
|
|
2087
|
+
}, async ({ ids, cwd, includeRelationships }) => {
|
|
2062
2088
|
await ensureBranchSynced(cwd);
|
|
2089
|
+
const project = await resolveProject(cwd);
|
|
2063
2090
|
const found = [];
|
|
2064
2091
|
const notFound = [];
|
|
2065
2092
|
for (const id of ids) {
|
|
@@ -2069,6 +2096,10 @@ server.registerTool("get", {
|
|
|
2069
2096
|
continue;
|
|
2070
2097
|
}
|
|
2071
2098
|
const { note, vault } = result;
|
|
2099
|
+
let relationships;
|
|
2100
|
+
if (includeRelationships) {
|
|
2101
|
+
relationships = await getRelationshipPreview(note, vaultManager.allKnownVaults(), { activeProjectId: project?.id, limit: 3 });
|
|
2102
|
+
}
|
|
2072
2103
|
found.push({
|
|
2073
2104
|
id: note.id,
|
|
2074
2105
|
title: note.title,
|
|
@@ -2081,6 +2112,7 @@ server.registerTool("get", {
|
|
|
2081
2112
|
createdAt: note.createdAt,
|
|
2082
2113
|
updatedAt: note.updatedAt,
|
|
2083
2114
|
vault: storageLabel(vault),
|
|
2115
|
+
relationships,
|
|
2084
2116
|
});
|
|
2085
2117
|
}
|
|
2086
2118
|
const lines = [];
|
|
@@ -2091,6 +2123,10 @@ server.registerTool("get", {
|
|
|
2091
2123
|
lines.push(`tags: ${note.tags.join(", ")}`);
|
|
2092
2124
|
lines.push("");
|
|
2093
2125
|
lines.push(note.content);
|
|
2126
|
+
if (note.relationships) {
|
|
2127
|
+
lines.push("");
|
|
2128
|
+
lines.push(formatRelationshipPreview(note.relationships));
|
|
2129
|
+
}
|
|
2094
2130
|
lines.push("");
|
|
2095
2131
|
}
|
|
2096
2132
|
if (notFound.length > 0) {
|
|
@@ -2247,24 +2283,42 @@ server.registerTool("list", {
|
|
|
2247
2283
|
};
|
|
2248
2284
|
return { content: [{ type: "text", text: textContent }], structuredContent };
|
|
2249
2285
|
});
|
|
2250
|
-
|
|
2286
|
+
function tokenizeTagDiscoveryText(value) {
|
|
2287
|
+
return value
|
|
2288
|
+
.toLowerCase()
|
|
2289
|
+
.split(/[^a-z0-9]+/)
|
|
2290
|
+
.filter(Boolean);
|
|
2291
|
+
}
|
|
2292
|
+
function countTokenOverlap(tokens, other) {
|
|
2293
|
+
let matches = 0;
|
|
2294
|
+
for (const token of other) {
|
|
2295
|
+
if (tokens.has(token)) {
|
|
2296
|
+
matches++;
|
|
2297
|
+
}
|
|
2298
|
+
}
|
|
2299
|
+
return matches;
|
|
2300
|
+
}
|
|
2301
|
+
function hasExactTagContextMatch(tag, values) {
|
|
2302
|
+
const normalizedTag = tag.toLowerCase();
|
|
2303
|
+
return values.some(value => value?.toLowerCase().includes(normalizedTag) ?? false);
|
|
2304
|
+
}
|
|
2251
2305
|
server.registerTool("discover_tags", {
|
|
2252
2306
|
title: "Discover Tags",
|
|
2253
|
-
description: "
|
|
2307
|
+
description: "Suggest canonical tags for a specific note before `remember` when tag choice is ambiguous.\n\n" +
|
|
2254
2308
|
"Use this when:\n" +
|
|
2255
|
-
"-
|
|
2256
|
-
"-
|
|
2257
|
-
"-
|
|
2309
|
+
"- You have a note title, content, or query and want compact tag suggestions\n" +
|
|
2310
|
+
"- You want canonical project terminology without exposing lots of unrelated tags\n" +
|
|
2311
|
+
"- You want to demote temporary-only tags unless they fit the note\n\n" +
|
|
2258
2312
|
"Do not use this when:\n" +
|
|
2259
2313
|
"- You need to browse notes by tag; use `list` with `tags` filter instead\n" +
|
|
2260
|
-
"- You already know the exact tags you want to use\n
|
|
2314
|
+
"- You already know the exact tags you want to use\n" +
|
|
2315
|
+
"- You want broad inventory output but are not explicitly requesting `mode: \"browse\"`\n\n" +
|
|
2261
2316
|
"Returns:\n" +
|
|
2262
|
-
"-
|
|
2263
|
-
"-
|
|
2264
|
-
"-
|
|
2265
|
-
"- isTemporaryOnly flag identifying cleanup candidates\n\n" +
|
|
2317
|
+
"- Default: bounded `recommendedTags` ranked by note relevance first and usage count second\n" +
|
|
2318
|
+
"- Each suggestion includes canonicality and lifecycle signals plus one compact example\n" +
|
|
2319
|
+
"- Optional `mode: \"browse\"` returns broader inventory output\n\n" +
|
|
2266
2320
|
"Typical next step:\n" +
|
|
2267
|
-
"-
|
|
2321
|
+
"- Reuse suggested canonical tags when they fit, or create a new tag only when genuinely novel.\n\n" +
|
|
2268
2322
|
"Performance: O(n) where n = total notes scanned. Expect 100-200ms for 500 notes.\n\n" +
|
|
2269
2323
|
"Read-only.",
|
|
2270
2324
|
annotations: {
|
|
@@ -2274,6 +2328,12 @@ server.registerTool("discover_tags", {
|
|
|
2274
2328
|
},
|
|
2275
2329
|
inputSchema: z.object({
|
|
2276
2330
|
cwd: projectParam,
|
|
2331
|
+
mode: z.enum(["suggest", "browse"]).optional().default("suggest"),
|
|
2332
|
+
title: z.string().optional(),
|
|
2333
|
+
content: z.string().optional(),
|
|
2334
|
+
query: z.string().optional(),
|
|
2335
|
+
candidateTags: z.array(z.string()).optional(),
|
|
2336
|
+
lifecycle: z.enum(NOTE_LIFECYCLES).optional(),
|
|
2277
2337
|
scope: z
|
|
2278
2338
|
.enum(["project", "global", "all"])
|
|
2279
2339
|
.optional()
|
|
@@ -2286,65 +2346,170 @@ server.registerTool("discover_tags", {
|
|
|
2286
2346
|
.optional()
|
|
2287
2347
|
.default("any")
|
|
2288
2348
|
.describe("Filter by vault storage label like list tool."),
|
|
2349
|
+
limit: z.number().int().min(1).max(50).optional(),
|
|
2289
2350
|
}),
|
|
2290
2351
|
outputSchema: DiscoverTagsResultSchema,
|
|
2291
|
-
}, async ({ cwd, scope, storedIn }) => {
|
|
2352
|
+
}, async ({ cwd, mode, title, content, query, candidateTags, lifecycle, scope, storedIn, limit }) => {
|
|
2292
2353
|
await ensureBranchSynced(cwd);
|
|
2293
2354
|
const startTime = Date.now();
|
|
2294
2355
|
const { project, entries } = await collectVisibleNotes(cwd, scope, undefined, storedIn);
|
|
2295
2356
|
const tagStats = new Map();
|
|
2357
|
+
const candidateTagSet = new Set((candidateTags || []).map(tag => tag.toLowerCase()));
|
|
2358
|
+
const contextValues = [title, content, query, ...(candidateTags || [])];
|
|
2359
|
+
const contextTokens = new Set(tokenizeTagDiscoveryText(contextValues.filter(Boolean).join(" ")));
|
|
2360
|
+
const isTemporaryTarget = lifecycle === "temporary";
|
|
2361
|
+
const effectiveLimit = limit ?? (mode === "browse" ? 20 : 10);
|
|
2296
2362
|
for (const { note } of entries) {
|
|
2363
|
+
const noteTokens = new Set(tokenizeTagDiscoveryText(`${note.title} ${note.content} ${note.tags.join(" ")}`));
|
|
2364
|
+
const contextMatches = contextTokens.size > 0 ? countTokenOverlap(contextTokens, noteTokens) : 0;
|
|
2297
2365
|
for (const tag of note.tags) {
|
|
2298
|
-
const stats = tagStats.get(tag) || {
|
|
2366
|
+
const stats = tagStats.get(tag) || {
|
|
2367
|
+
count: 0,
|
|
2368
|
+
examples: [],
|
|
2369
|
+
lifecycles: new Set(),
|
|
2370
|
+
contextMatches: 0,
|
|
2371
|
+
exactCandidateMatch: false,
|
|
2372
|
+
};
|
|
2299
2373
|
stats.count++;
|
|
2300
2374
|
if (stats.examples.length < 3) {
|
|
2301
2375
|
stats.examples.push(note.title);
|
|
2302
2376
|
}
|
|
2303
2377
|
stats.lifecycles.add(note.lifecycle);
|
|
2378
|
+
stats.contextMatches += contextMatches;
|
|
2379
|
+
if (candidateTagSet.has(tag.toLowerCase())) {
|
|
2380
|
+
stats.exactCandidateMatch = true;
|
|
2381
|
+
}
|
|
2304
2382
|
tagStats.set(tag, stats);
|
|
2305
2383
|
}
|
|
2306
2384
|
}
|
|
2307
|
-
const
|
|
2308
|
-
.map(([tag, stats]) =>
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2385
|
+
const rawTags = Array.from(tagStats.entries())
|
|
2386
|
+
.map(([tag, stats]) => {
|
|
2387
|
+
const lifecycleTypes = Array.from(stats.lifecycles);
|
|
2388
|
+
const isTemporaryOnly = stats.lifecycles.size === 1 && stats.lifecycles.has("temporary");
|
|
2389
|
+
const tagTokens = tokenizeTagDiscoveryText(tag);
|
|
2390
|
+
const exactContextMatch = stats.exactCandidateMatch || hasExactTagContextMatch(tag, contextValues);
|
|
2391
|
+
const tagTokenOverlap = contextTokens.size > 0
|
|
2392
|
+
? countTokenOverlap(contextTokens, tagTokens)
|
|
2393
|
+
: 0;
|
|
2394
|
+
const averageContextMatch = stats.count > 0 ? stats.contextMatches / stats.count : 0;
|
|
2395
|
+
const reason = exactContextMatch
|
|
2396
|
+
? "matches a candidate tag already present in the note context"
|
|
2397
|
+
: tagTokenOverlap > 0 || stats.contextMatches > 0
|
|
2398
|
+
? "matches the note context and existing project usage"
|
|
2399
|
+
: "high-usage canonical tag from existing project notes";
|
|
2400
|
+
return {
|
|
2401
|
+
tag,
|
|
2402
|
+
usageCount: stats.count,
|
|
2403
|
+
examples: stats.examples,
|
|
2404
|
+
example: stats.examples[0],
|
|
2405
|
+
reason,
|
|
2406
|
+
lifecycleTypes,
|
|
2407
|
+
isTemporaryOnly,
|
|
2408
|
+
exactContextMatch,
|
|
2409
|
+
tagTokenOverlap,
|
|
2410
|
+
averageContextMatch,
|
|
2411
|
+
isBroadSingleToken: tagTokens.length === 1,
|
|
2412
|
+
isHighFrequency: stats.count >= 4,
|
|
2413
|
+
specificityBoost: tagTokens.length > 1 ? 4 : 0,
|
|
2414
|
+
};
|
|
2415
|
+
});
|
|
2416
|
+
const hasStrongSpecificCandidate = rawTags.some(tag => tag.exactContextMatch && (!tag.isBroadSingleToken || tag.usageCount > 1));
|
|
2417
|
+
const tags = rawTags
|
|
2418
|
+
.map((tag) => {
|
|
2419
|
+
const hasWeakDirectMatch = tag.tagTokenOverlap <= 1 && tag.averageContextMatch <= 2;
|
|
2420
|
+
const genericPenalty = hasStrongSpecificCandidate && tag.isBroadSingleToken && tag.isHighFrequency && hasWeakDirectMatch
|
|
2421
|
+
? 10
|
|
2422
|
+
: 0;
|
|
2423
|
+
const score = hasStrongSpecificCandidate
|
|
2424
|
+
? (tag.exactContextMatch ? 12 : 0) +
|
|
2425
|
+
(tag.tagTokenOverlap * 5) +
|
|
2426
|
+
tag.specificityBoost +
|
|
2427
|
+
(tag.averageContextMatch * 3) +
|
|
2428
|
+
(tag.usageCount * 0.1) -
|
|
2429
|
+
genericPenalty -
|
|
2430
|
+
(!isTemporaryTarget && tag.isTemporaryOnly ? 2 : 0)
|
|
2431
|
+
: (tag.usageCount * 1.5) +
|
|
2432
|
+
(tag.isTemporaryOnly ? -3 : 1) -
|
|
2433
|
+
(!isTemporaryTarget && tag.isTemporaryOnly ? 2 : 0);
|
|
2434
|
+
return {
|
|
2435
|
+
...tag,
|
|
2436
|
+
score,
|
|
2437
|
+
};
|
|
2438
|
+
})
|
|
2439
|
+
.sort((a, b) => b.score - a.score || b.usageCount - a.usageCount || a.tag.localeCompare(b.tag));
|
|
2316
2440
|
const durationMs = Date.now() - startTime;
|
|
2441
|
+
const vaultsSearched = new Set(entries.map(e => storageLabel(e.vault))).size;
|
|
2317
2442
|
const lines = [];
|
|
2318
|
-
if (
|
|
2319
|
-
|
|
2443
|
+
if (mode === "browse") {
|
|
2444
|
+
if (project && scope !== "global") {
|
|
2445
|
+
lines.push(`Tags for ${project.name} (scope: ${scope}):`);
|
|
2446
|
+
}
|
|
2447
|
+
else {
|
|
2448
|
+
lines.push(`Tags (scope: ${scope}):`);
|
|
2449
|
+
}
|
|
2450
|
+
lines.push("");
|
|
2451
|
+
lines.push(`Total: ${tags.length} unique tags across ${entries.length} notes (${durationMs}ms)`);
|
|
2452
|
+
lines.push("");
|
|
2453
|
+
lines.push("Tags sorted by usage:");
|
|
2454
|
+
for (const t of tags.slice(0, effectiveLimit)) {
|
|
2455
|
+
const lifecycleMark = t.isTemporaryOnly ? " [temp-only]" : "";
|
|
2456
|
+
lines.push(` ${t.tag} (${t.usageCount})${lifecycleMark}`);
|
|
2457
|
+
if (t.examples.length > 0) {
|
|
2458
|
+
lines.push(` Example: "${t.examples[0]}"`);
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (tags.length > effectiveLimit) {
|
|
2462
|
+
lines.push(` ... and ${tags.length - effectiveLimit} more`);
|
|
2463
|
+
}
|
|
2320
2464
|
}
|
|
2321
2465
|
else {
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
lines.push(`
|
|
2331
|
-
|
|
2332
|
-
|
|
2466
|
+
const suggestedTags = tags.slice(0, effectiveLimit);
|
|
2467
|
+
if (project && scope !== "global") {
|
|
2468
|
+
lines.push(`Suggested tags for ${project.name} (scope: ${scope}):`);
|
|
2469
|
+
}
|
|
2470
|
+
else {
|
|
2471
|
+
lines.push(`Suggested tags (scope: ${scope}):`);
|
|
2472
|
+
}
|
|
2473
|
+
lines.push("");
|
|
2474
|
+
lines.push(`Considered ${tags.length} unique tags across ${entries.length} notes (${durationMs}ms)`);
|
|
2475
|
+
lines.push("");
|
|
2476
|
+
if (contextTokens.size === 0) {
|
|
2477
|
+
lines.push("No note context provided; showing the most canonical existing tags.");
|
|
2478
|
+
lines.push("");
|
|
2479
|
+
}
|
|
2480
|
+
lines.push("Recommended tags:");
|
|
2481
|
+
for (const t of suggestedTags) {
|
|
2482
|
+
const lifecycleMark = t.isTemporaryOnly ? " [temp-only]" : "";
|
|
2483
|
+
lines.push(` ${t.tag} (${t.usageCount})${lifecycleMark}`);
|
|
2484
|
+
if (t.example) {
|
|
2485
|
+
lines.push(` Example: "${t.example}"`);
|
|
2486
|
+
}
|
|
2487
|
+
lines.push(` Why: ${t.reason}`);
|
|
2333
2488
|
}
|
|
2334
2489
|
}
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2490
|
+
const structuredContent = mode === "browse"
|
|
2491
|
+
? {
|
|
2492
|
+
action: "tags_discovered",
|
|
2493
|
+
project: project ? { id: project.id, name: project.name } : undefined,
|
|
2494
|
+
mode,
|
|
2495
|
+
scope: scope || "all",
|
|
2496
|
+
tags: tags.slice(0, effectiveLimit).map(({ score, example, reason, ...tag }) => tag),
|
|
2497
|
+
totalTags: tags.length,
|
|
2498
|
+
totalNotes: entries.length,
|
|
2499
|
+
vaultsSearched,
|
|
2500
|
+
durationMs,
|
|
2501
|
+
}
|
|
2502
|
+
: {
|
|
2503
|
+
action: "tags_discovered",
|
|
2504
|
+
project: project ? { id: project.id, name: project.name } : undefined,
|
|
2505
|
+
mode,
|
|
2506
|
+
scope: scope || "all",
|
|
2507
|
+
recommendedTags: tags.slice(0, effectiveLimit).map(({ score, examples, ...tag }) => tag),
|
|
2508
|
+
totalTags: tags.length,
|
|
2509
|
+
totalNotes: entries.length,
|
|
2510
|
+
vaultsSearched,
|
|
2511
|
+
durationMs,
|
|
2512
|
+
};
|
|
2348
2513
|
return { content: [{ type: "text", text: lines.join("\n") }], structuredContent };
|
|
2349
2514
|
});
|
|
2350
2515
|
// ── recent_memories ───────────────────────────────────────────────────────────
|
|
@@ -2505,7 +2670,8 @@ server.registerTool("project_memory_summary", {
|
|
|
2505
2670
|
"- You need exact note contents; use `get`\n" +
|
|
2506
2671
|
"- You need direct semantic matches for a query; use `recall`\n\n" +
|
|
2507
2672
|
"Returns:\n" +
|
|
2508
|
-
"- A synthesized project-level summary based on stored memories\n
|
|
2673
|
+
"- A synthesized project-level summary based on stored memories\n" +
|
|
2674
|
+
"- Bounded 1-hop relationship previews on orientation entry points (primaryEntry and suggestedNext)\n\n" +
|
|
2509
2675
|
"Read-only.\n\n" +
|
|
2510
2676
|
"Typical next step:\n" +
|
|
2511
2677
|
"- Use `recall` or `list` to drill down into specific areas.",
|
|
@@ -2729,10 +2895,24 @@ server.registerTool("project_memory_summary", {
|
|
|
2729
2895
|
const confidence = computeConfidence("permanent", anchor.updatedAt, anchor.centrality);
|
|
2730
2896
|
return { provenance, confidence };
|
|
2731
2897
|
};
|
|
2898
|
+
// Helper to enrich an anchor with relationships (1-hop expansion)
|
|
2899
|
+
const enrichOrientationNoteWithRelationships = async (anchor) => {
|
|
2900
|
+
const vault = noteVaultMap.get(anchor.id);
|
|
2901
|
+
if (!vault)
|
|
2902
|
+
return {};
|
|
2903
|
+
const note = await vault.storage.readNote(anchor.id);
|
|
2904
|
+
if (!note)
|
|
2905
|
+
return {};
|
|
2906
|
+
const relationships = await getRelationshipPreview(note, vaultManager.allKnownVaults(), { activeProjectId: project.id, limit: 3 });
|
|
2907
|
+
return { relationships };
|
|
2908
|
+
};
|
|
2732
2909
|
const primaryEnriched = primaryAnchor ? await enrichOrientationNote(primaryAnchor) : {};
|
|
2910
|
+
const primaryRelationships = primaryAnchor ? await enrichOrientationNoteWithRelationships(primaryAnchor) : {};
|
|
2733
2911
|
const suggestedEnriched = await Promise.all(anchors.slice(1, 4).map(enrichOrientationNote));
|
|
2912
|
+
const suggestedRelationships = await Promise.all(anchors.slice(1, 4).map(enrichOrientationNoteWithRelationships));
|
|
2734
2913
|
// Enrich fallback primaryEntry when no anchors exist
|
|
2735
2914
|
let fallbackEnriched = {};
|
|
2915
|
+
let fallbackRelationships = {};
|
|
2736
2916
|
if (!primaryAnchor && recent[0]) {
|
|
2737
2917
|
const fallbackNote = recent[0].note;
|
|
2738
2918
|
const vault = noteVaultMap.get(fallbackNote.id);
|
|
@@ -2742,6 +2922,9 @@ server.registerTool("project_memory_summary", {
|
|
|
2742
2922
|
const confidence = computeConfidence(fallbackNote.lifecycle, fallbackNote.updatedAt, 0);
|
|
2743
2923
|
fallbackEnriched = { provenance, confidence };
|
|
2744
2924
|
}
|
|
2925
|
+
const preview = await getRelationshipPreview(fallbackNote, vaultManager.allKnownVaults(), { activeProjectId: project.id, limit: 3 });
|
|
2926
|
+
if (preview)
|
|
2927
|
+
fallbackRelationships = { relationships: preview };
|
|
2745
2928
|
}
|
|
2746
2929
|
const orientation = {
|
|
2747
2930
|
primaryEntry: primaryAnchor
|
|
@@ -2750,6 +2933,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2750
2933
|
title: primaryAnchor.title,
|
|
2751
2934
|
rationale: `Centrality ${primaryAnchor.centrality}, connects ${primaryAnchor.connectionDiversity} themes`,
|
|
2752
2935
|
...primaryEnriched,
|
|
2936
|
+
...primaryRelationships,
|
|
2753
2937
|
}
|
|
2754
2938
|
: {
|
|
2755
2939
|
id: recent[0]?.note.id ?? projectEntries[0]?.note.id ?? "",
|
|
@@ -2758,12 +2942,14 @@ server.registerTool("project_memory_summary", {
|
|
|
2758
2942
|
? "Most recent note — no high-centrality anchors found"
|
|
2759
2943
|
: "Only note available",
|
|
2760
2944
|
...fallbackEnriched,
|
|
2945
|
+
...fallbackRelationships,
|
|
2761
2946
|
},
|
|
2762
2947
|
suggestedNext: anchors.slice(1, 4).map((a, i) => ({
|
|
2763
2948
|
id: a.id,
|
|
2764
2949
|
title: a.title,
|
|
2765
2950
|
rationale: `Centrality ${a.centrality}, connects ${a.connectionDiversity} themes`,
|
|
2766
2951
|
...suggestedEnriched[i],
|
|
2952
|
+
...suggestedRelationships[i],
|
|
2767
2953
|
})),
|
|
2768
2954
|
};
|
|
2769
2955
|
// Warning for taxonomy dilution
|
|
@@ -2782,10 +2968,16 @@ server.registerTool("project_memory_summary", {
|
|
|
2782
2968
|
if (orientation.primaryEntry.confidence) {
|
|
2783
2969
|
sections.push(` Confidence: ${orientation.primaryEntry.confidence}`);
|
|
2784
2970
|
}
|
|
2971
|
+
if (orientation.primaryEntry.relationships) {
|
|
2972
|
+
sections.push(` ${formatRelationshipPreview(orientation.primaryEntry.relationships)}`);
|
|
2973
|
+
}
|
|
2785
2974
|
if (orientation.suggestedNext.length > 0) {
|
|
2786
2975
|
sections.push(`Then check:`);
|
|
2787
2976
|
for (const next of orientation.suggestedNext) {
|
|
2788
2977
|
sections.push(` - ${next.title} (\`${next.id}\`) — ${next.rationale}${next.confidence ? ` [${next.confidence}]` : ""}`);
|
|
2978
|
+
if (next.relationships) {
|
|
2979
|
+
sections.push(` ${formatRelationshipPreview(next.relationships)}`);
|
|
2980
|
+
}
|
|
2789
2981
|
}
|
|
2790
2982
|
}
|
|
2791
2983
|
if (orientation.warnings && orientation.warnings.length > 0) {
|
|
@@ -4268,7 +4460,7 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4268
4460
|
"1. Need to store, refine, or connect knowledge about a topic? Start with `recall`, or use `list` when you need deterministic browsing by scope, storage, or tags.\n" +
|
|
4269
4461
|
"2. If `recall` or `list` returns matching ids, use `get` to inspect the best match.\n" +
|
|
4270
4462
|
"3. If one memory should be refined, call `update`.\n" +
|
|
4271
|
-
"4. If no memory covers the topic, call `discover_tags`
|
|
4463
|
+
"4. If no memory covers the topic and tag choice is ambiguous, call `discover_tags` with note context, then call `remember`.\n" +
|
|
4272
4464
|
"5. After storing or updating, use `relate` for strong connections, `consolidate` for overlap, and `move_memory` for wrong storage location.\n\n" +
|
|
4273
4465
|
"### Anti-patterns\n\n" +
|
|
4274
4466
|
"- Bad: call `remember` immediately because the user said 'remember'.\n" +
|
|
@@ -4286,7 +4478,7 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4286
4478
|
"- project memory policy lookup\n\n" +
|
|
4287
4479
|
"### Tiny examples\n\n" +
|
|
4288
4480
|
"- Existing bug note found by `recall` -> inspect with `get` -> refine with `update`.\n" +
|
|
4289
|
-
"- No matching note found by `recall` -> optional `discover_tags` -> create with `remember`.\n" +
|
|
4481
|
+
"- No matching note found by `recall` -> optional `discover_tags` with note context -> create with `remember`.\n" +
|
|
4290
4482
|
"- Two notes overlap heavily -> inspect -> clean up with `consolidate`.",
|
|
4291
4483
|
},
|
|
4292
4484
|
},
|