claude-mem-lite 6.10.0 → 6.10.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.
@@ -9,7 +9,7 @@
9
9
  "plugins": [
10
10
  {
11
11
  "name": "claude-mem-lite",
12
- "version": "6.10.0",
12
+ "version": "6.10.1",
13
13
  "source": "./",
14
14
  "homepage": "https://github.com/sdsrss/claude-mem-lite",
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)."
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.10.0",
3
+ "version": "6.10.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/hook-handoff.mjs CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  notLowSignalTitleClause,
15
15
  neutralizeContextDelimiters,
16
16
  } from './utils.mjs';
17
- import { scrubRecord, scrubFilePath } from './lib/scrub-record.mjs';
17
+ import { scrubRecord, scrubFilePath, scrubFilePaths } from './lib/scrub-record.mjs';
18
18
  import {
19
19
  HANDOFF_EXPIRY_CLEAR,
20
20
  HANDOFF_EXPIRY_EXIT,
@@ -372,9 +372,10 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
372
372
  // rewrite the serialized form risks breaking the downstream JSON.parse. Same rule
373
373
  // key_files follows below.
374
374
  nextSteps = JSON.stringify({
375
- // scrubFilePath, not scrubSecrets: this field is a filesystem PATH, and eight of
376
- // the secret patterns carry a value class that does not exclude `/`, so a
377
- // whole-path scrub eats the separator and destroys the filename. That is the named
375
+ // scrubFilePath, not scrubSecrets: this field is a filesystem PATH, and many of
376
+ // the secret patterns carry a value class that does not exclude `/` (count and
377
+ // population: lib/scrub-record.mjs), so a whole-path scrub eats the separator and
378
+ // destroys the filename. That is the named
378
379
  // mechanism this repo grew for exactly this shape; the prose fields below are prose
379
380
  // and correctly take the plain scrub.
380
381
  file: scrubFilePath(String(note.file)),
@@ -386,9 +387,44 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
386
387
  /* best-effort, like the task reader above — never block the handoff */
387
388
  }
388
389
 
389
- // 6. Match keywords
390
- const allText = [workingOn, ...completed.map((c) => c.title).filter(Boolean), unfinished].join(' ');
391
- const keywords = extractMatchKeywords(allText, [...fileSet]);
390
+ // 6. Match keywords.
391
+ //
392
+ // Scrubbed at the DERIVATION, and the scrubbed file array is derived ONCE and feeds both
393
+ // sinks (here and key_files below) — the shape hook-llm.mjs uses where one path array
394
+ // reaches two columns. lib/scrub-record.mjs excludes match_keywords from scrubRecord, and
395
+ // the reason it recorded — "built from tokenizeHandoff() output (alphanumeric tokens
396
+ // only), so secrets cannot survive the upstream tokenizer" — does not hold on either arm:
397
+ // the FILE arm never reaches the tokenizer (it takes basename-minus-extension straight off
398
+ // this set, which holds RAW paths), and the tokenizer SPLITS a secret from its keyword
399
+ // rather than removing it, so `token=ghp_…` contributes `ghp_…` as a term of its own.
400
+ //
401
+ // Exposure, measured rather than asserted: nothing renders this column and no export face
402
+ // reads the table — `EXPORT_COLUMNS` is observations-only and `session_handoffs` has zero
403
+ // occurrences in server.mjs and the CLI. So this is local-DB-at-rest, with no egress path.
404
+ // A credential in a stored column is still worth removing; it is not a disclosure. (The
405
+ // sentence this replaces claimed egress through `export` and was false — a replacement
406
+ // justification written while retracting another one, unverified, which is this repo's
407
+ // signature recurrence. Pre-ship claims lens.)
408
+ //
409
+ // Per ELEMENT, then join — never scrub the concatenation. A credential noun ending one
410
+ // element and a `=`/`:` opening the next form a match that exists in NEITHER, and the
411
+ // derived term set then loses a word both columns keep (measured: `zebrafish`). Same rule
412
+ // next_steps and key_files already follow, and the same rule the truncation note below
413
+ // states. NOT identity on ordinary prose, which an earlier draft of this comment claimed:
414
+ // `api_key: handling` loses `handling` (5 of 6 ordinary developer prompts in a directed
415
+ // grid lose exactly one term). What is true, and is the actual justification, is that the
416
+ // term set now AGREES with what the resuming session is shown — `working_on` / `completed`
417
+ // / `unfinished` lose the same word through scrubRecord below. No value is scrubbed twice:
418
+ // these elements and the columns below are separate derivations from one raw source, each
419
+ // scrubbed once, which is the distinction D#46 is open about.
420
+ const safeFiles = scrubFilePaths([...fileSet]);
421
+ // The nullish guard mirrors what join() already did with a nullish element. Without it
422
+ // String(undefined) would put the literal token "undefined" into the term set — a behaviour
423
+ // change smuggled in by the per-element rewrite rather than chosen.
424
+ const allText = [workingOn, ...completed.map((c) => c.title).filter(Boolean), unfinished]
425
+ .map((t) => (t === null || t === undefined ? '' : scrubSecrets(String(t))))
426
+ .join(' ');
427
+ const keywords = extractMatchKeywords(allText, safeFiles);
392
428
 
393
429
  // T10d: capture HEAD sha so detectContinuationIntent can anchor on it later.
394
430
  // Best-effort — failures (non-git dir, missing binary, timeout) yield null.
@@ -427,7 +463,21 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
427
463
  key_decisions: decisions.map((d) => `[${d.type}] ${d.title}`).join('\n'),
428
464
  match_keywords: keywords,
429
465
  });
