@danielmarbach/mnemonic-mcp 0.13.1 → 0.14.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/build/index.js CHANGED
@@ -7,12 +7,13 @@ import path from "path";
7
7
  import { promises as fs } from "fs";
8
8
  import { NOTE_LIFECYCLES } from "./storage.js";
9
9
  import { embed, cosineSimilarity, embedModel } from "./embeddings.js";
10
+ import { buildTemporalHistoryEntry, computeConfidence, getNoteProvenance } from "./provenance.js";
10
11
  import { filterRelationships, mergeRelationshipsFromNotes, normalizeMergePlanSourceIds, resolveEffectiveConsolidationMode, } from "./consolidate.js";
11
12
  import { selectRecallResults } from "./recall.js";
12
13
  import { cleanMarkdown } from "./markdown.js";
13
14
  import { MnemonicConfigStore, readVaultSchemaVersion } from "./config.js";
14
15
  import { CONSOLIDATION_MODES, PROTECTED_BRANCH_BEHAVIORS, PROJECT_POLICY_SCOPES, WRITE_SCOPES, isProtectedBranch, resolveProtectedBranchBehavior, resolveProtectedBranchPatterns, resolveConsolidationMode, resolveWriteScope, } from "./project-memory-policy.js";
15
- import { classifyTheme, summarizePreview, titleCaseTheme } from "./project-introspection.js";
16
+ import { classifyTheme, summarizePreview, titleCaseTheme, withinThemeScore, anchorScore, buildThemeCache, computeConnectionDiversity, } from "./project-introspection.js";
16
17
  import { detectProject, getCurrentGitBranch, resolveProjectIdentity } from "./project.js";
17
18
  import { VaultManager } from "./vault.js";
18
19
  import { checkBranchChange } from "./branch-tracker.js";
@@ -276,6 +277,8 @@ const VAULT_PATH = process.env["VAULT_PATH"]
276
277
  : defaultVaultPath();
277
278
  const DEFAULT_RECALL_LIMIT = 5;
278
279
  const DEFAULT_MIN_SIMILARITY = 0.3;
280
+ const TEMPORAL_HISTORY_NOTE_LIMIT = 5;
281
+ const TEMPORAL_HISTORY_COMMIT_LIMIT = 5;
279
282
  async function readPackageVersion() {
280
283
  const packageJsonPath = path.resolve(import.meta.dirname, "../package.json");
281
284
  const packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
@@ -389,6 +392,17 @@ function formatNote(note, score) {
389
392
  `**tags:** ${note.tags.join(", ") || "none"} | **${describeLifecycle(note.lifecycle)}** | **updated:** ${note.updatedAt}${relStr}\n\n` +
390
393
  note.content);
391
394
  }
395
+ function formatTemporalHistory(history) {
396
+ if (history.length === 0) {
397
+ return "**history:** no git history found";
398
+ }
399
+ const lines = ["**history:**"];
400
+ for (const entry of history) {
401
+ const summary = entry.summary ? ` — ${entry.summary}` : "";
402
+ lines.push(`- \`${entry.commitHash.slice(0, 7)}\` ${entry.timestamp} — ${entry.message}${summary}`);
403
+ }
404
+ return lines.join("\n");
405
+ }
392
406
  // ── Git commit message helpers ────────────────────────────────────────────────
