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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/hook-handoff.mjs +58 -8
- package/lib/paused-reader.mjs +10 -2
- package/lib/scrub-record.mjs +55 -16
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.10.
|
|
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.
|
|
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
|
|
376
|
-
// the secret patterns carry a value class that does not exclude
|
|
377
|
-
// whole-path scrub eats the separator and
|
|
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
|
-
|
|
391
|
-
|
|
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
|
-
|
|
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.
|
package/lib/paused-reader.mjs
CHANGED
|
@@ -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
|
|
11
|
-
//
|
|
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
|
package/lib/scrub-record.mjs
CHANGED
|
@@ -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-
|
|
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
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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
|
-
//
|
|
104
|
-
//
|
|
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
|
-
//
|
|
113
|
-
//
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
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
|
|
160
|
+
return s
|
|
122
161
|
.split(/([/\\])/)
|
|
123
162
|
.map((part) => (part === '/' || part === '\\' ? part : scrubSecrets(part)))
|
|
124
163
|
.join('');
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.10.
|
|
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.
|
|
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.
|
|
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",
|