claude-mem-lite 2.41.0 → 2.42.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.41.0",
13
+ "version": "2.42.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.41.0",
3
+ "version": "2.42.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/cli/common.mjs CHANGED
@@ -1,10 +1,11 @@
1
1
  // cli/common.mjs — shared helpers used by every per-command file under cli/.
2
2
  // Extracted from mem-cli.mjs (v2.41) as first step in the god-module split.
3
3
  //
4
- // Scope: pure utilities only. No DB, no imports from other cli/ files. This
5
- // module is the single source of truth for stdout/stderr framing, arg parsing,
6
- // ID-token parsing, and relative-time formatting every command imports from
7
- // here so the CLI stays consistent.
4
+ // Scope: pure utilities only. No DB, no imports from other cli/ files; only
5
+ // `lib/` leaf utilities may be re-exported through here (currently:
6
+ // parseIdToken). This module is the single source of truth for stdout/stderr
7
+ // framing, arg parsing, ID-token parsing, and relative-time formatting —
8
+ // every command imports from here so the CLI stays consistent.
8
9
 
9
10
  // ─── Argument Parsing ────────────────────────────────────────────────────────
10
11
 
@@ -75,22 +76,10 @@ export function fmtDateShort(iso) {
75
76
  }
76
77
 
77
78
  // ─── ID Token Parsing ────────────────────────────────────────────────────────
78
-
79
- /**
80
- * Parse an ID token from a command positional argument.
81
- * Accepts: `123`, `#123`, `P#123` / `p123` (prompt), `S#123` / `s123` (session).
82
- * @returns {{ source: 'obs'|'session'|'prompt'|null, id: number } | null}
83
- * source===null means no explicit prefix — caller picks default (typically 'obs').
84
- */
85
- export function parseIdToken(raw) {
86
- const m = /^([PpSs]?)#?(\d+)$/.exec(String(raw).trim());
87
- if (!m) return null;
88
- const p = m[1].toUpperCase();
89
- const id = parseInt(m[2], 10);
90
- if (!Number.isFinite(id) || id <= 0) return null;
91
- const source = p === 'P' ? 'prompt' : p === 'S' ? 'session' : null;
92
- return { source, id };
93
- }
79
+ // Re-exported from lib/id-routing.mjs so CLI and MCP (server.mjs) share a single
80
+ // parser — parity per #8050. Keep this re-export for back-compat with the
81
+ // 5 CLI call sites that already import parseIdToken from cli/common.mjs.
82
+ export { parseIdToken } from '../lib/id-routing.mjs';
94
83
 
95
84
  /**
96
85
  * Format the shared `probeIdSources` output as CLI hint strings.
@@ -1,10 +1,29 @@
1
- // Shared probe for "ID-not-found-in-requested-source" hints.
2
- // Used by CLI cmdGet (mem-cli.mjs) and MCP mem_get (server.mjs) so both
3
- // produce consistent redirect hints if the probe schema drifts, both
4
- // paths update together.
1
+ // Shared probe for "ID-not-found-in-requested-source" hints + shared token
2
+ // parser. Used by CLI (mem-cli.mjs, cli/common.mjs re-export) and MCP
3
+ // (server.mjs) so both paths stay aligned parity per #8050.
5
4
  //
6
5
  // The formatter stays per-call-site because CLI and MCP surface format
7
- // differently (stderr vs response text); only the SQL layer is shared.
6
+ // differently (stderr vs response text); only the SQL + token-parse layers
7
+ // are shared.
8
+
9
+ // ─── ID Token Parsing ────────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Parse an ID token as it appears in search output or CLI positional args.
13
+ * Accepts: `123`, `#123`, `P#123` / `p123` (prompt), `S#123` / `s123` (session).
14
+ * @param {unknown} raw
15
+ * @returns {{ source: 'obs'|'session'|'prompt'|null, id: number } | null}
16
+ * source===null means no explicit prefix — caller picks default (typically 'obs').
17
+ */
18
+ export function parseIdToken(raw) {
19
+ const m = /^([PpSs]?)#?(\d+)$/.exec(String(raw).trim());
20
+ if (!m) return null;
21
+ const p = m[1].toUpperCase();
22
+ const id = parseInt(m[2], 10);
23
+ if (!Number.isFinite(id) || id <= 0) return null;
24
+ const source = p === 'P' ? 'prompt' : p === 'S' ? 'session' : null;
25
+ return { source, id };
26
+ }
8
27
 
