claude-mem-lite 3.28.2 → 3.29.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/format-utils.mjs +12 -8
- package/lib/import-jsonl.mjs +12 -1
- package/lib/search-core.mjs +29 -4
- package/mem-cli.mjs +16 -4
- package/package.json +1 -1
- package/tier.mjs +8 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.29.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/format-utils.mjs
CHANGED
|
@@ -29,16 +29,20 @@ export function truncate(str, max = 80) {
|
|
|
29
29
|
// memory-context / session-handoff). User-derived text containing one LITERALLY
|
|
30
30
|
// would prematurely open/close the block it lands in, spilling the rest as
|
|
31
31
|
// undelimited context.
|
|
32
|
-
// 2. Harness-authority tags the runtime injects (system-reminder /
|
|
33
|
-
//
|
|
34
|
-
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
32
|
+
// 2. Harness-authority + tool-call tags the runtime injects (system-reminder /
|
|
33
|
+
// task-notification / function_calls / function_results, the latter two also in
|
|
34
|
+
// their antml:-namespaced form). Memory replays arbitrary captured text \u2014 file
|
|
35
|
+
// contents, tool output, web pages \u2014 so a poisoned observation carrying a literal
|
|
36
|
+
// <system-reminder>\u2026</system-reminder> or a forged <function_calls>\u2026</function_calls>
|
|
37
|
+
// block would smuggle a privileged-channel instruction / fake tool-call narrative
|
|
38
|
+
// into the model's context. It can't escape its wrapper (class 1 closers are
|
|
39
|
+
// defanged), but a nested forged authority/tool tag is still an indirect-prompt-
|
|
40
|
+
// injection vector; strip the brackets so it reads as inert text. Other tags
|
|
41
|
+
// (<other-tag>) \u2014 and the attribute-bearing <invoke \u2026>/<parameter \u2026> forms \u2014 are
|
|
42
|
+
// deliberately left intact.
|
|
39
43
|
// Reachable by editing files that contain these tokens \u2014 e.g. developing claude-mem-lite
|
|
40
44
|
// itself, where source/observations carry the delimiter names.
|
|
41
|
-
const CONTEXT_DELIMITER_RE = /<\/?(?:claude-mem-context|memory-context|session-handoff|system-reminder|task-notification)>/gi;
|
|
45
|
+
const CONTEXT_DELIMITER_RE = /<\/?(?:claude-mem-context|memory-context|session-handoff|system-reminder|task-notification|(?:antml:)?function_calls|(?:antml:)?function_results)>/gi;
|
|
42
46
|
|
|
43
47
|
/**
|
|
44
48
|
* Defang the literal context-block delimiter tags in user-derived text. Strips just the
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -134,8 +134,19 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
134
134
|
* @param {{project: string}} opts
|
|
135
135
|
* @returns {Promise<{prompts:number, observations:number, skipped:number, orphans:number}>}
|
|
136
136
|
*/
|
|
137
|
+
// Cold-start backfill reads the whole transcript into one JS string (the dedup
|
|
138
|
+
// state + the better-sqlite3 transaction below are synchronous, so a drop-in async
|
|
139
|
+
// stream isn't available). V8 caps a single string near 512MB, so a larger file
|
|
140
|
+
// throws a cryptic "Cannot create a string longer than…" mid-read. Fail early with
|
|
141
|
+
// an actionable message instead — well under that hard limit.
|
|
142
|
+
export const MAX_IMPORT_BYTES = 450 * 1024 * 1024;
|
|
143
|
+
|
|
137
144
|
export async function importJsonl(db, path, { project }) {
|
|
138
|
-
statSync(path);
|
|
145
|
+
const st = statSync(path);
|
|
146
|
+
if (st.size > MAX_IMPORT_BYTES) {
|
|
147
|
+
const mb = (n) => Math.round(n / (1024 * 1024));
|
|
148
|
+
throw new Error(`transcript too large (${mb(st.size)}MB > ${mb(MAX_IMPORT_BYTES)}MB cap); split it (e.g. split -l 50000 into parts) and import the parts`);
|
|
149
|
+
}
|
|
139
150
|
const lines = readFileSync(path, 'utf8').split('\n');
|
|
140
151
|
const seenPrompts = new Set();
|
|
141
152
|
const seenObs = new Set();
|
package/lib/search-core.mjs
CHANGED
|
@@ -36,20 +36,45 @@ export function buildSearchFtsQuery(query, { or = false } = {}) {
|
|
|
36
36
|
return ftsQuery;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Relative-duration units for `--since`. Months/years intentionally omitted —
|
|
40
|
+
// they're calendar-ambiguous; absolute --from/--to cover longer windows.
|
|
41
|
+
const DURATION_UNIT_MS = { s: 1000, m: 60000, h: 3600000, d: 86400000, w: 604800000 };
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Parse a relative duration like "7d", "24h", "90m", "2w", "30s" into ms.
|
|
45
|
+
* Integer magnitude + single unit only (s/m/h/d/w, case-insensitive).
|
|
46
|
+
* @returns {{ ok: true, ms: number } | { ok: false }}
|
|
47
|
+
*/
|
|
48
|
+
export function parseDuration(raw) {
|
|
49
|
+
if (typeof raw !== 'string') return { ok: false };
|
|
50
|
+
const m = raw.trim().match(/^(\d+)\s*([smhdw])$/i);
|
|
51
|
+
if (!m) return { ok: false };
|
|
52
|
+
const n = parseInt(m[1], 10);
|
|
53
|
+
if (!Number.isInteger(n) || n <= 0) return { ok: false };
|
|
54
|
+
return { ok: true, ms: n * DURATION_UNIT_MS[m[2].toLowerCase()] };
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
/**
|
|
40
58
|
* Parse from/to date bounds to epoch ms. Date-only `to` (YYYY-MM-DD) extends
|
|
41
|
-
* to end-of-day so "to 2026-06-12" includes that day's rows.
|
|
59
|
+
* to end-of-day so "to 2026-06-12" includes that day's rows. An optional
|
|
60
|
+
* relative `sinceRaw` ("7d") sets the lower bound when no absolute `--from`
|
|
61
|
+
* is given; an explicit `--from` always wins (it's the more specific signal).
|
|
42
62
|
* @returns {{ ok: true, epochFrom: number|null, epochTo: number|null }
|
|
43
|
-
* | { ok: false, bad: 'from'|'to', value: string }}
|
|
63
|
+
* | { ok: false, bad: 'from'|'to'|'since', value: string }}
|
|
44
64
|
*/
|
|
45
|
-
export function parseDateBounds(fromRaw, toRaw) {
|
|
46
|
-
|
|
65
|
+
export function parseDateBounds(fromRaw, toRaw, sinceRaw = null, now = Date.now()) {
|
|
66
|
+
let epochFrom = fromRaw ? new Date(fromRaw).getTime() : null;
|
|
47
67
|
let epochTo = toRaw ? new Date(toRaw).getTime() : null;
|
|
48
68
|
if (epochTo !== null && toRaw && /^\d{4}-\d{2}-\d{2}$/.test(toRaw)) {
|
|
49
69
|
epochTo += 86400000 - 1; // extend to 23:59:59.999
|
|
50
70
|
}
|
|
51
71
|
if (epochFrom !== null && isNaN(epochFrom)) return { ok: false, bad: 'from', value: fromRaw };
|
|
52
72
|
if (epochTo !== null && isNaN(epochTo)) return { ok: false, bad: 'to', value: toRaw };
|
|
73
|
+
if (sinceRaw) {
|
|
74
|
+
const d = parseDuration(sinceRaw);
|
|
75
|
+
if (!d.ok) return { ok: false, bad: 'since', value: sinceRaw };
|
|
76
|
+
if (epochFrom === null) epochFrom = now - d.ms; // explicit --from takes precedence
|
|
77
|
+
}
|
|
53
78
|
return { ok: true, epochFrom, epochTo };
|
|
54
79
|
}
|
|
55
80
|
|
package/mem-cli.mjs
CHANGED
|
@@ -42,7 +42,7 @@ import { saveObservation } from './lib/save-observation.mjs';
|
|
|
42
42
|
import { rebuildObservationDerived } from './lib/observation-write.mjs';
|
|
43
43
|
import { recallByFile } from './lib/recall-core.mjs';
|
|
44
44
|
import { resolveAnchorToken, formatAnchorError, resolveQueryAnchor, fetchRecentTimeline, fetchTimelineWindow } from './lib/timeline-core.mjs';
|
|
45
|
-
import { buildSearchFtsQuery, parseDateBounds, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
45
|
+
import { buildSearchFtsQuery, parseDateBounds, parseDuration, coreRunSearchPipeline } from './lib/search-core.mjs';
|
|
46
46
|
import { AUTO_MERGE_THRESHOLD } from './lib/dedup-constants.mjs';
|
|
47
47
|
import { countRecentHookErrors } from './lib/hook-telemetry.mjs';
|
|
48
48
|
import { computeCitationFunnelTrend } from './lib/citation-tracker.mjs';
|
|
@@ -58,7 +58,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
58
58
|
const { positional, flags } = parseArgs(args);
|
|
59
59
|
const query = positional.join(' ');
|
|
60
60
|
if (!query) {
|
|
61
|
-
fail('[mem] Usage: claude-mem-lite search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance] [--include-noise] [--deep] [--no-deep] [--rerank]');
|
|
61
|
+
fail('[mem] Usage: claude-mem-lite search <query> [--type TYPE] [--source SOURCE] [--limit N] [--project P] [--from DATE] [--to DATE] [--since DUR] [--importance N] [--branch B] [--offset N] [--sort relevance|time|importance] [--include-noise] [--deep] [--no-deep] [--rerank]');
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -76,8 +76,12 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
76
76
|
}
|
|
77
77
|
const source = flags.source || null; // observations|sessions|prompts (null = all)
|
|
78
78
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
79
|
-
const bounds = parseDateBounds(flags.from, flags.to);
|
|
80
|
-
if (!bounds.ok) {
|
|
79
|
+
const bounds = parseDateBounds(flags.from, flags.to, flags.since);
|
|
80
|
+
if (!bounds.ok) {
|
|
81
|
+
if (bounds.bad === 'since') fail(`[mem] Invalid --since "${bounds.value}". Use <N><unit>, e.g. 7d, 24h, 90m, 2w.`);
|
|
82
|
+
else fail(`[mem] Invalid --${bounds.bad} date: "${bounds.value}". Use YYYY-MM-DD or ISO 8601.`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
81
85
|
const { epochFrom: dateFrom, epochTo: dateTo } = bounds;
|
|
82
86
|
// Inverted range silently returns 0 rows; warn so users see the cause, don't error
|
|
83
87
|
// (a deliberate "search for nothing in this window" is not malformed input).
|
|
@@ -352,6 +356,12 @@ function cmdRecent(db, args) {
|
|
|
352
356
|
const wheres = ['COALESCE(compressed_into, 0) = 0', 'superseded_at IS NULL'];
|
|
353
357
|
if (project) { wheres.push('project = ?'); params.push(project); }
|
|
354
358
|
if (type) { wheres.push('type = ?'); params.push(type); }
|
|
359
|
+
// --since: relative lower bound on created_at (e.g. "recent 1000 --since 24h").
|
|
360
|
+
if (flags.since !== undefined) {
|
|
361
|
+
const d = parseDuration(flags.since);
|
|
362
|
+
if (!d.ok) { fail(`[mem] Invalid --since "${flags.since}". Use <N><unit>, e.g. 7d, 24h, 90m, 2w.`); return; }
|
|
363
|
+
wheres.push('created_at_epoch >= ?'); params.push(Date.now() - d.ms);
|
|
364
|
+
}
|
|
355
365
|
params.push(limit);
|
|
356
366
|
|
|
357
367
|
const rows = db.prepare(`
|
|
@@ -2485,6 +2495,7 @@ Commands:
|
|
|
2485
2495
|
--project P Filter by project
|
|
2486
2496
|
--from DATE Start date (YYYY-MM-DD or ISO 8601)
|
|
2487
2497
|
--to DATE End date (YYYY-MM-DD or ISO 8601)
|
|
2498
|
+
--since DUR Relative lower bound: 7d|24h|90m|2w|30s (ignored if --from set)
|
|
2488
2499
|
--importance N Minimum importance (1=routine, 2=notable, 3=critical)
|
|
2489
2500
|
--branch B Filter by git branch
|
|
2490
2501
|
--offset N Skip first N results (pagination)
|
|
@@ -2496,6 +2507,7 @@ Commands:
|
|
|
2496
2507
|
|
|
2497
2508
|
recent [N] Show N most recent observations (default 10)
|
|
2498
2509
|
--limit N Sibling-parity alias for [N] (max 1000)
|
|
2510
|
+
--since DUR Only items newer than a relative window: 7d|24h|90m|2w|30s
|
|
2499
2511
|
--project P Filter by project
|
|
2500
2512
|
--type T Filter obs type (bugfix|decision|discovery|feature|refactor|change)
|
|
2501
2513
|
--json Output as JSON: {project,limit,type,total,results:[…]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.29.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/tier.mjs
CHANGED
|
@@ -73,7 +73,14 @@ export const TIER_CASE_SQL = `(CASE
|
|
|
73
73
|
WHEN type = 'bugfix' AND created_at_epoch >= ? THEN 'active'
|
|
74
74
|
WHEN type = 'refactor' AND created_at_epoch >= ? THEN 'active'
|
|
75
75
|
WHEN type = 'change' AND created_at_epoch >= ? THEN 'active'
|
|
76
|
-
|
|
76
|
+
-- Default-window fallthrough is for UNKNOWN/null types ONLY, mirroring
|
|
77
|
+
-- computeTier's ACTIVE_WINDOWS[type] with a DEFAULT fallback (a KNOWN type
|
|
78
|
+
-- never takes the fallback). Without the type guard, a known type whose window
|
|
79
|
+
-- was configured SHORTER than the default would wrongly re-qualify as 'active'
|
|
80
|
+
-- here after its own window expired — diverging from the JS classifier.
|
|
81
|
+
-- The "type IS NULL" arm keeps null-type rows on the default window (JS parity).
|
|
82
|
+
WHEN (type IS NULL OR type NOT IN ('decision','discovery','feature','bugfix','refactor','change'))
|
|
83
|
+
AND created_at_epoch >= ? THEN 'active'
|
|
77
84
|
ELSE 'archive'
|
|
78
85
|
END)`;
|
|
79
86
|
|