claude-mem-lite 3.96.0 → 3.97.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/adopt-cli.mjs +125 -51
- package/bash-utils.mjs +105 -29
- package/claudemd.mjs +63 -19
- package/cli/activity.mjs +36 -18
- package/cli/common.mjs +101 -18
- package/cli/doctor.mjs +29 -9
- package/cli.mjs +69 -10
- package/deep-search.mjs +63 -20
- package/format-utils.mjs +15 -7
- package/haiku-client.mjs +189 -111
- package/hash-utils.mjs +15 -6
- package/hook-context.mjs +135 -46
- package/hook-episode.mjs +76 -23
- package/hook-handoff.mjs +216 -84
- package/hook-llm.mjs +476 -216
- package/hook-memory.mjs +106 -43
- package/hook-optimize.mjs +272 -109
- package/hook-precompact.mjs +1 -1
- package/hook-semaphore.mjs +46 -12
- package/hook-shared.mjs +101 -131
- package/hook-update.mjs +286 -88
- package/hook.mjs +1207 -617
- package/install-metadata.mjs +246 -125
- package/install.mjs +1327 -917
- package/lib/activity.mjs +52 -29
- package/lib/atomic-write.mjs +23 -4
- package/lib/binding-probe.mjs +40 -38
- package/lib/browse-core.mjs +22 -8
- package/lib/citation-tracker.mjs +189 -79
- package/lib/cite-back-hint.mjs +37 -19
- package/lib/cite-recall-path.mjs +3 -1
- package/lib/cli-flags.mjs +4 -2
- package/lib/cli-project.mjs +8 -5
- package/lib/compress-core.mjs +34 -9
- package/lib/cooldown-path.mjs +3 -1
- package/lib/db-backup.mjs +33 -11
- package/lib/deferred-work.mjs +73 -32
- package/lib/delete-core.mjs +25 -9
- package/lib/doctor-benchmark.mjs +5 -1
- package/lib/doctor-drift.mjs +25 -8
- package/lib/edge-attribution.mjs +23 -9
- package/lib/env-number.mjs +7 -8
- package/lib/err-sampler.mjs +22 -11
- package/lib/error-recall-core.mjs +15 -17
- package/lib/events-injection.mjs +25 -8
- package/lib/export-columns.mjs +49 -10
- package/lib/fast-summary.mjs +22 -9
- package/lib/file-edge-match.mjs +4 -2
- package/lib/file-intel.mjs +38 -13
- package/lib/frontmatter.mjs +22 -5
- package/lib/get-core.mjs +78 -11
- package/lib/handoff-constants.mjs +18 -0
- package/lib/hook-stdin.mjs +10 -3
- package/lib/hook-stdout.mjs +21 -7
- package/lib/hook-telemetry.mjs +44 -17
- package/lib/id-routing.mjs +11 -6
- package/lib/import-jsonl.mjs +92 -41
- package/lib/inject-search-core.mjs +5 -2
- package/lib/injected-ids.mjs +19 -12
- package/lib/install-shape.mjs +47 -23
- package/lib/keyctx-marker.mjs +3 -1
- package/lib/lesson-bridge.mjs +8 -5
- package/lib/llm-call.mjs +76 -0
- package/lib/llm-provider-probe.mjs +24 -12
- package/lib/low-signal-patterns.mjs +25 -22
- package/lib/maintain-core.mjs +221 -92
- package/lib/mem-override.mjs +4 -2
- package/lib/metrics.mjs +30 -25
- package/lib/native-binding-hint.mjs +29 -8
- package/lib/observation-write.mjs +74 -19
- package/lib/patha-exclude-meter.mjs +10 -6
- package/lib/persist-reminder.mjs +22 -10
- package/lib/plan-reader.mjs +2 -2
- package/lib/private-strip.mjs +3 -3
- package/lib/proc-lock.mjs +15 -3
- package/lib/proxy-fetch.mjs +49 -13
- package/lib/quiet-scope.mjs +42 -0
- package/lib/recall-core.mjs +19 -7
- package/lib/recent-core.mjs +18 -5
- package/lib/registry-core.mjs +12 -6
- package/lib/release-digest.mjs +4 -1
- package/lib/relevance-floor.mjs +5 -2
- package/lib/reread-guard.mjs +16 -8
- package/lib/resolve-data-dir.mjs +1 -1
- package/lib/rrf.mjs +4 -1
- package/lib/save-enrich.mjs +36 -18
- package/lib/save-observation.mjs +78 -35
- package/lib/scrub-record.mjs +20 -6
- package/lib/search-core.mjs +456 -123
- package/lib/shard-gc.mjs +50 -0
- package/lib/startup-dashboard.mjs +20 -9
- package/lib/stats-core.mjs +101 -35
- package/lib/stats-quality.mjs +56 -20
- package/lib/summary-extractor.mjs +2 -2
- package/lib/task-imperative.mjs +12 -4
- package/lib/timeline-core.mjs +88 -28
- package/lib/tmp-fixture-sweep.mjs +32 -9
- package/lib/transcript-scan.mjs +20 -4
- package/lib/upgrade-banner.mjs +11 -7
- package/mem-cli.mjs +1314 -506
- package/memdir.mjs +53 -21
- package/nlp.mjs +68 -38
- package/npm-shrinkwrap.json +2 -2
- package/package.json +9 -2
- package/plugin-cache-guard.mjs +31 -15
- package/project-utils.mjs +44 -20
- package/registry-enricher.mjs +26 -6
- package/registry-github.mjs +4 -2
- package/registry-importer.mjs +151 -40
- package/registry-recommend.mjs +189 -96
- package/registry-retriever.mjs +229 -70
- package/registry-scanner.mjs +11 -6
- package/registry.mjs +163 -66
- package/rerank.mjs +18 -4
- package/resource-discovery.mjs +14 -4
- package/schema.mjs +258 -99
- package/scoring-sql.mjs +21 -10
- package/scripts/binding-probe-cli.mjs +17 -12
- package/scripts/hook-launcher.mjs +148 -31
- package/scripts/launch-preflight.mjs +5 -7
- package/scripts/launch.mjs +21 -9
- package/scripts/post-tool-recall.js +21 -5
- package/scripts/pre-agent-inject.js +51 -13
- package/scripts/pre-skill-bridge.js +22 -7
- package/scripts/pre-tool-recall.js +157 -77
- package/scripts/prompt-search-utils.mjs +70 -25
- package/scripts/setup.sh +16 -1
- package/scripts/user-prompt-search.js +238 -97
- package/search-engine.mjs +329 -96
- package/search-scoring.mjs +75 -33
- package/secret-scrub.mjs +48 -12
- package/server/fts-check.mjs +12 -9
- package/server.mjs +674 -243
- package/skip-tools.mjs +19 -8
- package/source-files.mjs +54 -21
- package/stop-words.mjs +152 -19
- package/synonyms.mjs +579 -164
- package/tfidf.mjs +130 -40
- package/tier.mjs +1 -1
- package/tool-schemas.mjs +381 -131
- package/utils.mjs +184 -36
package/search-scoring.mjs
CHANGED
|
@@ -79,13 +79,17 @@ export function reRankWithContext(db, results, project) {
|
|
|
79
79
|
if (!results || results.length === 0) return;
|
|
80
80
|
// Get recently active files (last 2 hours, same project) via observation_files junction table
|
|
81
81
|
const twoHoursAgo = Date.now() - 2 * 3600000;
|
|
82
|
-
const recentFiles = db
|
|
82
|
+
const recentFiles = db
|
|
83
|
+
.prepare(
|
|
84
|
+
`
|
|
83
85
|
SELECT DISTINCT of2.filename FROM observation_files of2
|
|
84
86
|
JOIN observations o ON o.id = of2.obs_id
|
|
85
87
|
WHERE o.project = ? AND o.created_at_epoch > ?
|
|
86
|
-
|
|
88
|
+
`,
|
|
89
|
+
)
|
|
90
|
+
.all(project, twoHoursAgo);
|
|
87
91
|
|
|
88
|
-
const activeFiles = new Set(recentFiles.map(r => r.filename));
|
|
92
|
+
const activeFiles = new Set(recentFiles.map((r) => r.filename));
|
|
89
93
|
if (activeFiles.size === 0) return;
|
|
90
94
|
|
|
91
95
|
// Pre-compute active directories for directory-level matching
|
|
@@ -96,13 +100,13 @@ export function reRankWithContext(db, results, project) {
|
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
// Batch-fetch observation_files for all obs result IDs
|
|
99
|
-
const obsResults = results.filter(r => r.source === 'obs' && r.id);
|
|
103
|
+
const obsResults = results.filter((r) => r.source === 'obs' && r.id);
|
|
100
104
|
if (obsResults.length === 0) return;
|
|
101
|
-
const obsIds = obsResults.map(r => r.id);
|
|
105
|
+
const obsIds = obsResults.map((r) => r.id);
|
|
102
106
|
const placeholders = obsIds.map(() => '?').join(',');
|
|
103
|
-
const fileRows = db
|
|
104
|
-
`SELECT obs_id, filename FROM observation_files WHERE obs_id IN (${placeholders})`
|
|
105
|
-
|
|
107
|
+
const fileRows = db
|
|
108
|
+
.prepare(`SELECT obs_id, filename FROM observation_files WHERE obs_id IN (${placeholders})`)
|
|
109
|
+
.all(...obsIds);
|
|
106
110
|
|
|
107
111
|
// Build map: obs_id → [filenames]
|
|
108
112
|
const obsFileMap = new Map();
|
|
@@ -114,9 +118,9 @@ export function reRankWithContext(db, results, project) {
|
|
|
114
118
|
for (const result of obsResults) {
|
|
115
119
|
const resultFiles = obsFileMap.get(result.id);
|
|
116
120
|
if (!resultFiles || resultFiles.length === 0) continue;
|
|
117
|
-
const exactMatches = resultFiles.filter(f => activeFiles.has(f)).length;
|
|
121
|
+
const exactMatches = resultFiles.filter((f) => activeFiles.has(f)).length;
|
|
118
122
|
// Directory-level: same parent dir but different file (half weight)
|
|
119
|
-
const dirMatches = resultFiles.filter(f => {
|
|
123
|
+
const dirMatches = resultFiles.filter((f) => {
|
|
120
124
|
if (activeFiles.has(f)) return false; // already counted as exact
|
|
121
125
|
const lastSlash = f.lastIndexOf('/');
|
|
122
126
|
return lastSlash > 0 && activeDirs.has(f.substring(0, lastSlash));
|
|
@@ -124,7 +128,7 @@ export function reRankWithContext(db, results, project) {
|
|
|
124
128
|
const fileOverlap = (exactMatches + 0.5 * dirMatches) / resultFiles.length;
|
|
125
129
|
// BM25 scores are negative — multiply by >1 makes more negative = better rank
|
|
126
130
|
if (result.score !== null && result.score !== undefined && fileOverlap > 0) {
|
|
127
|
-
result.score *=
|
|
131
|
+
result.score *= 1.0 + 0.3 * fileOverlap;
|
|
128
132
|
}
|
|
129
133
|
}
|
|
130
134
|
// Note: caller re-sorts the main results array after this — no sort needed here
|
|
@@ -138,8 +142,18 @@ export function reRankWithContext(db, results, project) {
|
|
|
138
142
|
/** @type {Set<string>} Common words excluded from PRF term extraction */
|
|
139
143
|
export const PRF_STOP_WORDS = new Set([
|
|
140
144
|
...BASE_STOP_WORDS,
|
|
141
|
-
'use',
|
|
142
|
-
'
|
|
145
|
+
'use',
|
|
146
|
+
'used',
|
|
147
|
+
'using',
|
|
148
|
+
'new',
|
|
149
|
+
'added',
|
|
150
|
+
'updated',
|
|
151
|
+
'file',
|
|
152
|
+
'files',
|
|
153
|
+
'code',
|
|
154
|
+
'change',
|
|
155
|
+
'changed',
|
|
156
|
+
'changes',
|
|
143
157
|
]);
|
|
144
158
|
|
|
145
159
|
/**
|
|
@@ -155,9 +169,11 @@ export function extractPRFTerms(results, ftsQuery, limit = 3) {
|
|
|
155
169
|
// query term (e.g. "authenticate" when the user searched "authentication") are also
|
|
156
170
|
// excluded, not just the exact surface form.
|
|
157
171
|
const queryTokens = new Set(
|
|
158
|
-
ftsQuery
|
|
159
|
-
.
|
|
160
|
-
.
|
|
172
|
+
ftsQuery
|
|
173
|
+
.replace(/["()]/g, ' ')
|
|
174
|
+
.split(/\s+/)
|
|
175
|
+
.map((t) => porterStem(t.toLowerCase()))
|
|
176
|
+
.filter((t) => t.length > 1 && t !== 'or' && t !== 'and'),
|
|
161
177
|
);
|
|
162
178
|
|
|
163
179
|
// Bucket morphological variants by porter STEM so "cache"/"caching"/"cached" jointly
|
|
@@ -172,13 +188,16 @@ export function extractPRFTerms(results, ftsQuery, limit = 3) {
|
|
|
172
188
|
// narratives) made `stemSurfaces[stem] ||= new Map()` read the INHERITED function
|
|
173
189
|
// as truthy, skip the assignment, and crash on sm.get (surfaced 2026-08-16 when
|
|
174
190
|
// the M-2 gate fix first ran PRF over OR-rescued rows).
|
|
175
|
-
const stemDocCount = Object.create(null);
|
|
176
|
-
const stemSurfaces = Object.create(null);
|
|
191
|
+
const stemDocCount = Object.create(null); // stem -> # of top docs it appears in (the >=2 bar)
|
|
192
|
+
const stemSurfaces = Object.create(null); // stem -> Map(surface -> total occurrences)
|
|
177
193
|
const docCount = Math.min(results.length, 8);
|
|
178
194
|
for (let i = 0; i < docCount; i++) {
|
|
179
195
|
const r = results[i];
|
|
180
196
|
const text = ((r.title || '') + ' ' + (r.narrative || '')).toLowerCase();
|
|
181
|
-
const surfaces = text
|
|
197
|
+
const surfaces = text
|
|
198
|
+
.replace(/[^a-z0-9_-]/g, ' ')
|
|
199
|
+
.split(/\s+/)
|
|
200
|
+
.filter((t) => t.length >= 3);
|
|
182
201
|
const docStems = new Set();
|
|
183
202
|
for (const surface of surfaces) {
|
|
184
203
|
// Stop-word filter at BOTH surface and stem level: PRF_STOP_WORDS lists surface
|
|
@@ -201,9 +220,13 @@ export function extractPRFTerms(results, ftsQuery, limit = 3) {
|
|
|
201
220
|
.sort((a, b) => b[1] - a[1])
|
|
202
221
|
.slice(0, limit)
|
|
203
222
|
.map(([stem]) => {
|
|
204
|
-
let best = null,
|
|
223
|
+
let best = null,
|
|
224
|
+
bestN = -1;
|
|
205
225
|
for (const [surface, n] of stemSurfaces[stem]) {
|
|
206
|
-
if (n > bestN) {
|
|
226
|
+
if (n > bestN) {
|
|
227
|
+
best = surface;
|
|
228
|
+
bestN = n;
|
|
229
|
+
}
|
|
207
230
|
}
|
|
208
231
|
return best;
|
|
209
232
|
});
|
|
@@ -224,15 +247,22 @@ export function extractPRFTerms(results, ftsQuery, limit = 3) {
|
|
|
224
247
|
export function expandQueryByConcepts(db, ftsQuery, project) {
|
|
225
248
|
let rows;
|
|
226
249
|
try {
|
|
227
|
-
rows = db
|
|
250
|
+
rows = db
|
|
251
|
+
.prepare(
|
|
252
|
+
`
|
|
228
253
|
SELECT o.concepts FROM observations_fts
|
|
229
254
|
JOIN observations o ON observations_fts.rowid = o.id
|
|
230
255
|
WHERE observations_fts MATCH ? AND ${liveObsFilterSql('o')}
|
|
231
256
|
AND (? IS NULL OR o.project = ?)
|
|
232
257
|
ORDER BY ${OBS_BM25}
|
|
233
258
|
LIMIT 20
|
|
234
|
-
|
|
235
|
-
|
|
259
|
+
`,
|
|
260
|
+
)
|
|
261
|
+
.all(ftsQuery, project ?? null, project ?? null);
|
|
262
|
+
} catch (e) {
|
|
263
|
+
debugCatch(e, 'expandQueryByConcepts-fts');
|
|
264
|
+
return [];
|
|
265
|
+
}
|
|
236
266
|
|
|
237
267
|
if (rows.length === 0) return [];
|
|
238
268
|
|
|
@@ -247,9 +277,11 @@ export function expandQueryByConcepts(db, ftsQuery, project) {
|
|
|
247
277
|
|
|
248
278
|
// Filter out terms already present in the query
|
|
249
279
|
const queryTokens = new Set(
|
|
250
|
-
ftsQuery
|
|
251
|
-
.
|
|
252
|
-
.
|
|
280
|
+
ftsQuery
|
|
281
|
+
.replace(/["()]/g, ' ')
|
|
282
|
+
.split(/\s+/)
|
|
283
|
+
.map((t) => t.toLowerCase())
|
|
284
|
+
.filter((t) => t.length > 1 && t !== 'or' && t !== 'and'),
|
|
253
285
|
);
|
|
254
286
|
|
|
255
287
|
return Object.entries(freq)
|
|
@@ -271,12 +303,14 @@ export function expandQueryByConcepts(db, ftsQuery, project) {
|
|
|
271
303
|
export function autoBoostIfNeeded(db, ids) {
|
|
272
304
|
if (!ids || ids.length === 0) return;
|
|
273
305
|
const placeholders = ids.map(() => '?').join(',');
|
|
274
|
-
db.prepare(
|
|
306
|
+
db.prepare(
|
|
307
|
+
`
|
|
275
308
|
UPDATE observations SET importance = 2
|
|
276
309
|
WHERE id IN (${placeholders})
|
|
277
310
|
AND COALESCE(importance, 1) = 1
|
|
278
311
|
AND COALESCE(access_count, 0) >= 2
|
|
279
|
-
|
|
312
|
+
`,
|
|
313
|
+
).run(...ids);
|
|
280
314
|
}
|
|
281
315
|
|
|
282
316
|
// ─── Idle Cleanup ────────────────────────────────────────────────────────────
|
|
@@ -305,7 +339,9 @@ export function runIdleCleanup(db) {
|
|
|
305
339
|
for (const { types, days } of staleThresholds) {
|
|
306
340
|
const cutoff = Date.now() - days * DAY_MS;
|
|
307
341
|
|
|
308
|
-
const marked = db
|
|
342
|
+
const marked = db
|
|
343
|
+
.prepare(
|
|
344
|
+
`
|
|
309
345
|
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
310
346
|
WHERE importance <= 1 AND COALESCE(access_count, 0) = 0
|
|
311
347
|
-- injection_count=0, the second half of decayAndMarkIdle's engagement guard
|
|
@@ -331,10 +367,14 @@ export function runIdleCleanup(db) {
|
|
|
331
367
|
-- SAME "lessons never auto-GC" guard; without it a lesson demoted to imp≤1
|
|
332
368
|
-- by citation-decay gets pending-purge'd here and hard-deleted by purgeStale.
|
|
333
369
|
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
334
|
-
|
|
370
|
+
`,
|
|
371
|
+
)
|
|
372
|
+
.run(cutoff);
|
|
335
373
|
totalMarked += marked.changes;
|
|
336
374
|
|
|
337
|
-
const compressed = db
|
|
375
|
+
const compressed = db
|
|
376
|
+
.prepare(
|
|
377
|
+
`
|
|
338
378
|
UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
|
|
339
379
|
WHERE COALESCE(compressed_into, 0) = 0 AND importance = 1
|
|
340
380
|
-- Same engagement guard as the mark-idle pass above: COMPRESSED_AUTO also hides
|
|
@@ -346,7 +386,9 @@ export function runIdleCleanup(db) {
|
|
|
346
386
|
-- recoverBuriedLessons only re-floors live (compressed_into=0) rows, so a
|
|
347
387
|
-- compressed lesson is unrecoverable. Parity with selectCompressionCandidates.
|
|
348
388
|
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
349
|
-
|
|
389
|
+
`,
|
|
390
|
+
)
|
|
391
|
+
.run(cutoff);
|
|
350
392
|
totalCompressed += compressed.changes;
|
|
351
393
|
}
|
|
352
394
|
})();
|
package/secret-scrub.mjs
CHANGED
|
@@ -29,7 +29,10 @@ export const SECRET_PATTERNS = [
|
|
|
29
29
|
// excludes "Marker token: …". `secret` added so a bare SECRET=… with a mixed-alnum
|
|
30
30
|
// value is covered (the hex-only assignment pattern below misses non-hex values).
|
|
31
31
|
// 1a. `=` assignment → ALWAYS scrub (config syntax, never prose):
|
|
32
|
-
[
|
|
32
|
+
[
|
|
33
|
+
/((?:\b|_)(?:password|passwd|passphrase|token|bearer|secret)\s*=\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi,
|
|
34
|
+
'$1***',
|
|
35
|
+
],
|
|
33
36
|
// 1b. `:` separator, PASSWORD nouns. Position decides how permissive the value
|
|
34
37
|
// class may be, because the two positions have opposite error costs.
|
|
35
38
|
//
|
|
@@ -67,10 +70,19 @@ export const SECRET_PATTERNS = [
|
|
|
67
70
|
// corrupting prose — which is exactly the error this arm exists to undo.
|
|
68
71
|
// Both arms emit `***` (3 chars, under the {6,} floor), so they cannot
|
|
69
72
|
// double-apply.
|
|
70
|
-
[
|
|
71
|
-
|
|
73
|
+
[
|
|
74
|
+
/((?<![A-Za-z][ \t])(?:\b|_)(?:password|passwd|passphrase)\s*:\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi,
|
|
75
|
+
'$1***',
|
|
76
|
+
],
|
|
77
|
+
[
|
|
78
|
+
/((?:\b|_)(?:password|passwd|passphrase)\s*:\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)(?![A-Za-z]{1,15}(?=[\s,;'"}\]]|$))[^\s,;'"}\]]{6,}/gi,
|
|
79
|
+
'$1***',
|
|
80
|
+
],
|
|
72
81
|
// 1c. `:` separator, prose-ambiguous nouns → keep the lookbehind ("the token: alice"):
|
|
73
|
-
[
|
|
82
|
+
[
|
|
83
|
+
/((?<![A-Za-z][ \t])(?:\b|_)(?:token|bearer|secret)\s*:\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi,
|
|
84
|
+
'$1***',
|
|
85
|
+
],
|
|
74
86
|
// access_token / refresh_token are the canonical OAuth2 field names — they were
|
|
75
87
|
// missing from this KV list (drift vs the JSON list below). `(?:\b|_)` for the same
|
|
76
88
|
// underscore-prefix reason.
|
|
@@ -82,7 +94,10 @@ export const SECRET_PATTERNS = [
|
|
|
82
94
|
// Enumerating known names (not a blanket letter-prefix) preserves the deliberate
|
|
83
95
|
// low-FP decision that `topsecret=` / `access_token_count:` are non-credentials
|
|
84
96
|
// (#8283 + utils.test.mjs:1089-1100); bare `pwd` is omitted so `PWD=` (a path) survives.
|
|
85
|
-
[
|
|
97
|
+
[
|
|
98
|
+
/((?:\b|_)(?:api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|access[_-]?token|refresh[_-]?token|pgpassword|pgpass|mysql_pwd)\s*[=:]\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi,
|
|
99
|
+
'$1***',
|
|
100
|
+
],
|
|
86
101
|
// Space-separated credential CLI flag: `--password <value>` (long-form). The KV
|
|
87
102
|
// patterns above require `=`/`:`; the shell long-flag form uses a space. Long-form
|
|
88
103
|
// only — `-p`/`-u` short flags collide with unit/user/update flags (too FP-risky).
|
|
@@ -104,7 +119,10 @@ export const SECRET_PATTERNS = [
|
|
|
104
119
|
[/((?<![A-Za-z][ \t])(?:\b|_)(?:token|bearer|secret)\s*:\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
|
|
105
120
|
// (b) structured keys + named env vars are unambiguous config even after a word
|
|
106
121
|
// (`see api_key: "x"` DOES scrub, mirroring the unquoted structured-key path):
|
|
107
|
-
[
|
|
122
|
+
[
|
|
123
|
+
/((?:\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,
|
|
124
|
+
'$1$2***$2',
|
|
125
|
+
],
|
|
108
126
|
// AWS access keys: AKIA (long-term) + ASIA (STS temp) + AROA (role) + AIDA
|
|
109
127
|
// (user) + ANPA/ANVA/AGPA (other principal types). All share the 4-letter
|
|
110
128
|
// prefix + exactly 16 base32 chars shape — specific enough for near-zero FP.
|
|
@@ -126,7 +144,10 @@ export const SECRET_PATTERNS = [
|
|
|
126
144
|
// PEM private key blocks. `[A-Z0-9 ]*` covers every armor label — RSA/EC/DSA/
|
|
127
145
|
// OPENSSH plus ENCRYPTED and PGP (… PRIVATE KEY BLOCK) — that the fixed
|
|
128
146
|
// alternation missed; the block delimiters make FP impossible.
|
|
129
|
-
[
|
|
147
|
+
[
|
|
148
|
+
/-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g,
|
|
149
|
+
'***PEM_KEY***',
|
|
150
|
+
],
|
|
130
151
|
// Long hex strings in credential assignments (e.g. SECRET_KEY=abc123def456...).
|
|
131
152
|
// `hash` deliberately excluded: `hash: <40hex>` / `hash=<md5>` are git SHAs and
|
|
132
153
|
// checksums (real, preserved data in this hash-heavy repo), not credentials.
|
|
@@ -137,7 +158,10 @@ export const SECRET_PATTERNS = [
|
|
|
137
158
|
// and GitHub's `token` scheme all carry secrets after the scheme word.
|
|
138
159
|
[/(Authorization:\s*(?:Bearer|Basic|token)\s+)[^\s,;'"}\]]+/gi, '$1***'],
|
|
139
160
|
// Supabase / generic long base64 keys (40+ chars, common in env vars)
|
|
140
|
-
[
|
|
161
|
+
[
|
|
162
|
+
/(\b(?:SUPABASE_KEY|SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*)[^\s,;'"}\]]+/gi,
|
|
163
|
+
'$1***',
|
|
164
|
+
],
|
|
141
165
|
// Basic auth in URLs (https://user:password@host). ftp/ftps added — file-drop
|
|
142
166
|
// creds are a common leak shape the https-only form missed. The userinfo run
|
|
143
167
|
// EXCLUDES `:` (`[^@/\s:]+`) so the two runs can't overlap on a colon — the
|
|
@@ -147,7 +171,10 @@ export const SECRET_PATTERNS = [
|
|
|
147
171
|
// Database connection strings (postgres, mysql, mariadb, mssql, mongodb, redis,
|
|
148
172
|
// amqp) incl. their TLS/alias variants (rediss/amqps/mssql/sqlserver) — managed
|
|
149
173
|
// cloud DBs almost always use the TLS scheme, which the base-only list leaked.
|
|
150
|
-
[
|
|
174
|
+
[
|
|
175
|
+
/\b(postgres(?:ql)?|mysql|mariadb|mssql|sqlserver|mongodb(?:\+srv)?|rediss?|amqps?):\/\/[^\s,;'"}\]]+/gi,
|
|
176
|
+
'$1://***',
|
|
177
|
+
],
|
|
151
178
|
// npm tokens (npm_...)
|
|
152
179
|
[/\bnpm_[a-zA-Z0-9]{36,}\b/g, '***'],
|
|
153
180
|
// Stripe keys (sk_live_, rk_live_, pk_live_, sk_test_, pk_test_) + webhook signing secret (whsec_)
|
|
@@ -169,14 +196,20 @@ export const SECRET_PATTERNS = [
|
|
|
169
196
|
// as `{"api_key": "..."}`. The base key=value pattern stops at quotes, so
|
|
170
197
|
// these slip through. Match the value-quoted form explicitly. Length floor
|
|
171
198
|
// (6) avoids tripping on intentional placeholder shorts ("...", "secret").
|
|
172
|
-
[
|
|
199
|
+
[
|
|
200
|
+
/("(?: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,
|
|
201
|
+
'$1***$2',
|
|
202
|
+
],
|
|
173
203
|
// JSON keys with vendor PREFIX/SUFFIX around the core credential noun —
|
|
174
204
|
// `"x_api_key"`, `"aws_secret_access_key"`, `"my_password"`, `"gh_token"`.
|
|
175
205
|
// The exact-name list above misses these. Anchored to the credential nouns
|
|
176
206
|
// (password|secret|api_key|auth_token|access_token|private_key) so a benign
|
|
177
207
|
// `"token_count"` value (numeric, <6 non-quote chars after scrub) and prose
|
|
178
208
|
// keys stay low-FP; over-scrub is the safe direction for at-rest memory.
|
|
179
|
-
[
|
|
209
|
+
[
|
|
210
|
+
/("\w*(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*"\s*:\s*")[^"]{6,}(")/gi,
|
|
211
|
+
'$1***$2',
|
|
212
|
+
],
|
|
180
213
|
// Quoted-KEY credential values — Python dict reprs `{'api_key': '...'}`, single-quoted
|
|
181
214
|
// JS/JSON, and any mixed quoting. The quoted-VALUE patterns above match an UNQUOTED key
|
|
182
215
|
// (the key's closing quote sits between the key name and the `[=:]`, so `keyword\s*[=:]`
|
|
@@ -189,7 +222,10 @@ export const SECRET_PATTERNS = [
|
|
|
189
222
|
// vendor-prefix JSON pattern above (bare `token`/`bearer` deliberately excluded to avoid
|
|
190
223
|
// `'token_count': 123456`); `passphrase` added here too (double-quoted JSON passphrase is
|
|
191
224
|
// subsumed by this pattern since `['"]` matches `"`). Over-scrub is the safe direction.
|
|
192
|
-
[
|
|
225
|
+
[
|
|
226
|
+
/(['"]\w*(?:password|passwd|passphrase|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*['"]\s*:\s*)(['"])[^'"]{6,}\2/gi,
|
|
227
|
+
'$1$2***$2',
|
|
228
|
+
],
|
|
193
229
|
// Session cookies in headers / urlencoded bodies (sessionid=, session_id=, JSESSIONID=, PHPSESSID=).
|
|
194
230
|
// 16+ chars filters out short test fixtures like sessionid=abc.
|
|
195
231
|
[/\b((?:session[_-]?id|sessionid|jsessionid|phpsessid)\s*[=:]\s*)[^\s,;'"}\]]{16,}/gi, '$1***'],
|
package/server/fts-check.mjs
CHANGED
|
@@ -16,18 +16,21 @@ export function handleMemFtsCheck(db, args) {
|
|
|
16
16
|
if (args.action === 'check') {
|
|
17
17
|
const result = checkFTSIntegrity(db);
|
|
18
18
|
return {
|
|
19
|
-
content: [
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
content: [
|
|
20
|
+
{
|
|
21
|
+
type: 'text',
|
|
22
|
+
text: result.healthy
|
|
23
|
+
? 'FTS5 indexes are healthy — all integrity checks passed.'
|
|
24
|
+
: `FTS5 issues found:\n${result.details.join('\n')}`,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
25
27
|
};
|
|
26
28
|
}
|
|
27
29
|
// args.action === 'rebuild' (Zod enum enforces one of the two)
|
|
28
30
|
const result = rebuildFTS(db);
|
|
29
|
-
const summary =
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
const summary =
|
|
32
|
+
result.errors.length > 0
|
|
33
|
+
? `Rebuilt: ${result.rebuilt.join(', ')}. Errors: ${result.errors.join(', ')}`
|
|
34
|
+
: `Successfully rebuilt: ${result.rebuilt.join(', ')}`;
|
|
32
35
|
return { content: [{ type: 'text', text: summary }] };
|
|
33
36
|
}
|