430
- const safeKeyFiles = JSON.stringify([...fileSet].slice(0, 20).map((f) => scrubSecrets(String(f))));
466
+ // scrubFilePath, not scrubSecrets — the same correction next_steps.file took above, and
467
+ // key_files was the last of the six path columns still taking the whole-string form. It
468
+ // was already element-wise, which is what made it look compliant with this module's
469
+ // prescription; the function was the wrong one. Many SECRET_PATTERNS have a value class
470
+ // that does not exclude `/` (count and population: lib/scrub-record.mjs), so a whole-path
471
+ // match eats the separator and the filename
472
+ // with it, and the renderer below emits `basename(f)` — `## Key Files` read `password=***`
473
+ // where the file was `notes.mjs`. Worse than a wrong name: fileSet is keyed on the RAW
474
+ // path, so two files under one credential-bearing directory survive dedup and then
475
+ // collapse onto the identical stored string.
476
+ // `safeFiles` was derived at the keywords block above, so both sinks see one scrubbed
477
+ // array rather than two independent scrubs of the same paths. Slicing after the map is
478
+ // equivalent to mapping after the slice (per-element, order-preserving) and keeps the
479
+ // keyword arm on the FULL set, which is what it read before.
480
+ const safeKeyFiles = JSON.stringify(safeFiles.slice(0, 20));
431
481
  // The UPSERT below resets `consumed_at`. Rewriting a handoff makes it fresh again, so it
432
482
  // must become injectable again: the DELETE that consumeHandoff replaced did this
433
483
  // implicitly (row gone, next build INSERTed a new one), while the UPSERT reuses the row.
@@ -7,8 +7,16 @@
7
7
  // while the answer sat in the repo in prose.
8
8
  //
9
9
  // It is also why the next step is taken from a FILE rather than from a model summary:
10
- // session_summaries.next_steps is non-empty in 11 of 310 rows (3.5%), so that route has
11
- // already been measured and does not work.
10
+ // session_summaries.next_steps is thin, but quote BOTH numbers, because the lifetime
11
+ // average hides a trend and a single average was the original justification: 15 of 318 rows
12
+ // non-empty over the corpus lifetime (4.7%), and 4 of the newest 20 (20%). So the honest
13
+ // form is "unreliable", not "does not work" — the recent regime is four times the average.
14
+ // The reason to read a FILE instead is not the rate anyway: a paused note is written by a
15
+ // human on purpose and names its own verify command, which is a different kind of signal
16
+ // from a field an LLM fills in when it happens to.
17
+ //
18
+ // The pre-ship claims lens raised this, reporting 4 of the newest 5 non-empty; that specific
19
+ // figure did not reproduce (measured 1 of 5, 4 of 20). The caveat stood, its number did not.
12
20
  //
