claude-mem-lite 3.17.0 → 3.19.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/hook-optimize.mjs +15 -0
- package/hook.mjs +3 -1
- package/lib/activity.mjs +8 -2
- package/lib/lesson-bridge.mjs +39 -0
- package/lib/scrub-record.mjs +5 -0
- package/package.json +2 -1
- package/registry-recommend.mjs +26 -1
- package/registry.mjs +4 -1
- package/schema.mjs +2 -2
- package/scripts/pre-tool-recall.js +32 -1
- package/secret-scrub.mjs +18 -6
- package/server.mjs +1 -1
- package/source-files.mjs +5 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.19.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.19.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/hook-optimize.mjs
CHANGED
|
@@ -467,6 +467,21 @@ Return ONLY valid JSON:
|
|
|
467
467
|
lesson_learned: lessonLearned,
|
|
468
468
|
});
|
|
469
469
|
db.transaction(() => {
|
|
470
|
+
// Snapshot the keeper's pre-merge row BEFORE overwriting it, so its original
|
|
471
|
+
// full text survives as a recoverable compressed_into child (mirroring
|
|
472
|
+
// compressGroup / recoverChildrenOf). The keeper is the cluster's most-
|
|
473
|
+
// important member; an in-place overwrite by the LLM's ≤800-char summary
|
|
474
|
+
// would otherwise destroy its original text irreversibly (HIGH-3 data loss).
|
|
475
|
+
// Column list is derived from the live schema (minus id/compressed_into) so
|
|
476
|
+
// it stays correct as migrations add columns; names are internal identifiers.
|
|
477
|
+
const snapCols = db.prepare(`PRAGMA table_info(observations)`).all()
|
|
478
|
+
.map(c => c.name).filter(c => c !== 'id' && c !== 'compressed_into');
|
|
479
|
+
const snapColList = snapCols.join(', ');
|
|
480
|
+
db.prepare(
|
|
481
|
+
`INSERT INTO observations (${snapColList}, compressed_into)
|
|
482
|
+
SELECT ${snapColList}, ? FROM observations WHERE id = ?`
|
|
483
|
+
).run(keeper.id, keeper.id);
|
|
484
|
+
|
|
470
485
|
db.prepare(`
|
|
471
486
|
UPDATE observations SET title=?, narrative=?, concepts=?, facts=?, text=?,
|
|
472
487
|
importance=?, lesson_learned=?, minhash_sig=?, optimized_at=?
|
package/hook.mjs
CHANGED
|
@@ -58,7 +58,7 @@ import {
|
|
|
58
58
|
} from './lib/citation-tracker.mjs';
|
|
59
59
|
import { extractTailAssistantText, extractStructuredSummary } from './lib/summary-extractor.mjs';
|
|
60
60
|
import { searchRelevantMemories, formatMemoryLine } from './hook-memory.mjs';
|
|
61
|
-
import { recordSkillAdoption } from './registry-recommend.mjs';
|
|
61
|
+
import { recordSkillAdoption, gcOldShadowShards } from './registry-recommend.mjs';
|
|
62
62
|
import { detectMemOverride } from './lib/mem-override.mjs';
|
|
63
63
|
import { buildAndSaveHandoff, detectContinuationIntent, renderHandoffInjection, pickHandoffToInject, extractUnfinishedSummary } from './hook-handoff.mjs';
|
|
64
64
|
import { checkForUpdate, getCachedUpdateBanner, isUpdateCheckDue } from './hook-update.mjs';
|
|
@@ -1054,6 +1054,8 @@ async function handleSessionStart() {
|
|
|
1054
1054
|
// GC stale per-session cooldown files. Cheap (<5ms typical) and idempotent;
|
|
1055
1055
|
// moved here from pre-tool-recall.js's hot path.
|
|
1056
1056
|
gcStalePreRecallCooldowns();
|
|
1057
|
+
// Bound the shadow-recommendation log (daily JSONL shards, no GC at write time).
|
|
1058
|
+
try { gcOldShadowShards(); } catch { /* best-effort, never blocks SessionStart */ }
|
|
1057
1059
|
|
|
1058
1060
|
// Plugin cache self-heal: Claude Code auto-updates the marketplace plugin can
|
|
1059
1061
|
// re-populate cache/<ver>/hooks/hooks.json, reintroducing duplicate hook
|
package/lib/activity.mjs
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// so they don't pollute the L1 system-prompt memory section.
|
|
6
6
|
|
|
7
7
|
import { sanitizeFtsQuery } from '../utils.mjs';
|
|
8
|
+
import { scrubRecord } from './scrub-record.mjs';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Canonical event_type enum — mirrors the events.event_type CHECK constraint.
|
|
@@ -48,14 +49,19 @@ export function saveEvent(db, {
|
|
|
48
49
|
importance = 1,
|
|
49
50
|
created_at_epoch = Date.now(),
|
|
50
51
|
}) {
|
|
52
|
+
// Scrub secrets at the single write choke-point so BOTH the auto-capture
|
|
53
|
+
// (persistHaikuSummary event branch, raw Haiku output) and the CLI /bug,/lesson
|
|
54
|
+
// paths are covered — title/body otherwise land verbatim and are FTS-indexed,
|
|
55
|
+
// searchable, and exportable (HIGH-2 at-rest leak).
|
|
56
|
+
const safe = scrubRecord('events', { title, body });
|
|
51
57
|
const info = db.prepare(`
|
|
52
58
|
INSERT INTO events (project, event_type, title, body, file_paths, git_sha, importance, created_at_epoch)
|
|
53
59
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
54
60
|
`).run(
|
|
55
61
|
project,
|
|
56
62
|
event_type,
|
|
57
|
-
title,
|
|
58
|
-
body,
|
|
63
|
+
safe.title,
|
|
64
|
+
safe.body,
|
|
59
65
|
file_paths ? JSON.stringify(file_paths) : null,
|
|
60
66
|
git_sha,
|
|
61
67
|
importance,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// lib/lesson-bridge.mjs — pure prompt builder + fail-open Haiku bridge for the
|
|
2
|
+
// comprehension-bridge forcing-function (CLAUDE_MEM_SALIENCE=bridge). Loaded by
|
|
3
|
+
// scripts/pre-tool-recall.js via dynamic import ONLY when the flag is on, so the
|
|
4
|
+
// fast hook never pulls the LLM stack by default (lesson #8447).
|
|
5
|
+
import { callLLM } from '../hook-shared.mjs';
|
|
6
|
+
|
|
7
|
+
const LESSON_MAX = 600; // chars — a lesson_learned fits well under this
|
|
8
|
+
const HUNK_MAX = 1200; // chars — the change region; bounds Haiku input cost
|
|
9
|
+
const CHECK_MAX = 200; // chars — bounded injected payload
|
|
10
|
+
|
|
11
|
+
export function buildBridgePrompt(lesson, hunk) {
|
|
12
|
+
const l = String(lesson || '').slice(0, LESSON_MAX);
|
|
13
|
+
const h = String(hunk || '').slice(0, HUNK_MAX);
|
|
14
|
+
return [
|
|
15
|
+
'A past lesson and the code change about to be made are below.',
|
|
16
|
+
'In ONE line, state the single concrete check the lesson forces on THIS change,',
|
|
17
|
+
'naming the actual symbol involved. If the lesson cannot apply, output exactly: N/A',
|
|
18
|
+
'Be specific. No preamble, no markdown.',
|
|
19
|
+
'',
|
|
20
|
+
`LESSON: ${l}`,
|
|
21
|
+
'',
|
|
22
|
+
'CHANGE:',
|
|
23
|
+
h,
|
|
24
|
+
].join('\n');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// { ok:true, check } when the bridge produced a usable, applicable check;
|
|
28
|
+
// { ok:false } on N/A / empty / error / timeout. NEVER throws — the caller
|
|
29
|
+
// falls back to the baseline ACK_DIRECTIVE on { ok:false }.
|
|
30
|
+
export async function bridgeLesson({ lesson, hunk, timeoutMs = 2500, _callLLM = callLLM }) {
|
|
31
|
+
try {
|
|
32
|
+
const raw = await _callLLM(buildBridgePrompt(lesson, hunk), timeoutMs);
|
|
33
|
+
const line = String(raw || '').trim().split('\n')[0].trim();
|
|
34
|
+
if (!line || /^n\s*\/?\s*a$/i.test(line)) return { ok: false };
|
|
35
|
+
return { ok: true, check: line.slice(0, CHECK_MAX) };
|
|
36
|
+
} catch {
|
|
37
|
+
return { ok: false };
|
|
38
|
+
}
|
|
39
|
+
}
|
package/lib/scrub-record.mjs
CHANGED
|
@@ -19,6 +19,11 @@ export const TEXT_FIELDS_BY_TABLE = {
|
|
|
19
19
|
'title', 'subtitle', 'text', 'narrative',
|
|
20
20
|
'concepts', 'facts', 'lesson_learned', 'search_aliases',
|
|
21
21
|
],
|
|
22
|
+
// events: the auto-captured bugfix/lesson/decision path (saveEvent) and the
|
|
23
|
+
// CLI /bug + /lesson commands both land here. title/body carry LLM output and
|
|
24
|
+
// user-pasted repro text verbatim, so they must scrub like observations do —
|
|
25
|
+
// event_type/project/git_sha are enums/identifiers/hash, left untouched.
|
|
26
|
+
events: ['title', 'body'],
|
|
22
27
|
session_summaries: [
|
|
23
28
|
'request', 'investigated', 'learned',
|
|
24
29
|
'completed', 'next_steps', 'remaining_items', 'notes',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.19.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",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"lib/reread-guard.mjs",
|
|
72
72
|
"lib/metrics.mjs",
|
|
73
73
|
"lib/lesson-idents.mjs",
|
|
74
|
+
"lib/lesson-bridge.mjs",
|
|
74
75
|
"lib/binding-probe.mjs",
|
|
75
76
|
"lib/proc-lock.mjs",
|
|
76
77
|
"lib/atomic-write.mjs",
|
package/registry-recommend.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
// Phase-1 invariant: shadow AND live only LOG. Neither emits to stdout nor writes
|
|
5
5
|
// invocations/recommend_count. Live injection is Phase 2. `off` skips all work.
|
|
6
|
-
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
6
|
+
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
|
|
@@ -109,6 +109,31 @@ function appendShadow(row) {
|
|
|
109
109
|
} catch { /* shadow sink must never crash the hook */ }
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Prune shadow-log daily shards older than `retainDays`. appendShadow writes one
|
|
114
|
+
* YYYY-MM-DD.jsonl per day with no GC, so a long-lived install grows the dir
|
|
115
|
+
* unbounded (audit: shadow log non-bounded). Shard date is read from the filename
|
|
116
|
+
* (ISO dates sort lexicographically = chronologically). 90d keeps a full quarter
|
|
117
|
+
* for recommend-stats --days while bounding the dir to ~90 sub-MB files.
|
|
118
|
+
* Best-effort, never throws — called from the SessionStart GC sweep.
|
|
119
|
+
* @returns {number} shards removed
|
|
120
|
+
*/
|
|
121
|
+
export function gcOldShadowShards(retainDays = 90) {
|
|
122
|
+
try {
|
|
123
|
+
const dir = shadowDir();
|
|
124
|
+
if (!existsSync(dir)) return 0;
|
|
125
|
+
const cutoff = new Date(Date.now() - retainDays * 86_400_000).toISOString().slice(0, 10);
|
|
126
|
+
let removed = 0;
|
|
127
|
+
for (const name of readdirSync(dir)) {
|
|
128
|
+
const m = /^(\d{4}-\d{2}-\d{2})\.jsonl$/.exec(name);
|
|
129
|
+
if (m && m[1] < cutoff) {
|
|
130
|
+
try { unlinkSync(join(dir, name)); removed++; } catch { /* per-entry, silent */ }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return removed;
|
|
134
|
+
} catch { return 0; }
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
export function logShadowReco(project, rec) { appendShadow({ ts: new Date().toISOString(), kind: 'reco', project, ...rec }); }
|
|
113
138
|
export function logShadowAdoption(project, rec) { appendShadow({ ts: new Date().toISOString(), kind: 'adopt', project, ...rec }); }
|
|
114
139
|
|
package/registry.mjs
CHANGED
|
@@ -165,7 +165,10 @@ export function ensureRegistryDb(dbPath) {
|
|
|
165
165
|
|
|
166
166
|
const db = new Database(dbPath);
|
|
167
167
|
db.pragma('journal_mode = WAL');
|
|
168
|
-
|
|
168
|
+
// 5000ms to match the main DB: registry writes (install indexing rewriting
|
|
169
|
+
// resources + resources_fts) race shadow-recommend writes + mem_registry reads
|
|
170
|
+
// on the same file; 3000ms was insufficient under that concurrency (schema.mjs).
|
|
171
|
+
db.pragma('busy_timeout = 5000');
|
|
169
172
|
db.pragma('synchronous = NORMAL');
|
|
170
173
|
db.pragma('foreign_keys = ON');
|
|
171
174
|
|
package/schema.mjs
CHANGED
|
@@ -918,7 +918,7 @@ export function ensureDb() {
|
|
|
918
918
|
* @returns {{rebuilt: string[], errors: string[]}} Results per FTS table
|
|
919
919
|
*/
|
|
920
920
|
export function rebuildFTS(db) {
|
|
921
|
-
const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts'];
|
|
921
|
+
const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts', 'events_fts'];
|
|
922
922
|
const idRe = /^[a-z][a-z0-9_]*$/;
|
|
923
923
|
const rebuilt = [];
|
|
924
924
|
const errors = [];
|
|
@@ -942,7 +942,7 @@ export function rebuildFTS(db) {
|
|
|
942
942
|
* @returns {{healthy: boolean, details: string[]}}
|
|
943
943
|
*/
|
|
944
944
|
export function checkFTSIntegrity(db) {
|
|
945
|
-
const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts'];
|
|
945
|
+
const FTS_TABLES = ['observations_fts', 'session_summaries_fts', 'user_prompts_fts', 'events_fts'];
|
|
946
946
|
const idRe = /^[a-z][a-z0-9_]*$/;
|
|
947
947
|
const details = [];
|
|
948
948
|
let healthy = true;
|
|
@@ -42,6 +42,7 @@ const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes (used only for legacy fallback)
|
|
|
42
42
|
const SALIENCE_LEGACY = process.env.CLAUDE_MEM_SALIENCE === 'legacy'
|
|
43
43
|
|| process.env.CLAUDE_MEM_SALIENCE === '0';
|
|
44
44
|
const SALIENCE_BIND = process.env.CLAUDE_MEM_SALIENCE === 'bind';
|
|
45
|
+
const SALIENCE_BRIDGE = process.env.CLAUDE_MEM_SALIENCE === 'bridge';
|
|
45
46
|
const ACK_DIRECTIVE = "apply each lesson to this edit or rule it out — state '#NN applied' or '#NN n/a — <reason>' in your next user-facing message.";
|
|
46
47
|
// v-bind salience forcing-function (#8771 audit: ack ≠ act). Instead of a cheap
|
|
47
48
|
// '#NN applied / n/a' verdict, demand the model bind the lesson to the concrete
|
|
@@ -77,6 +78,30 @@ function cooldownPathFor(sessionId) {
|
|
|
77
78
|
return join(RUNTIME_DIR, `pre-recall-cooldown-${safe}.json`);
|
|
78
79
|
}
|
|
79
80
|
|
|
81
|
+
// Comprehension-bridge (CLAUDE_MEM_SALIENCE=bridge): rewrite the top bound lesson
|
|
82
|
+
// into a check naming a symbol in THIS change. Dynamic import keeps the LLM stack
|
|
83
|
+
// out of the default fast path (#8447). Fail-open: null → caller uses ACK line.
|
|
84
|
+
async function bridgeTopLesson(rows, changeText) {
|
|
85
|
+
if (!SALIENCE_BRIDGE || !changeText) return null;
|
|
86
|
+
const fake = process.env.CLAUDE_MEM_BRIDGE_FAKE;
|
|
87
|
+
let extractIdents, bridgeLesson;
|
|
88
|
+
try {
|
|
89
|
+
({ extractIdents } = await import('../lib/lesson-idents.mjs'));
|
|
90
|
+
if (!fake) ({ bridgeLesson } = await import('../lib/lesson-bridge.mjs'));
|
|
91
|
+
} catch { return null; }
|
|
92
|
+
for (const r of rows) {
|
|
93
|
+
const lesson = r.lesson_learned;
|
|
94
|
+
if (!lesson) continue;
|
|
95
|
+
if (!extractIdents(lesson).some((id) => changeText.includes(id))) continue;
|
|
96
|
+
let res;
|
|
97
|
+
if (fake) res = /^n\s*\/?\s*a$/i.test(fake.trim()) ? { ok: false } : { ok: true, check: fake.trim().slice(0, 200) };
|
|
98
|
+
else res = await bridgeLesson({ lesson, hunk: changeText });
|
|
99
|
+
if (res.ok) return { id: r.id, check: res.check };
|
|
100
|
+
return null; // top bound lesson abstained → fall back to ACK, don't scan further
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
|
|
80
105
|
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
81
106
|
|
|
82
107
|
function inferProject() {
|
|
@@ -181,11 +206,13 @@ try {
|
|
|
181
206
|
let filePath;
|
|
182
207
|
let sessionId;
|
|
183
208
|
let toolName;
|
|
209
|
+
let toolInput;
|
|
184
210
|
// isFullRead: a Read with no offset/limit reads the whole file. The reread
|
|
185
211
|
// guard only flags full-vs-full re-reads, so paging never trips it.
|
|
186
212
|
let isFullRead = true;
|
|
187
213
|
try {
|
|
188
214
|
const event = JSON.parse(input);
|
|
215
|
+
toolInput = event.tool_input;
|
|
189
216
|
filePath = event.tool_input?.file_path;
|
|
190
217
|
sessionId = event.session_id || null;
|
|
191
218
|
toolName = event.tool_name || null;
|
|
@@ -442,7 +469,11 @@ try {
|
|
|
442
469
|
// Read keeps the quiet form; its forcing-function fires at the later Edit
|
|
443
470
|
// via the Read→Edit ack nudge above.
|
|
444
471
|
if (!isRead && !SALIENCE_LEGACY) {
|
|
445
|
-
|
|
472
|
+
const changeText = [toolInput?.old_string, toolInput?.new_string, toolInput?.content]
|
|
473
|
+
.filter(Boolean).join('\n');
|
|
474
|
+
const bridged = await bridgeTopLesson(allRows, changeText);
|
|
475
|
+
if (bridged) lines.push(`[mem] ⚠ #${bridged.id} → this edit must: ${bridged.check}. Confirm your new code satisfies it.`);
|
|
476
|
+
else lines.push(`[mem] ⚠ Before this edit: ${ACTIVE_DIRECTIVE}`);
|
|
446
477
|
}
|
|
447
478
|
} else if (!isRead && process.env.CLAUDE_MEM_PRETOOL_NUDGE === '1') {
|
|
448
479
|
// R-4: Edit/Write empty → short backfill reminder. OPT-IN (default off) as
|
package/secret-scrub.mjs
CHANGED
|
@@ -60,8 +60,10 @@ export const SECRET_PATTERNS = [
|
|
|
60
60
|
// (b) structured keys + named env vars are unambiguous config even after a word
|
|
61
61
|
// (`see api_key: "x"` DOES scrub, mirroring the unquoted structured-key path):
|
|
62
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'],
|
|
63
|
-
// AWS access keys (
|
|
64
|
-
|
|
63
|
+
// AWS access keys: AKIA (long-term) + ASIA (STS temp) + AROA (role) + AIDA
|
|
64
|
+
// (user) + ANPA/ANVA/AGPA (other principal types). All share the 4-letter
|
|
65
|
+
// prefix + exactly 16 base32 chars shape — specific enough for near-zero FP.
|
|
66
|
+
[/\b(?:AKIA|ASIA|AROA|AIDA|ANPA|ANVA|AGPA)[A-Z0-9]{16}\b/g, '***'],
|
|
65
67
|
// OpenAI / Anthropic keys (sk-...) — specific prefixes have lower length threshold
|
|
66
68
|
[/\bsk-(?:proj|ant|ant-api\d{2})-[a-zA-Z0-9_-]{8,}\b/g, '***'],
|
|
67
69
|
[/\bsk-[a-zA-Z0-9_-]{20,}\b/g, '***'],
|
|
@@ -74,8 +76,10 @@ export const SECRET_PATTERNS = [
|
|
|
74
76
|
[/\bxox[bpas]-[a-zA-Z0-9-]{10,}\b/g, '***'],
|
|
75
77
|
// JWT tokens (eyJ...eyJ...)
|
|
76
78
|
[/\beyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]+\b/g, '***'],
|
|
77
|
-
// PEM private key blocks
|
|
78
|
-
|
|
79
|
+
// PEM private key blocks. `[A-Z0-9 ]*` covers every armor label — RSA/EC/DSA/
|
|
80
|
+
// OPENSSH plus ENCRYPTED and PGP (… PRIVATE KEY BLOCK) — that the fixed
|
|
81
|
+
// alternation missed; the block delimiters make FP impossible.
|
|
82
|
+
[/-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g, '***PEM_KEY***'],
|
|
79
83
|
// Long hex strings in assignments (e.g. SECRET_KEY=abc123def456...)
|
|
80
84
|
[/(\b(?:key|secret|token|hash)\s*[=:]\s*)[0-9a-f]{32,}\b/gi, '$1***'],
|
|
81
85
|
// Google Cloud API keys (AIza...)
|
|
@@ -84,8 +88,9 @@ export const SECRET_PATTERNS = [
|
|
|
84
88
|
[/(Authorization:\s*Bearer\s+)[^\s,;'"}\]]+/gi, '$1***'],
|
|
85
89
|
// Supabase / generic long base64 keys (40+ chars, common in env vars)
|
|
86
90
|
[/(\b(?:SUPABASE_KEY|SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*)[^\s,;'"}\]]+/gi, '$1***'],
|
|
87
|
-
// Basic auth in URLs (https://user:password@host)
|
|
88
|
-
|
|
91
|
+
// Basic auth in URLs (https://user:password@host). ftp/ftps added — file-drop
|
|
92
|
+
// creds are a common leak shape the https-only form missed.
|
|
93
|
+
[/(https?|ftps?):\/\/[^@/\s]+:[^@/\s]+@/gi, '$1://***:***@'],
|
|
89
94
|
// Database connection strings (postgres, mysql, mongodb, redis, amqp)
|
|
90
95
|
[/\b(postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/[^\s,;'"}\]]+/gi, '$1://***'],
|
|
91
96
|
// npm tokens (npm_...)
|
|
@@ -109,6 +114,13 @@ export const SECRET_PATTERNS = [
|
|
|
109
114
|
// these slip through. Match the value-quoted form explicitly. Length floor
|
|
110
115
|
// (6) avoids tripping on intentional placeholder shorts ("...", "secret").
|
|
111
116
|
[/("(?:password|passwd|token|api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|access[_-]?token|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer|refresh[_-]?token|session[_-]?id|sessionid)"\s*:\s*")[^"]{6,}(")/gi, '$1***$2'],
|
|
117
|
+
// JSON keys with vendor PREFIX/SUFFIX around the core credential noun —
|
|
118
|
+
// `"x_api_key"`, `"aws_secret_access_key"`, `"my_password"`, `"gh_token"`.
|
|
119
|
+
// The exact-name list above misses these. Anchored to the credential nouns
|
|
120
|
+
// (password|secret|api_key|auth_token|access_token|private_key) so a benign
|
|
121
|
+
// `"token_count"` value (numeric, <6 non-quote chars after scrub) and prose
|
|
122
|
+
// keys stay low-FP; over-scrub is the safe direction for at-rest memory.
|
|
123
|
+
[/("\w*(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*"\s*:\s*")[^"]{6,}(")/gi, '$1***$2'],
|
|
112
124
|
// Session cookies in headers / urlencoded bodies (sessionid=, session_id=, JSESSIONID=, PHPSESSID=).
|
|
113
125
|
// 16+ chars filters out short test fixtures like sessionid=abc.
|
|
114
126
|
[/\b((?:session[_-]?id|sessionid|jsessionid|phpsessid)\s*[=:]\s*)[^\s,;'"}\]]{16,}/gi, '$1***'],
|
package/server.mjs
CHANGED
|
@@ -98,7 +98,7 @@ function getRegistryDb() {
|
|
|
98
98
|
if (registryDb) return registryDb;
|
|
99
99
|
try {
|
|
100
100
|
registryDb = ensureRegistryDb(REGISTRY_DB_PATH);
|
|
101
|
-
registryDb.pragma('busy_timeout =
|
|
101
|
+
registryDb.pragma('busy_timeout = 5000'); // match main DB + ensureRegistryDb (was 3000, overriding it back down)
|
|
102
102
|
} catch (e) {
|
|
103
103
|
debugLog('WARN', 'server', `Registry DB not available: ${e.message}`);
|
|
104
104
|
}
|
package/source-files.mjs
CHANGED
|
@@ -66,6 +66,11 @@ export const SOURCE_FILES = [
|
|
|
66
66
|
// are present in the pre-edit file (component 2). Imported ONLY by
|
|
67
67
|
// scripts/pre-tool-recall.js; kept here for the same reason as file-intel.mjs.
|
|
68
68
|
'lib/lesson-idents.mjs',
|
|
69
|
+
// comprehension-bridge forcing-function (CLAUDE_MEM_SALIENCE=bridge): rewrites
|
|
70
|
+
// a recalled lesson into a check bound to the change hunk. Dynamic-imported by
|
|
71
|
+
// scripts/pre-tool-recall.js ONLY under the flag, but must still ship so the
|
|
72
|
+
// hook can resolve it at runtime when a user opts in.
|
|
73
|
+
'lib/lesson-bridge.mjs',
|
|
69
74
|
// v2.71.x: better-sqlite3 ABI probe + auto-rebuild. Shared by install.mjs
|
|
70
75
|
// (post-`npm install` verify) and scripts/launch.mjs (pre-server-launch
|
|
71
76
|
// self-heal after Node ABI changes). Missing from manifest → auto-update
|