linksee-memory 0.7.1 → 0.8.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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // linksee-memory MCP server (stdio transport).
3
- // Tools: remember / recall / read_smart
4
- // v0.7.0 — unified 3-tool surface (Context7-style simplification)
3
+ // Tools: remember / recall / read_smart / drift_status / check_decision / declare_anchor / resolve_drift
4
+ // v0.8.0 — drift MCP tools (P0: agents can now query & act on drift state)
5
5
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
6
6
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
7
7
  import { CallToolRequestSchema, ListToolsRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
@@ -19,7 +19,9 @@ import { PROMPTS, getPrompt } from './prompts.js';
19
19
  import { fetchRoots, isInsideRoots } from './roots.js';
20
20
  import { sampleConsolidation } from './sampling.js';
21
21
  import { confirmForget } from './elicitation.js';
22
- const SERVER_VERSION = '0.7.0';
22
+ import { getTruthView, getDecisionDetail, resolveDrift } from '../lib/truth-engine.js';
23
+ import { declareAnchor, setNodeFields } from '../lib/drift-anchors.js';
24
+ const SERVER_VERSION = '0.8.0';
23
25
  const db = openDb();
24
26
  runMigrations(db);
25
27
  // Auto-maintenance: consolidate stale memories on startup (non-blocking)
@@ -142,6 +144,68 @@ const TOOLS = [
142
144
  required: ['path'],
143
145
  },
144
146
  },
