claude-mem-lite 3.9.1 → 3.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/lib/import-jsonl.mjs +9 -1
- package/lib/maintain-core.mjs +5 -0
- package/lib/search-core.mjs +19 -2
- package/mem-cli.mjs +69 -14
- package/package.json +1 -1
- package/secret-scrub.mjs +17 -9
- package/server.mjs +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.11.0",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark)."
|
|
16
16
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sdsrss"
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -163,6 +163,12 @@ export async function importJsonl(db, path, { project }) {
|
|
|
163
163
|
|
|
164
164
|
const pendingToolUse = new Map();
|
|
165
165
|
let prompts = 0, observations = 0, skipped = 0;
|
|
166
|
+
// Count lines that ARE Claude Code transcript events (user/assistant/tool_result),
|
|
167
|
+
// independent of whether they produced a new row. Lets the caller tell apart a
|
|
168
|
+
// genuine wrong-shape file (export output / garbage → recognized 0) from a valid
|
|
169
|
+
// transcript that was simply already imported (recognized > 0, all deduped) — the
|
|
170
|
+
// "0 imported, N skipped" warning must not cry "wrong shape" at an idempotent re-run.
|
|
171
|
+
let recognized = 0;
|
|
166
172
|
|
|
167
173
|
// Snapshot importToolPair so we can wrap it with a per-run uniqueness
|
|
168
174
|
// check that hits both in-call and cross-call dedup. (Inline because we
|
|
@@ -193,6 +199,8 @@ export async function importJsonl(db, path, { project }) {
|
|
|
193
199
|
if (!line.trim()) continue;
|
|
194
200
|
const ev = parseLine(line);
|
|
195
201
|
if (!ev) { skipped++; continue; }
|
|
202
|
+
// Transcript-shape signal (incl. embedded + top-level tool_result, #8413).
|
|
203
|
+
if (ev.type === 'user' || ev.type === 'assistant' || ev.type === 'tool_result') recognized++;
|
|
196
204
|
if (ev.type === 'user') {
|
|
197
205
|
// Real Claude Code transcripts wrap tool_result inside a user-typed
|
|
198
206
|
// event's message.content array (alongside the rare text part). The
|
|
@@ -256,5 +264,5 @@ export async function importJsonl(db, path, { project }) {
|
|
|
256
264
|
tx2();
|
|
257
265
|
}
|
|
258
266
|
|
|
259
|
-
return { prompts, observations, skipped, orphans };
|
|
267
|
+
return { prompts, observations, skipped, orphans, recognized };
|
|
260
268
|
}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -301,7 +301,12 @@ export function maintenanceStats(db, { projectFilter, baseParams, staleAge }) {
|
|
|
301
301
|
const stats = db.prepare(`
|
|
302
302
|
SELECT
|
|
303
303
|
COUNT(*) as total,
|
|
304
|
+
-- injection_count=0 MUST mirror decayAndMarkIdle's mark-idle guard (#8614):
|
|
305
|
+
-- the scan stat previews what decay will mark idle, and decay protects
|
|
306
|
+
-- injected rows. Omitting it over-counted "stale" by the injected-but-decayed
|
|
307
|
+
-- rows decay never touches (e.g. demote_pinned's output: imp=1 but inj>0).
|
|
304
308
|
COALESCE(SUM(CASE WHEN COALESCE(importance, 1) = 1 AND COALESCE(access_count, 0) = 0
|
|
309
|
+
AND COALESCE(injection_count, 0) = 0
|
|
305
310
|
AND created_at_epoch < ? THEN 1 ELSE 0 END), 0) as stale,
|
|
306
311
|
COALESCE(SUM(CASE WHEN (title IS NULL OR title = '') AND (narrative IS NULL OR narrative = '')
|
|
307
312
|
THEN 1 ELSE 0 END), 0) as broken,
|
package/lib/search-core.mjs
CHANGED
|
@@ -59,9 +59,26 @@ export function parseDateBounds(fromRaw, toRaw) {
|
|
|
59
59
|
* per-source SQL double-applied it and gapped/overlapped pages, because the
|
|
60
60
|
* obs hybrid path (AND→OR fallback / vector / concept stages) re-adds rows the
|
|
61
61
|
* SQL OFFSET already skipped (#8217/#8638).
|
|
62
|
+
*
|
|
63
|
+
* D#30 re-audit (reopened): perSourceLimit MUST be offset-independent. The old
|
|
64
|
+
* `offset + limit + 10` term grew the pool for deeper pages, and because RRF
|
|
65
|
+
* fusion of FTS + vector ranks is candidate-pool-sensitive (an item present in
|
|
66
|
+
* BOTH lists outranks one in a single list), a larger pool RE-RANKS the prefix —
|
|
67
|
+
* so page(offset=0) and page(offset=N) sliced DIFFERENT orderings and overlapped
|
|
68
|
+
* /gapped on the real (vector-populated) DB. #8642 missed this because its guard
|
|
69
|
+
* test seeds no vectors (FTS-only is prefix-stable). The fix makes the pool a
|
|
70
|
+
* function of `limit` only:
|
|
71
|
+
* - MIN_FUSION_POOL floor (= default limit 20 × the 3× buffer): every limit ≤ 20
|
|
72
|
+
* fuses ONE 60-candidate pool, so same-limit --offset pages are disjoint/stable
|
|
73
|
+
* and top-N is limit-stable (top-5 ⊂ top-10 ⊂ top-20). limit=20 offset=0 stays
|
|
74
|
+
* byte-identical to before (60) → no change on the benchmarked single-page path.
|
|
75
|
+
* - limit > 20 keeps limit*3 (the over-fetch buffer the fallback stages need).
|
|
76
|
+
* Trade-off: pages beyond the pool now return empty instead of the (overlapping,
|
|
77
|
+
* wrong) rows the offset-scaling used to surface — stability over deep reach.
|
|
62
78
|
*/
|
|
63
|
-
export
|
|
64
|
-
|
|
79
|
+
export const MIN_FUSION_POOL = 60;
|
|
80
|
+
export function computePerSourceWindow(limit, offset) { // eslint-disable-line no-unused-vars
|
|
81
|
+
return { perSourceLimit: Math.max(limit * 3, MIN_FUSION_POOL), perSourceOffset: 0 };
|
|
65
82
|
}
|
|
66
83
|
|
|
67
84
|
/** obs-side total query: when the AND→OR fallback fired, count the OR set. */
|
package/mem-cli.mjs
CHANGED
|
@@ -133,7 +133,15 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
133
133
|
// --deep proceeds even when the literal query sanitizes to nothing — its LLM
|
|
134
134
|
// rewrite may still produce searchable variants (F3, parity with server.mjs).
|
|
135
135
|
if (!ftsQuery && deepMode === 'normal') {
|
|
136
|
-
|
|
136
|
+
// A query that sanitizes to an empty FTS expression (only operators/punctuation/
|
|
137
|
+
// sub-min-length tokens) is a zero-result search, not a malformed one. In --json
|
|
138
|
+
// mode emit the same empty envelope as the no-match path below so programmatic
|
|
139
|
+
// consumers always get parseable stdout (the human path keeps the stderr hint).
|
|
140
|
+
if (jsonOutput) {
|
|
141
|
+
out(JSON.stringify({ query, total: 0, returned: 0, offset, limit, deep: false, results: [] }));
|
|
142
|
+
} else {
|
|
143
|
+
fail(`[mem] No valid search terms in "${query}"`);
|
|
144
|
+
}
|
|
137
145
|
return;
|
|
138
146
|
}
|
|
139
147
|
// --deep ignores --or: each variant runs AND + the engine's built-in
|
|
@@ -300,18 +308,26 @@ function cmdRecent(db, args) {
|
|
|
300
308
|
const { positional, flags } = parseArgs(args);
|
|
301
309
|
const rawArg = positional[0];
|
|
302
310
|
const rawLimit = parseInt(rawArg, 10);
|
|
311
|
+
// Single source of the upper bound for BOTH the positional [N] and the --limit
|
|
312
|
+
// flag (help: "alias for [N] (max 1000)"). Pre-fix the positional path skipped
|
|
313
|
+
// this cap, so `recent 999999` issued an uncapped `LIMIT 999999` full-table dump
|
|
314
|
+
// while `recent --limit 999999` correctly rejected → default — exactly the
|
|
315
|
+
// "none capped --limit dumps the whole set" footgun parseIntFlag was extracted
|
|
316
|
+
// to close (lib/cli-flags.mjs). Keep the literal in one place so the two paths
|
|
317
|
+
// can't drift apart again.
|
|
318
|
+
const RECENT_MAX = 1000;
|
|
303
319
|
// isNumericToken first: "2abc"→2 / "1e2"→1 are positive integers that the bare check
|
|
304
320
|
// accepted silently; the positional path must reject garbage like the --limit flag does.
|
|
305
|
-
const isValid = rawArg !== undefined && isNumericToken(rawArg) && Number.isInteger(rawLimit) && rawLimit > 0;
|
|
321
|
+
const isValid = rawArg !== undefined && isNumericToken(rawArg) && Number.isInteger(rawLimit) && rawLimit > 0 && rawLimit <= RECENT_MAX;
|
|
306
322
|
if (rawArg !== undefined && !isValid) {
|
|
307
|
-
process.stderr.write(`[mem] Invalid count "${rawArg}" (must be
|
|
323
|
+
process.stderr.write(`[mem] Invalid count "${rawArg}" (must be an integer between 1 and ${RECENT_MAX}); using default 10\n`);
|
|
308
324
|
}
|
|
309
325
|
// Positional [N] wins for backward-compat; --limit is sibling-parity alias
|
|
310
326
|
// (search/recall/browse/stats all accept --limit). Pre-2.69 `recent --limit N`
|
|
311
327
|
// was silently ignored — surprising users extrapolating from siblings.
|
|
312
328
|
const limit = isValid
|
|
313
329
|
? rawLimit
|
|
314
|
-
: parseIntFlag(flags.limit, { name: '--limit', defaultValue: 10, max:
|
|
330
|
+
: parseIntFlag(flags.limit, { name: '--limit', defaultValue: 10, max: RECENT_MAX });
|
|
315
331
|
const project = flags.project ? resolveProject(db, flags.project) : inferProject();
|
|
316
332
|
const jsonOutput = flags.json === true || flags.json === 'true';
|
|
317
333
|
|
|
@@ -607,7 +623,20 @@ function cmdTimeline(db, args) {
|
|
|
607
623
|
if (flags.anchor !== undefined && flags.anchor !== true) {
|
|
608
624
|
const resolved = resolveAnchorToken(db, flags.anchor, { project });
|
|
609
625
|
if (!resolved.ok) {
|
|
610
|
-
|
|
626
|
+
// --json must always emit a parseable envelope. An explicit-but-missing anchor is
|
|
627
|
+
// a direct-lookup miss (like `get` on a bad id) → anchor:null + error code, rc=1.
|
|
628
|
+
if (jsonOutput) {
|
|
629
|
+
process.exitCode = 1;
|
|
630
|
+
out(JSON.stringify({
|
|
631
|
+
anchor: null,
|
|
632
|
+
anchor_note: formatAnchorError(resolved.error, 'mcp'),
|
|
633
|
+
before: [],
|
|
634
|
+
after: [],
|
|
635
|
+
error: resolved.error.code || 'anchor_resolution_failed',
|
|
636
|
+
}));
|
|
637
|
+
} else {
|
|
638
|
+
fail(formatAnchorError(resolved.error, 'cli'));
|
|
639
|
+
}
|
|
611
640
|
return;
|
|
612
641
|
}
|
|
613
642
|
anchorId = resolved.anchorId;
|
|
@@ -663,7 +692,20 @@ function cmdTimeline(db, args) {
|
|
|
663
692
|
// Window fetch (access-count bump + project auto-scope) shared with MCP.
|
|
664
693
|
const win = fetchTimelineWindow(db, anchorId, { before, after, project });
|
|
665
694
|
if (!win) {
|
|
666
|
-
|
|
695
|
+
// Anchor resolved to a real id but the window fetch found no row (e.g. project
|
|
696
|
+
// mismatch). Same --json contract as the resolution-miss path above.
|
|
697
|
+
if (jsonOutput) {
|
|
698
|
+
process.exitCode = 1;
|
|
699
|
+
out(JSON.stringify({
|
|
700
|
+
anchor: null,
|
|
701
|
+
anchor_note: `Observation #${anchorId} not found.`,
|
|
702
|
+
before: [],
|
|
703
|
+
after: [],
|
|
704
|
+
error: 'id-not-found',
|
|
705
|
+
}));
|
|
706
|
+
} else {
|
|
707
|
+
fail(`[mem] Observation #${anchorId} not found`);
|
|
708
|
+
}
|
|
667
709
|
return;
|
|
668
710
|
}
|
|
669
711
|
const { anchor, beforeRows, afterRows } = win;
|
|
@@ -1734,7 +1776,7 @@ function cmdMaintain(db, args) {
|
|
|
1734
1776
|
out(`[mem] Maintenance scan:`);
|
|
1735
1777
|
out(` Total active: ${stats.total}`);
|
|
1736
1778
|
out(` Near-duplicate pairs: ${duplicates.length}`);
|
|
1737
|
-
out(` Stale (>30d, imp=1, no access): ${stats.stale}`);
|
|
1779
|
+
out(` Stale (>30d, imp=1, no access, never injected): ${stats.stale}`);
|
|
1738
1780
|
out(` Broken (no title/narrative): ${stats.broken}`);
|
|
1739
1781
|
out(` Boostable (accessed>3, imp<3): ${stats.boostable}`);
|
|
1740
1782
|
out(` Pinned-but-uncited (inj>=${PINNED_INJ_THRESHOLD}, cited=0, imp>1): ${stats.pinned} — run: maintain execute --ops demote_pinned`);
|
|
@@ -2334,6 +2376,8 @@ Commands:
|
|
|
2334
2376
|
--project P Filter by project
|
|
2335
2377
|
drop <D#N|ordinal>[,...] Drop one or more deferred items (no fix needed)
|
|
2336
2378
|
--reason "..." Required audit trail
|
|
2379
|
+
--project P Project for ordinal resolution (default: current; must
|
|
2380
|
+
match the "defer list --project P" you read ordinals from)
|
|
2337
2381
|
|
|
2338
2382
|
delete <id1,id2,...> Delete observations by ID
|
|
2339
2383
|
--confirm Execute deletion (preview by default)
|
|
@@ -2549,7 +2593,7 @@ async function cmdImportJsonl(db, argv) {
|
|
|
2549
2593
|
if (files.length === 0) { out('[mem] No .jsonl files found.'); return; }
|
|
2550
2594
|
|
|
2551
2595
|
const { importJsonl } = await import('./lib/import-jsonl.mjs');
|
|
2552
|
-
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0, errorCount = 0;
|
|
2596
|
+
let totalPrompts = 0, totalObs = 0, totalSkip = 0, totalOrphans = 0, totalRecognized = 0, errorCount = 0;
|
|
2553
2597
|
for (const f of files) {
|
|
2554
2598
|
// Per-file isolation: one unreadable file (EACCES, EBUSY, mid-batch IO error)
|
|
2555
2599
|
// shouldn't crash the whole import — readFileSync inside importJsonl would
|
|
@@ -2569,18 +2613,29 @@ async function cmdImportJsonl(db, argv) {
|
|
|
2569
2613
|
totalObs += r.observations;
|
|
2570
2614
|
totalSkip += r.skipped;
|
|
2571
2615
|
totalOrphans += r.orphans || 0;
|
|
2616
|
+
totalRecognized += r.recognized || 0;
|
|
2572
2617
|
out(`[mem] ${f}: +${r.prompts} prompts, +${r.observations} observations, ${r.orphans || 0} orphan tool_use, ${r.skipped} skipped`);
|
|
2573
2618
|
}
|
|
2574
2619
|
const errorTail = errorCount > 0 ? `, ${errorCount} file(s) errored` : '';
|
|
2575
2620
|
out(`[mem] Total: ${totalPrompts} prompts, ${totalObs} observations, ${totalOrphans} orphan tool_use, ${totalSkip} skipped from ${files.length} file(s)${errorTail}.`);
|
|
2576
|
-
if (totalPrompts > 0 || totalObs > 0) {
|
|
2621
|
+
if (totalPrompts > 0 || totalObs > 0 || totalOrphans > 0) {
|
|
2622
|
+
// Orphan tool_use events persist as (truncated) observations, so they count as
|
|
2623
|
+
// "something was imported" — otherwise an orphan-only first import would wrongly
|
|
2624
|
+
// fall through to the "already imported" no-op branch below.
|
|
2577
2625
|
out(`[mem] Try: claude-mem-lite recent 5 --project ${project}`);
|
|
2626
|
+
} else if (totalRecognized > 0) {
|
|
2627
|
+
// Lines WERE Claude Code transcript events but produced no new rows — the file
|
|
2628
|
+
// was already imported (idempotent re-run) or carried no extractable content.
|
|
2629
|
+
// Distinct from the wrong-shape case below: do NOT cry "wrong shape" at a valid
|
|
2630
|
+
// transcript the user successfully imported earlier (cold-start backfill re-runs
|
|
2631
|
+
// hit this on every already-ingested file).
|
|
2632
|
+
out(`[mem] Nothing new: ${totalRecognized} transcript event(s) already imported (re-running import-jsonl on the same transcript is a safe no-op).`);
|
|
2578
2633
|
} else if (totalSkip > 0 && errorCount === 0) {
|
|
2579
|
-
//
|
|
2580
|
-
//
|
|
2581
|
-
//
|
|
2582
|
-
//
|
|
2583
|
-
//
|
|
2634
|
+
// No transcript event recognized at all — almost always the wrong file format
|
|
2635
|
+
// (import-jsonl ingests Claude Code transcript JSONL, not `export` output, which
|
|
2636
|
+
// is observation-shaped). Pre-fix this exited 0 with no signal, so pointing it at
|
|
2637
|
+
// the wrong file looked like success. Make the no-op explicit (stdout, like the
|
|
2638
|
+
// summary lines above).
|
|
2584
2639
|
out(`[mem] Warning: 0 imported, ${totalSkip} line(s) skipped — none matched the expected Claude Code transcript JSONL shape (user/assistant/tool_result). 'export' output is NOT re-importable via import-jsonl.`);
|
|
2585
2640
|
}
|
|
2586
2641
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.11.0",
|
|
4
4
|
"description": "Persistent long-term memory for Claude Code via MCP — captures coding decisions, bugfixes, and context across sessions. Hybrid FTS5 + TF-IDF search with episode batching. Single SQLite DB, no external services. A lighter, lower-cost alternative to claude-mem (episode batching + a smaller model; cost savings are an internal estimate, not a measured benchmark).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "npm@10.9.2",
|
package/secret-scrub.mjs
CHANGED
|
@@ -11,12 +11,14 @@ export const SECRET_PATTERNS = [
|
|
|
11
11
|
// and short values (<6 chars) that are typically variable names not secrets.
|
|
12
12
|
//
|
|
13
13
|
// Split into two patterns so prose mentions don't get scrubbed:
|
|
14
|
-
// 1. Bare credential nouns (password|passwd|token|bearer) commonly appear
|
|
15
|
-
// English prose — "Marker token: xyzpdq", "the bearer: alice".
|
|
16
|
-
// the
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
14
|
+
// 1. Bare credential nouns (password|passwd|token|bearer|secret) commonly appear
|
|
15
|
+
// in English prose — "Marker token: xyzpdq", "the bearer: alice". The prose
|
|
16
|
+
// mention shape is the `:` form, so the prose lookbehind (NOT preceded by
|
|
17
|
+
// English-word + horizontal-space) guards ONLY the `:` separator. An `=` is
|
|
18
|
+
// config-assignment syntax, never prose, so `<word> password=<secret>` ALWAYS
|
|
19
|
+
// scrubs — without this split that leaked (the lookbehind skipped any noun
|
|
20
|
+
// after "word ", regardless of separator). No pinned prose case uses `=` (all
|
|
21
|
+
// are `:`), so the `=` arm is leak-closing with no FP shift on the protected set.
|
|
20
22
|
// 2. Structured keys (api_key, auth_token, …) keep the original behavior —
|
|
21
23
|
// a separator/compound key is unambiguous config syntax even when
|
|
22
24
|
// preceded by prose ("see auth_token: shhhhhh").
|
|
@@ -26,7 +28,10 @@ export const SECRET_PATTERNS = [
|
|
|
26
28
|
// keyword. Allowing a leading `_` catches those while the prose lookbehind still
|
|
27
29
|
// excludes "Marker token: …". `secret` added so a bare SECRET=… with a mixed-alnum
|
|
28
30
|
// value is covered (the hex-only assignment pattern below misses non-hex values).
|
|
29
|
-
|
|
31
|
+
// 1a. `=` assignment → ALWAYS scrub (config syntax, never prose):
|
|
32
|
+
[/((?:\b|_)(?:password|passwd|token|bearer|secret)\s*=\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi, '$1***'],
|
|
33
|
+
// 1b. `:` separator → keep the prose lookbehind ("the token: alice" is prose):
|
|
34
|
+
[/((?<![A-Za-z][ \t])(?:\b|_)(?:password|passwd|token|bearer|secret)\s*:\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi, '$1***'],
|
|
30
35
|
// access_token / refresh_token are the canonical OAuth2 field names — they were
|
|
31
36
|
// missing from this KV list (drift vs the JSON list below). `(?:\b|_)` for the same
|
|
32
37
|
// underscore-prefix reason.
|
|
@@ -47,8 +52,11 @@ export const SECRET_PATTERNS = [
|
|
|
47
52
|
// object-literal / YAML / quoted-.env shapes. Split into the SAME two patterns as the
|
|
48
53
|
// unquoted KV pairs above so prose survives — a quoted value does not turn prose into
|
|
49
54
|
// config (`the token: "x"` is still prose, must NOT scrub; #8283 / utils.test.mjs:1090).
|
|
50
|
-
// (a) bare credential nouns
|
|
51
|
-
|
|
55
|
+
// (a) bare credential nouns: `=` always scrubs; `:` keeps the prose lookbehind
|
|
56
|
+
// (mirrors the unquoted 1a/1b split — a quoted value doesn't turn `:` prose
|
|
57
|
+
// into config, but `<word> password="x"` is still a leak):
|
|
58
|
+
[/((?:\b|_)(?:password|passwd|token|bearer|secret)\s*=\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
|
|
59
|
+
[/((?<![A-Za-z][ \t])(?:\b|_)(?:password|passwd|token|bearer|secret)\s*:\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
|
|
52
60
|
// (b) structured keys + named env vars are unambiguous config even after a word
|
|
53
61
|
// (`see api_key: "x"` DOES scrub, mirroring the unquoted structured-key path):
|
|
54
62
|
[/((?:\b|_)(?:pgpassword|pgpass|mysql_pwd|api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|access[_-]?token|refresh[_-]?token)\s*[=:]\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
|
package/server.mjs
CHANGED
|
@@ -997,7 +997,7 @@ server.registerTool(
|
|
|
997
997
|
`Memory maintenance scan:`,
|
|
998
998
|
` Total active observations: ${stats.total}`,
|
|
999
999
|
` Near-duplicate pairs: ${duplicates.length}`,
|
|
1000
|
-
` Stale (>30d, imp=1, no access): ${stats.stale}`,
|
|
1000
|
+
` Stale (>30d, imp=1, no access, never injected): ${stats.stale}`,
|
|
1001
1001
|
` Broken (no title/narrative): ${stats.broken}`,
|
|
1002
1002
|
` Boostable (accessed>3, imp<3): ${stats.boostable}`,
|
|
1003
1003
|
` Pending purge (idle-marked): ${stats.pendingPurge}`,
|