393
407
  /**
394
408
  * Extract a short human-readable summary from note content.
@@ -1594,15 +1608,18 @@ server.registerTool("get_project_memory_policy", {
1594
1608
  server.registerTool("recall", {
1595
1609
  title: "Recall",
1596
1610
  description: "Semantic search over stored memories using embeddings.\n\n" +
1611
+ "Supports opt-in temporal mode (`mode: \"temporal\"`) to enrich top semantic matches with compact git-backed history.\n\n" +
1597
1612
  "Use this when:\n" +
1598
1613
  "- You know the topic but not the exact memory id\n" +
1599
1614
  "- You are starting a session and want relevant prior context\n" +
1600
- "- You want to check whether a memory already exists before creating another one\n\n" +
1615
+ "- You want to check whether a memory already exists before creating another one\n" +
1616
+ "- You explicitly want to inspect how a note evolved over time\n\n" +
1601
1617
  "Do not use this when:\n" +
1602
1618
  "- You already know the exact id; use `get`\n" +
1603
1619
  "- You just want to browse by tags or scope; use `list`\n\n" +
1604
1620
  "Returns:\n" +
1605
- "- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n\n" +
1621
+ "- Ranked memory matches with scores, vault label, tags, lifecycle, and updated time\n" +
1622
+ "- In temporal mode, optional compact history entries for top matches\n\n" +
1606
1623
  "Read-only.\n\n" +
1607
1624
  "Typical next step:\n" +
1608
1625
  "- Use `get`, `update`, `relate`, or `consolidate` based on the results.",
@@ -1616,6 +1633,8 @@ server.registerTool("recall", {
1616
1633
  cwd: projectParam,
1617
1634
  limit: z.number().int().min(1).max(20).optional().default(DEFAULT_RECALL_LIMIT),
1618
1635
  minSimilarity: z.number().min(0).max(1).optional().default(DEFAULT_MIN_SIMILARITY),
1636
+ mode: z.enum(["default", "temporal"]).optional().default("default").describe("Temporal history is opt-in. Use `temporal` to enrich top semantic matches with compact git-backed history."),
1637
+ verbose: z.boolean().optional().default(false).describe("Only meaningful with `mode: \"temporal\"`. Adds richer stats-based history context without returning raw diffs."),
1619
1638
  tags: z.array(z.string()).optional().describe("Filter results to notes with all of these tags."),
1620
1639
  scope: z
1621
1640
  .enum(["project", "global", "all"])
@@ -1626,7 +1645,7 @@ server.registerTool("recall", {
1626
1645
  "'all' = both, with project notes boosted (default)"),
1627
1646
  }),
1628
1647
  outputSchema: RecallResultSchema,
1629
- }, async ({ query, cwd, limit, minSimilarity, tags, scope }) => {
1648
+ }, async ({ query, cwd, limit, minSimilarity, mode, verbose, tags, scope }) => {
1630
1649
  await ensureBranchSynced(cwd);
1631
1650
  const project = await resolveProject(cwd);
1632
1651
  const queryVec = await embed(query);
@@ -1687,10 +1706,27 @@ server.registerTool("recall", {
1687
1706
  : `Recall results (global):`;
1688
1707
  const sections = [];
1689
1708
  const structuredResults = [];
1690
- for (const { id, score, vault, boosted } of top) {
1709
+ for (const [index, { id, score, vault, boosted }] of top.entries()) {
1691
1710
  const note = await readCachedNote(vault, id);
1692
1711
  if (note) {
1693
- sections.push(formatNote(note, score));
1712
+ const centrality = note.relatedTo?.length ?? 0;
1713
+ const filePath = `${vault.notesRelDir}/${id}.md`;
1714
+ const provenance = await getNoteProvenance(vault.git, filePath);
1715
+ const confidence = computeConfidence(note.lifecycle, note.updatedAt, centrality);
1716
+ let history;
1717
+ if (mode === "temporal") {
1718
+ if (index < TEMPORAL_HISTORY_NOTE_LIMIT) {
1719
+ const commits = await vault.git.getFileHistory(filePath, TEMPORAL_HISTORY_COMMIT_LIMIT);
1720
+ history = await Promise.all(commits.map(async (commit) => {
1721
+ const stats = await vault.git.getCommitStats(filePath, commit.hash);
1722
+ return buildTemporalHistoryEntry(commit, stats, verbose);
1723
+ }));
1724
+ }
1725
+ }
1726
+ const formattedHistory = mode === "temporal" && history !== undefined
1727
+ ? `\n\n${formatTemporalHistory(history)}`
1728
+ : "";
1729
+ sections.push(`${formatNote(note, score)}${formattedHistory}`);
1694
1730
  structuredResults.push({
1695
1731
  id,
1696
1732
  title: note.title,
@@ -1702,6 +1738,9 @@ server.registerTool("recall", {
1702
1738
  tags: note.tags,
1703
1739
  lifecycle: note.lifecycle,
1704
1740
  updatedAt: note.updatedAt,
1741
+ provenance,
1742
+ confidence,
1743
+ history,
1705
1744
  });
1706
1745
  }
1707
1746
  }
@@ -2461,77 +2500,298 @@ server.registerTool("project_memory_summary", {
2461
2500
  cwd: z.string().describe("Absolute project working directory. Pass this whenever the task is project-related so routing, search boosting, policy, and vault selection work correctly."),
2462
2501
  maxPerTheme: z.number().int().min(1).max(5).optional().default(3),
2463
2502
  recentLimit: z.number().int().min(1).max(10).optional().default(5),
2503
+ anchorLimit: z.number().int().min(1).max(10).optional().default(5),
2504
+ includeRelatedGlobal: z.boolean().optional().default(false),
2505
+ relatedGlobalLimit: z.number().int().min(1).max(5).optional().default(3),
2464
2506
  }),
2465
2507
  outputSchema: ProjectSummaryResultSchema,
2466
- }, async ({ cwd, maxPerTheme, recentLimit }) => {
2508
+ }, async ({ cwd, maxPerTheme, recentLimit, anchorLimit, includeRelatedGlobal, relatedGlobalLimit }) => {
2467
2509
  await ensureBranchSynced(cwd);
2468
2510
  const { project, entries } = await collectVisibleNotes(cwd, "all");
2469
2511
  if (!project) {
2470
2512
  return { content: [{ type: "text", text: `Could not detect a project for: ${cwd}` }], isError: true };
2471
2513
  }
2472
- if (entries.length === 0) {
2473
- const structuredContent = { action: "project_summary_shown", project: { id: project.id, name: project.name }, notes: { total: 0, projectVault: 0, mainVault: 0, privateProject: 0 }, themes: {}, recent: [] };
2514
+ // Separate project-scoped notes (for themes/anchors) from global notes
2515
+ const projectEntries = entries.filter(e => e.note.project === project.id || e.vault.isProject);
2516
+ // Empty-project case: no project-scoped notes exist
2517
+ if (projectEntries.length === 0) {
2518
+ const structuredContent = {
2519
+ action: "project_summary_shown",
2520
+ project: { id: project.id, name: project.name },
2521
+ notes: { total: 0, projectVault: 0, mainVault: 0, privateProject: 0 },
2522
+ themes: {},
2523
+ recent: [],
2524
+ anchors: [],
2525
+ orientation: {
2526
+ primaryEntry: { id: "", title: "No notes", rationale: "Empty project vault" },
2527
+ suggestedNext: [],
2528
+ },
2529
+ };
2474
2530
  return { content: [{ type: "text", text: `No memories found for project ${project.name}.` }], structuredContent };
2475
2531
  }
2476
2532
  const policyLine = await formatProjectPolicyLine(project.id);
2533
+ // Build theme cache for connection diversity scoring (project-scoped only)
2534
+ const themeCache = buildThemeCache(projectEntries.map(e => e.note));
2535
+ // Categorize by theme (project-scoped only)
2477
2536
  const themed = new Map();
2478
- for (const entry of entries) {
2537
+ for (const entry of projectEntries) {
2479
2538
  const theme = classifyTheme(entry.note);
2480
2539
  const bucket = themed.get(theme) ?? [];
2481
2540
  bucket.push(entry);
2482
2541
  themed.set(theme, bucket);
2483
2542
  }
2543
+ // Theme order for display
2484
2544
  const themeOrder = ["overview", "decisions", "tooling", "bugs", "architecture", "quality", "other"];
2485
- const projectVaultCount = entries.filter((entry) => entry.vault.isProject).length;
2486
- const mainVaultCount = entries.length - projectVaultCount;
2545
+ // Calculate notes distribution (project-scoped only)
2546
+ const projectVaultCount = projectEntries.filter(e => e.vault.isProject).length;
2547
+ const mainVaultProjectEntries = projectEntries.filter(e => !e.vault.isProject);
2548
+ const mainVaultCount = mainVaultProjectEntries.length;
2549
+ const totalProjectNotes = projectEntries.length;
2550
+ // Build output sections
2487
2551
  const sections = [];
2488
2552
  sections.push(`Project summary: **${project.name}**`);
2489
2553
  sections.push(`- id: \`${project.id}\``);
2490
2554
  sections.push(`- ${policyLine.replace(/^Policy:\s*/, "policy: ")}`);
2491
- sections.push(`- memories: ${entries.length} (project-vault: ${projectVaultCount}, main-vault: ${mainVaultCount})`);
2492
- const mainVaultProjectEntries = entries.filter((entry) => !entry.vault.isProject && entry.note.project === project.id);
2555
+ sections.push(`- memories: ${totalProjectNotes} (project-vault: ${projectVaultCount}, main-vault: ${mainVaultCount})`);
2493
2556
  if (mainVaultProjectEntries.length > 0) {
2494
2557
  sections.push(`- private project memories: ${mainVaultProjectEntries.length}`);
2495
2558
  }
2496
- const themes = [];
2559
+ const themes = {};
2497
2560
  for (const theme of themeOrder) {
2498
2561
  const bucket = themed.get(theme);
2499
- if (!bucket || bucket.length === 0) {
2562
+ if (!bucket || bucket.length === 0)
2500
2563
  continue;
2501
- }
2502
- const top = bucket.slice(0, maxPerTheme);
2564
+ // Sort by within-theme score
2565
+ const sorted = [...bucket].sort((a, b) => withinThemeScore(b.note) - withinThemeScore(a.note));
2566
+ const top = sorted.slice(0, maxPerTheme);
2503
2567
  sections.push(`\n${titleCaseTheme(theme)}:`);
2504
- sections.push(...top.map((entry) => `- ${entry.note.title} (\`${entry.note.id}\`)`));
2505
- themes.push({
2506
- name: theme,
2568
+ sections.push(...top.map(e => `- ${e.note.title} (\`${e.note.id}\`)`));
2569
+ themes[theme] = {
2507
2570
  count: bucket.length,
2508
- examples: top.map((entry) => entry.note.title),
2509
- });
2571
+ examples: top.map(e => ({
2572
+ id: e.note.id,
2573
+ title: e.note.title,
2574
+ updatedAt: e.note.updatedAt,
2575
+ })),
2576
+ };
2510
2577
  }