147
+ // ── Drift tools (v0.8.0) ─────────────────────────────────────────────────
148
+ {
149
+ name: 'drift_status',
150
+ description: 'Check what\'s drifting right now — the "Intent Datadog" for your product decisions.\n\nReturns a structured truth map showing which decisions/constraints/hypotheses are:\n🔴 drift (unaccounted divergence from intent)\n🟡 review (soft signal, awaiting human decision)\n⚪ held (acknowledged, time-boxed, not forgotten)\n🔵 aligned (reality matches intent)\n\nNodes are classified into 4 species:\n• hypothesis → Decision Cards (decision journal format)\n• constraint → Rules (pass/fail checklist)\n• commitment → Heartbeats (cadence monitoring)\n• source_of_truth → Reference (stable anchors)\n\nWHEN TO CALL:\n• At session start — "what needs my attention?"\n• Before making a decision — check for existing anchors on the topic\n• After completing work — verify drift state changed\n• When the user asks about product health / what\'s broken / what\'s stale',
151
+ inputSchema: {
152
+ type: 'object',
153
+ properties: {
154
+ domain: { type: 'string', description: 'Filter by domain (strategy, product, engineering, growth, etc.)' },
155
+ decision_mode: { type: 'string', description: 'Filter by decision_mode (hypothesis, constraint, commitment, source_of_truth)' },
156
+ },
157
+ },
158
+ },
159
+ {
160
+ name: 'check_decision',
161
+ description: 'Deep-dive into a specific decision/anchor — its state, premises, drift edges, and pending candidates.\n\nReturns the full context for one truth-map node: what was decided, why, what reality says, whether it\'s drifting, and what actions are pending.\n\nWHEN TO CALL:\n• When the user asks about a specific decision ("what happened with X?")\n• Before resolving a drift signal — understand the full picture first\n• When reviewing premises of a decision ("is this still true?")',
162
+ inputSchema: {
163
+ type: 'object',
164
+ properties: {
165
+ anchor_id: { type: 'number', description: 'The drift_anchor ID to inspect' },
166
+ },
167
+ required: ['anchor_id'],
168
+ },
169
+ },
170
+ {
171
+ name: 'declare_anchor',
172
+ description: 'Declare a new decision, constraint, or prohibition as a truth-map anchor.\n\nAnchors are NORMATIVE claims: "we decided X", "Y is forbidden", "Z must always hold."\nThe drift detector later checks these against committed reality.\n\ndeclare-don\'t-mine: anchors come ONLY from explicit human declaration, never from pattern extraction.\n\nWHEN TO CALL:\n• When the user makes a product decision ("let\'s go with approach A")\n• When a constraint is established ("never do X")\n• When a commitment is made ("we ship weekly")\n• When the user says "anchor this" / "record this decision"',
173
+ inputSchema: {
174
+ type: 'object',
175
+ properties: {
176
+ kind: { type: 'string', enum: ['prohibition', 'decision', 'constraint'], description: 'Anchor type' },
177
+ statement: { type: 'string', description: 'The normative claim (>= 8 chars)' },
178
+ rationale: { type: 'string', description: 'Why this was decided' },
179
+ affects: { type: 'array', items: { type: 'string' }, description: 'Path globs that scope this anchor' },
180
+ detect_terms: { type: 'array', items: { type: 'string' }, description: 'Keywords for scoping' },
181
+ violation_signal: { type: 'array', items: { type: 'string' }, description: 'Terms whose presence = violation (required for prohibition/decision)' },
182
+ tier: { type: 'string', enum: ['human', 'explicit'], description: 'Declaration tier (default: human)' },
183
+ // v9 ProjectCoreNode fields
184
+ node_type: { type: 'string', description: 'Node type label' },
185
+ domain: { type: 'string', description: 'Domain (strategy, product, engineering, etc.)' },
186
+ decision_mode: { type: 'string', enum: ['hypothesis', 'constraint', 'commitment', 'source_of_truth'], description: 'Classification for 4-species display' },
187
+ confidence: { type: 'number', minimum: 0, maximum: 1, description: 'Confidence level (0.0-1.0)' },
188
+ lifecycle: { type: 'string', description: 'Lifecycle state (active, at_risk, retired)' },
189
+ review_after: { type: 'string', description: 'ISO date for next review (e.g. "2026-07-04")' },
190
+ },
191
+ required: ['kind', 'statement'],
192
+ },
193
+ },
194
+ {
195
+ name: 'resolve_drift',
196
+ description: 'Record a resolution for a drifting anchor — the human feedback loop.\n\n4 resolution actions:\n• fix — "we fixed the code/reality to match intent" → state becomes aligned\n• supersede — "intent evolved, this is the new direction" → state becomes aligned\n• acknowledge — "we know, parking it for now" → state becomes held (with optional review date)\n• dismiss — "false positive, not actually drifting" → edges dismissed\n\nWHEN TO CALL:\n• After drift_status shows 🔴 drift or 🟡 review items\n• When the user says "that\'s fixed" / "ignore that" / "we changed direction"\n• When acknowledging a known gap with a review date',
197
+ inputSchema: {
198
+ type: 'object',
199
+ properties: {
200
+ anchor_id: { type: 'number', description: 'The drift_anchor ID to resolve' },
201
+ action: { type: 'string', enum: ['fix', 'supersede', 'acknowledge', 'dismiss'], description: 'Resolution action' },
202
+ rationale: { type: 'string', description: 'Why this resolution (recorded for audit trail)' },
203
+ review_after: { type: 'string', description: 'For acknowledge: ISO date to re-check (e.g. "2026-07-04")' },
204
+ superseded_by: { type: 'number', description: 'For supersede: the new anchor ID that replaces this one' },
205
+ },
206
+ required: ['anchor_id', 'action'],
207
+ },
208
+ },
145
209
  ];
146
210
  // ============================================================
147
211
  // Handlers
@@ -345,12 +409,35 @@ function refreshStaleMomentum(entityIds) {
345
409
  catch { /* non-fatal */ }
346
410
  }
347
411
  }
