claude-mem-lite 3.58.2 → 3.59.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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/cli/common.mjs +33 -2
- package/lib/activity.mjs +4 -2
- package/lib/low-signal-patterns.mjs +18 -2
- package/lib/stats-quality.mjs +6 -4
- package/mem-cli.mjs +65 -28
- package/memdir.mjs +45 -8
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scoring-sql.mjs +10 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.59.1",
|
|
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.59.1",
|
|
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/cli/common.mjs
CHANGED
|
@@ -89,6 +89,37 @@ export function rejectBareStringFlags(flags, keys) {
|
|
|
89
89
|
return false;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Resolve a required value that may arrive positionally or via an MCP-field flag alias.
|
|
94
|
+
* LLM callers map the MCP tool schema onto flags (#233): mem_save.content → `--content`,
|
|
95
|
+
* mem_defer.title → `defer add --title`, mem_search.query → `--query`, mem_get.ids →
|
|
96
|
+
* `--ids` — each previously fell to a stderr-only usage line that a `2>/dev/null`
|
|
97
|
+
* caller reads as "CLI doesn't support this".
|
|
98
|
+
*
|
|
99
|
+
* Returns the resolved string ('' when neither shape is present — caller emits its own
|
|
100
|
+
* usage). On ambiguity (positional AND an alias, or two aliases at once) emits fail()
|
|
101
|
+
* and returns null — caller must `return` on null. Bare alias flags (boolean true) are
|
|
102
|
+
* ignored here; guard them with rejectBareStringFlags BEFORE calling.
|
|
103
|
+
*
|
|
104
|
+
* @param {string} positionalStr Joined positional tokens (caller picks the separator).
|
|
105
|
+
* @param {object} flags Parsed flags from parseArgs.
|
|
106
|
+
* @param {string[]} aliasKeys Alias flag names, first match wins (without dashes).
|
|
107
|
+
* @returns {string|null} Resolved value, or null after a conflict fail().
|
|
108
|
+
*/
|
|
109
|
+
export function resolvePositionalAlias(positionalStr, flags, aliasKeys) {
|
|
110
|
+
const given = aliasKeys.filter(k => typeof flags[k] === 'string' && flags[k].trim() !== '');
|
|
111
|
+
if (given.length > 1) {
|
|
112
|
+
fail(`[mem] Both --${given[0]} and --${given[1]} provided — pass the value once.`);
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
const flagVal = given.length === 1 ? flags[given[0]] : '';
|
|
116
|
+
if (positionalStr.trim() !== '' && flagVal.trim() !== '') {
|
|
117
|
+
fail(`[mem] Value given both positionally and via --${given[0]} — pass it once.`);
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return positionalStr.trim() !== '' ? positionalStr : flagVal;
|
|
121
|
+
}
|
|
122
|
+
|
|
92
123
|
// ─── Unknown-flag typo guard ─────────────────────────────────────────────────
|
|
93
124
|
|
|
94
125
|
/**
|
|
@@ -103,12 +134,12 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
103
134
|
'after', 'age-days', 'all', 'anchor', 'batch', 'before', 'benchmark', 'body', 'branch',
|
|
104
135
|
'capability-summary', 'category', 'closes-deferred', 'concepts', 'confirm', 'days', 'deep',
|
|
105
136
|
'detail', 'domain-tags', 'dry-run', 'enrich', 'execute', 'fields', 'file', 'files', 'floors',
|
|
106
|
-
'force', 'format', 'from', 'has', 'help', 'importance', 'include-compressed', 'include-noise',
|
|
137
|
+
'force', 'format', 'from', 'has', 'help', 'id', 'ids', 'content', 'importance', 'include-compressed', 'include-noise',
|
|
107
138
|
'intent-tags', 'invocation-name', 'json', 'key', 'keywords', 'lesson', 'lesson-learned', 'limit',
|
|
108
139
|
'local-path', 'margins', 'max', 'memdir', 'merge-ids', 'metrics', 'name', 'narrative', 'no-deep',
|
|
109
140
|
'offset', 'ops', 'or', 'out', 'priority', 'project', 'quality', 'query', 'reason', 'repo-url',
|
|
110
141
|
'rerank', 'resource-type', 'retain-days', 'retry', 'run', 'run-all', 'scope', 'session-audit',
|
|
111
|
-
'sidechain', 'since', 'sort', 'source', 'status', 'sweep', 'task', 'tech-stack', 'tier', 'title',
|
|
142
|
+
'sidechain', 'since', 'sort', 'source', 'status', 'sweep', 'task', 'tech-stack', 'text', 'tier', 'title',
|
|
112
143
|
'to', 'trigger-patterns', 'type', 'use-cases', 'verbose',
|
|
113
144
|
]);
|
|
114
145
|
|
package/lib/activity.mjs
CHANGED
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
import { sanitizeFtsQuery } from '../utils.mjs';
|
|
8
8
|
import { scrubRecord } from './scrub-record.mjs';
|
|
9
9
|
import { saveObservation } from './save-observation.mjs';
|
|
10
|
-
|
|
10
|
+
// Pure title-only builder: this query runs on the EVENTS table, which has no
|
|
11
|
+
// lesson_learned column — the lesson-escape variant would be a SQL error here.
|
|
12
|
+
import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
11
13
|
import { OBS_TYPE_SET } from './obs-types.mjs';
|
|
12
14
|
|
|
13
15
|
// Observation types (mirrors the observations.type enum) — events carry a wider
|
|
@@ -150,7 +152,7 @@ export function promoteInsightEvents(db, { project = null, minImportance = 2, ex
|
|
|
150
152
|
FROM events
|
|
151
153
|
WHERE body IS NOT NULL AND TRIM(body) != '' AND importance >= ?
|
|
152
154
|
AND superseded_at_epoch IS NULL
|
|
153
|
-
AND ${
|
|
155
|
+
AND ${buildNotLowSignalSql('')}
|
|
154
156
|
${projClause}
|
|
155
157
|
ORDER BY created_at_epoch DESC LIMIT ?
|
|
156
158
|
`;
|
|
@@ -50,13 +50,29 @@ export function buildLowSignalRegex() {
|
|
|
50
50
|
* Build the SQL NOT LIKE clause chain, optionally prefixed with a table alias.
|
|
51
51
|
* Output is a single parenthesized AND-chain — safe to combine with other AND/OR.
|
|
52
52
|
*
|
|
53
|
+
* `lessonEscape` (2026-07-24 audit P1, D#11): retrieval consumers on the
|
|
54
|
+
* observations table pass `{ lessonEscape: true }` to admit rows whose title
|
|
55
|
+
* matches a LOW_SIGNAL pattern but which carry a real lesson_learned — the
|
|
56
|
+
* read-side counterpart of the isNoiseObservation/capNoiseImportance write-side
|
|
57
|
+
* signal escapes. Without it, a substantive obs titled "npm pack drops …" is
|
|
58
|
+
* unsearchable forever. Escape is lesson-only by design: an importance>=2
|
|
59
|
+
* escape would resurrect pre-v2.47 Haiku-inflated noise on legacy DBs.
|
|
60
|
+
* Default stays title-only because two consumers query the events table
|
|
61
|
+
* (no lesson_learned column) and two are noise-title METRICS, not filters.
|
|
62
|
+
*
|
|
53
63
|
* @param {string} [alias=''] Table alias (e.g. 'o') — empty for unqualified.
|
|
64
|
+
* @param {object} [opts]
|
|
65
|
+
* @param {boolean} [opts.lessonEscape=false] Admit rows with non-empty, non-'none' lesson_learned.
|
|
54
66
|
* @returns {string} SQL boolean expression
|
|
55
67
|
*/
|
|
56
|
-
export function buildNotLowSignalSql(alias = '') {
|
|
68
|
+
export function buildNotLowSignalSql(alias = '', { lessonEscape = false } = {}) {
|
|
57
69
|
const p = alias ? `${alias}.` : '';
|
|
58
70
|
const clauses = LOW_SIGNAL_PATTERNS.map(({ like }) => `${p}title NOT LIKE '${like}'`);
|
|
59
|
-
|
|
71
|
+
const chain = '(\n ' + clauses.join('\n AND ') + '\n )';
|
|
72
|
+
if (!lessonEscape) return chain;
|
|
73
|
+
// Mirrors the write-side lesson test: String(lesson).trim().toLowerCase() not in ('', 'none').
|
|
74
|
+
const escape = `(${p}lesson_learned IS NOT NULL AND LOWER(TRIM(${p}lesson_learned)) NOT IN ('', 'none'))`;
|
|
75
|
+
return `(${chain} OR ${escape})`;
|
|
60
76
|
}
|
|
61
77
|
|
|
62
78
|
// Cached singleton — isNoiseObservation is called once per observation insert.
|
package/lib/stats-quality.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// Splits pure data aggregation from text rendering so MCP handlers don't
|
|
4
4
|
// collide with CLI's `out()` stdout-write pattern.
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { buildNotLowSignalSql } from './low-signal-patterns.mjs';
|
|
7
7
|
import { truncate } from '../format-utils.mjs';
|
|
8
8
|
import { COMPRESSED_PENDING_PURGE } from '../utils.mjs';
|
|
9
9
|
|
|
@@ -27,9 +27,11 @@ export function computeQualityStats(db, { project, days }) {
|
|
|
27
27
|
const baseParams = project ? [project] : [];
|
|
28
28
|
const cutoff = Date.now() - days * 86400000;
|
|
29
29
|
|
|
30
|
-
// LOW_SIGNAL match = NOT notLowSignal.
|
|
31
|
-
//
|
|
32
|
-
|
|
30
|
+
// LOW_SIGNAL match = NOT notLowSignal. Pure title-only builder: this is a
|
|
31
|
+
// METRIC counting pattern-titled rows ("Low-signal titles" in stats output),
|
|
32
|
+
// not a retrieval filter — the lesson-escape variant would silently exclude
|
|
33
|
+
// lesson-bearing rows from the count and understate title degradation.
|
|
34
|
+
const lowSignalIsMatchExpr = `NOT ${buildNotLowSignalSql('')}`;
|
|
33
35
|
|
|
34
36
|
// Narrative-text proxy for bugfix investigations that never landed a fix.
|
|
35
37
|
const unresolvedNarrativeExpr = `(
|
package/mem-cli.mjs
CHANGED
|
@@ -40,7 +40,7 @@ import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
|
40
40
|
// v2.41: shared CLI helpers extracted to cli/common.mjs. Keep this file as the
|
|
41
41
|
// router + remaining-command bodies during the incremental split. Future work:
|
|
42
42
|
// move each cmdXxx into its own cli/<cmd>.mjs; mem-cli.mjs becomes pure dispatch.
|
|
43
|
-
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue } from './cli/common.mjs';
|
|
43
|
+
import { parseArgs, out, fail, relativeTime, fmtDateShort, parseIdToken, formatProbeHints, rejectBareStringFlags, resolvePositionalAlias, suggestUnknownFlags, OBS_TIME_FIELDS, formatObsFieldValue } from './cli/common.mjs';
|
|
44
44
|
import { saveObservation } from './lib/save-observation.mjs';
|
|
45
45
|
import { rebuildObservationDerived, normalizeScope, insertObservationVector } from './lib/observation-write.mjs';
|
|
46
46
|
import { EXPORT_COLUMNS_SQL } from './lib/export-columns.mjs';
|
|
@@ -65,16 +65,18 @@ import { shouldQueueSaveEnrich, queueSaveEnrich } from './lib/save-enrich.mjs';
|
|
|
65
65
|
|
|
66
66
|
async function cmdSearch(db, args, { llm } = {}) {
|
|
67
67
|
const { positional, flags } = parseArgs(args);
|
|
68
|
-
const query = positional.join(' ');
|
|
69
|
-
if (!query) {
|
|
70
|
-
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]');
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
68
|
|
|
74
69
|
// Bare string flags parse to boolean `true`; without this guard `--branch` reaches
|
|
75
70
|
// the SQLite bind and crashes, while `--to`/`--project` silently change results
|
|
76
71
|
// (epoch-1 upper bound → zero rows; unscoped search). (audit P1 #3)
|
|
77
|
-
if (rejectBareStringFlags(flags, ['source', 'project', 'from', 'to', 'branch'])) return;
|
|
72
|
+
if (rejectBareStringFlags(flags, ['query', 'source', 'project', 'from', 'to', 'branch'])) return;
|
|
73
|
+
|
|
74
|
+
const query = resolvePositionalAlias(positional.join(' '), flags, ['query']);
|
|
75
|
+
if (query === null) return;
|
|
76
|
+
if (!query) {
|
|
77
|
+
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] — query may also be passed via --query "<query>"');
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
78
80
|
|
|
79
81
|
const limit = parseIntFlag(flags.limit, { name: '--limit', defaultValue: 20, max: 1000 });
|
|
80
82
|
const type = flags.type || null;
|
|
@@ -446,9 +448,11 @@ function cmdRecent(db, args) {
|
|
|
446
448
|
|
|
447
449
|
function cmdRecall(db, args) {
|
|
448
450
|
const { positional, flags } = parseArgs(args);
|
|
449
|
-
|
|
451
|
+
if (rejectBareStringFlags(flags, ['file'])) return;
|
|
452
|
+
const file = resolvePositionalAlias(positional.join(' '), flags, ['file']);
|
|
453
|
+
if (file === null) return;
|
|
450
454
|
if (!file) {
|
|
451
|
-
fail('[mem] Usage: claude-mem-lite recall <file> [--limit N] [--include-noise] [--json]');
|
|
455
|
+
fail('[mem] Usage: claude-mem-lite recall <file> [--limit N] [--include-noise] [--json] — file may also be passed via --file <file>');
|
|
452
456
|
return;
|
|
453
457
|
}
|
|
454
458
|
|
|
@@ -586,9 +590,11 @@ function renderEventRows(db, ids) {
|
|
|
586
590
|
|
|
587
591
|
function cmdGet(db, args) {
|
|
588
592
|
const { positional, flags } = parseArgs(args);
|
|
589
|
-
|
|
593
|
+
if (rejectBareStringFlags(flags, ['ids'])) return;
|
|
594
|
+
const idStr = resolvePositionalAlias(positional.join(','), flags, ['ids']);
|
|
595
|
+
if (idStr === null) return;
|
|
590
596
|
if (!idStr) {
|
|
591
|
-
fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt|event] [--fields f1,f2,...]\n' +
|
|
597
|
+
fail('[mem] Usage: claude-mem-lite get <id1,id2,...> [--source obs|session|prompt|event] [--fields f1,f2,...] — ids may also be passed via --ids 1,2\n' +
|
|
592
598
|
' IDs accept prefix from search output: #123 (obs), P#123 (prompt), S#123 (session), E#123 (event), D#123 (deferred item, full detail).');
|
|
593
599
|
return;
|
|
594
600
|
}
|
|
@@ -834,15 +840,22 @@ function cmdTimeline(db, args) {
|
|
|
834
840
|
|
|
835
841
|
function cmdSave(db, args) {
|
|
836
842
|
const { positional, flags } = parseArgs(args);
|
|
837
|
-
const text = positional.join(' ');
|
|
838
|
-
if (!text.trim()) {
|
|
839
|
-
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42] [--supersedes 8754,8771]');
|
|
840
|
-
return;
|
|
841
|
-
}
|
|
842
843
|
|
|
843
844
|
// Reject value-less string flags before they reach .split()/saveObservation as a
|
|
844
845
|
// boolean `true` (#8470): bare --files/--title/--lesson crashed with a raw stacktrace.
|
|
845
|
-
|
|
846
|
+
// Runs before content resolution so a bare --text gets this clean error, not the usage line.
|
|
847
|
+
if (rejectBareStringFlags(flags, ['text', 'content', 'title', 'files', 'lesson', 'lesson-learned', 'project', 'type'])) return;
|
|
848
|
+
|
|
849
|
+
// Content: positional, or --text/--content as flags-only aliases (--content is the
|
|
850
|
+
// literal MCP mem_save field name, #233). Callers coming from the MCP schema map
|
|
851
|
+
// every field to a named flag and omit the positional — the usage error then lands
|
|
852
|
+
// on stderr and reads as "CLI doesn't support save".
|
|
853
|
+
const text = resolvePositionalAlias(positional.join(' '), flags, ['text', 'content']);
|
|
854
|
+
if (text === null) return;
|
|
855
|
+
if (!text.trim()) {
|
|
856
|
+
fail('[mem] Usage: claude-mem-lite save "<text>" [--type T] [--title T] [--importance N] [--project P] [--files f1,f2] [--lesson T] [--closes-deferred 1,D#42] [--supersedes 8754,8771] — content may also be passed via --text/--content "<text>"');
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
846
859
|
|
|
847
860
|
const type = flags.type || 'discovery';
|
|
848
861
|
const validTypes = OBS_TYPE_SET;
|
|
@@ -980,9 +993,16 @@ function cmdDefer(db, args) {
|
|
|
980
993
|
|
|
981
994
|
function cmdDeferAdd(db, args) {
|
|
982
995
|
const { positional, flags } = parseArgs(args);
|
|
983
|
-
|
|
996
|
+
// Reject bare --files/--detail/--project before .split()/bind sees a boolean true (#8470).
|
|
997
|
+
// Runs before title resolution so a bare --title gets this clean error, not the usage line.
|
|
998
|
+
if (rejectBareStringFlags(flags, ['title', 'files', 'detail', 'project'])) return;
|
|
999
|
+
// --title alias: the MCP mem_defer schema's required field IS `title` (#233), so
|
|
1000
|
+
// flags-only callers emit `defer add --title "..." --detail "..."` with no positional.
|
|
1001
|
+
const resolvedTitle = resolvePositionalAlias(positional.join(' '), flags, ['title']);
|
|
1002
|
+
if (resolvedTitle === null) return;
|
|
1003
|
+
const title = resolvedTitle.trim();
|
|
984
1004
|
if (!title) {
|
|
985
|
-
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P]');
|
|
1005
|
+
fail('[mem] Usage: claude-mem-lite defer add "<title>" [--priority 1|2|3] [--detail T] [--files f1,f2] [--project P] — title may also be passed via --title "<title>"');
|
|
986
1006
|
return;
|
|
987
1007
|
}
|
|
988
1008
|
// Mirror MCP memDeferSchema.title (z.string().min(1).max(200)). CLI used to
|
|
@@ -992,8 +1012,6 @@ function cmdDeferAdd(db, args) {
|
|
|
992
1012
|
fail(`[mem] defer add: title too long (${title.length} chars, max 200). Move detail to --detail "<text>".`);
|
|
993
1013
|
return;
|
|
994
1014
|
}
|
|
995
|
-
// Reject bare --files/--detail/--project before .split()/bind sees a boolean true (#8470).
|
|
996
|
-
if (rejectBareStringFlags(flags, ['files', 'detail', 'project'])) return;
|
|
997
1015
|
const priority = flags.priority !== undefined ? parseInt(flags.priority, 10) : 2;
|
|
998
1016
|
// isNumericToken first: bare parseInt would coerce "3xyz"→3 and silently escalate a
|
|
999
1017
|
// deferred item's urgency. Float literals still truncate (#8277).
|
|
@@ -1047,8 +1065,12 @@ function cmdDeferList(db, args) {
|
|
|
1047
1065
|
|
|
1048
1066
|
function cmdDeferDrop(db, args) {
|
|
1049
1067
|
const { positional, flags } = parseArgs(args);
|
|
1050
|
-
|
|
1051
|
-
|
|
1068
|
+
// --id alias (MCP mem_defer_drop.id field shape, #233).
|
|
1069
|
+
if (rejectBareStringFlags(flags, ['id'])) return;
|
|
1070
|
+
const idStr = resolvePositionalAlias(positional.join(' '), flags, ['id']);
|
|
1071
|
+
if (idStr === null) return;
|
|
1072
|
+
if (!idStr.trim()) {
|
|
1073
|
+
fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N>[,id2,...] --reason "<reason>" [--project P] — id may also be passed via --id D#N');
|
|
1052
1074
|
return;
|
|
1053
1075
|
}
|
|
1054
1076
|
const reason = flags.reason;
|
|
@@ -1060,7 +1082,7 @@ function cmdDeferDrop(db, args) {
|
|
|
1060
1082
|
// already accepts the batch form (cmdSave uses resolveDeferredIds on a split list);
|
|
1061
1083
|
// drop now mirrors that ergonomic so users can prune multiple items in one call
|
|
1062
1084
|
// without N shell invocations.
|
|
1063
|
-
const rawTokens =
|
|
1085
|
+
const rawTokens = idStr.split(',').map(s => s.trim()).filter(Boolean);
|
|
1064
1086
|
const tokens = rawTokens.map(t => /^\d+$/.test(t) ? parseInt(t, 10) : t);
|
|
1065
1087
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
1066
1088
|
|
|
@@ -1435,9 +1457,11 @@ function getActiveSessionId(db, project) {
|
|
|
1435
1457
|
|
|
1436
1458
|
function cmdDelete(db, args) {
|
|
1437
1459
|
const { positional, flags } = parseArgs(args);
|
|
1438
|
-
|
|
1460
|
+
if (rejectBareStringFlags(flags, ['ids'])) return;
|
|
1461
|
+
const idStr = resolvePositionalAlias(positional.join(','), flags, ['ids']);
|
|
1462
|
+
if (idStr === null) return;
|
|
1439
1463
|
if (!idStr) {
|
|
1440
|
-
fail('[mem] Usage: claude-mem-lite delete <id1,id2,...> [--confirm]');
|
|
1464
|
+
fail('[mem] Usage: claude-mem-lite delete <id1,id2,...> [--confirm] — ids may also be passed via --ids 1,2');
|
|
1441
1465
|
return;
|
|
1442
1466
|
}
|
|
1443
1467
|
|
|
@@ -1490,7 +1514,11 @@ function cmdDelete(db, args) {
|
|
|
1490
1514
|
|
|
1491
1515
|
function cmdUpdate(db, args) {
|
|
1492
1516
|
const { positional, flags } = parseArgs(args);
|
|
1493
|
-
|
|
1517
|
+
// --id alias (MCP mem_update.id field shape, #233). Bare --id → resolved as absent
|
|
1518
|
+
// here (boolean true is not a string), falling through to the usage line below.
|
|
1519
|
+
const resolvedId = resolvePositionalAlias(positional[0] ?? '', flags, ['id']);
|
|
1520
|
+
if (resolvedId === null) return;
|
|
1521
|
+
const raw = resolvedId || undefined;
|
|
1494
1522
|
if (raw && /^[EePpSs]#?\d+$/.test(String(raw).trim())) {
|
|
1495
1523
|
fail(`[mem] update only works on observations. Rejected: ${raw}. ` +
|
|
1496
1524
|
`Prompts, sessions, and events are not editable here.`);
|
|
@@ -1502,7 +1530,7 @@ function cmdUpdate(db, args) {
|
|
|
1502
1530
|
const parsed = raw ? parseIdToken(raw) : null;
|
|
1503
1531
|
const id = parsed && parsed.source === null ? parsed.id : NaN;
|
|
1504
1532
|
if (!id || isNaN(id)) {
|
|
1505
|
-
fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T]');
|
|
1533
|
+
fail('[mem] Usage: claude-mem-lite update <id> [--title T] [--type T] [--importance N] [--lesson T] [--narrative T] [--concepts T] — id may also be passed via --id N');
|
|
1506
1534
|
return;
|
|
1507
1535
|
}
|
|
1508
1536
|
|
|
@@ -2606,6 +2634,7 @@ function cmdHelp() {
|
|
|
2606
2634
|
|
|
2607
2635
|
Commands:
|
|
2608
2636
|
search <query> FTS5 search across observations, sessions, and prompts
|
|
2637
|
+
--query Q Query as a flag (alias for the positional; use one, not both)
|
|
2609
2638
|
--source S Table: observations|sessions|prompts (default: all)
|
|
2610
2639
|
--type T Filter obs type (bugfix|decision|discovery|feature|refactor|change)
|
|
2611
2640
|
--limit N Max results (default 20)
|
|
@@ -2630,6 +2659,7 @@ Commands:
|
|
|
2630
2659
|
--json Output as JSON: {project,limit,type,total,results:[…]}
|
|
2631
2660
|
|
|
2632
2661
|
recall <file> Show observations related to a file
|
|
2662
|
+
--file F File as a flag (alias for the positional)
|
|
2633
2663
|
--limit N Max results (default 10)
|
|
2634
2664
|
--include-noise Include hook-llm fallback titles ("Modified X", raw error logs)
|
|
2635
2665
|
--json Output as JSON: {file,limit,include_noise,total,results:[…]}
|
|
@@ -2638,6 +2668,7 @@ Commands:
|
|
|
2638
2668
|
IDs accept search-output prefixes: #123 (obs), P#123 (prompt), S#123 (session),
|
|
2639
2669
|
D#123 (deferred item — FULL detail; defer list is title-only).
|
|
2640
2670
|
Bare N defaults to obs. Mixed prefixes in one call route each token correctly.
|
|
2671
|
+
--ids 1,2 IDs as a flag (alias for the positional list)
|
|
2641
2672
|
--source S Force record type (obs|session|prompt); overrides prefixes
|
|
2642
2673
|
(D# tokens exempt — they always read deferred_work).
|
|
2643
2674
|
--fields f1,f2,... Select specific fields to return (observations only).
|
|
@@ -2657,6 +2688,8 @@ Commands:
|
|
|
2657
2688
|
(or {anchor:null,fallback:"recent",results:[…]} when no anchor)
|
|
2658
2689
|
|
|
2659
2690
|
save "<text>" Save a new observation
|
|
2691
|
+
--text T Content as a flag (alias for the positional; use one, not both)
|
|
2692
|
+
--content T Same alias under the MCP mem_save field name
|
|
2660
2693
|
--type T Observation type (default: discovery)
|
|
2661
2694
|
--title T Title (auto-generated if omitted)
|
|
2662
2695
|
--importance N 1=routine, 2=notable, 3=critical (default: 2)
|
|
@@ -2667,6 +2700,7 @@ Commands:
|
|
|
2667
2700
|
|
|
2668
2701
|
defer <action> First-class deferred work (v2.70+)
|
|
2669
2702
|
add "<title>" Mark deferred work for next session (≤200 chars)
|
|
2703
|
+
--title T Title as a flag (alias for the positional)
|
|
2670
2704
|
--priority N 1=low, 2=normal, 3=urgent (default: 2)
|
|
2671
2705
|
--detail T Constraint + why deferred
|
|
2672
2706
|
--files f1,f2 Comma-separated file paths
|
|
@@ -2675,14 +2709,17 @@ Commands:
|
|
|
2675
2709
|
--limit N Max results (default 10)
|
|
2676
2710
|
--project P Filter by project
|
|
2677
2711
|
drop <D#N|ordinal>[,...] Drop one or more deferred items (no fix needed)
|
|
2712
|
+
--id D#N ID as a flag (alias for the positional)
|
|
2678
2713
|
--reason "..." Required audit trail
|
|
2679
2714
|
--project P Project for ordinal resolution (default: current; must
|
|
2680
2715
|
match the "defer list --project P" you read ordinals from)
|
|
2681
2716
|
|
|
2682
2717
|
delete <id1,id2,...> Delete observations by ID
|
|
2718
|
+
--ids 1,2 IDs as a flag (alias for the positional list)
|
|
2683
2719
|
--confirm Execute deletion (preview by default)
|
|
2684
2720
|
|
|
2685
2721
|
update <id> Update an existing observation
|
|
2722
|
+
--id N ID as a flag (alias for the positional)
|
|
2686
2723
|
--title T New title
|
|
2687
2724
|
--type T New type
|
|
2688
2725
|
--importance N New importance (1=routine, 2=notable, 3=critical)
|
package/memdir.mjs
CHANGED
|
@@ -304,9 +304,30 @@ export function removePluginDoc(memdir, slug) {
|
|
|
304
304
|
// noise.
|
|
305
305
|
|
|
306
306
|
const AUDIT_FILE_RE = /^(feedback|project)_[A-Za-z0-9_-]+\.md$/;
|
|
307
|
+
// Legacy skip prefixes: user_*/reference_* have no Why/How requirement. A known
|
|
308
|
+
// filename prefix wins over frontmatter type (a user_*.md stays excluded even if
|
|
309
|
+
// its frontmatter says feedback).
|
|
310
|
+
const SKIP_FILE_RE = /^(user|reference)_[A-Za-z0-9_-]+\.md$/;
|
|
307
311
|
const WHY_RE = /^\s*\*\*Why:\*\*/m;
|
|
308
312
|
const HOW_RE = /^\s*\*\*How to apply:\*\*/m;
|
|
309
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Extract the memory type from a file's YAML frontmatter (2026-07-24 audit P2).
|
|
316
|
+
* The current CC harness writes kebab-case filenames (ship-runbook.md) and puts
|
|
317
|
+
* the type in frontmatter — either top-level `type: X` or nested under
|
|
318
|
+
* `metadata:` as ` type: X`. Anchored `^\s*type:` cannot match `node_type:`.
|
|
319
|
+
*
|
|
320
|
+
* @param {string} raw Full file content
|
|
321
|
+
* @returns {string|null} Lowercased type, or null when absent/no frontmatter
|
|
322
|
+
*/
|
|
323
|
+
function frontmatterType(raw) {
|
|
324
|
+
if (!raw.startsWith('---\n') && !raw.startsWith('---\r\n')) return null;
|
|
325
|
+
const m = raw.match(/^---\r?\n([\s\S]*?)\r?\n---(\r?\n|$)/);
|
|
326
|
+
if (!m) return null;
|
|
327
|
+
const t = m[1].match(/^\s*type:\s*([A-Za-z]+)\s*$/m);
|
|
328
|
+
return t ? t[1].toLowerCase() : null;
|
|
329
|
+
}
|
|
330
|
+
|
|
310
331
|
/**
|
|
311
332
|
* Strip the leading YAML frontmatter block (between `---` fences) so audit
|
|
312
333
|
* checks run only against body content. Returns input unchanged if no
|
|
@@ -339,13 +360,29 @@ export function auditMemdir(memdir) {
|
|
|
339
360
|
let entries;
|
|
340
361
|
try { entries = readdirSync(memdir); } catch { return result; }
|
|
341
362
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
363
|
+
// Two selection paths (2026-07-24 audit P2):
|
|
364
|
+
// 1. legacy filename prefix — feedback_*/project_* audited, user_*/reference_* skipped
|
|
365
|
+
// 2. kebab-case (current harness) — frontmatter type ∈ {feedback, project}
|
|
366
|
+
// Path 2 needs the file content; read once and reuse for the body check.
|
|
367
|
+
const candidates = entries
|
|
368
|
+
.filter(n => n.endsWith('.md') && n !== 'MEMORY.md' && !n.startsWith('.'))
|
|
369
|
+
.sort();
|
|
370
|
+
let total = 0;
|
|
371
|
+
for (const name of candidates) {
|
|
372
|
+
const legacyAudit = AUDIT_FILE_RE.test(name);
|
|
373
|
+
if (!legacyAudit && SKIP_FILE_RE.test(name)) continue;
|
|
374
|
+
|
|
375
|
+
let raw = null;
|
|
376
|
+
try { raw = readFileSync(join(memdir, name), 'utf8'); } catch { /* unreadable */ }
|
|
377
|
+
|
|
378
|
+
if (!legacyAudit) {
|
|
379
|
+
// Frontmatter decides; unreadable or untyped files carry no Why/How contract.
|
|
380
|
+
const type = raw === null ? null : frontmatterType(raw);
|
|
381
|
+
if (type !== 'feedback' && type !== 'project') continue;
|
|
382
|
+
}
|
|
383
|
+
// Legacy-prefixed unreadable file keeps the old behavior: counted as missingBoth.
|
|
384
|
+
const body = raw === null ? '' : stripFrontmatter(raw);
|
|
385
|
+
total += 1;
|
|
349
386
|
|
|
350
387
|
const hasWhy = WHY_RE.test(body);
|
|
351
388
|
const hasHow = HOW_RE.test(body);
|
|
@@ -354,6 +391,6 @@ export function auditMemdir(memdir) {
|
|
|
354
391
|
else if (!hasWhy) result.missingWhy.push(name);
|
|
355
392
|
else result.missingHowToApply.push(name);
|
|
356
393
|
}
|
|
357
|
-
result.total =
|
|
394
|
+
result.total = total;
|
|
358
395
|
return result;
|
|
359
396
|
}
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.59.1",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "3.
|
|
9
|
+
"version": "3.59.1",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
12
12
|
"better-sqlite3": "^12.6.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.59.1",
|
|
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/scoring-sql.mjs
CHANGED
|
@@ -152,8 +152,17 @@ export function noisePenaltyClause(alias = 'o') {
|
|
|
152
152
|
// The SQL path (this), the regex path (utils.mjs::LOW_SIGNAL_TITLE), and the
|
|
153
153
|
// pre-tool-recall.js inline SQL now all derive from one authoritative
|
|
154
154
|
// pattern list. Previously hand-mirrored with "keep in sync" comments.
|
|
155
|
+
//
|
|
156
|
+
// lessonEscape (2026-07-24 audit P1, D#11): every consumer of THIS clause is an
|
|
157
|
+
// observations-table retrieval surface (search, recall, error-recall, context/
|
|
158
|
+
// handoff/UPS injection, optimize candidates), so all get the read-side lesson
|
|
159
|
+
// escape — a low-signal TITLE no longer hides a row with a real lesson_learned.
|
|
160
|
+
// Consumers that must stay title-only import buildNotLowSignalSql directly:
|
|
161
|
+
// events-table queries (lib/activity.mjs, pre-tool-recall.js events fallback —
|
|
162
|
+
// no lesson_learned column) and noise-title metrics (lib/stats-core.mjs,
|
|
163
|
+
// lib/stats-quality.mjs — they COUNT pattern-titled rows, not filter them).
|
|
155
164
|
export function notLowSignalTitleClause(alias = 'o') {
|
|
156
|
-
return buildNotLowSignalSql(alias);
|
|
165
|
+
return buildNotLowSignalSql(alias, { lessonEscape: true });
|
|
157
166
|
}
|
|
158
167
|
|
|
159
168
|
// ─── Cite-history factor (A1, v2.83) ────────────────────────────────────────
|