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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli/common.mjs +9 -20
- package/lib/id-routing.mjs +24 -5
- package/mem-cli.mjs +3 -3
- package/package.json +1 -1
- package/server.mjs +53 -2
- package/tool-schemas.mjs +23 -1
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
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
-
|
|
81
|
-
|
|
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.
|
package/lib/id-routing.mjs
CHANGED
|
@@ -1,10 +1,29 @@
|
|
|
1
|
-
// Shared probe for "ID-not-found-in-requested-source" hints
|
|
2
|
-
// Used by CLI
|
|
3
|
-
//
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
843
|
+
out(`${('#' + r.id).padEnd(6)} ${typeIcon(r.type)} ${time.padEnd(8)} ${title}${marker}`);
|
|
844
844
|
}
|
|
845
845
|
}
|
|
846
846
|
|
package/package.json
CHANGED
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:
|
|
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)'),
|