@danielmarbach/mnemonic-mcp 0.17.0 → 0.18.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 +17 -0
- package/README.md +12 -0
- package/build/index.js +99 -14
- package/build/index.js.map +1 -1
- package/build/project-introspection.d.ts +12 -0
- package/build/project-introspection.d.ts.map +1 -1
- package/build/project-introspection.js +119 -0
- package/build/project-introspection.js.map +1 -1
- package/build/structured-content.d.ts +257 -11
- package/build/structured-content.d.ts.map +1 -1
- package/build/structured-content.js +17 -1
- package/build/structured-content.js.map +1 -1
- package/build/theme-validation.d.ts +14 -0
- package/build/theme-validation.d.ts.map +1 -0
- package/build/theme-validation.js +46 -0
- package/build/theme-validation.js.map +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ 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.18.0] - 2026-03-27
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- Theme graduation from "other": keywords appearing across multiple notes become named themes automatically.
|
|
12
|
+
- Keyword extraction with synonym normalization for theme classification.
|
|
13
|
+
- `analyzeThemeQuality()` utility for checking theme distribution metrics.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- All 23 MCP tools now include complete SDK tool hints (`readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`). Previously, read-only tools were missing `destructiveHint: false`.
|
|
18
|
+
- Mutating tool retry results now return explicit recovery guidance instead of vague `Retry: safe` text, including recovery kind and no-inference instructions for partial-persistence failures.
|
|
19
|
+
- Recovery precedence is now encoded in retry metadata: deterministic tool reconciliation is preferred where available, and same-vault retries must be replayed serially.
|
|
20
|
+
- Retry metadata now uses `attemptedCommit.subject` instead of `attemptedCommit.message`.
|
|
21
|
+
- `classifyTheme` now uses keyword-based graduation in addition to tag matching.
|
|
22
|
+
- `project_memory_summary` includes both fixed themes and dynamically promoted keywords.
|
|
23
|
+
|
|
7
24
|
## [0.17.0] - 2026-03-25
|
|
8
25
|
|
|
9
26
|
### Added
|
package/README.md
CHANGED
|
@@ -452,6 +452,18 @@ Imported notes are written to the main vault with `lifecycle: permanent` and `sc
|
|
|
452
452
|
| `update` | Update note content/title/tags/lifecycle, re-embeds always |
|
|
453
453
|
| `where_is_memory` | Show note's project association and storage location |
|
|
454
454
|
|
|
455
|
+
### Theme emergence
|
|
456
|
+
|
|
457
|
+
`project_memory_summary` categorizes notes by theme. Themes **emerge automatically** from your notes:
|
|
458
|
+
|
|
459
|
+
- **Tag-based classification** — notes with matching tags (e.g., `["decisions"]`, `["bugs"]`) are grouped immediately
|
|
460
|
+
- **Keyword graduation** — keywords that appear across multiple notes become named themes over time
|
|
461
|
+
- **"other" bucket** — notes that don't match any theme are grouped here; this shrinks as themes emerge
|
|
462
|
+
|
|
463
|
+
No predefined schema required. The system adapts to your project's vocabulary.
|
|
464
|
+
|
|
465
|
+
**Language handling:** The system degrades gracefully for non-English notes. Stopwords and synonyms are optional English enhancements; keywords that don't match pass through unchanged, allowing non-English keywords to graduate if they meet frequency thresholds.
|
|
466
|
+
|
|
455
467
|
### Relationships
|
|
456
468
|
|
|
457
469
|
Notes can be linked with typed edges stored in frontmatter:
|
package/build/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { getRelationshipPreview } from "./relationships.js";
|
|
|
17
17
|
import { cleanMarkdown } from "./markdown.js";
|
|
18
18
|
import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
|
|
19
19
|
import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
|
|
20
|
-
import { classifyTheme, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
|
|
20
|
+
import { classifyTheme, classifyThemeWithGraduation, computeThemesWithGraduation, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
|
|
21
21
|
import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
|
|
22
22
|
import { VaultManager } from "./vault.js";
|
|
23
23
|
import { checkBranchChange } from "./branch-tracker.js";
|
|
@@ -693,9 +693,22 @@ function buildMutationRetryContract(args) {
|
|
|
693
693
|
if (args.commit.status !== "failed") {
|
|
694
694
|
return undefined;
|
|
695
695
|
}
|
|
696
|
+
const recoveryKind = args.preferredRecovery ?? (args.mutationApplied
|
|
697
|
+
? "manual-exact-git-recovery"
|
|
698
|
+
: "no-manual-recovery");
|
|
699
|
+
const recoveryReason = recoveryKind === "rerun-tool-call-serial"
|
|
700
|
+
? "Tool-level reconciliation exists for this mutation; rerun the same tool call serially for the affected vault."
|
|
701
|
+
: recoveryKind === "manual-exact-git-recovery"
|
|
702
|
+
? "Mutation is already persisted on disk; manual git recovery is allowed only with the exact attemptedCommit values."
|
|
703
|
+
: "Mutation was not applied deterministically; manual git recovery is not authorized.";
|
|
696
704
|
return {
|
|
705
|
+
recovery: {
|
|
706
|
+
kind: recoveryKind,
|
|
707
|
+
allowed: recoveryKind !== "no-manual-recovery",
|
|
708
|
+
reason: recoveryReason,
|
|
709
|
+
},
|
|
697
710
|
attemptedCommit: {
|
|
698
|
-
|
|
711
|
+
subject: args.commitMessage,
|
|
699
712
|
body: args.commitBody,
|
|
700
713
|
files: args.files,
|
|
701
714
|
cwd: args.cwd,
|
|
@@ -708,19 +721,69 @@ function buildMutationRetryContract(args) {
|
|
|
708
721
|
rationale: args.mutationApplied
|
|
709
722
|
? "Mutation is already persisted on disk; commit can be retried deterministically."
|
|
710
723
|
: "Mutation was not applied; retry may require re-running the operation.",
|
|
724
|
+
instructions: {
|
|
725
|
+
sourceOfTruth: recoveryKind === "manual-exact-git-recovery" ? "attemptedCommit" : "tool-response",
|
|
726
|
+
useExactSubject: recoveryKind === "manual-exact-git-recovery",
|
|
727
|
+
useExactBody: recoveryKind === "manual-exact-git-recovery",
|
|
728
|
+
useExactFiles: recoveryKind === "manual-exact-git-recovery",
|
|
729
|
+
forbidInferenceFromHistory: true,
|
|
730
|
+
forbidInferenceFromTitleOrSummary: true,
|
|
731
|
+
forbidParallelSameVaultRetries: true,
|
|
732
|
+
preferToolReconciliation: recoveryKind === "rerun-tool-call-serial",
|
|
733
|
+
rerunSameToolCallSerially: recoveryKind === "rerun-tool-call-serial",
|
|
734
|
+
},
|
|
711
735
|
};
|
|
712
736
|
}
|
|
713
737
|
function formatRetrySummary(retry) {
|
|
714
738
|
if (!retry) {
|
|
715
739
|
return undefined;
|
|
716
740
|
}
|
|
717
|
-
const safety = retry.retrySafe ? "safe" : "requires review";
|
|
718
741
|
const opLabel = retry.attemptedCommit.operation === "add" ? "add" : "commit";
|
|
719
742
|
const error = retry.attemptedCommit.error;
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
743
|
+
const lines = [];
|
|
744
|
+
switch (retry.recovery.kind) {
|
|
745
|
+
case "rerun-tool-call-serial":
|
|
746
|
+
lines.push("Recovery: rerun same tool call serially");
|
|
747
|
+
lines.push(retry.recovery.reason);
|
|
748
|
+
lines.push("Rerun the same mnemonic tool call one time for the affected vault.");
|
|
749
|
+
lines.push("Do not replay same-vault mutations in parallel.");
|
|
750
|
+
lines.push("Manual git recovery is not authorized for this failure.");
|
|
751
|
+
lines.push("Git failure:");
|
|
752
|
+
lines.push(`${opLabel}: ${error}`);
|
|
753
|
+
break;
|
|
754
|
+
case "manual-exact-git-recovery":
|
|
755
|
+
lines.push("Recovery: manual exact git recovery allowed");
|
|
756
|
+
lines.push(retry.recovery.reason);
|
|
757
|
+
lines.push("Use only the exact values below. Do not infer from git history, note title, summary, or repo state.");
|
|
758
|
+
lines.push("");
|
|
759
|
+
lines.push("Commit subject:");
|
|
760
|
+
lines.push(retry.attemptedCommit.subject);
|
|
761
|
+
if (retry.attemptedCommit.body) {
|
|
762
|
+
lines.push("");
|
|
763
|
+
lines.push("Commit body:");
|
|
764
|
+
lines.push(retry.attemptedCommit.body);
|
|
765
|
+
}
|
|
766
|
+
lines.push("");
|
|
767
|
+
lines.push("Files:");
|
|
768
|
+
for (const file of retry.attemptedCommit.files) {
|
|
769
|
+
lines.push(`- ${file}`);
|
|
770
|
+
}
|
|
771
|
+
lines.push("");
|
|
772
|
+
lines.push("Git failure:");
|
|
773
|
+
lines.push(`${opLabel}: ${error}`);
|
|
774
|
+
break;
|
|
775
|
+
case "no-manual-recovery":
|
|
776
|
+
lines.push("Recovery: no manual recovery authorized");
|
|
777
|
+
lines.push(retry.recovery.reason);
|
|
778
|
+
lines.push("Git failure:");
|
|
779
|
+
lines.push(`${opLabel}: ${error}`);
|
|
780
|
+
break;
|
|
781
|
+
default: {
|
|
782
|
+
const _exhaustive = retry.recovery.kind;
|
|
783
|
+
throw new Error(`Unknown recovery kind: ${_exhaustive}`);
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
return lines.join("\n");
|
|
724
787
|
}
|
|
725
788
|
function formatPersistenceSummary(persistence) {
|
|
726
789
|
const parts = [
|
|
@@ -731,14 +794,14 @@ function formatPersistenceSummary(persistence) {
|
|
|
731
794
|
if (persistence.embedding.reason) {
|
|
732
795
|
lines[0] += ` | embedding reason=${persistence.embedding.reason}`;
|
|
733
796
|
}
|
|
734
|
-
|
|
797
|
+
const retrySummary = formatRetrySummary(persistence.retry);
|
|
798
|
+
if (!retrySummary && persistence.git.commit === "failed" && persistence.git.commitError) {
|
|
735
799
|
const opLabel = persistence.git.commitOperation === "add" ? "add" : "commit";
|
|
736
800
|
lines.push(`Git ${opLabel} error: ${persistence.git.commitError}`);
|
|
737
801
|
}
|
|
738
802
|
if (persistence.git.push === "failed" && persistence.git.pushError) {
|
|
739
803
|
lines.push(`Git push error: ${persistence.git.pushError}`);
|
|
740
804
|
}
|
|
741
|
-
const retrySummary = formatRetrySummary(persistence.retry);
|
|
742
805
|
if (retrySummary) {
|
|
743
806
|
lines.push(retrySummary);
|
|
744
807
|
}
|
|
@@ -969,6 +1032,7 @@ server.registerTool("detect_project", {
|
|
|
969
1032
|
"- Use `recall` or `project_memory_summary` to orient on existing memory.",
|
|
970
1033
|
annotations: {
|
|
971
1034
|
readOnlyHint: true,
|
|
1035
|
+
destructiveHint: false,
|
|
972
1036
|
idempotentHint: true,
|
|
973
1037
|
openWorldHint: false,
|
|
974
1038
|
},
|
|
@@ -1023,6 +1087,7 @@ server.registerTool("get_project_identity", {
|
|
|
1023
1087
|
"- Use `set_project_identity` only if the wrong remote is defining identity.",
|
|
1024
1088
|
annotations: {
|
|
1025
1089
|
readOnlyHint: true,
|
|
1090
|
+
destructiveHint: false,
|
|
1026
1091
|
idempotentHint: true,
|
|
1027
1092
|
openWorldHint: false,
|
|
1028
1093
|
},
|
|
@@ -1179,6 +1244,7 @@ server.registerTool("list_migrations", {
|
|
|
1179
1244
|
"- Run `execute_migration` with `dryRun: true` first.",
|
|
1180
1245
|
annotations: {
|
|
1181
1246
|
readOnlyHint: true,
|
|
1247
|
+
destructiveHint: false,
|
|
1182
1248
|
idempotentHint: true,
|
|
1183
1249
|
openWorldHint: false,
|
|
1184
1250
|
},
|
|
@@ -1600,6 +1666,7 @@ server.registerTool("get_project_memory_policy", {
|
|
|
1600
1666
|
"- Call `remember` with explicit `scope` for a one-off override, or `set_project_memory_policy` to change defaults.",
|
|
1601
1667
|
annotations: {
|
|
1602
1668
|
readOnlyHint: true,
|
|
1669
|
+
destructiveHint: false,
|
|
1603
1670
|
idempotentHint: true,
|
|
1604
1671
|
openWorldHint: false,
|
|
1605
1672
|
},
|
|
@@ -1673,6 +1740,7 @@ server.registerTool("recall", {
|
|
|
1673
1740
|
"- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
|
|
1674
1741
|
annotations: {
|
|
1675
1742
|
readOnlyHint: true,
|
|
1743
|
+
destructiveHint: false,
|
|
1676
1744
|
idempotentHint: true,
|
|
1677
1745
|
openWorldHint: true,
|
|
1678
1746
|
},
|
|
@@ -2107,6 +2175,7 @@ server.registerTool("get", {
|
|
|
2107
2175
|
"- Use `update`, `forget`, `move_memory`, or `relate` after inspection.",
|
|
2108
2176
|
annotations: {
|
|
2109
2177
|
readOnlyHint: true,
|
|
2178
|
+
destructiveHint: false,
|
|
2110
2179
|
idempotentHint: true,
|
|
2111
2180
|
openWorldHint: false,
|
|
2112
2181
|
},
|
|
@@ -2204,6 +2273,7 @@ server.registerTool("where_is_memory", {
|
|
|
2204
2273
|
"- Use `move_memory` if the storage location is wrong, or `get` for full inspection.",
|
|
2205
2274
|
annotations: {
|
|
2206
2275
|
readOnlyHint: true,
|
|
2276
|
+
destructiveHint: false,
|
|
2207
2277
|
idempotentHint: true,
|
|
2208
2278
|
openWorldHint: false,
|
|
2209
2279
|
},
|
|
@@ -2260,6 +2330,7 @@ server.registerTool("list", {
|
|
|
2260
2330
|
"- Use `get` for exact inspection or `update` / `consolidate` for cleanup.",
|
|
2261
2331
|
annotations: {
|
|
2262
2332
|
readOnlyHint: true,
|
|
2333
|
+
destructiveHint: false,
|
|
2263
2334
|
idempotentHint: true,
|
|
2264
2335
|
openWorldHint: false,
|
|
2265
2336
|
},
|
|
@@ -2370,6 +2441,7 @@ server.registerTool("discover_tags", {
|
|
|
2370
2441
|
"Read-only.",
|
|
2371
2442
|
annotations: {
|
|
2372
2443
|
readOnlyHint: true,
|
|
2444
|
+
destructiveHint: false,
|
|
2373
2445
|
idempotentHint: true,
|
|
2374
2446
|
openWorldHint: false,
|
|
2375
2447
|
},
|
|
@@ -2576,6 +2648,7 @@ server.registerTool("recent_memories", {
|
|
|
2576
2648
|
"- Use `get` for exact inspection or `update` to continue refining a recent note.",
|
|
2577
2649
|
annotations: {
|
|
2578
2650
|
readOnlyHint: true,
|
|
2651
|
+
destructiveHint: false,
|
|
2579
2652
|
idempotentHint: true,
|
|
2580
2653
|
openWorldHint: false,
|
|
2581
2654
|
},
|
|
@@ -2645,6 +2718,7 @@ server.registerTool("memory_graph", {
|
|
|
2645
2718
|
"- Use `get`, `relate`, `unrelate`, or `consolidate` based on what the graph reveals.",
|
|
2646
2719
|
annotations: {
|
|
2647
2720
|
readOnlyHint: true,
|
|
2721
|
+
destructiveHint: false,
|
|
2648
2722
|
idempotentHint: true,
|
|
2649
2723
|
openWorldHint: false,
|
|
2650
2724
|
},
|
|
@@ -2724,6 +2798,7 @@ server.registerTool("project_memory_summary", {
|
|
|
2724
2798
|
"- Use `recall` or `list` to drill down into specific areas.",
|
|
2725
2799
|
annotations: {
|
|
2726
2800
|
readOnlyHint: true,
|
|
2801
|
+
destructiveHint: false,
|
|
2727
2802
|
idempotentHint: true,
|
|
2728
2803
|
openWorldHint: false,
|
|
2729
2804
|
},
|
|
@@ -2765,17 +2840,23 @@ server.registerTool("project_memory_summary", {
|
|
|
2765
2840
|
}
|
|
2766
2841
|
const policyLine = await formatProjectPolicyLine(project.id);
|
|
2767
2842
|
// Build theme cache for connection diversity scoring (project-scoped only)
|
|
2843
|
+
// This uses simple classifyTheme for consistent diversity calculations
|
|
2768
2844
|
const themeCache = buildThemeCache(projectEntries.map(e => e.note));
|
|
2769
|
-
//
|
|
2845
|
+
// Compute promoted themes from keywords (graduation system)
|
|
2846
|
+
const graduationResult = computeThemesWithGraduation(projectEntries.map(e => e.note));
|
|
2847
|
+
const promotedThemes = new Set(graduationResult.promotedThemes);
|
|
2848
|
+
// Categorize by theme with graduation (project-scoped only)
|
|
2770
2849
|
const themed = new Map();
|
|
2771
2850
|
for (const entry of projectEntries) {
|
|
2772
|
-
const theme =
|
|
2851
|
+
const theme = classifyThemeWithGraduation(entry.note, promotedThemes);
|
|
2773
2852
|
const bucket = themed.get(theme) ?? [];
|
|
2774
2853
|
bucket.push(entry);
|
|
2775
2854
|
themed.set(theme, bucket);
|
|
2776
2855
|
}
|
|
2777
|
-
// Theme order
|
|
2778
|
-
const
|
|
2856
|
+
// Theme order: fixed themes first, then promoted themes alphabetically, then "other"
|
|
2857
|
+
const fixedThemes = ["overview", "decisions", "tooling", "bugs", "architecture", "quality"];
|
|
2858
|
+
const dynamicThemes = graduationResult.promotedThemes.filter(t => !fixedThemes.includes(t));
|
|
2859
|
+
const themeOrder = [...fixedThemes, ...dynamicThemes.sort(), "other"];
|
|
2779
2860
|
// Calculate notes distribution (project-scoped only)
|
|
2780
2861
|
const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
|
|
2781
2862
|
const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
|
|
@@ -3051,7 +3132,7 @@ server.registerTool("project_memory_summary", {
|
|
|
3051
3132
|
id: e.note.id,
|
|
3052
3133
|
title: e.note.title,
|
|
3053
3134
|
updatedAt: e.note.updatedAt,
|
|
3054
|
-
theme:
|
|
3135
|
+
theme: classifyThemeWithGraduation(e.note, promotedThemes),
|
|
3055
3136
|
})),
|
|
3056
3137
|
anchors,
|
|
3057
3138
|
orientation,
|
|
@@ -3403,6 +3484,7 @@ server.registerTool("relate", {
|
|
|
3403
3484
|
cwd,
|
|
3404
3485
|
vault,
|
|
3405
3486
|
mutationApplied: true,
|
|
3487
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3406
3488
|
});
|
|
3407
3489
|
const structuredContent = {
|
|
3408
3490
|
action: "related",
|
|
@@ -3452,6 +3534,7 @@ server.registerTool("relate", {
|
|
|
3452
3534
|
cwd,
|
|
3453
3535
|
vault,
|
|
3454
3536
|
mutationApplied: true,
|
|
3537
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3455
3538
|
});
|
|
3456
3539
|
}
|
|
3457
3540
|
if (commitStatus.status === "committed") {
|
|
@@ -3574,6 +3657,7 @@ server.registerTool("unrelate", {
|
|
|
3574
3657
|
cwd,
|
|
3575
3658
|
vault,
|
|
3576
3659
|
mutationApplied: true,
|
|
3660
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3577
3661
|
});
|
|
3578
3662
|
const structuredContent = {
|
|
3579
3663
|
action: "unrelated",
|
|
@@ -3617,6 +3701,7 @@ server.registerTool("unrelate", {
|
|
|
3617
3701
|
cwd,
|
|
3618
3702
|
vault,
|
|
3619
3703
|
mutationApplied: true,
|
|
3704
|
+
preferredRecovery: "rerun-tool-call-serial",
|
|
3620
3705
|
});
|
|
3621
3706
|
}
|
|
3622
3707
|
if (commitStatus.status === "committed") {
|