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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "3.58.2",
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.58.2",
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
- import { notLowSignalTitleClause } from '../scoring-sql.mjs';
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 ${notLowSignalTitleClause('')}
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
- return '(\n ' + clauses.join('\n AND ') + '\n )';
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.
@@ -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 { notLowSignalTitleClause } from '../scoring-sql.mjs';
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. Shared helper keeps SQL in sync
31
- // with scoring-sql.mjs and pre-tool-recall.js Edit-fallback filter.
32
- const lowSignalIsMatchExpr = `NOT ${notLowSignalTitleClause('')}`;
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
- const file = positional.join(' ');
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
- const idStr = positional.join(',');
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
- if (rejectBareStringFlags(flags, ['title', 'files', 'lesson', 'lesson-learned', 'project', 'type'])) return;
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
- const title = positional.join(' ').trim();
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
- if (positional.length === 0) {
1051
- fail('[mem] Usage: claude-mem-lite defer drop <id-or-D#N>[,id2,...] --reason "<reason>" [--project P]');
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 = positional.join(' ').split(',').map(s => s.trim()).filter(Boolean);
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
- const idStr = positional.join(',');
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
- const raw = positional[0];
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
- const targets = entries.filter(n => AUDIT_FILE_RE.test(n)).sort();
343
- for (const name of targets) {
344
- let body = '';
345
- try {
346
- const raw = readFileSync(join(memdir, name), 'utf8');
347
- body = stripFrontmatter(raw);
348
- } catch { /* unreadable — count as missingBoth */ }
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 = targets.length;
394
+ result.total = total;
358
395
  return result;
359
396
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "3.58.2",
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.58.2",
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.58.2",
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) ────────────────────────────────────────