13
21
  // A leaf on purpose — `fs`/`path` only, no package imports and no edge back into the hook
14
22
  // layer, so importing it costs nothing at load time (same reason lib/data-paths.mjs is a
@@ -17,6 +17,13 @@
17
17
  // .files_modified / .files_read, observation_files.filename and events
18
18
  // .file_paths all stored raw paths while the title DERIVED FROM THE SAME PATH
19
19
  // was scrubbed. A prescription in a comment is not a mechanism; the helper is.
20
+ //
21
+ // Second axis, found after D#44 closed: that one compliant call site was compliant
22
+ // in SHAPE only. It mapped `scrubSecrets` over the elements — element-wise, as
23
+ // prescribed — and so read as the model the other five were measured against, while
24
+ // taking the whole-string function this module exists to keep off a path. "Follows the
25
+ // rule" and "calls the helper" are different claims, and only the second is checkable.
26
+ // Which is why the prescription above names `scrubFilePaths` rather than describing it.
20
27
 
21
28
  import { scrubSecrets } from '../secret-scrub.mjs';
22
29
 
@@ -52,7 +59,12 @@ export const TEXT_FIELDS_BY_TABLE = {
52
59
  'completed',
53
60
  'unfinished',
54
61
  // Excluded:
55
- // key_files — JSON.stringify(array); pre-scrub elements at call site
62
+ // key_files — JSON.stringify(array); pre-scrubbed at the call site via
63
+ // `scrubFilePaths`, NOT a bare per-element scrubSecrets. It held
64
+ // filesystem paths and took the whole-string function through
65
+ // v6.10.0, which is the defect scrubFilePath's own docblock
66
+ // describes; the renderer emits basename(), so the destroyed
67
+ // filename was what the resuming session was shown.
56
68
  // next_steps — same shape, same reason: JSON.stringify({file,title,items}),
57
69
  // pre-scrubbed element-wise in buildAndSaveHandoff. Listing it
58
70
  // here would let scrubSecrets rewrite the SERIALIZED JSON, and the
@@ -60,12 +72,20 @@ export const TEXT_FIELDS_BY_TABLE = {
60
72
  // Next steps section would disappear silently rather than fail.
61
73
  // Named here because the pre-ship review found the column had been
62
74
  // added without updating this block, which is the file's contract.
63
- // match_keywords — currently a space-joined plain string; keeping it
64
- // here would scrub safely, but the value is built from
65
- // tokenizeHandoff() output (alphanumeric tokens only),
66
- // so secrets cannot survive the upstream tokenizer.
67
- // Excluded to avoid double-work + future-proof against
68
- // a refactor that switches to JSON.stringify.
75
+ // match_keywords — currently a space-joined plain string. Excluded because the
76
+ // value is scrubbed at its DERIVATION in buildAndSaveHandoff
77
+ // (`scrubSecrets(allText)` + the shared `safeFiles` array), which
78
+ // also future-proofs against a refactor to JSON.stringify.
79
+ //
80
+ // RETRACTED, measured: the reason recorded here until v6.10.0 was
81
+ // "built from tokenizeHandoff() output (alphanumeric tokens only),
82
+ // so secrets cannot survive the upstream tokenizer." Neither half
83
+ // holds. extractMatchKeywords has TWO arms and the FILE arm never
84
+ // reaches the tokenizer at all — it takes basename-minus-extension
85
+ // off the raw path set. And the tokenizer splits a secret from its
86
+ // keyword rather than removing it: `token=ghp_…` yields `ghp_…` as
87
+ // a term of its own. A no-op justification is worse than no
88
+ // justification, because it retires the question.
69
89
  // key_decisions is kept: call site uses '\n'.join (plain string), and
70
90
  // decision titles can carry secrets verbatim (LLM output).
71
91
  'key_decisions',
@@ -100,8 +120,16 @@ export const TEXT_FIELDS_BY_TABLE = {
100
120
  */
101
121
  export function scrubFilePath(p) {
102
122
  // SEGMENT-WISE, and that is the whole point rather than a micro-optimisation.
103
- // Eight SECRET_PATTERNS carry a value class that does not exclude `/`
104
- // (secret-scrub.mjs:33/74/78/83/98/109/259/260). On prose that is correct; run
123
+ // Many SECRET_PATTERNS carry a value class that does not exclude `/`. This is the one place
124
+ // that number is stated, with its population, because it is GRID-DEPENDENT and five copies of
125
+ // a bare "eight" is how it went wrong: measured here, 11 of the 40 patterns match text
126
+ // spanning `/` on a 15-shape credential grid; the two v6.10.1 pre-ship lenses measured 12
127
+ // (1130 path probes) and 15 (26-shape grid), and 21 on a structural reading of the value
128
+ // classes. "Eight", carried since v6.8.2 with the enumeration
129
+ // secret-scrub.mjs:33/74/78/83/98/109/259/260, is an UNDER-count on every one of those
130
+ // populations — it omits at least the `Authorization:`, `AccountKey=`, `DATABASE_URL`/
131
+ // `SUPABASE_KEY` and db-connection-URL arms. The mechanism does not depend on the count.
132
+ // On prose the wide value class is correct; run
105
133
  // whole-path, the match eats the separator and everything after it, so
106
134
  // `/repo/token=<secret>/notes.mjs` became `/repo/token=***` — the filename
107
135
  // destroyed at WRITE time and unrecoverable, and every file under such a
@@ -109,16 +137,27 @@ export function scrubFilePath(p) {
109
137
  // recalled another file's observations). Splitting first bounds every pattern to
110
138
  // the segment it matched in.
111
139
  //
112
- // The trade, stated rather than glossed: a credential whose own syntax spans a
113
- // separator is no longer caught here — the Slack webhook path and the
114
- // `scheme://user:pass@host` arms both need their `/` characters. Those are URL
115
- // shapes, and these columns hold filesystem paths; `scrubSecrets` still runs
116
- // whole-string on every prose field, which is where a URL actually lands.
117
- // Preserving path structure wins because the path IS the recall key.
140
+ // A credential whose own syntax SPANS a separator — `scheme://user:pass@host`, the Slack
141
+ // webhook path, a `postgres://` connection string — needs its `/` characters to match at all,
142
+ // so segment-wise scrubbing cannot see it. That was accepted here until v6.10.1 on the stated
143
+ // reason "these columns hold filesystem paths", and the pre-ship review measured that premise
144
+ // FALSE: `lib/save-observation.mjs` filters `params.files` on `typeof f === 'string'` and
145
+ // nothing else, and `extractFilePaths` returns a URL verbatim from a `{path}` / `{filePath}`
146
+ // tool input under a `PostToolUse: *` matcher. A URL reaches these columns. Measured: four URL
147
+ // shapes v6.10.0 redacted were being stored verbatim.
118
148
  //
149
+ // So the value decides, not the column: when it carries `://` it is scrubbed whole-string, the
150
+ // only way those patterns match. NAMED COST, pinned in tests/secret-scrub-coverage.test.mjs: a
151
+ // URL whose credential sits in a path SEGMENT now loses its filename to the greedy value class,
152
+ // which is the thing segment-wise scrubbing exists to prevent, traded back on this one shape.
153
+ // Redacting a live credential wins over preserving a filename that is not a recall key. The
154
+ // guard is `scrubSecrets`-decided, not scheme-decided — a credential-free URL comes back
155
+ // byte-identical, so ordinary `https://` / `s3://` / `file://` values are untouched.
156
+ const s = String(p ?? '');
157
+ if (s.includes('://')) return scrubSecrets(s);
119
158
  // The capture group keeps the separators in the split output, so join()
120
159
  // reconstructs the original byte-for-byte when nothing matches.
121
- return String(p ?? '')
160
+ return s
122
161
  .split(/([/\\])/)
123
162
  .map((part) => (part === '/' || part === '\\' ? part : scrubSecrets(part)))
124
163
  .join('');
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.10.0",
3
+ "version": "6.10.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "6.10.0",
9
+ "version": "6.10.1",
10
10
  "os": [
11
11
  "darwin",
12
12
  "linux",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.10.0",
3
+ "version": "6.10.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",