@danielmarbach/mnemonic-mcp 0.23.0 → 0.25.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 +21 -0
- package/README.md +67 -4
- package/build/index.js +158 -19
- package/build/index.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 +1 -0
- package/build/recall.d.ts.map +1 -1
- package/build/recall.js +31 -0
- 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/build/storage.d.ts +3 -3
- package/build/storage.d.ts.map +1 -1
- package/build/storage.js +1 -1
- package/build/storage.js.map +1 -1
- package/build/structured-content.d.ts +65 -1
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +7 -1
- package/build/structured-content.js.map +1 -1
- package/package.json +12 -2
- package/scripts/install-skills.mjs +211 -0
- package/skills/mnemonic-rpi-workflow/SKILL.md +151 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,27 @@ 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
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.25.0] - 2026-04-24
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `recall` now supports `mode: "workflow"` for RPIR-oriented chain retrieval while keeping compatibility with existing note graphs.
|
|
14
|
+
- Relationship types now include `derives-from` and `follows` so workflows can express directional sequencing and derivation explicitly.
|
|
15
|
+
- The npm package now ships bundled skills and includes a `mnemonic-install-skills` command to install or update local skill directories for Claude, OpenCode, and custom targets.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- RPIR workflow guidance now uses `mnemonic-rpi-workflow` as the prompt name.
|
|
20
|
+
- The RPIR skill is now published under `skills/mnemonic-rpi-workflow`.
|
|
21
|
+
|
|
22
|
+
## [0.24.0] - 2026-04-24
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `update` now supports `semanticPatch` for token-efficient targeted edits (insert, replace, append, remove content under specific headings) without round-tripping the full note body.
|
|
27
|
+
|
|
7
28
|
## [0.23.0] - 2026-04-17
|
|
8
29
|
|
|
9
30
|
### Added
|
package/README.md
CHANGED
|
@@ -51,8 +51,13 @@ No code changes required — set `EMBED_MODEL=qwen3-embedding:0.6b` in your envi
|
|
|
51
51
|
npm install
|
|
52
52
|
npm run build
|
|
53
53
|
npm test
|
|
54
|
+
|
|
55
|
+
# release-confidence gate (build + full tests + isolated dogfooding)
|
|
56
|
+
npm run verify:release
|
|
54
57
|
```
|
|
55
58
|
|
|
59
|
+
The gate fails on required dogfood checks and reports advisory findings separately in the dogfood output.
|
|
60
|
+
|
|
56
61
|
`npm run build` already runs `typecheck`, but running it explicitly first gives a faster failure loop when iterating on the codebase.
|
|
57
62
|
|
|
58
63
|
For local dogfooding, start the built MCP server with:
|
|
@@ -94,6 +99,45 @@ npm install @danielmarbach/mnemonic-mcp
|
|
|
94
99
|
npm install @danielmarbach/mnemonic-mcp@0.2.0
|
|
95
100
|
```
|
|
96
101
|
|
|
102
|
+
### Install bundled skills (Claude/OpenCode)
|
|
103
|
+
|
|
104
|
+
The npm package now includes `skills/**` plus a helper binary to install them into local skill directories.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# If mnemonic is installed in this project:
|
|
108
|
+
npx mnemonic-install-skills --target all --mode copy
|
|
109
|
+
|
|
110
|
+
# One-off install without adding dependency:
|
|
111
|
+
npx -y -p @danielmarbach/mnemonic-mcp mnemonic-install-skills --target all --mode copy
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Supported targets:
|
|
115
|
+
|
|
116
|
+
- `--target claude` -> `~/.claude/skills`
|
|
117
|
+
- `--target opencode` -> `~/.config/opencode/skills`
|
|
118
|
+
- `--target all` -> both (default)
|
|
119
|
+
- `--target custom` -> only use `--target-dir` destinations
|
|
120
|
+
- `--target-dir <path>` -> add any custom client skill directory
|
|
121
|
+
|
|
122
|
+
Update flow after upgrading `@danielmarbach/mnemonic-mcp`:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
npx mnemonic-install-skills --target all --mode copy --update
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
If you prefer automatic propagation without copy refreshes, use symlink mode:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
npx mnemonic-install-skills --target all --mode symlink --update
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
After install, load and use the skill by name:
|
|
135
|
+
|
|
136
|
+
- Skill name: `mnemonic-rpi-workflow`
|
|
137
|
+
- Prompt counterpart: `mnemonic-rpi-workflow`
|
|
138
|
+
|
|
139
|
+
In clients that support explicit skill loading (for example Claude Code or OpenCode), load `mnemonic-rpi-workflow` before running multi-step RPIR workflows.
|
|
140
|
+
|
|
97
141
|
### Homebrew
|
|
98
142
|
|
|
99
143
|
The formula lives in this repository. Tap it with an explicit URL so no separate repository is needed:
|
|
@@ -312,7 +356,11 @@ Project identity derives from the **git remote URL**, normalized to a stable slu
|
|
|
312
356
|
|
|
313
357
|
**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.
|
|
314
358
|
|
|
315
|
-
|
|
359
|
+
Recall modes:
|
|
360
|
+
|
|
361
|
+
- `mode: "default"` (default): semantic recall with optional lexical reranking and bounded relationship previews.
|
|
362
|
+
- `mode: "temporal"`: enrich top matches with compact git-backed history (no raw diffs by default).
|
|
363
|
+
- `mode: "workflow"`: prioritize RPIR-style chain reconstruction while remaining compatible with legacy `related-to` links.
|
|
316
364
|
|
|
317
365
|
**What temporal mode shows:**
|
|
318
366
|
|
|
@@ -341,11 +389,21 @@ Each note carries a `lifecycle`:
|
|
|
341
389
|
|
|
342
390
|
### Roles and lifecycle
|
|
343
391
|
|
|
344
|
-
Roles are optional prioritization hints, not required schema. mnemonic infers a `role` and `importance` from structural signals (heading count, bullet density, inbound references, relationship types) — inference is language-independent and never overwrites explicit frontmatter. Valid roles: `summary`, `decision`, `plan`, `
|
|
392
|
+
Roles are optional prioritization hints, not required schema. mnemonic infers a `role` and `importance` from structural signals (heading count, bullet density, inbound references, relationship types) — inference is language-independent and never overwrites explicit frontmatter. Valid roles: `summary`, `decision`, `plan`, `context`, `reference`, `research`, `review`. Valid importance values: `high`, `normal`, `low`.
|
|
345
393
|
|
|
346
394
|
Set `alwaysLoad: true` in a note's frontmatter to mark it as an explicit session anchor; it receives the highest recall and relationship-expansion priority regardless of inferred role.
|
|
347
395
|
|
|
348
|
-
mnemonic works without roles. Inferred roles stay internal-only, prioritization is language-independent by default, and lifecycle remains the separate durability axis.
|
|
396
|
+
mnemonic works without roles. Inferred roles stay internal-only, prioritization is language-independent by default, and lifecycle remains the separate durability axis. When `lifecycle` is omitted, `remember` applies soft defaults based on role: `research`, `plan`, and `review` default to `temporary`; `decision`, `summary`, and `reference` default to `permanent`. Explicit `lifecycle` always overrides the role-based default.
|
|
397
|
+
|
|
398
|
+
### RPIR workflow conventions
|
|
399
|
+
|
|
400
|
+
For structured workflows, use the RPIR stages: research -> plan -> implement -> review (iterate only when needed).
|
|
401
|
+
|
|
402
|
+
- Create one request root note per workflow: `role: context`, `lifecycle: temporary`, `tags: ["workflow", "request"]`.
|
|
403
|
+
- Keep one current plan note per request (`role: plan`) and update or supersede as the plan evolves.
|
|
404
|
+
- For apply/task notes, do not add a new role: use `role: plan` for executable steps and `role: context` for execution observations; tag both with `apply`.
|
|
405
|
+
- Keep relationships sparse and immediate-upstream only: research -> request, plan -> request/research, apply -> plan, review -> apply/plan, outcome -> plan (optionally request).
|
|
406
|
+
- Consolidate at workflow end: promote durable outcomes into permanent decision/summary/reference notes; let temporary scaffolding expire.
|
|
349
407
|
|
|
350
408
|
### Note format
|
|
351
409
|
|
|
@@ -444,6 +502,7 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
|
|
|
444
502
|
|
|
445
503
|
| Prompt | Description |
|
|
446
504
|
|--------|-------------|
|
|
505
|
+
| `mnemonic-rpi-workflow` | Optional. Returns RPIR stage protocol and conventions: request root note pattern, stage checklists, apply/task split, sparse relationships, subagent handoff contract, and commit discipline. |
|
|
447
506
|
| `mnemonic-workflow-hint` | Optional. Returns a compact decision protocol: use `recall` or `list` first, inspect with `get`, update existing memories, remember only when nothing matches, then organize with `relate`, `consolidate`, or `move_memory`. It also reinforces summary-first orientation via `project_memory_summary`, temporary-note recovery only after orientation, and that roles are optional prioritization hints while lifecycle stays separate. |
|
|
448
507
|
|
|
449
508
|
## Tools
|
|
@@ -463,7 +522,7 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
|
|
|
463
522
|
| `memory_graph` | Show compact adjacency list of relationships |
|
|
464
523
|
| `move_memory` | Move note between vaults without changing id |
|
|
465
524
|
| `project_memory_summary` | Session-start entrypoint: themed notes, anchors, and orientation for fast project orientation |
|
|
466
|
-
| `recall` | Semantic search with optional project boost and
|
|
525
|
+
| `recall` | Semantic search with optional project boost, temporal history mode, and workflow chain mode |
|
|
467
526
|
| `recent_memories` | Show most recently updated notes for scope |
|
|
468
527
|
| `remember` | Write note + embedding; `cwd` sets context, `scope` picks storage, `lifecycle` picks temporary vs permanent |
|
|
469
528
|
| `relate` | Create typed relationship between notes (bidirectional) |
|
|
@@ -504,6 +563,10 @@ relatedTo:
|
|
|
504
563
|
| `explains` | `fromId` explains `toId` |
|
|
505
564
|
| `example-of` | `fromId` is a concrete example of `toId` |
|
|
506
565
|
| `supersedes` | `fromId` is the newer version of `toId` |
|
|
566
|
+
| `derives-from` | `fromId` is derived from `toId` |
|
|
567
|
+
| `follows` | `fromId` follows `toId` in sequence |
|
|
568
|
+
|
|
569
|
+
`workflow` recall mode prefers directional and typed relationships first, then falls back to `related-to` for long-term compatibility with older vaults.
|
|
507
570
|
|
|
508
571
|
`relate` is bidirectional by default. `forget` automatically removes any edges pointing at the deleted note.
|
|
509
572
|
|
package/build/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { z } from "zod";
|
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
6
|
import path from "path";
|
|
7
7
|
import { promises as fs } from "fs";
|
|
8
|
-
import { NOTE_LIFECYCLES } from "./storage.js";
|
|
8
|
+
import { NOTE_LIFECYCLES, NOTE_ROLES } from "./storage.js";
|
|
9
9
|
import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
|
|
10
10
|
import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
|
|
11
11
|
import { enrichTemporalHistory } from "./temporal-interpretation.js";
|
|
@@ -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, applyCanonicalExplanationPromotion, } from "./recall.js";
|
|
17
|
+
import { computeRecallMetadataBoost, computeHybridScore, selectRecallResults, selectWorkflowResults, applyLexicalReranking, applyCanonicalExplanationPromotion, } from "./recall.js";
|
|
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";
|
|
@@ -406,12 +407,13 @@ function describeLifecycle(lifecycle) {
|
|
|
406
407
|
function formatNote(note, score, showRawRelated = true) {
|
|
407
408
|
const scoreStr = score !== undefined ? ` | similarity: ${score.toFixed(3)}` : "";
|
|
408
409
|
const projectStr = note.project ? ` | project: ${note.projectName ?? note.project}` : " | global";
|
|
410
|
+
const roleStr = note.role ? ` | **role: ${note.role}**` : "";
|
|
409
411
|
const relStr = showRawRelated && note.relatedTo && note.relatedTo.length > 0
|
|
410
412
|
? `\n**related:** ${note.relatedTo.map((r) => `\`${r.id}\` (${r.type})`).join(", ")}`
|
|
411
413
|
: "";
|
|
412
414
|
return (`## ${note.title}\n` +
|
|
413
415
|
`**id:** \`${note.id}\`${projectStr}${scoreStr}\n` +
|
|
414
|
-
`**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}
|
|
416
|
+
`**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}**${roleStr} | **updated:** ${note.updatedAt}${relStr}\n\n` +
|
|
415
417
|
note.content);
|
|
416
418
|
}
|
|
417
419
|
function formatTemporalHistory(history) {
|
|
@@ -923,11 +925,13 @@ function formatListEntry(entry, options = {}) {
|
|
|
923
925
|
const extras = [];
|
|
924
926
|
if (note.tags.length > 0)
|
|
925
927
|
extras.push(note.tags.join(", "));
|
|
926
|
-
extras.push(`lifecycle
|
|
928
|
+
extras.push(`lifecycle: ${note.lifecycle}`);
|
|
929
|
+
if (note.role)
|
|
930
|
+
extras.push(`role: ${note.role}`);
|
|
927
931
|
if (options.includeStorage)
|
|
928
|
-
extras.push(`stored
|
|
932
|
+
extras.push(`stored: ${storageLabel(vault)}`);
|
|
929
933
|
if (options.includeUpdated)
|
|
930
|
-
extras.push(`updated
|
|
934
|
+
extras.push(`updated: ${note.updatedAt}`);
|
|
931
935
|
const lines = [`- **${note.title}** \`${note.id}\` ${proj}${extras.length > 0 ? ` — ${extras.join(" | ")}` : ""}`];
|
|
932
936
|
if (options.includeRelations && note.relatedTo && note.relatedTo.length > 0) {
|
|
933
937
|
lines.push(` related: ${note.relatedTo.map((rel) => `${rel.id} (${rel.type})`).join(", ")}`);
|
|
@@ -1028,6 +1032,14 @@ function addVaultChange(vaultChanges, vault, file) {
|
|
|
1028
1032
|
vaultChanges.set(vault, files);
|
|
1029
1033
|
}
|
|
1030
1034
|
}
|
|
1035
|
+
const ROLE_LIFECYCLE_DEFAULTS = {
|
|
1036
|
+
research: "temporary",
|
|
1037
|
+
plan: "temporary",
|
|
1038
|
+
review: "temporary",
|
|
1039
|
+
decision: "permanent",
|
|
1040
|
+
summary: "permanent",
|
|
1041
|
+
reference: "permanent",
|
|
1042
|
+
};
|
|
1031
1043
|
// ── MCP Server ────────────────────────────────────────────────────────────────
|
|
1032
1044
|
const server = new McpServer({
|
|
1033
1045
|
name: "mnemonic",
|
|
@@ -1425,7 +1437,13 @@ server.registerTool("remember", {
|
|
|
1425
1437
|
.enum(NOTE_LIFECYCLES)
|
|
1426
1438
|
.optional()
|
|
1427
1439
|
.describe("Memory lifetime. Use `temporary` for short-lived working context such as active investigations or transient status. " +
|
|
1428
|
-
"Use `permanent` for durable knowledge such as decisions, fixes, patterns, and preferences."
|
|
1440
|
+
"Use `permanent` for durable knowledge such as decisions, fixes, patterns, and preferences. " +
|
|
1441
|
+
"When omitted, defaults based on role: research/plan/review → temporary, decision/summary/reference → permanent."),
|
|
1442
|
+
role: z
|
|
1443
|
+
.enum(NOTE_ROLES)
|
|
1444
|
+
.optional()
|
|
1445
|
+
.describe("Optional prioritization hint for the note. Inferred automatically when omitted. " +
|
|
1446
|
+
"Set explicitly for workflow artifacts like research or review notes."),
|
|
1429
1447
|
summary: z.string().optional().describe("Git commit summary only. Imperative mood, concise, and focused on why the change matters."),
|
|
1430
1448
|
alwaysLoad: z
|
|
1431
1449
|
.boolean()
|
|
@@ -1453,7 +1471,7 @@ server.registerTool("remember", {
|
|
|
1453
1471
|
.describe("Optional agent hint indicating that `recall` or `list` was already used to check for an existing memory on this topic."),
|
|
1454
1472
|
}),
|
|
1455
1473
|
outputSchema: RememberResultSchema,
|
|
1456
|
-
}, async ({ title, content, tags, lifecycle, summary, alwaysLoad, cwd, scope, allowProtectedBranch = false }) => {
|
|
1474
|
+
}, async ({ title, content, tags, lifecycle, role, summary, alwaysLoad, cwd, scope, allowProtectedBranch = false }) => {
|
|
1457
1475
|
await ensureBranchSynced(cwd);
|
|
1458
1476
|
const project = await resolveProject(cwd);
|
|
1459
1477
|
const cleanedContent = await cleanMarkdown(content);
|
|
@@ -1482,7 +1500,8 @@ server.registerTool("remember", {
|
|
|
1482
1500
|
const now = new Date().toISOString();
|
|
1483
1501
|
const note = {
|
|
1484
1502
|
id, title, content: cleanedContent, tags,
|
|
1485
|
-
lifecycle: lifecycle ?? "permanent",
|
|
1503
|
+
lifecycle: lifecycle ?? (role ? ROLE_LIFECYCLE_DEFAULTS[role] : undefined) ?? "permanent",
|
|
1504
|
+
...(role ? { role } : {}),
|
|
1486
1505
|
alwaysLoad: alwaysLoad ?? false,
|
|
1487
1506
|
project: project?.id,
|
|
1488
1507
|
projectName: project?.name,
|
|
@@ -1840,6 +1859,7 @@ server.registerTool("recall", {
|
|
|
1840
1859
|
title: "Recall",
|
|
1841
1860
|
description: "Semantic search over stored memories using embeddings.\n\n" +
|
|
1842
1861
|
"Supports opt-in temporal mode (`mode: \"temporal\"`) to enrich top semantic matches with compact git-backed history.\n\n" +
|
|
1862
|
+
"Supports workflow mode (`mode: \"workflow\"`) to prioritize RPIR-style chain reconstruction while retaining compatibility with legacy relationships.\n\n" +
|
|
1843
1863
|
"Use this when:\n" +
|
|
1844
1864
|
"- You know the topic but not the exact memory id\n" +
|
|
1845
1865
|
"- You are starting a session and want relevant prior context\n" +
|
|
@@ -1866,7 +1886,7 @@ server.registerTool("recall", {
|
|
|
1866
1886
|
cwd: projectParam,
|
|
1867
1887
|
limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECALL_LIMIT),
|
|
1868
1888
|
minSimilarity: z.number().min(0).max(1).optional().default(DEFAULT_MIN_SIMILARITY),
|
|
1869
|
-
mode: z.enum(["default", "temporal"]).optional().default("default").describe("
|
|
1889
|
+
mode: z.enum(["default", "temporal", "workflow"]).optional().default("default").describe("Use `temporal` for compact git-backed history, or `workflow` for RPIR-oriented chain reconstruction."),
|
|
1870
1890
|
verbose: z.boolean().optional().default(false).describe("Only meaningful with `mode: \"temporal\"`. Adds richer stats-based history context without returning raw diffs."),
|
|
1871
1891
|
tags: z.array(z.string()).optional().describe("Filter results to notes with all of these tags."),
|
|
1872
1892
|
scope: z
|
|
@@ -1995,7 +2015,9 @@ server.registerTool("recall", {
|
|
|
1995
2015
|
promoted.push(...rescueCandidates);
|
|
1996
2016
|
promoted = applyCanonicalExplanationPromotion(promoted);
|
|
1997
2017
|
}
|
|
1998
|
-
const top =
|
|
2018
|
+
const top = mode === "workflow"
|
|
2019
|
+
? selectWorkflowResults(promoted, limit, scope)
|
|
2020
|
+
: selectRecallResults(promoted, limit, scope);
|
|
1999
2021
|
if (top.length === 0) {
|
|
2000
2022
|
const structuredContent = { action: "recalled", query, scope: scope || "all", results: [] };
|
|
2001
2023
|
return { content: [{ type: "text", text: "No memories found matching that query." }], structuredContent };
|
|
@@ -2056,6 +2078,7 @@ server.registerTool("recall", {
|
|
|
2056
2078
|
vault: storageLabel(vault),
|
|
2057
2079
|
tags: note.tags,
|
|
2058
2080
|
lifecycle: note.lifecycle,
|
|
2081
|
+
role: note.role,
|
|
2059
2082
|
updatedAt: note.updatedAt,
|
|
2060
2083
|
provenance,
|
|
2061
2084
|
confidence,
|
|
@@ -2103,7 +2126,8 @@ server.registerTool("update", {
|
|
|
2103
2126
|
"- The updated memory id, changed fields, and persistence status\n\n" +
|
|
2104
2127
|
"Side effects: rewrites the note, refreshes embeddings, git commits, and may push.\n\n" +
|
|
2105
2128
|
"Typical next step:\n" +
|
|
2106
|
-
"- Use `relate` or `consolidate` if the update changes how this note connects to others
|
|
2129
|
+
"- Use `relate` or `consolidate` if the update changes how this note connects to others.\n\n" +
|
|
2130
|
+
"Use `semanticPatch` for targeted edits (more token-efficient). Use `content` only for complete rewrites.",
|
|
2107
2131
|
annotations: {
|
|
2108
2132
|
readOnlyHint: false,
|
|
2109
2133
|
destructiveHint: false,
|
|
@@ -2112,13 +2136,41 @@ server.registerTool("update", {
|
|
|
2112
2136
|
},
|
|
2113
2137
|
inputSchema: z.object({
|
|
2114
2138
|
id: z.string().describe("Exact memory id. Use an id returned by `recall`, `list`, `recent_memories`, or `where_is`."),
|
|
2115
|
-
|
|
2139
|
+
semanticPatch: z
|
|
2140
|
+
.array(z.object({
|
|
2141
|
+
selector: z.object({
|
|
2142
|
+
heading: z.string().optional(),
|
|
2143
|
+
headingStartsWith: z.string().optional(),
|
|
2144
|
+
nthChild: z.number().int().optional(),
|
|
2145
|
+
lastChild: z.literal(true).optional(),
|
|
2146
|
+
}).refine((sel) => {
|
|
2147
|
+
const keys = [sel.heading, sel.headingStartsWith, sel.nthChild, sel.lastChild].filter((v) => v !== undefined);
|
|
2148
|
+
return keys.length === 1;
|
|
2149
|
+
}, { message: "Selector must have exactly one of: heading, headingStartsWith, nthChild, lastChild" }),
|
|
2150
|
+
operation: z.discriminatedUnion("op", [
|
|
2151
|
+
z.object({ op: z.literal("appendChild"), value: z.string() }),
|
|
2152
|
+
z.object({ op: z.literal("prependChild"), value: z.string() }),
|
|
2153
|
+
z.object({ op: z.literal("replace"), value: z.string() }),
|
|
2154
|
+
z.object({ op: z.literal("replaceChildren"), value: z.string() }),
|
|
2155
|
+
z.object({ op: z.literal("insertAfter"), value: z.string() }),
|
|
2156
|
+
z.object({ op: z.literal("insertBefore"), value: z.string() }),
|
|
2157
|
+
z.object({ op: z.literal("remove") }),
|
|
2158
|
+
]),
|
|
2159
|
+
}))
|
|
2160
|
+
.optional()
|
|
2161
|
+
.describe("Use for targeted edits when you know the structure. More token-efficient than passing full content. " +
|
|
2162
|
+
"Mutually exclusive with content."),
|
|
2163
|
+
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
2164
|
title: z.string().optional().describe("Specific, retrieval-friendly title. Prefer the concrete topic or decision, not a vague label."),
|
|
2117
2165
|
tags: z.array(z.string()).optional().describe("Optional tags for later filtering. Use a small number of stable, meaningful tags."),
|
|
2118
2166
|
lifecycle: z
|
|
2119
2167
|
.enum(NOTE_LIFECYCLES)
|
|
2120
2168
|
.optional()
|
|
2121
2169
|
.describe("Change lifecycle. Preserve the existing value unless you're intentionally switching it."),
|
|
2170
|
+
role: z
|
|
2171
|
+
.enum(NOTE_ROLES)
|
|
2172
|
+
.optional()
|
|
2173
|
+
.describe("Change role. Preserve the existing value unless you're intentionally switching it."),
|
|
2122
2174
|
summary: z.string().optional().describe("Git commit summary only. Imperative mood, concise, and focused on why the change matters."),
|
|
2123
2175
|
alwaysLoad: z
|
|
2124
2176
|
.boolean()
|
|
@@ -2133,12 +2185,18 @@ server.registerTool("update", {
|
|
|
2133
2185
|
"When true, update can commit on a protected branch without changing project policy."),
|
|
2134
2186
|
}),
|
|
2135
2187
|
outputSchema: UpdateResultSchema,
|
|
2136
|
-
}, async ({ id, content, title, tags, lifecycle, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
|
|
2188
|
+
}, async ({ id, content, semanticPatch, title, tags, lifecycle, role, summary, alwaysLoad, cwd, allowProtectedBranch = false }) => {
|
|
2137
2189
|
await ensureBranchSynced(cwd);
|
|
2138
2190
|
const found = await vaultManager.findNote(id, cwd);
|
|
2139
2191
|
if (!found) {
|
|
2140
2192
|
return { content: [{ type: "text", text: `No memory found with id '${id}'` }], isError: true };
|
|
2141
2193
|
}
|
|
2194
|
+
// Validate: content and semanticPatch are mutually exclusive
|
|
2195
|
+
const hasContent = content !== undefined;
|
|
2196
|
+
const hasSemanticPatch = semanticPatch !== undefined && semanticPatch.length > 0;
|
|
2197
|
+
if (hasContent && hasSemanticPatch) {
|
|
2198
|
+
return { content: [{ type: "text", text: "Exactly one of content or semanticPatch must be provided, not both." }], isError: true };
|
|
2199
|
+
}
|
|
2142
2200
|
const { note, vault } = found;
|
|
2143
2201
|
if (vault.isProject) {
|
|
2144
2202
|
const resolvedProject = await resolveProject(cwd);
|
|
@@ -2163,13 +2221,24 @@ server.registerTool("update", {
|
|
|
2163
2221
|
}
|
|
2164
2222
|
}
|
|
2165
2223
|
const now = new Date().toISOString();
|
|
2224
|
+
let patchedContent;
|
|
2225
|
+
if (semanticPatch && semanticPatch.length > 0) {
|
|
2226
|
+
try {
|
|
2227
|
+
patchedContent = await applySemanticPatches(note.content, semanticPatch);
|
|
2228
|
+
}
|
|
2229
|
+
catch (err) {
|
|
2230
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2231
|
+
return { content: [{ type: "text", text: `Semantic patch failed: ${message}` }], isError: true };
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2166
2234
|
const cleanedContent = content === undefined ? undefined : await cleanMarkdown(content);
|
|
2167
2235
|
const updated = {
|
|
2168
2236
|
...note,
|
|
2169
2237
|
title: title ?? note.title,
|
|
2170
|
-
content: cleanedContent ?? note.content,
|
|
2238
|
+
content: patchedContent ?? cleanedContent ?? note.content,
|
|
2171
2239
|
tags: tags ?? note.tags,
|
|
2172
2240
|
lifecycle: lifecycle ?? note.lifecycle,
|
|
2241
|
+
...(role !== undefined ? { role } : (note.role ? { role: note.role } : {})),
|
|
2173
2242
|
alwaysLoad: alwaysLoad !== undefined ? alwaysLoad : note.alwaysLoad,
|
|
2174
2243
|
updatedAt: now,
|
|
2175
2244
|
};
|
|
@@ -2211,10 +2280,14 @@ server.registerTool("update", {
|
|
|
2211
2280
|
changes.push("title");
|
|
2212
2281
|
if (content !== undefined)
|
|
2213
2282
|
changes.push("content");
|
|
2283
|
+
if (semanticPatch !== undefined)
|
|
2284
|
+
changes.push("semanticPatch");
|
|
2214
2285
|
if (tags !== undefined)
|
|
2215
2286
|
changes.push("tags");
|
|
2216
2287
|
if (lifecycle !== undefined && lifecycle !== note.lifecycle)
|
|
2217
2288
|
changes.push("lifecycle");
|
|
2289
|
+
if (role !== undefined && role !== note.role)
|
|
2290
|
+
changes.push("role");
|
|
2218
2291
|
if (alwaysLoad !== undefined && alwaysLoad !== note.alwaysLoad)
|
|
2219
2292
|
changes.push("alwaysLoad");
|
|
2220
2293
|
const changeDesc = changes.length > 0 ? `Updated ${changes.join(", ")}` : "No changes";
|
|
@@ -2259,10 +2332,12 @@ server.registerTool("update", {
|
|
|
2259
2332
|
timestamp: now,
|
|
2260
2333
|
project: noteProjectRef(updated),
|
|
2261
2334
|
lifecycle: updated.lifecycle,
|
|
2335
|
+
role: updated.role,
|
|
2262
2336
|
persistence,
|
|
2263
2337
|
};
|
|
2264
2338
|
invalidateActiveProjectCache();
|
|
2265
|
-
|
|
2339
|
+
const fieldText = changes.length > 0 ? `\nfields modified: ${changes.join(", ")}` : "";
|
|
2340
|
+
return { content: [{ type: "text", text: `Updated memory '${id}'${fieldText}\n${formatPersistenceSummary(persistence)}` }], structuredContent };
|
|
2266
2341
|
});
|
|
2267
2342
|
// ── forget ────────────────────────────────────────────────────────────────────
|
|
2268
2343
|
server.registerTool("forget", {
|
|
@@ -2442,6 +2517,7 @@ server.registerTool("get", {
|
|
|
2442
2517
|
project: noteProjectRef(note),
|
|
2443
2518
|
tags: note.tags,
|
|
2444
2519
|
lifecycle: note.lifecycle,
|
|
2520
|
+
role: note.role,
|
|
2445
2521
|
alwaysLoad: note.alwaysLoad,
|
|
2446
2522
|
relatedTo: note.relatedTo,
|
|
2447
2523
|
createdAt: note.createdAt,
|
|
@@ -2457,7 +2533,7 @@ server.registerTool("get", {
|
|
|
2457
2533
|
const lines = [];
|
|
2458
2534
|
for (const note of found) {
|
|
2459
2535
|
lines.push(`## ${note.title} (${note.id})`);
|
|
2460
|
-
lines.push(`project: ${note.project?.name ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}`);
|
|
2536
|
+
lines.push(`project: ${note.project?.name ?? "global"} | stored: ${note.vault} | lifecycle: ${note.lifecycle}${note.role ? ` | role: ${note.role}` : ""}`);
|
|
2461
2537
|
if (note.tags.length > 0)
|
|
2462
2538
|
lines.push(`tags: ${note.tags.join(", ")}`);
|
|
2463
2539
|
lines.push("");
|
|
@@ -2603,6 +2679,7 @@ server.registerTool("list", {
|
|
|
2603
2679
|
project: noteProjectRef(note),
|
|
2604
2680
|
tags: note.tags,
|
|
2605
2681
|
lifecycle: note.lifecycle,
|
|
2682
|
+
role: note.role,
|
|
2606
2683
|
vault: storageLabel(vault),
|
|
2607
2684
|
updatedAt: note.updatedAt,
|
|
2608
2685
|
hasRelated: note.relatedTo && note.relatedTo.length > 0,
|
|
@@ -3740,6 +3817,8 @@ const RELATIONSHIP_TYPES = [
|
|
|
3740
3817
|
"explains",
|
|
3741
3818
|
"example-of",
|
|
3742
3819
|
"supersedes",
|
|
3820
|
+
"derives-from",
|
|
3821
|
+
"follows",
|
|
3743
3822
|
];
|
|
3744
3823
|
server.registerTool("relate", {
|
|
3745
3824
|
title: "Relate Memories",
|
|
@@ -3765,7 +3844,7 @@ server.registerTool("relate", {
|
|
|
3765
3844
|
inputSchema: z.object({
|
|
3766
3845
|
fromId: z.string().describe("Source memory id"),
|
|
3767
3846
|
toId: z.string().describe("Target memory id"),
|
|
3768
|
-
type: z.enum(RELATIONSHIP_TYPES).default("related-to").describe("Relationship type: 'related-to' (same topic), 'explains' (clarifies why), 'example-of' (instance of pattern), 'supersedes' (replaces)"),
|
|
3847
|
+
type: z.enum(RELATIONSHIP_TYPES).default("related-to").describe("Relationship type: 'related-to' (same topic), 'explains' (clarifies why), 'example-of' (instance of pattern), 'supersedes' (replaces), 'derives-from' (derived artifact), 'follows' (sequence order)"),
|
|
3769
3848
|
bidirectional: z.boolean().optional().default(true).describe("Add relationship in both directions (default: true)"),
|
|
3770
3849
|
cwd: projectParam,
|
|
3771
3850
|
}),
|
|
@@ -4937,7 +5016,7 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4937
5016
|
"- When unsure, prefer `recall` over `remember`.\n" +
|
|
4938
5017
|
"- For repo-related tasks, pass `cwd` so mnemonic can route project memories correctly.\n\n" +
|
|
4939
5018
|
"Workflow: `recall`/`list` -> `get` -> `update` or `remember` -> `relate`/`consolidate`/`move_memory`. Use `discover_tags` only when tag choice is ambiguous.\n\n" +
|
|
4940
|
-
"Roles are optional prioritization hints, not schema. Lifecycle still governs durability. `role: plan`
|
|
5019
|
+
"Roles are optional prioritization hints, not schema. Lifecycle still governs durability. When `lifecycle` is omitted, `remember` applies soft defaults based on role: `research`, `plan`, and `review` default to `temporary`; `decision`, `summary`, and `reference` default to `permanent`. Explicit `lifecycle` always overrides the role-based default. Inferred roles are internal hints only. Prioritization is language-independent by default.\n\n" +
|
|
4941
5020
|
"### Working-state continuity\n\n" +
|
|
4942
5021
|
"Preserve in-progress work as temporary notes when continuation value is high. Recovery happens after project orientation.\n\n" +
|
|
4943
5022
|
"**Checkpoint note structure (temporary notes):**\n" +
|
|
@@ -4982,6 +5061,66 @@ server.registerPrompt("mnemonic-workflow-hint", {
|
|
|
4982
5061
|
},
|
|
4983
5062
|
],
|
|
4984
5063
|
}));
|
|
5064
|
+
// ── mnemonic-rpi-workflow prompt ───────────────────────────────────────────────
|
|
5065
|
+
const rpiWorkflowPrompt = async () => ({
|
|
5066
|
+
messages: [
|
|
5067
|
+
{
|
|
5068
|
+
role: "user",
|
|
5069
|
+
content: {
|
|
5070
|
+
type: "text",
|
|
5071
|
+
text: "## RPIR workflow: research → plan → implement → review\n\n" +
|
|
5072
|
+
"mnemonic is the artifact store, not the runtime. Store workflow artifacts with correct roles and lifecycle; do not build orchestration in core.\n\n" +
|
|
5073
|
+
"### Request root note\n\n" +
|
|
5074
|
+
"For each RPIR workflow, create one request root note: `role: context`, `lifecycle: temporary`, `tags: [\"workflow\", \"request\"]`. All artifacts relate to it.\n\n" +
|
|
5075
|
+
"### Stage 1 — Research\n\n" +
|
|
5076
|
+
"- Create or update request root note.\n" +
|
|
5077
|
+
"- Create research notes: `role: research`, `lifecycle: temporary`.\n" +
|
|
5078
|
+
"- Distill a short research summary when findings are scattered.\n" +
|
|
5079
|
+
"- Link research `related-to` request root.\n" +
|
|
5080
|
+
"- Before creating research notes, call `recall` to check whether existing notes already cover the topic.\n\n" +
|
|
5081
|
+
"### Stage 2 — Plan\n\n" +
|
|
5082
|
+
"- Create or update one plan note: `role: plan`, `lifecycle: temporary`.\n" +
|
|
5083
|
+
"- Link plan `related-to` request root + key research notes.\n" +
|
|
5084
|
+
"- Keep plan concise and executable.\n" +
|
|
5085
|
+
"- REQUIRES: One current plan per request. Update or supersede when plan evolves.\n" +
|
|
5086
|
+
"- Material changes (architecture, scope, ordering, validation, assumptions): update plan note first, then continue.\n" +
|
|
5087
|
+
"- Non-material changes (wording, phrasing, detail): update inline without branching.\n\n" +
|
|
5088
|
+
"### Stage 3 — Implement\n\n" +
|
|
5089
|
+
"- Create temporary apply/task notes, tagged with `apply`.\n" +
|
|
5090
|
+
"- Use `role: plan` for executable steps. Use `role: context` for observations and checkpoints.\n" +
|
|
5091
|
+
"- Link apply notes `related-to` plan.\n" +
|
|
5092
|
+
"- For non-trivial work, hand narrow context to subagent: request note, current plan or relevant slice, key research notes, narrow file/task scope.\n" +
|
|
5093
|
+
"- Subagent returns: updated apply note, optional review note, recommendation (continue / block / update plan).\n\n" +
|
|
5094
|
+
"### Stage 4 — Review\n\n" +
|
|
5095
|
+
"- Create review notes: `role: review`, `lifecycle: temporary`.\n" +
|
|
5096
|
+
"- Link review `related-to` apply or plan.\n" +
|
|
5097
|
+
"- Fix directly or mark blockers.\n" +
|
|
5098
|
+
"- If review changes the plan materially, update plan note first.\n\n" +
|
|
5099
|
+
"### Stage 5 — Consolidate\n\n" +
|
|
5100
|
+
"At workflow end:\n" +
|
|
5101
|
+
"- Create decision note for resolved approaches (`lifecycle: permanent`).\n" +
|
|
5102
|
+
"- Create summary note for outcome recaps (`lifecycle: permanent`).\n" +
|
|
5103
|
+
"- Promote reusable facts and patterns into permanent reference notes.\n" +
|
|
5104
|
+
"- Let pure scaffolding and redundant checkpoints expire as temporary notes.\n\n" +
|
|
5105
|
+
"### Relationship conventions\n\n" +
|
|
5106
|
+
"Minimal set. Link to immediate upstream artifacts only. No dense cross-linking.\n" +
|
|
5107
|
+
"- research → request root\n" +
|
|
5108
|
+
"- plan → request root + key research notes\n" +
|
|
5109
|
+
"- apply/task → plan\n" +
|
|
5110
|
+
"- review → apply or plan\n" +
|
|
5111
|
+
"- outcome → plan (optionally request root)\n\n" +
|
|
5112
|
+
"### Commit discipline\n\n" +
|
|
5113
|
+
"Three classes: memory (research/plan/review artifacts), work (code/test/docs), memory (consolidation/promotion). When plan changes materially: update notes, commit memory, then continue work.\n\n" +
|
|
5114
|
+
"### Iterate?\n\n" +
|
|
5115
|
+
"Only when review or checks warrant it. Not the default.",
|
|
5116
|
+
},
|
|
5117
|
+
},
|
|
5118
|
+
],
|
|
5119
|
+
});
|
|
5120
|
+
server.registerPrompt("mnemonic-rpi-workflow", {
|
|
5121
|
+
title: "RPI Workflow: Research → Plan → Implement → Review",
|
|
5122
|
+
description: "Stage protocol and conventions for structured task workflows using mnemonic as artifact store.",
|
|
5123
|
+
}, rpiWorkflowPrompt);
|
|
4985
5124
|
// ── start ─────────────────────────────────────────────────────────────────────
|
|
4986
5125
|
await warnAboutPendingMigrationsOnStartup();
|
|
4987
5126
|
const transport = new StdioServerTransport();
|