412
+ // --- recall helpers ---------------------------------------------------------
413
+ // Rough token estimate for budgeting. ~3 chars/token is deliberately conservative
414
+ // for mixed JP/EN (Japanese is token-dense), so we under-fill rather than blow
415
+ // the caller's max_tokens budget.
416
+ function estimateTokens(s) {
417
+ return Math.ceil(s.length / 3);
418
+ }
419
+ // Signature for near-duplicate collapsing in recall results. Extracts the
420
+ // meaningful core text (what/title/decision/intent/learned) from the memory's
421
+ // content (JSON or plain), normalizes whitespace/case, and keys it by entity so
422
+ // the same message stored under two layers collapses to a single result.
423
+ function contentSignature(raw, entityId) {
424
+ let text = raw;
425
+ try {
426
+ const o = JSON.parse(raw);
427
+ text = o?.what ?? o?.title ?? o?.decision ?? o?.intent ?? o?.learned ?? (typeof o === 'string' ? o : raw);
428
+ }
429
+ catch { /* plain-text content */ }
430
+ const norm = String(text).toLowerCase().replace(/\s+/g, ' ').trim().slice(0, 120);
431
+ if (norm.length < 16)
432
+ return ''; // too short to dedup safely
433
+ return `${entityId}::${norm}`;
434
+ }
348
435
  function handleRecall(args) {
436
+ // max_tokens is enforced for real at assembly time (greedy fill by measured
437
+ // serialized size), not via a crude fixed per-memory estimate. hardLimit caps COUNT.
349
438
  const maxTokens = Math.max(100, Number(args.max_tokens ?? 2000));
350
- const approxTokensPerMemory = 100;
351
- const tokenBudgetLimit = Math.max(1, Math.floor(maxTokens / approxTokensPerMemory));
352
- const hardLimit = Number.isFinite(args.limit) ? Math.max(1, Math.min(200, args.limit)) : tokenBudgetLimit;
353
- const returnLimit = Math.min(tokenBudgetLimit, hardLimit);
439
+ const DEFAULT_COUNT_CAP = 50;
440
+ const hardLimit = Number.isFinite(args.limit) ? Math.max(1, Math.min(200, args.limit)) : DEFAULT_COUNT_CAP;
354
441
  const offset = Math.max(0, Number(args.offset ?? 0));
355
442
  const markAccessed = args.mark_accessed !== false;
356
443
  const layer = resolveLayer(args.layer);
@@ -361,8 +448,9 @@ function handleRecall(args) {
361
448
  mem_state: args.mem_state,
362
449
  thread_id: args.thread_id,
363
450
  };
364
- // Fetch extra rows for composite re-rank, pagination, and band filtering
365
- const fetchLimit = Math.max(returnLimit * 3, 30) + offset;
451
+ // Fetch a generous candidate pool for composite re-rank, dedup, pagination, band filter.
452
+ // Token-budget trimming happens at assembly, so fetch enough that many small memories can fill it.
453
+ const fetchLimit = Math.max(hardLimit * 3, 90) + offset;
366
454
  let rows = [];
367
455
  let searchMethod = 'like';
368
456
  const ftsQuery = toFtsQuery(args.query ?? '');
@@ -410,6 +498,26 @@ function handleRecall(args) {
410
498
  const minBm = Math.min(...bm25Values, 0);
411
499
  const maxBm = Math.max(...bm25Values, 1);
412
500
  const bmSpan = Math.max(0.001, maxBm - minBm);
501
+ // Composite weights adapt to query specificity:
502
+ // - broad / no query → importance & heat carry more (relevance is a flat 0.5 anyway)
503
+ // - focused (1-2 terms) → relevance leads
504
+ // - narrow (3+ terms) → relevance dominates, so off-topic-but-pinned memories don't crowd in
505
+ const queryTermCount = String(args.query ?? '').trim().split(/\s+/).filter(Boolean).length;
506
+ let w_rel = 0.45, w_heat = 0.25, w_mom = 0.15, w_imp = 0.15;
507
+ if (useFts) {
508
+ if (queryTermCount >= 3) {
509
+ w_rel = 0.72;
510
+ w_heat = 0.12;
511
+ w_mom = 0.08;
512
+ w_imp = 0.08;
513
+ }
514
+ else {
515
+ w_rel = 0.60;
516
+ w_heat = 0.18;
517
+ w_mom = 0.12;
518
+ w_imp = 0.10;
519
+ }
520
+ }
413
521
  const scored = rows.map((r) => {
414
522
  const daysSince = (now - r.last_accessed_at) / 86400;
415
523
  const heat = computeHeat({
@@ -424,8 +532,7 @@ function handleRecall(args) {
424
532
  const heatNorm = heat.score / 100;
425
533
  const momNorm = Math.min(1, (r.momentum_score ?? 0) / 10);
426
534
  const importanceBoost = r.importance; // 0-1
427
- // Composite: give a bit to importance so pinned (>=0.9) memories always rank high
428
- const w_rel = 0.45, w_heat = 0.25, w_mom = 0.15, w_imp = 0.15;
535
+ // Composite (weights adapt to query specificity computed above)
429
536
  const composite = w_rel * relevance + w_heat * heatNorm + w_mom * momNorm + w_imp * importanceBoost;
430
537
  // match_reasons: human-readable WHY this row is here
431
538
  const reasons = [];
@@ -468,31 +575,89 @@ function handleRecall(args) {
468
575
  });
469
576
  // Apply band filter AFTER scoring (needs heat.band)
470
577
  const filtered = band ? scored.filter((s) => s.heat_band === band) : scored;
471
- // Sort by composite
578
+ // Sort by composite (best first)
472
579
  filtered.sort((a, b) => b.composite_score - a.composite_score);
473
- // Pagination: skip offset, take returnLimit
474
- const total = filtered.length;
475
- const windowed = filtered.slice(offset, offset + returnLimit);
476
- const hasMore = total > offset + windowed.length;
477
- // Mark accessed (only for the returned window, and only if asked)
478
- if (markAccessed && windowed.length > 0) {
580
+ // Dedup near-identical memories (same entity + near-identical core text).
581
+ // filtered is sorted desc, so the first occurrence kept is the highest-ranked.
582
+ // This collapses the common "same message saved under two layers" duplication.
583
+ const seenSig = new Set();
584
+ const deduped = filtered.filter((r) => {
585
+ const sig = contentSignature(r.content, r.entity_id);
586
+ if (!sig)
587
+ return true; // too short / no usable text → never dedup
588
+ if (seenSig.has(sig))
589
+ return false; // duplicate → drop
590
+ seenSig.add(sig);
591
+ return true;
592
+ });
593
+ const total = deduped.length;
594
+ // Greedy assembly under a REAL token budget: add memories (deduped + ranked) until
595
+ // including the next would blow max_tokens, capped at hardLimit by COUNT. Always
596
+ // returns at least one memory if anything matched.
597
+ const memoriesOut = [];
598
+ const returnedIds = [];
599
+ let accTokens = 0;
600
+ let stoppedBy = 'end';
601
+ for (let i = offset; i < total; i++) {
602
+ if (memoriesOut.length >= hardLimit) {
603
+ stoppedBy = 'limit';
604
+ break;
605
+ }
606
+ const r = deduped[i];
607
+ let parsedContent = r.content;
608
+ try {
609
+ parsedContent = JSON.parse(r.content);
610
+ }
611
+ catch { /* leave as string */ }
612
+ const memObj = {
613
+ id: r.id,
614
+ entity: {
615
+ id: r.entity_id,
616
+ name: r.entity_name,
617
+ kind: r.entity_kind,
618
+ momentum: Number((r.momentum_score ?? 0).toFixed(2)),
619
+ },
620
+ layer: r.layer,
621
+ axis: {
622
+ altitude: r._altitude ?? null,
623
+ type: r._mem_type ?? null,
624
+ state: r._mem_state ?? null,
625
+ thread_id: r._thread_id ?? null,
626
+ },
627
+ content: parsedContent,
628
+ importance: r.importance,
629
+ pinned: r.importance >= 0.9,
630
+ heat: Number(r.heat_score.toFixed(1)),
631
+ band: r.heat_band,
632
+ composite: Number(r.composite_score.toFixed(3)),
633
+ match_reasons: r._reasons,
634
+ score_breakdown: r._breakdown,
635
+ };
636
+ const memTokens = estimateTokens(JSON.stringify(memObj));
637
+ if (memoriesOut.length > 0 && accTokens + memTokens > maxTokens) {
638
+ stoppedBy = 'tokens';
639
+ break;
640
+ }
641
+ accTokens += memTokens;
642
+ memoriesOut.push(memObj);
643
+ returnedIds.push(r.id);
644
+ }
645
+ const hasMore = offset + memoriesOut.length < total;
646
+ // Mark accessed (only the returned set, and only if asked)
647
+ if (markAccessed && returnedIds.length > 0) {
479
648
  const mark = db.prepare('UPDATE memories SET last_accessed_at = ?, access_count = access_count + 1 WHERE id = ?');
480
649
  const tx = db.transaction((ids) => { for (const id of ids)
481
650
  mark.run(now, id); });
482
- tx(windowed.map((r) => r.id));
483
- }
484
- // Determine what stopped iteration — max_tokens vs limit vs offset+n=total
485
- let stoppedBy = 'end';
486
- if (windowed.length === returnLimit && total > offset + returnLimit) {
487
- stoppedBy = hardLimit <= tokenBudgetLimit ? 'limit' : 'tokens';
651
+ tx(returnedIds);
488
652
  }
489
653
  return JSON.stringify({
490
654
  ok: true,
491
- count: windowed.length,
655
+ count: memoriesOut.length,
492
656
  total_candidates: total,
493
657
  offset,
494
658
  has_more: hasMore,
495
659
  stopped_by: stoppedBy,
660
+ approx_tokens: accTokens,
496
661
  search: searchMethod,
497
662
  resolved_layer: layer ?? null,
498
663
  resolved_axis: {
@@ -500,38 +665,7 @@ function handleRecall(args) {
500
665
  type: axis.mem_type ?? null,
501
666
  state: axis.mem_state ?? null,
502
667
  },
503
- memories: windowed.map((r) => {
504
- let parsedContent = r.content;
505
- try {
506
- parsedContent = JSON.parse(r.content);
507
- }
508
- catch { /* leave as string */ }
509
- return {
510
- id: r.id,
511
- entity: {
512
- id: r.entity_id,
513
- name: r.entity_name,
514
- kind: r.entity_kind,
515
- momentum: Number((r.momentum_score ?? 0).toFixed(2)),
516
- },
517
- layer: r.layer,
518
- axis: {
519
- altitude: r._altitude ?? null,
520
- type: r._mem_type ?? null,
521
- state: r._mem_state ?? null,
522
- thread_id: r._thread_id ?? null,
523
- },
524
- content: parsedContent,
525
- content_raw: r.content,
526
- importance: r.importance,
527
- pinned: r.importance >= 0.9,
528
- heat: Number(r.heat_score.toFixed(1)),
529
- band: r.heat_band,
530
- composite: Number(r.composite_score.toFixed(3)),
531
- match_reasons: r._reasons,
532
- score_breakdown: r._breakdown,
533
- };
534
- }),
668
+ memories: memoriesOut,
535
669
  });
536
670
  }
537
671
  function handleForget(args) {
@@ -997,6 +1131,94 @@ async function handleRecallUnified(args) {
997
1131
  return handleRecall(args);
998
1132
  }
999
1133
  // ============================================================
1134
+ // Drift tool handlers (v0.8.0)
1135
+ // ============================================================
1136
+ function handleDriftStatus(args) {
1137
+ const view = getTruthView(db, {
1138
+ domain: args?.domain,
1139
+ decision_mode: args?.decision_mode,
1140
+ });
1141
+ // Build a concise triage line
1142
+ const { by_state, nodes } = view.counts;
1143
+ const triage = [
1144
+ by_state.drift > 0 ? `🔴 ${by_state.drift} drifting` : null,
1145
+ by_state.review > 0 ? `🟡 ${by_state.review} needs review` : null,
1146
+ by_state.held > 0 ? `⚪ ${by_state.held} held` : null,
1147
+ `🔵 ${by_state.aligned} aligned`,
1148
+ ].filter(Boolean).join(' · ');
1149
+ return JSON.stringify({
1150
+ ok: true,
1151
+ triage: `${nodes} anchors: ${triage}`,
1152
+ nextReopen: view.nextReopen,
1153
+ attention: view.attention,
1154
+ alignedByDomain: view.alignedByDomain,
1155
+ candidates: view.candidates,
1156
+ counts: view.counts,
1157
+ });
1158
+ }
1159
+ function handleCheckDecision(args) {
1160
+ if (!args?.anchor_id)
1161
+ throw new Error('anchor_id is required');
1162
+ const detail = getDecisionDetail(db, args.anchor_id);
1163
+ if (!detail) {
1164
+ return JSON.stringify({ ok: false, error: `Anchor ${args.anchor_id} not found or not active` });
1165
+ }
1166
+ return JSON.stringify({ ok: true, decision: detail });
1167
+ }
1168
+ function handleDeclareAnchor(args) {
1169
+ if (!args?.kind || !args?.statement) {
1170
+ throw new Error('kind and statement are required');
1171
+ }
1172
+ // Create the base anchor
1173
+ const anchor = declareAnchor(db, {
1174
+ kind: args.kind,
1175
+ statement: args.statement,
1176
+ rationale: args.rationale,
1177
+ affects: args.affects,
1178
+ detect_terms: args.detect_terms,
1179
+ violation_signal: args.violation_signal,
1180
+ tier: args.tier ?? 'human',
1181
+ });
1182
+ // Apply v9 ProjectCoreNode fields if provided
1183
+ const nodeFields = {};
1184
+ if (args.node_type !== undefined)
1185
+ nodeFields.node_type = args.node_type;
1186
+ if (args.domain !== undefined)
1187
+ nodeFields.domain = args.domain;
1188
+ if (args.decision_mode !== undefined)
1189
+ nodeFields.decision_mode = args.decision_mode;
1190
+ if (args.confidence !== undefined)
1191
+ nodeFields.confidence = args.confidence;
1192
+ if (args.lifecycle !== undefined)
1193
+ nodeFields.lifecycle = args.lifecycle;
1194
+ if (args.review_after !== undefined) {
1195
+ nodeFields.review_after = Math.floor(new Date(args.review_after).getTime() / 1000);
1196
+ }
1197
+ if (Object.keys(nodeFields).length > 0) {
1198
+ setNodeFields(db, anchor.id, nodeFields);
1199
+ }
1200
+ return JSON.stringify({
1201
+ ok: true,
1202
+ anchor_id: anchor.id,
1203
+ statement: anchor.statement,
1204
+ kind: anchor.kind,
1205
+ message: `Anchor #${anchor.id} declared: "${anchor.statement.substring(0, 80)}"`,
1206
+ });
1207
+ }
1208
+ function handleResolveDrift(args) {
1209
+ if (!args?.anchor_id || !args?.action) {
1210
+ throw new Error('anchor_id and action are required');
1211
+ }
1212
+ const result = resolveDrift(db, {
1213
+ anchor_id: args.anchor_id,
1214
+ action: args.action,
1215
+ rationale: args.rationale,
1216
+ review_after: args.review_after,
1217
+ superseded_by: args.superseded_by,
1218
+ });
1219
+ return JSON.stringify(result);
1220
+ }
1221
+ // ============================================================
1000
1222
  // MCP wiring
1001
1223
  // ============================================================
1002
1224
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
@@ -1014,6 +1236,19 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1014
1236
  case 'read_smart':
1015
1237
  text = handleReadSmart(args);
1016
1238
  break;
1239
+ // Drift tools (v0.8.0)
1240
+ case 'drift_status':
1241
+ text = handleDriftStatus(args);
1242
+ break;
1243
+ case 'check_decision':
1244
+ text = handleCheckDecision(args);
1245
+ break;
1246
+ case 'declare_anchor':
1247
+ text = handleDeclareAnchor(args);
1248
+ break;
1249
+ case 'resolve_drift':
1250
+ text = handleResolveDrift(args);
1251
+ break;
1017
1252
  default: {
1018
1253
  const migrations = {
1019
1254
  update_memory: 'remember({ memory_id: <id>, content: "...", importance: 0.8 })',
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "linksee-memory",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "mcpName": "io.github.michielinksee/linksee-memory",
5
- "description": "Local-first agent memory MCP — cross-agent brain with 6-layer structured memory + token-saving file diff cache",
5
+ "description": "Local-first agent memory MCP — cross-agent brain with drift detection, 6-layer structured memory + token-saving file diff cache",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "linksee-memory": "dist/mcp/server.js",
@@ -10,7 +10,9 @@
10
10
  "linksee-memory-sync": "dist/bin/sync-session.js",
11
11
  "linksee-memory-install-skill": "dist/bin/install-skill.js",
12
12
  "linksee-memory-stats": "dist/bin/stats.js",
13
- "linksee-memory-setup": "dist/bin/setup.js"
13
+ "linksee-memory-setup": "dist/bin/setup.js",
14
+ "linksee-memory-declare": "dist/bin/declare-anchor.js",
15
+ "linksee-memory-detect": "dist/bin/detect-drift.js"
14
16
  },
15
17
  "main": "./dist/mcp/server.js",
16
18
  "files": [