linksee-memory 0.10.0 → 0.11.1

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.
@@ -23,7 +23,50 @@ import { confirmForget } from './elicitation.js';
23
23
  import { getTruthView, getDecisionDetail, resolveDrift } from '../lib/truth-engine.js';
24
24
  import { declareAnchor, setNodeFields } from '../lib/drift-anchors.js';
25
25
  import { getReinjectionFriction, setGateMode } from '../lib/guard.js';
26
- const SERVER_VERSION = '0.10.0';
26
+ import { whereAmI } from '../lib/map-view.js';
27
+ import { readFileSync, existsSync } from 'node:fs';
28
+ import { fileURLToPath } from 'node:url';
29
+ import { dirname, join } from 'node:path';
30
+ import { spawnSync } from 'node:child_process';
31
+ // Single source of truth: read our own version from package.json so the MCP
32
+ // serverInfo can never drift from the published package version. (We catch
33
+ // doc/code drift for a living — our own version string shouldn't drift.)
34
+ function readServerVersion() {
35
+ try {
36
+ const here = fileURLToPath(import.meta.url);
37
+ // dist/mcp/server.js → ../../package.json
38
+ const pkgPath = join(dirname(dirname(dirname(here))), 'package.json');
39
+ if (existsSync(pkgPath)) {
40
+ return String(JSON.parse(readFileSync(pkgPath, 'utf8')).version || '0.0.0');
41
+ }
42
+ }
43
+ catch { /* fall through */ }
44
+ return '0.0.0';
45
+ }
46
+ const SERVER_VERSION = readServerVersion();
47
+ // ── Subcommand dispatch ───────────────────────────────────────────────────────
48
+ // Each CLI ships as its own bin (linksee-memory-setup, …) AND as a subcommand of
49
+ // the main bin, so `npx -y linksee-memory <sub>` works for a cold user: npx can
50
+ // only resolve the package name (linksee-memory), not sibling bin names like
51
+ // linksee-memory-setup. Run as the MCP server (no subcommand / stdio) → skip this.
52
+ const SUBCOMMANDS = {
53
+ setup: 'setup.js',
54
+ map: 'map-import.js',
55
+ sync: 'sync-session.js',
56
+ guard: 'guard-hook.js',
57
+ stats: 'stats.js',
58
+ import: 'import-sessions.js',
59
+ 'install-skill': 'install-skill.js',
60
+ declare: 'declare-anchor.js',
61
+ detect: 'detect-drift.js',
62
+ };
63
+ const subcommand = process.argv[2];
64
+ if (subcommand && Object.prototype.hasOwnProperty.call(SUBCOMMANDS, subcommand)) {
65
+ // Spawn the target bin as a child (shebang-safe, argv/stdio correct) and exit with its code.
66
+ const binPath = join(dirname(fileURLToPath(import.meta.url)), '..', 'bin', SUBCOMMANDS[subcommand]);
67
+ const r = spawnSync(process.execPath, [binPath, ...process.argv.slice(3)], { stdio: 'inherit' });
68
+ process.exit(r.status ?? (r.signal ? 1 : 0));
69
+ }
27
70
  const db = openDb();
28
71
  runMigrations(db);
29
72
  // Auto-maintenance: consolidate stale memories on startup (non-blocking)
@@ -158,6 +201,19 @@ const TOOLS = [
158
201
  },
159
202
  },
160
203
  },
204
+ {
205
+ name: 'where_am_i',
206
+ description: 'Locate the current topic on the Current Truth Map and report "you are here" + blast radius — the per-turn re-anchor.\n\nReturns the matching Map node(s) + journey stage (discover → … → expand), the BLAST RADIUS (what becomes suspect if you change this — the must-stay-consistent-with / should-align-with / realizes dependents; e.g. editing the README implicates the LP), and the decision behind the node (linked anchor), if any.\n\nThree ways to call:\n• NO ARGS → auto-locates from the files you JUST edited this session (the zero-effort re-anchor — call it freely as you work).\n• query: "<topic>" → lexical locate by topic.\n• node_id: "<id>" → exact node.\n\nThis is how you avoid optimizing one node while silently breaking its neighbors (change the spec → npm/Docs/LP must move too). Matching is lexical (no embeddings).\n\nWHEN TO CALL:\n• Right after editing files — call with no args to see what you just touched + its blast radius.\n• When the topic shifts — re-anchor to the new node.\n• When the user asks "what does changing X affect?" / "where does this fit?"',
207
+ inputSchema: {
208
+ type: 'object',
209
+ properties: {
210
+ query: { type: 'string', description: 'Topic to locate (e.g. "changing the telemetry contract"). Omit to auto-locate from your recent edits.' },
211
+ node_id: { type: 'string', description: 'Exact Map node id, if known (e.g. "readme") — bypasses lexical match' },
212
+ project: { type: 'string', description: 'Map project slug (defaults to the most recently imported Map)' },
213
+ limit: { type: 'number', description: 'Max matches (default 3)' },
214
+ },
215
+ },
216
+ },
161
217
  {
162
218
  name: 'check_decision',
163
219
  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?")',
@@ -1238,6 +1294,34 @@ function handleDriftStatus(args) {
1238
1294
  counts: view.counts,
1239
1295
  });
1240
1296
  }
1297
+ function handleWhereAmI(args) {
1298
+ const res = whereAmI(db, {
1299
+ query: args?.query, node_id: args?.node_id, project: args?.project, limit: args?.limit,
1300
+ });
1301
+ if (res.matched.length === 0) {
1302
+ return JSON.stringify({
1303
+ ok: true, project: res.project, located: false,
1304
+ hint: res.project ? 'No node matched. Try a node id or a topical keyword.' : 'No Map imported yet — run linksee-memory-map.',
1305
+ });
1306
+ }
1307
+ const located = res.matched.map((m) => ({
1308
+ node: m.node.id,
1309
+ stage: m.stage_label,
1310
+ status: m.node.status,
1311
+ statement: m.node.statement,
1312
+ why: m.match_reason,
1313
+ // the whole point: what else moves if you touch this
1314
+ blast_radius: m.blast.map((b) => `${b.id} (${b.relation})`),
1315
+ decision: m.anchor ? `#${m.anchor.id}: ${m.anchor.statement}` : null,
1316
+ }));
1317
+ const top = res.matched[0];
1318
+ const youAreHere = `You are at "${top.node.id}"`
1319
+ + (top.stage_label ? ` in stage 「${top.stage_label}」` : '')
1320
+ + `. Touching it implicates ${top.blast.length} node(s)`
1321
+ + (top.blast.length ? `: ${top.blast.map((b) => b.id).join(', ')}` : ' (isolated)')
1322
+ + '.';
1323
+ return JSON.stringify({ ok: true, project: res.project, located: true, you_are_here: youAreHere, job: res.job, matched: located });
1324
+ }
1241
1325
  function handleCheckDecision(args) {
1242
1326
  if (!args?.anchor_id)
1243
1327
  throw new Error('anchor_id is required');
@@ -1635,6 +1719,9 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1635
1719
  case 'drift_status':
1636
1720
  text = handleDriftStatus(args);
1637
1721
  break;
1722
+ case 'where_am_i':
1723
+ text = handleWhereAmI(args);
1724
+ break;
1638
1725
  case 'check_decision':
1639
1726
  text = handleCheckDecision(args);
1640
1727
  break;