@danielmarbach/mnemonic-mcp 0.22.0 → 0.24.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 +20 -0
- package/README.md +13 -3
- package/build/index.js +129 -32
- package/build/index.js.map +1 -1
- package/build/lexical.d.ts +36 -0
- package/build/lexical.d.ts.map +1 -1
- package/build/lexical.js +147 -0
- package/build/lexical.js.map +1 -1
- package/build/markdown-ast.d.ts +4 -0
- package/build/markdown-ast.d.ts.map +1 -0
- package/build/markdown-ast.js +10 -0
- package/build/markdown-ast.js.map +1 -0
- package/build/recall.d.ts +20 -4
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +37 -5
- package/build/recall.js.map +1 -1
- package/build/semantic-patch.d.ts +36 -0
- package/build/semantic-patch.d.ts.map +1 -0
- package/build/semantic-patch.js +141 -0
- package/build/semantic-patch.js.map +1 -0
- package/package.json +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,26 @@ 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.24.0] - 2026-04-24
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `update` now supports `semanticPatch` for token-efficient targeted edits (insert, replace, append, remove content under specific headings) without round-tripping the full note body.
|
|
12
|
+
|
|
13
|
+
## [0.23.0] - 2026-04-17
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- Lexical rescue now ranks candidates by TF-IDF similarity, improving recall for identifier-heavy and jargon queries without affecting semantic ranking.
|
|
18
|
+
- Recall now boosts notes that explain key decisions and concepts when you ask "why"-style questions, using structural signals like role, connections, and format rather than keyword matching.
|
|
19
|
+
- `run-dogfood-packs.mjs --isolated` copies notes into a temporary workspace for reproducible validation runs without polluting the live vault.
|
|
20
|
+
- Rescue candidates no longer appear when `minSimilarity` is set above the default, so explicit quality filters are respected.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Decision and overview notes now surface more reliably for questions like "why are embeddings gitignored" instead of being outranked by incidental mentions.
|
|
25
|
+
- Lexical rescue now correctly activates when no semantic results are found at all.
|
|
26
|
+
|
|
7
27
|
## [0.22.0] - 2026-04-13
|
|
8
28
|
|
|
9
29
|
### Added
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ For the high-level system map, see [`ARCHITECTURE.md`](ARCHITECTURE.md). For rel
|
|
|
17
17
|
|
|
18
18
|
## Stability
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
The storage format is stable with migration support for any future changes. Keep an eye on the changelog; `list_migrations` shows pending work per vault after each update.
|
|
21
21
|
|
|
22
22
|
**Scale:** Designed for simplicity and portability — not large-scale knowledge bases.
|
|
23
23
|
|
|
@@ -45,7 +45,7 @@ No code changes required — set `EMBED_MODEL=qwen3-embedding:0.6b` in your envi
|
|
|
45
45
|
|
|
46
46
|
## Setup
|
|
47
47
|
|
|
48
|
-
### Native (Node.js
|
|
48
|
+
### Native (Node.js 20+)
|
|
49
49
|
|
|
50
50
|
```bash
|
|
51
51
|
npm install
|
|
@@ -63,6 +63,8 @@ npm run mcp:local
|
|
|
63
63
|
|
|
64
64
|
This rebuilds first, then launches `build/index.js`, so MCP clients always point at the latest source.
|
|
65
65
|
|
|
66
|
+
For reproducible dogfooding of recency and relationship-navigation behavior, prefer the isolated dogfood runner over the live project vault. The isolated runner copies the current `.mnemonic` notes into a temporary workspace, runs the chosen pack there, and deletes the workspace afterward.
|
|
67
|
+
|
|
66
68
|
### Docker
|
|
67
69
|
|
|
68
70
|
```bash
|
|
@@ -308,7 +310,7 @@ Project identity derives from the **git remote URL**, normalized to a stable slu
|
|
|
308
310
|
|
|
309
311
|
`recall` with `cwd` searches both vaults. Project notes get a **+0.15 similarity boost** — a soft signal, not a hard filter — so global memories remain accessible while project context floats to the top.
|
|
310
312
|
|
|
311
|
-
**Hybrid recall** enhances semantic search with lightweight lexical reranking over note projections. When semantic results are weak, a bounded lexical rescue path scans projections for additional candidates, improving exact-match and
|
|
313
|
+
**Hybrid recall** enhances semantic search with lightweight lexical reranking over note projections. When semantic results are weak, a bounded lexical rescue path scans projections for additional candidates, improving exact-match and identifier-heavy recall without changing the storage model or adding new infrastructure. **Canonical explanation promotion** boosts notes that explain key decisions and concepts for "why"-style questions, using structural signals like role, connections, and format rather than keyword matching.
|
|
312
314
|
|
|
313
315
|
Temporal recall is opt-in via `mode: "temporal"`. It keeps semantic selection first, then enriches only the top matches with compact git-backed history so agents can inspect how a note evolved without turning recall into raw log or diff output.
|
|
314
316
|
|
|
@@ -572,6 +574,14 @@ This keeps early ideation reusable as personal/global knowledge while moving con
|
|
|
572
574
|
|
|
573
575
|
mnemonic and Beads address complementary concerns. mnemonic is a **knowledge graph**: it stores notes, relationships between them, and lets agents retrieve relevant context through semantic search. [Beads](https://github.com/steveyegge/beads) is a **task and dependency tracker**: it models work items and their dependencies so agents can determine what is ready to execute next. Both tools can coexist in the same workflow — mnemonic stores knowledge and reasoning while Beads manages execution.
|
|
574
576
|
|
|
577
|
+
**How does mnemonic differ from Memory Bank MCP?**
|
|
578
|
+
|
|
579
|
+
mnemonic and Memory Bank MCP both provide persistent memory for agents, but differ in hosting and scope. Memory Bank MCP is a **centralized service** — your memory lives in a remote MCP service and is accessed across projects through that single endpoint. mnemonic is **local-first** — your memories live as plain markdown files on your machine: project-scoped notes in `.mnemonic/` within each repo, and personal notes in a global vault under your home directory. There is no always-on server to configure or depend on; the MCP server spawns on demand per session.
|
|
580
|
+
|
|
581
|
+
**How does mnemonic differ from Basic Memory?**
|
|
582
|
+
|
|
583
|
+
Both tools are local-first and use markdown, but with different scoping models. [Basic Memory](https://github.com/basicmachines/basicmemory) maintains a **knowledge base per project** that agents can search and update, with optional cloud sync. mnemonic splits memory into **two distinct vaults**: a global personal vault (`~/mnemonic-vault/`) for cross-project knowledge, and a project-scoped vault (`.mnemonic/`) that travels with the repo and is shared via git. This lets you capture early ideas globally before a repo exists, then migrate only project-relevant notes into the shared vault once collaboration begins.
|
|
584
|
+
|
|
575
585
|
**What are temporary notes?**
|
|
576
586
|
|
|
577
587
|
mnemonic distinguishes between two lifecycle states. `temporary` notes capture evolving working-state: hypotheses, in-progress plans, experiment results, draft reasoning. `permanent` notes capture durable knowledge: decisions, root cause explanations, architectural guidance, lessons learned. As an investigation progresses, a cluster of temporary notes is typically `consolidate`d into one or more permanent notes, and the scaffolding is discarded. This two-phase lifecycle keeps exploratory thinking from polluting long-term memory while still giving agents a place to reason incrementally before committing to a conclusion.
|
package/build/index.js
CHANGED
|
@@ -14,10 +14,11 @@ import { invalidateActiveProjectCache, getOrBuildVaultEmbeddings, getOrBuildVaul
|
|
|
14
14
|
import { performance } from "perf_hooks";
|
|
15
15
|
import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
|
|
16
16
|
import { suggestAutoRelationships } from "./auto-relate.js";
|
|
17
|
-
import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, applyLexicalReranking } from "./recall.js";
|
|
18
|
-
import { shouldTriggerLexicalRescue,
|
|
17
|
+
import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, applyLexicalReranking, applyCanonicalExplanationPromotion, } from "./recall.js";
|
|
18
|
+
import { shouldTriggerLexicalRescue, rankDocumentsByTfIdf, LEXICAL_RESCUE_CANDIDATE_LIMIT, LEXICAL_RESCUE_THRESHOLD, LEXICAL_RESCUE_RESULT_LIMIT, } from "./lexical.js";
|
|
19
19
|
import { getRelationshipPreview } from "./relationships.js";
|
|
20
20
|
import { cleanMarkdown } from "./markdown.js";
|
|
21
|
+
import { applySemanticPatches } from "./semantic-patch.js";
|
|
21
22
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
22
23
|
import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
|
|
23
24
|
import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, daysSinceUpdate, withinThemeScore, anchorScore, computeConnectionDiversity, workingStateScore, extractNextAction, } from "./project-introspection.js";
|
|
@@ -1757,9 +1758,25 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1757
1758
|
};
|
|
1758
1759
|
});
|
|
1759
1760
|
// ── Lexical rescue helper ─────────────────────────────────────────────────────
|
|
1760
|
-
|
|
1761
|
+
function buildRecallCandidateContext(note) {
|
|
1762
|
+
const metadata = getEffectiveMetadata(note);
|
|
1763
|
+
const relatedCount = note.relatedTo?.length ?? 0;
|
|
1764
|
+
return {
|
|
1765
|
+
metadata,
|
|
1766
|
+
metadataBoost: computeRecallMetadataBoost(metadata),
|
|
1767
|
+
lifecycle: note.lifecycle,
|
|
1768
|
+
relatedCount,
|
|
1769
|
+
connectionDiversity: new Set((note.relatedTo ?? []).map((rel) => rel.type)).size,
|
|
1770
|
+
structureScore: Math.min(0.04, [
|
|
1771
|
+
note.content.includes("## ") ? 0.02 : 0,
|
|
1772
|
+
note.content.includes("- ") || note.content.includes("1. ") ? 0.01 : 0,
|
|
1773
|
+
note.content.length >= 400 ? 0.01 : 0,
|
|
1774
|
+
].reduce((sum, value) => sum + value, 0)),
|
|
1775
|
+
};
|
|
1776
|
+
}
|
|
1777
|
+
async function collectLexicalRescueCandidates(vaults, query, project, scope, tags, lifecycle, existingIds) {
|
|
1761
1778
|
const existingIdSet = new Set(existingIds.map((c) => c.id));
|
|
1762
|
-
const
|
|
1779
|
+
const rescuePool = [];
|
|
1763
1780
|
for (const vault of vaults) {
|
|
1764
1781
|
const notes = await vault.storage.listNotes().catch(() => []);
|
|
1765
1782
|
for (const note of notes) {
|
|
@@ -1770,6 +1787,8 @@ async function collectLexicalRescueCandidates(vaults, query, project, scope, tag
|
|
|
1770
1787
|
if (!tags.every((t) => noteTags.has(t)))
|
|
1771
1788
|
continue;
|
|
1772
1789
|
}
|
|
1790
|
+
if (lifecycle && note.lifecycle !== lifecycle)
|
|
1791
|
+
continue;
|
|
1773
1792
|
const isProjectNote = note.project !== undefined;
|
|
1774
1793
|
const isCurrentProject = project && note.project === project.id;
|
|
1775
1794
|
if (scope === "project" && !isCurrentProject)
|
|
@@ -1779,24 +1798,39 @@ async function collectLexicalRescueCandidates(vaults, query, project, scope, tag
|
|
|
1779
1798
|
const projection = await getOrBuildProjection(vault.storage, note).catch(() => undefined);
|
|
1780
1799
|
if (!projection)
|
|
1781
1800
|
continue;
|
|
1782
|
-
|
|
1783
|
-
if (lexicalScore < LEXICAL_RESCUE_THRESHOLD)
|
|
1784
|
-
continue;
|
|
1785
|
-
const metadataBoost = computeRecallMetadataBoost(getEffectiveMetadata(note));
|
|
1786
|
-
const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
|
|
1787
|
-
candidates.push({
|
|
1801
|
+
rescuePool.push({
|
|
1788
1802
|
id: note.id,
|
|
1789
|
-
score: 0,
|
|
1790
|
-
boosted: boost,
|
|
1791
1803
|
vault,
|
|
1792
1804
|
isCurrentProject: Boolean(isCurrentProject),
|
|
1793
|
-
|
|
1805
|
+
projectionText: projection.projectionText,
|
|
1806
|
+
context: buildRecallCandidateContext(note),
|
|
1794
1807
|
});
|
|
1795
|
-
if (candidates.length >= LEXICAL_RESCUE_CANDIDATE_LIMIT)
|
|
1796
|
-
break;
|
|
1797
1808
|
}
|
|
1798
|
-
|
|
1799
|
-
|
|
1809
|
+
}
|
|
1810
|
+
const rankedRescueIds = new Map(rankDocumentsByTfIdf(query, rescuePool.map((candidate) => ({ id: candidate.id, text: candidate.projectionText })), LEXICAL_RESCUE_CANDIDATE_LIMIT).map((candidate) => [candidate.id, candidate.score]));
|
|
1811
|
+
const candidates = [];
|
|
1812
|
+
for (const candidate of rescuePool) {
|
|
1813
|
+
const tfIdfScore = rankedRescueIds.get(candidate.id);
|
|
1814
|
+
if (tfIdfScore === undefined || tfIdfScore <= 0)
|
|
1815
|
+
continue;
|
|
1816
|
+
const lexicalScore = tfIdfScore;
|
|
1817
|
+
if (lexicalScore < LEXICAL_RESCUE_THRESHOLD)
|
|
1818
|
+
continue;
|
|
1819
|
+
const boost = (candidate.isCurrentProject ? 0.15 : 0) + candidate.context.metadataBoost;
|
|
1820
|
+
candidates.push({
|
|
1821
|
+
id: candidate.id,
|
|
1822
|
+
score: lexicalScore,
|
|
1823
|
+
semanticScoreForPromotion: 0,
|
|
1824
|
+
boosted: boost,
|
|
1825
|
+
vault: candidate.vault,
|
|
1826
|
+
isCurrentProject: candidate.isCurrentProject,
|
|
1827
|
+
lexicalScore,
|
|
1828
|
+
lifecycle: candidate.context.lifecycle,
|
|
1829
|
+
relatedCount: candidate.context.relatedCount,
|
|
1830
|
+
connectionDiversity: candidate.context.connectionDiversity,
|
|
1831
|
+
structureScore: candidate.context.structureScore,
|
|
1832
|
+
metadata: candidate.context.metadata,
|
|
1833
|
+
});
|
|
1800
1834
|
}
|
|
1801
1835
|
return candidates
|
|
1802
1836
|
.sort((a, b) => computeHybridScore(b) - computeHybridScore(a))
|
|
@@ -1905,9 +1939,21 @@ server.registerTool("recall", {
|
|
|
1905
1939
|
if (isProjectNote)
|
|
1906
1940
|
continue;
|
|
1907
1941
|
}
|
|
1908
|
-
const
|
|
1909
|
-
const boost = (isCurrentProject ? 0.15 : 0) + metadataBoost;
|
|
1910
|
-
scored.push({
|
|
1942
|
+
const context = buildRecallCandidateContext(note);
|
|
1943
|
+
const boost = (isCurrentProject ? 0.15 : 0) + context.metadataBoost;
|
|
1944
|
+
scored.push({
|
|
1945
|
+
id: rec.id,
|
|
1946
|
+
score: rawScore,
|
|
1947
|
+
semanticScoreForPromotion: rawScore,
|
|
1948
|
+
boosted: rawScore + boost,
|
|
1949
|
+
vault,
|
|
1950
|
+
isCurrentProject: Boolean(isCurrentProject),
|
|
1951
|
+
lifecycle: context.lifecycle,
|
|
1952
|
+
relatedCount: context.relatedCount,
|
|
1953
|
+
connectionDiversity: context.connectionDiversity,
|
|
1954
|
+
structureScore: context.structureScore,
|
|
1955
|
+
metadata: context.metadata,
|
|
1956
|
+
});
|
|
1911
1957
|
}
|
|
1912
1958
|
}
|
|
1913
1959
|
const projectionTexts = new Map();
|
|
@@ -1938,14 +1984,19 @@ server.registerTool("recall", {
|
|
|
1938
1984
|
}
|
|
1939
1985
|
return undefined;
|
|
1940
1986
|
};
|
|
1987
|
+
const strongestSemanticScore = scored.reduce((max, candidate) => max === undefined ? candidate.score : Math.max(max, candidate.score), undefined);
|
|
1941
1988
|
const reranked = applyLexicalReranking(scored, query, getProjectionText);
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1989
|
+
let promoted = applyCanonicalExplanationPromotion(reranked);
|
|
1990
|
+
// Lexical rescue: when semantic results are weak, scan projections for additional candidates.
|
|
1991
|
+
// Skip rescue when the caller set a strict minSimilarity above the default,
|
|
1992
|
+
// because rescue candidates lack genuine semantic backing.
|
|
1993
|
+
const rescueAllowed = minSimilarity <= DEFAULT_MIN_SIMILARITY;
|
|
1994
|
+
if (rescueAllowed && shouldTriggerLexicalRescue(strongestSemanticScore, scored.length)) {
|
|
1995
|
+
const rescueCandidates = await collectLexicalRescueCandidates(vaults, query, project ?? undefined, scope, tags, lifecycle, promoted);
|
|
1996
|
+
promoted.push(...rescueCandidates);
|
|
1997
|
+
promoted = applyCanonicalExplanationPromotion(promoted);
|
|
1998
|
+
}
|
|
1999
|
+
const top = selectRecallResults(promoted, limit, scope);
|
|
1949
2000
|
if (top.length === 0) {
|
|
1950
2001
|
const structuredContent = { action: "recalled", query, scope: scope || "all", results: [] };
|
|
1951
2002
|
return { content: [{ type: "text", text: "No memories found matching that query." }], structuredContent };
|
|
@@ -1992,8 +2043,11 @@ server.registerTool("recall", {
|
|
|
1992
2043
|
const formattedRelationships = relationships !== undefined
|
|
1993
2044
|
? `\n\n${formatRelationshipPreview(relationships)}`
|
|
1994
2045
|
: "";
|
|
2046
|
+
const provenanceLine = provenance || confidence
|
|
2047
|
+
? `\n**confidence:** ${confidence ?? "medium"}${provenance?.recentlyChanged ? " | **recently changed**" : ""}`
|
|
2048
|
+
: "";
|
|
1995
2049
|
// Suppress raw related IDs when enriched preview is shown to avoid duplication
|
|
1996
|
-
sections.push(`${formatNote(note, score, relationships === undefined)}${formattedHistory}${formattedRelationships}`);
|
|
2050
|
+
sections.push(`${formatNote(note, score, relationships === undefined)}${provenanceLine}${formattedHistory}${formattedRelationships}`);
|
|
1997
2051
|
structuredResults.push({
|
|
1998
2052
|
id,
|
|
1999
2053
|
title: note.title,
|
|
@@ -2050,7 +2104,8 @@ server.registerTool("update", {
|
|
|
2050
2104
|
"- The updated memory id, changed fields, and persistence status\n\n" +
|
|
2051
2105
|
"Side effects: rewrites the note, refreshes embeddings, git commits, and may push.\n\n" +
|
|
2052
2106
|
"Typical next step:\n" +
|
|
2053
|
-
"- Use `relate` or `consolidate` if the update changes how this note connects to others
|
|
2107
|
+
"- Use `relate` or `consolidate` if the update changes how this note connects to others.\n\n" +
|
|
2108
|
+
"Use `semanticPatch` for targeted edits (more token-efficient). Use `content` only for complete rewrites.",
|
|
2054
2109
|
annotations: {
|
|
2055
2110
|
readOnlyHint: false,
|
|
2056
2111
|
destructiveHint: false,
|
|
@@ -2059,7 +2114,31 @@ server.registerTool("update", {
|
|
|
2059
2114
|
},
|
|
2060
2115
|
inputSchema: z.object({
|
|
2061
2116
|
id: z.string().describe("Exact memory id. Use an id returned by `recall`, `list`, `recent_memories`, or `where_is`."),
|
|
2062
|
-
|
|
2117
|
+
semanticPatch: z
|
|
2118
|
+
.array(z.object({
|
|
2119
|
+
selector: z.object({
|
|
2120
|
+
heading: z.string().optional(),
|
|
2121
|
+
headingStartsWith: z.string().optional(),
|
|
2122
|
+
nthChild: z.number().int().optional(),
|
|
2123
|
+
lastChild: z.literal(true).optional(),
|
|
2124
|
+
}).refine((sel) => {
|
|
2125
|
+
const keys = [sel.heading, sel.headingStartsWith, sel.nthChild, sel.lastChild].filter((v) => v !== undefined);
|
|
2126
|
+
return keys.length === 1;
|
|
2127
|
+
}, { message: "Selector must have exactly one of: heading, headingStartsWith, nthChild, lastChild" }),
|
|
2128
|
+
operation: z.discriminatedUnion("op", [
|
|
2129
|
+
z.object({ op: z.literal("appendChild"), value: z.string() }),
|
|
2130
|
+
z.object({ op: z.literal("prependChild"), value: z.string() }),
|
|
2131
|
+
z.object({ op: z.literal("replace"), value: z.string() }),
|
|
2132
|
+
z.object({ op: z.literal("replaceChildren"), value: z.string() }),
|
|
2133
|
+
z.object({ op: z.literal("insertAfter"), value: z.string() }),
|
|
2134
|
+
z.object({ op: z.literal("insertBefore"), value: z.string() }),
|
|
2135
|
+
z.object({ op: z.literal("remove") }),
|
|
2136
|
+
]),
|
|
2137
|
+
}))
|
|
2138
|
+
.optional()
|
|
2139
|
+
.describe("Use for targeted edits when you know the structure. More token-efficient than passing full content. " +
|
|
2140
|
+
"Mutually exclusive with content."),
|
|
2141
|
+
content: z.string().optional().describe("Full note body replacement. Use only for complete rewrites or when the note is small. Mutually exclusive with semanticPatch."),
|
|
2063
2142
|
title: z.string().optional().describe("Specific, retrieval-friendly title. Prefer the concrete topic or decision, not a vague label."),
|
|
2064
2143
|
tags: z.array(z.string()).optional().describe("Optional tags for later filtering. Use a small number of stable, meaningful tags."),
|
|
2065
2144
|
lifecycle: z
|
|
@@ -2080,12 +2159,18 @@ server.registerTool("update", {
|
|
|
2080
2159
|
"When true, update can commit on a protected branch without changing project policy."),
|
|
2081
2160
|
}),
|
|
2082
2161
|
outputSchema: UpdateResultSchema,
|
|
2083
|
-
}, async ({ id, content, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
|
|
2162
|
+
}, async ({ id, content, semanticPatch, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
|
|
2084
2163
|
await ensureBranchSynced(cwd);
|
|
2085
2164
|
const found = await vaultManager.findNote(id, cwd);
|
|
2086
2165
|
if (!found) {
|
|
2087
2166
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
2088
2167
|
}
|
|
2168
|
+
// Validate: content and semanticPatch are mutually exclusive
|
|
2169
|
+
const hasContent = content !== undefined;
|
|
2170
|
+
const hasSemanticPatch = semanticPatch !== undefined && semanticPatch.length > 0;
|
|
2171
|
+
if (hasContent && hasSemanticPatch) {
|
|
2172
|
+
return { content: [{ type: "text", text: "Exactly one of content or semanticPatch must be provided, not both." }], isError: true };
|
|
2173
|
+
}
|
|
2089
2174
|
const { note, vault } = found;
|
|
2090
2175
|
if (vault.isProject) {
|
|
2091
2176
|
const resolvedProject = await resolveProject(cwd);
|
|
@@ -2110,11 +2195,21 @@ server.registerTool("update", {
|
|
|
2110
2195
|
}
|
|
2111
2196
|
}
|
|
2112
2197
|
const now = new Date().toISOString();
|
|
2198
|
+
let patchedContent;
|
|
2199
|
+
if (semanticPatch && semanticPatch.length > 0) {
|
|
2200
|
+
try {
|
|
2201
|
+
patchedContent = await applySemanticPatches(note.content, semanticPatch);
|
|
2202
|
+
}
|
|
2203
|
+
catch (err) {
|
|
2204
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2205
|
+
return { content: [{ type: "text", text: `Semantic patch failed: ${message}` }], isError: true };
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2113
2208
|
const cleanedContent = content === undefined ? undefined : await cleanMarkdown(content);
|
|
2114
2209
|
const updated = {
|
|
2115
2210
|
...note,
|
|
2116
2211
|
title: title ?? note.title,
|
|
2117
|
-
content: cleanedContent ?? note.content,
|
|
2212
|
+
content: patchedContent ?? cleanedContent ?? note.content,
|
|
2118
2213
|
tags: tags ?? note.tags,
|
|
2119
2214
|
lifecycle: lifecycle ?? note.lifecycle,
|
|
2120
2215
|
alwaysLoad: alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad,
|
|
@@ -2158,6 +2253,8 @@ server.registerTool("update", {
|
|
|
2158
2253
|
changes.push("title");
|
|
2159
2254
|
if (content !== undefined)
|
|
2160
2255
|
changes.push("content");
|
|
2256
|
+
if (semanticPatch !== undefined)
|
|
2257
|
+
changes.push("semanticPatch");
|
|
2161
2258
|
if (tags !== undefined)
|
|
2162
2259
|
changes.push("tags");
|
|
2163
2260
|
if (lifecycle !== undefined && lifecycle !== note.lifecycle)
|