9
28
  /**
10
29
  * Probe the observations / session_summaries / user_prompts tables for any
package/mem-cli.mjs CHANGED
@@ -476,7 +476,7 @@ function cmdRecent(db, args) {
476
476
  for (const r of rows) {
477
477
  const time = relativeTime(r.created_at_epoch);
478
478
  const title = truncate(r.title || r.subtitle || '(untitled)', 80);
479
- out(`#${String(r.id).padStart(5)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}`);
479
+ out(`${('#' + r.id).padEnd(6)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}`);
480
480
  }
481
481
  }
482
482
 
@@ -787,7 +787,7 @@ function cmdTimeline(db, args) {
787
787
  for (const r of rows.reverse()) {
788
788
  const time = relativeTime(r.created_at_epoch);
789
789
  const title = truncate(r.title || r.subtitle || '(untitled)', 80);
790
- out(`#${String(r.id).padStart(5)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}`);
790
+ out(`${('#' + r.id).padEnd(6)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}`);
791
791
  }
792
792
  return;
793
793
  }
@@ -840,7 +840,7 @@ function cmdTimeline(db, args) {
840
840
  const marker = r.id === anchorId ? ' <--' : '';
841
841
  const time = relativeTime(r.created_at_epoch);
842
842
  const title = truncate(r.title || r.subtitle || '(untitled)', 80);
843
- out(`#${String(r.id).padStart(5)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}${marker}`);
843
+ out(`${('#' + r.id).padEnd(6)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}${marker}`);
844
844
  }
845
845
  }
846
846
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.41.0",
3
+ "version": "2.42.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "engines": {
package/server.mjs CHANGED
@@ -27,7 +27,7 @@ import { basename, join } from 'path';
27
27
  import { homedir } from 'os';
28
28
  import { ensureRegistryDb, upsertResource } from './registry.mjs';
29
29
  import { searchResources } from './registry-retriever.mjs';
30
- import { probeOtherSources as probeIdSources } from './lib/id-routing.mjs';
30
+ import { probeOtherSources as probeIdSources, parseIdToken } from './lib/id-routing.mjs';
31
31
  import { getVocabulary, rebuildVocabulary, _resetVocabCache, computeVector, vectorSearch, rrfMerge } from './tfidf.mjs';
32
32
  import { createRequire } from 'module';
33
33
 
@@ -762,6 +762,57 @@ server.registerTool(
762
762
  const before = args.before ?? 5;
763
763
  const after = args.after ?? 5;
764
764
  let anchorId = args.anchor;
765
+ let anchorNote = null;
766
+
767
+ // Resolve prefixed-token anchor (e.g. "P#3462" / "S#53" / "#8121") — users pasting
768
+ // from mem_search results expect the same routing as CLI `timeline --anchor`.
769
+ // Prompt/session anchors resolve to the nearest-in-time observation so
770
+ // before/after semantics still apply to the observations timeline.
771
+ if (typeof anchorId === 'string') {
772
+ const parsed = parseIdToken(anchorId);
773
+ if (!parsed) {
774
+ return { content: [{ type: 'text', text: `Invalid anchor "${args.anchor}". Expected N, #N, P#N, or S#N.` }] };
775
+ }
776
+ if (parsed.source === 'prompt' || parsed.source === 'session') {
777
+ const srcTable = parsed.source === 'prompt' ? 'user_prompts' : 'session_summaries';
778
+ const srcPrefix = parsed.source === 'prompt' ? 'P#' : 'S#';
779
+ const row = db.prepare(`SELECT created_at_epoch FROM ${srcTable} WHERE id = ?`).get(parsed.id);
780
+ if (!row) return { content: [{ type: 'text', text: `${parsed.source === 'prompt' ? 'Prompt' : 'Session'} ${srcPrefix}${parsed.id} not found.` }] };
781
+ const projArg = args.project;
782
+ const nearest = db.prepare(`
783
+ SELECT id FROM observations
784
+ WHERE COALESCE(compressed_into, 0) = 0 ${projArg ? 'AND project = ?' : ''}
785
+ ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
786
+ `).get(...(projArg ? [projArg, row.created_at_epoch] : [row.created_at_epoch]));
787
+ if (!nearest) return { content: [{ type: 'text', text: `No observations near ${srcPrefix}${parsed.id}.` }] };
788
+ anchorId = nearest.id;
789
+ anchorNote = `(anchored to #${nearest.id}, closest obs to ${srcPrefix}${parsed.id})`;
790
+ } else {
791
+ // Bare "#N" or "N" — resolve to obs, falling back to prompt/session like CLI bare-int path.
792
+ const obsExists = db.prepare('SELECT 1 FROM observations WHERE id = ?').get(parsed.id);
793
+ if (obsExists) {
794
+ anchorId = parsed.id;
795
+ } else {
796
+ const promptRow = db.prepare('SELECT created_at_epoch FROM user_prompts WHERE id = ?').get(parsed.id);
797
+ const sessionRow = promptRow ? null : db.prepare('SELECT created_at_epoch FROM session_summaries WHERE id = ?').get(parsed.id);
798
+ const hit = promptRow ? { row: promptRow, prefix: 'P#', name: 'prompt' }
799
+ : sessionRow ? { row: sessionRow, prefix: 'S#', name: 'session' }
800
+ : null;
801
+ if (!hit) {
802
+ return { content: [{ type: 'text', text: `Observation, prompt, or session with id ${parsed.id} not found.` }] };
803
+ }
804
+ const projArg = args.project;
805
+ const nearest = db.prepare(`
806
+ SELECT id FROM observations
807
+ WHERE COALESCE(compressed_into, 0) = 0 ${projArg ? 'AND project = ?' : ''}
808
+ ORDER BY ABS(created_at_epoch - ?) ASC LIMIT 1
809
+ `).get(...(projArg ? [projArg, hit.row.created_at_epoch] : [hit.row.created_at_epoch]));
810
+ if (!nearest) return { content: [{ type: 'text', text: `No observations near ${hit.prefix}${parsed.id} (${hit.name}).` }] };
811
+ anchorId = nearest.id;
812
+ anchorNote = `(anchored to #${nearest.id}, closest obs to ${hit.prefix}${parsed.id})`;
813
+ }
814
+ }
815
+ }
765
816
 
766
817
  // Auto-find anchor via FTS (with recency decay)
767
818
  if (!anchorId && args.query) {
@@ -845,7 +896,7 @@ server.registerTool(
845
896
  const anchor = db.prepare('SELECT id, type, title, subtitle, project, created_at FROM observations WHERE id = ?').get(anchorId);
846
897
 
847
898
  const all = [...beforeRows.reverse(), anchor, ...afterRows];
848
- const lines = [`Timeline around #${anchorId}:\n`];
899
+ const lines = [`Timeline around #${anchorId}${anchorNote ? ' ' + anchorNote : ''}:\n`];
849
900
  for (const r of all) {
850
901
  const marker = r.id === anchorId ? ' ◀' : '';
851
902
  lines.push(`#${r.id} ${typeIcon(r.type)} [${r.type}] ${truncate(r.title || r.subtitle || '(untitled)')} | ${r.project} | ${fmtDate(r.created_at)}${marker}`);
package/tool-schemas.mjs CHANGED
@@ -50,8 +50,27 @@ export const memRecentSchema = {
50
50
  project: z.string().optional().describe('Filter by project (default: inferred from CWD)'),
51
51
  };
52
52
 
53
+ // Anchor accepts plain int, "123" string-int, or prefixed token from search output:
54
+ // "#123" / "P#123" (prompt) / "S#123" (session). Prompt/session anchors resolve to
55
+ // the nearest-in-time observation so timeline semantics still apply. CLI --anchor
56
+ // parity per #8050 (CLI has supported prefixed tokens since v2.39.0).
57
+ const coerceAnchor = z.preprocess(
58
+ (v) => {
59
+ if (typeof v === 'string') {
60
+ const s = v.trim();
61
+ if (/^-?\d+$/.test(s)) return parseInt(s, 10);
62
+ return s;
63
+ }
64
+ return v;
65
+ },
66
+ z.union([
67
+ z.number().int(),
68
+ z.string().regex(/^[PpSs]?#?\d+$/, 'Expected N, #N, P#N, or S#N'),
69
+ ])
70
+ );
71
+
53
72
  export const memTimelineSchema = {
54
- anchor: coerceInt.pipe(z.number().int()).optional().describe('Observation ID as center point. Takes precedence over query when both are provided.'),
73
+ anchor: coerceAnchor.optional().describe('Anchor as observation ID (int) or prefixed token string: "#123", "P#123" (prompt → nearest obs), "S#123" (session → nearest obs). Takes precedence over query.'),
55
74
  query: z.string().optional().describe('FTS5 query to auto-find anchor. Ignored when anchor is also given; use one or the other.'),
56
75
  before: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items before anchor (default 5)'),
57
76
  after: coerceInt.pipe(z.number().int().min(0).max(50)).optional().describe('Items after anchor (default 5)'),
@@ -59,6 +78,9 @@ export const memTimelineSchema = {
59
78
  };
60
79
 
61
80
  export const memGetSchema = {
81
+ // TODO(#8126): accept P#/S#/# prefix strings for paste-from-search parity with
82
+ // CLI cmdGet bucketed routing (~40 LOC handler refactor). mem_timeline already
83
+ // accepts prefixes via coerceAnchor; this is the matched-pair gap.
62
84
  ids: coerceIntArray.pipe(z.array(z.number().int()).min(1).max(20)).describe('Observation IDs to retrieve'),
63
85
  source: z.enum(['obs', 'session', 'prompt']).optional().describe('Record type: obs (default), session (S# from search), prompt (P# from search)'),
64
86
  fields: z.array(z.string()).optional().describe('Specific fields to return (default: all)'),