2511
- const recent = [...entries]
2578
+ // Recent notes (project-scoped only)
2579
+ const recent = [...projectEntries]
2512
2580
  .sort((a, b) => b.note.updatedAt.localeCompare(a.note.updatedAt))
2513
2581
  .slice(0, recentLimit);
2514
- sections.push(`\nRecent:`);
2515
- sections.push(...recent.map((entry) => `- ${entry.note.updatedAt} — ${entry.note.title}`));
2516
- const themeCounts = {};
2517
- for (const theme of themes) {
2518
- themeCounts[theme.name] = theme.count;
2582
+ sections.push(`\nRecent activity (start here):`);
2583
+ sections.push(...recent.map(e => `- ${e.note.updatedAt} — ${e.note.title}`));
2584
+ // Anchor notes with diversity constraint (project-scoped only)
2585
+ // Separate tagged anchors (can have no relationships) from scored anchors (need relationships)
2586
+ const taggedAnchorEntries = projectEntries.filter(e => e.note.lifecycle === "permanent" &&
2587
+ e.note.tags.some(t => t.toLowerCase() === "anchor" || t.toLowerCase() === "alwaysload"));
2588
+ const scoredAnchorCandidates = projectEntries
2589
+ .filter(e => e.note.lifecycle === "permanent" && (e.note.relatedTo?.length ?? 0) > 0)
2590
+ .map(e => ({
2591
+ entry: e,
2592
+ score: anchorScore(e.note, themeCache),
2593
+ theme: classifyTheme(e.note),
2594
+ }))
2595
+ .filter(x => x.score > -Infinity)
2596
+ .sort((a, b) => b.score - a.score);
2597
+ // Score tagged anchors too, so they can compete for primaryEntry
2598
+ const scoredTaggedAnchors = taggedAnchorEntries
2599
+ .map(e => ({
2600
+ entry: e,
2601
+ score: anchorScore(e.note, themeCache),
2602
+ theme: classifyTheme(e.note),
2603
+ }))
2604
+ .sort((a, b) => b.score - a.score);
2605
+ // Enforce max 2 per theme for scored anchors
2606
+ const anchorThemeCounts = new Map();
2607
+ const anchors = [];
2608
+ const anchorIds = new Set();
2609
+ // Add tagged anchors first (capped at 10 total across all themes), scored by anchorScore
2610
+ for (const candidate of scoredTaggedAnchors.slice(0, 10)) {
2611
+ if (anchors.length >= 10)
2612
+ break;
2613
+ anchors.push({
2614
+ id: candidate.entry.note.id,
2615
+ title: candidate.entry.note.title,
2616
+ centrality: candidate.entry.note.relatedTo?.length ?? 0,
2617
+ connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
2618
+ updatedAt: candidate.entry.note.updatedAt,
2619
+ });
2620
+ anchorIds.add(candidate.entry.note.id);
2621
+ }
2622
+ // Add scored anchors with theme diversity constraint
2623
+ for (const candidate of scoredAnchorCandidates) {
2624
+ if (anchors.length >= 10)
2625
+ break;
2626
+ if (anchorIds.has(candidate.entry.note.id))
2627
+ continue;
2628
+ const theme = candidate.theme;
2629
+ const themeCount = anchorThemeCounts.get(theme) ?? 0;
2630
+ if (themeCount >= 2)
2631
+ continue;
2632
+ anchors.push({
2633
+ id: candidate.entry.note.id,
2634
+ title: candidate.entry.note.title,
2635
+ centrality: candidate.entry.note.relatedTo?.length ?? 0,
2636
+ connectionDiversity: computeConnectionDiversity(candidate.entry.note, themeCache),
2637
+ updatedAt: candidate.entry.note.updatedAt,
2638
+ });
2639
+ anchorIds.add(candidate.entry.note.id);
2640
+ anchorThemeCounts.set(theme, themeCount + 1);
2519
2641
  }
2642
+ if (anchors.length > 0) {
2643
+ sections.push(`\nAnchors:`);
2644
+ sections.push(...anchors.slice(0, 5).map(a => `- ${a.title} (\`${a.id}\`) — centrality: ${a.centrality}, diversity: ${a.connectionDiversity}`));
2645
+ }
2646
+ // Compute orientation after anchors are computed (for text output)
2647
+ let relatedGlobal;
2648
+ if (includeRelatedGlobal) {
2649
+ const anchorEmbeddings = await Promise.all(anchors.slice(0, 5).map(async (a) => {
2650
+ for (const vault of vaultManager.allKnownVaults()) {
2651
+ const emb = await vault.storage.readEmbedding(a.id);
2652
+ if (emb)
2653
+ return { id: a.id, embedding: emb.embedding };
2654
+ }
2655
+ return null;
2656
+ }));
2657
+ const validAnchors = anchorEmbeddings.filter((e) => e !== null);
2658
+ if (validAnchors.length > 0) {
2659
+ // Get global notes (not project-scoped)
2660
+ const globalEntries = entries.filter(e => !e.note.project);
2661
+ const globalCandidates = [];
2662
+ for (const entry of globalEntries) {
2663
+ const emb = await entry.vault.storage.readEmbedding(entry.note.id);
2664
+ if (!emb)
2665
+ continue;
2666
+ // Find max similarity to any anchor
2667
+ let maxSim = 0;
2668
+ for (const anchor of validAnchors) {
2669
+ const sim = cosineSimilarity(anchor.embedding, emb.embedding);
2670
+ if (sim > maxSim)
2671
+ maxSim = sim;
2672
+ }
2673
+ if (maxSim > 0.4) {
2674
+ globalCandidates.push({
2675
+ id: entry.note.id,
2676
+ title: entry.note.title,
2677
+ similarity: maxSim,
2678
+ preview: summarizePreview(entry.note.content, 100),
2679
+ });
2680
+ }
2681
+ }
2682
+ globalCandidates.sort((a, b) => b.similarity - a.similarity);
2683
+ if (globalCandidates.length > 0) {
2684
+ relatedGlobal = {
2685
+ notes: globalCandidates.slice(0, relatedGlobalLimit),
2686
+ computedAt: new Date().toISOString(),
2687
+ };
2688
+ sections.push(`\nRelated Global:`);
2689
+ sections.push(...relatedGlobal.notes.map(n => `- ${n.title} (\`${n.id}\`) — similarity: ${n.similarity.toFixed(2)}`));
2690
+ }
2691
+ }
2692
+ }
2693
+ // Compute orientation layer for actionable guidance
2694
+ const primaryAnchor = anchors[0];
2695
+ // Build noteId -> vault lookup for provenance enrichment
2696
+ const noteVaultMap = new Map();
2697
+ for (const entry of projectEntries) {
2698
+ noteVaultMap.set(entry.note.id, entry.vault);
2699
+ }
2700
+ // Helper to enrich an anchor with provenance and confidence
2701
+ const enrichOrientationNote = async (anchor) => {
2702
+ const vault = noteVaultMap.get(anchor.id);
2703
+ if (!vault)
2704
+ return {};
2705
+ const filePath = `${vault.notesRelDir}/${anchor.id}.md`;
2706
+ const provenance = await getNoteProvenance(vault.git, filePath);
2707
+ const confidence = computeConfidence("permanent", anchor.updatedAt, anchor.centrality);
2708
+ return { provenance, confidence };
2709
+ };
2710
+ const primaryEnriched = primaryAnchor ? await enrichOrientationNote(primaryAnchor) : {};
2711
+ const suggestedEnriched = await Promise.all(anchors.slice(1, 4).map(enrichOrientationNote));
2712
+ // Enrich fallback primaryEntry when no anchors exist
2713
+ let fallbackEnriched = {};
2714
+ if (!primaryAnchor && recent[0]) {
2715
+ const fallbackNote = recent[0].note;
2716
+ const vault = noteVaultMap.get(fallbackNote.id);
2717
+ if (vault) {
2718
+ const filePath = `${vault.notesRelDir}/${fallbackNote.id}.md`;
2719
+ const provenance = await getNoteProvenance(vault.git, filePath);
2720
+ const confidence = computeConfidence(fallbackNote.lifecycle, fallbackNote.updatedAt, 0);
2721
+ fallbackEnriched = { provenance, confidence };
2722
+ }
2723
+ }
2724
+ const orientation = {
2725
+ primaryEntry: primaryAnchor
2726
+ ? {
2727
+ id: primaryAnchor.id,
2728
+ title: primaryAnchor.title,
2729
+ rationale: `Centrality ${primaryAnchor.centrality}, connects ${primaryAnchor.connectionDiversity} themes`,
2730
+ ...primaryEnriched,
2731
+ }
2732
+ : {
2733
+ id: recent[0]?.note.id ?? projectEntries[0]?.note.id ?? "",
2734
+ title: recent[0]?.note.title ?? projectEntries[0]?.note.title ?? "No notes",
2735
+ rationale: recent[0]
2736
+ ? "Most recent note — no high-centrality anchors found"
2737
+ : "Only note available",
2738
+ ...fallbackEnriched,
2739
+ },
2740
+ suggestedNext: anchors.slice(1, 4).map((a, i) => ({
2741
+ id: a.id,
2742
+ title: a.title,
2743
+ rationale: `Centrality ${a.centrality}, connects ${a.connectionDiversity} themes`,
2744
+ ...suggestedEnriched[i],
2745
+ })),
2746
+ };
2747
+ // Warning for taxonomy dilution
2748
+ const otherBucket = themed.get("other");
2749
+ const otherCount = otherBucket?.length ?? 0;
2750
+ const otherRatio = projectEntries.length > 0 ? otherCount / projectEntries.length : 0;
2751
+ if (otherRatio > 0.3) {
2752
+ orientation.warnings = [
2753
+ `${Math.round(otherRatio * 100)}% of notes in "other" bucket — consider improving thematic classification`,
2754
+ ];
2755
+ }
2756
+ // Orientation text output
2757
+ sections.push(`\nOrientation:`);
2758
+ sections.push(`Start with: ${orientation.primaryEntry.title} (\`${orientation.primaryEntry.id}\`)`);
2759
+ sections.push(` Rationale: ${orientation.primaryEntry.rationale}`);
2760
+ if (orientation.primaryEntry.confidence) {
2761
+ sections.push(` Confidence: ${orientation.primaryEntry.confidence}`);
2762
+ }
2763
+ if (orientation.suggestedNext.length > 0) {
2764
+ sections.push(`Then check:`);
2765
+ for (const next of orientation.suggestedNext) {
2766
+ sections.push(` - ${next.title} (\`${next.id}\`) — ${next.rationale}${next.confidence ? ` [${next.confidence}]` : ""}`);
2767
+ }
2768
+ }
2769
+ if (orientation.warnings && orientation.warnings.length > 0) {
2770
+ sections.push(`Warnings:`);
2771
+ for (const w of orientation.warnings) {
2772
+ sections.push(` - ${w}`);
2773
+ }
2774
+ }
2775
+ // Related global notes (optional, anchor-based similarity)
2520
2776
  const structuredContent = {
2521
2777
  action: "project_summary_shown",
2522
2778
  project: { id: project.id, name: project.name },
2523
2779
  notes: {
2524
- total: entries.length,
2780
+ total: totalProjectNotes,
2525
2781
  projectVault: projectVaultCount,
2526
2782
  mainVault: mainVaultCount,
2527
2783
  privateProject: mainVaultProjectEntries.length,
2528
2784
  },
2529
- themes: themeCounts,
2530
- recent: recent.map((entry) => ({
2531
- id: entry.note.id,
2532
- title: entry.note.title,
2533
- updatedAt: entry.note.updatedAt,
2785
+ themes,
2786
+ recent: recent.map(e => ({
2787
+ id: e.note.id,
2788
+ title: e.note.title,
2789
+ updatedAt: e.note.updatedAt,
2790
+ theme: classifyTheme(e.note),
2534
2791
  })),
2792
+ anchors,
2793
+ orientation,
2794
+ relatedGlobal,
2535
2795
  };
2536
2796
  return { content: [{ type: "text", text: sections.join("\n") }], structuredContent };
2537
2797
  });