@danielmarbach/mnemonic-mcp 0.23.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 CHANGED
@@ -4,6 +4,12 @@ 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
+
7
13
  ## [0.23.0] - 2026-04-17
8
14
 
9
15
  ### Added
package/build/index.js CHANGED
@@ -18,6 +18,7 @@ import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, ap
18
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";
@@ -2103,7 +2104,8 @@ server.registerTool("update", {
2103
2104
  "- The updated memory id, changed fields, and persistence status\n\n" +
2104
2105
  "Side effects: rewrites the note, refreshes embeddings, git commits, and may push.\n\n" +
2105
2106
  "Typical next step:\n" +
2106
- "- 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.",
2107
2109
  annotations: {
2108
2110
  readOnlyHint: false,
2109
2111
  destructiveHint: false,
@@ -2112,7 +2114,31 @@ server.registerTool("update", {
2112
2114
  },
2113
2115
  inputSchema: z.object({
2114
2116
  id: z.string().describe("Exact memory id. Use an id returned by `recall`, `list`, `recent_memories`, or `where_is`."),
2115
- content: z.string().optional().describe("Markdown note body. Put the key fact, decision, or outcome in the opening lines, then supporting detail."),
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."),
2116
2142
  title: z.string().optional().describe("Specific, retrieval-friendly title. Prefer the concrete topic or decision, not a vague label."),
2117
2143
  tags: z.array(z.string()).optional().describe("Optional tags for later filtering. Use a small number of stable, meaningful tags."),
2118
2144
  lifecycle: z
@@ -2133,12 +2159,18 @@ server.registerTool("update", {
2133
2159
  "When true, update can commit on a protected branch without changing project policy."),
2134
2160
  }),
2135
2161
  outputSchema: UpdateResultSchema,
2136
- }, async ({ id, content, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
2162
+ }, async ({ id, content, semanticPatch, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
2137
2163
  await ensureBranchSynced(cwd);
2138
2164
  const found = await vaultManager.findNote(id, cwd);
2139
2165
  if (!found) {
2140
2166
  return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
2141
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
+ }
2142
2174
  const { note, vault } = found;
2143
2175
  if (vault.isProject) {
2144
2176
  const resolvedProject = await resolveProject(cwd);
@@ -2163,11 +2195,21 @@ server.registerTool("update", {
2163
2195
  }
2164
2196
  }
2165
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
+ }
2166
2208
  const cleanedContent = content === undefined ? undefined : await cleanMarkdown(content);
2167
2209
  const updated = {
2168
2210
  ...note,
2169
2211
  title: title ?? note.title,
2170
- content: cleanedContent ?? note.content,
2212
+ content: patchedContent ?? cleanedContent ?? note.content,
2171
2213
  tags: tags ?? note.tags,
2172
2214
  lifecycle: lifecycle ?? note.lifecycle,
2173
2215
  alwaysLoad: alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad,
@@ -2211,6 +2253,8 @@ server.registerTool("update", {
2211
2253
  changes.push("title");
2212
2254
  if (content !== undefined)
2213
2255
  changes.push("content");
2256
+ if (semanticPatch !== undefined)
2257
+ changes.push("semanticPatch");
2214
2258
  if (tags !== undefined)
2215
2259
  changes.push("tags");
2216
2260
  if (lifecycle !== undefined && lifecycle !== note.lifecycle)