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.
Files changed (143) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/adopt-cli.mjs +125 -51
  4. package/bash-utils.mjs +105 -29
  5. package/claudemd.mjs +63 -19
  6. package/cli/activity.mjs +36 -18
  7. package/cli/common.mjs +101 -18
  8. package/cli/doctor.mjs +29 -9
  9. package/cli.mjs +69 -10
  10. package/deep-search.mjs +63 -20
  11. package/format-utils.mjs +15 -7
  12. package/haiku-client.mjs +189 -111
  13. package/hash-utils.mjs +15 -6
  14. package/hook-context.mjs +135 -46
  15. package/hook-episode.mjs +76 -23
  16. package/hook-handoff.mjs +216 -84
  17. package/hook-llm.mjs +476 -216
  18. package/hook-memory.mjs +106 -43
  19. package/hook-optimize.mjs +272 -109
  20. package/hook-precompact.mjs +1 -1
  21. package/hook-semaphore.mjs +46 -12
  22. package/hook-shared.mjs +101 -131
  23. package/hook-update.mjs +286 -88
  24. package/hook.mjs +1207 -617
  25. package/install-metadata.mjs +246 -125
  26. package/install.mjs +1327 -917
  27. package/lib/activity.mjs +52 -29
  28. package/lib/atomic-write.mjs +23 -4
  29. package/lib/binding-probe.mjs +40 -38
  30. package/lib/browse-core.mjs +22 -8
  31. package/lib/citation-tracker.mjs +189 -79
  32. package/lib/cite-back-hint.mjs +37 -19
  33. package/lib/cite-recall-path.mjs +3 -1
  34. package/lib/cli-flags.mjs +4 -2
  35. package/lib/cli-project.mjs +8 -5
  36. package/lib/compress-core.mjs +34 -9
  37. package/lib/cooldown-path.mjs +3 -1
  38. package/lib/db-backup.mjs +33 -11
  39. package/lib/deferred-work.mjs +73 -32
  40. package/lib/delete-core.mjs +25 -9
  41. package/lib/doctor-benchmark.mjs +5 -1
  42. package/lib/doctor-drift.mjs +25 -8
  43. package/lib/edge-attribution.mjs +23 -9
  44. package/lib/env-number.mjs +7 -8
  45. package/lib/err-sampler.mjs +22 -11
  46. package/lib/error-recall-core.mjs +15 -17
  47. package/lib/events-injection.mjs +25 -8
  48. package/lib/export-columns.mjs +49 -10
  49. package/lib/fast-summary.mjs +22 -9
  50. package/lib/file-edge-match.mjs +4 -2
  51. package/lib/file-intel.mjs +38 -13
  52. package/lib/frontmatter.mjs +22 -5
  53. package/lib/get-core.mjs +78 -11
  54. package/lib/handoff-constants.mjs +18 -0
  55. package/lib/hook-stdin.mjs +10 -3
  56. package/lib/hook-stdout.mjs +21 -7
  57. package/lib/hook-telemetry.mjs +44 -17
  58. package/lib/id-routing.mjs +11 -6
  59. package/lib/import-jsonl.mjs +92 -41
  60. package/lib/inject-search-core.mjs +5 -2
  61. package/lib/injected-ids.mjs +19 -12
  62. package/lib/install-shape.mjs +47 -23
  63. package/lib/keyctx-marker.mjs +3 -1
  64. package/lib/lesson-bridge.mjs +8 -5
  65. package/lib/llm-call.mjs +76 -0
  66. package/lib/llm-provider-probe.mjs +24 -12
  67. package/lib/low-signal-patterns.mjs +25 -22
  68. package/lib/maintain-core.mjs +221 -92
  69. package/lib/mem-override.mjs +4 -2
  70. package/lib/metrics.mjs +30 -25
  71. package/lib/native-binding-hint.mjs +29 -8
  72. package/lib/observation-write.mjs +74 -19
  73. package/lib/patha-exclude-meter.mjs +10 -6
  74. package/lib/persist-reminder.mjs +22 -10
  75. package/lib/plan-reader.mjs +2 -2
  76. package/lib/private-strip.mjs +3 -3
  77. package/lib/proc-lock.mjs +15 -3
  78. package/lib/proxy-fetch.mjs +49 -13
  79. package/lib/quiet-scope.mjs +42 -0
  80. package/lib/recall-core.mjs +19 -7
  81. package/lib/recent-core.mjs +18 -5
  82. package/lib/registry-core.mjs +12 -6
  83. package/lib/release-digest.mjs +4 -1
  84. package/lib/relevance-floor.mjs +5 -2
  85. package/lib/reread-guard.mjs +16 -8
  86. package/lib/resolve-data-dir.mjs +1 -1
  87. package/lib/rrf.mjs +4 -1
  88. package/lib/save-enrich.mjs +36 -18
  89. package/lib/save-observation.mjs +78 -35
  90. package/lib/scrub-record.mjs +20 -6
  91. package/lib/search-core.mjs +456 -123
  92. package/lib/shard-gc.mjs +50 -0
  93. package/lib/startup-dashboard.mjs +20 -9
  94. package/lib/stats-core.mjs +101 -35
  95. package/lib/stats-quality.mjs +56 -20
  96. package/lib/summary-extractor.mjs +2 -2
  97. package/lib/task-imperative.mjs +12 -4
  98. package/lib/timeline-core.mjs +88 -28
  99. package/lib/tmp-fixture-sweep.mjs +32 -9
  100. package/lib/transcript-scan.mjs +20 -4
  101. package/lib/upgrade-banner.mjs +11 -7
  102. package/mem-cli.mjs +1314 -506
  103. package/memdir.mjs +53 -21
  104. package/nlp.mjs +68 -38
  105. package/npm-shrinkwrap.json +2 -2
  106. package/package.json +9 -2
  107. package/plugin-cache-guard.mjs +31 -15
  108. package/project-utils.mjs +44 -20
  109. package/registry-enricher.mjs +26 -6
  110. package/registry-github.mjs +4 -2
  111. package/registry-importer.mjs +151 -40
  112. package/registry-recommend.mjs +189 -96
  113. package/registry-retriever.mjs +229 -70
  114. package/registry-scanner.mjs +11 -6
  115. package/registry.mjs +163 -66
  116. package/rerank.mjs +18 -4
  117. package/resource-discovery.mjs +14 -4
  118. package/schema.mjs +258 -99
  119. package/scoring-sql.mjs +21 -10
  120. package/scripts/binding-probe-cli.mjs +17 -12
  121. package/scripts/hook-launcher.mjs +148 -31
  122. package/scripts/launch-preflight.mjs +5 -7
  123. package/scripts/launch.mjs +21 -9
  124. package/scripts/post-tool-recall.js +21 -5
  125. package/scripts/pre-agent-inject.js +51 -13
  126. package/scripts/pre-skill-bridge.js +22 -7
  127. package/scripts/pre-tool-recall.js +157 -77
  128. package/scripts/prompt-search-utils.mjs +70 -25
  129. package/scripts/setup.sh +16 -1
  130. package/scripts/user-prompt-search.js +238 -97
  131. package/search-engine.mjs +329 -96
  132. package/search-scoring.mjs +75 -33
  133. package/secret-scrub.mjs +48 -12
  134. package/server/fts-check.mjs +12 -9
  135. package/server.mjs +674 -243
  136. package/skip-tools.mjs +19 -8
  137. package/source-files.mjs +54 -21
  138. package/stop-words.mjs +152 -19
  139. package/synonyms.mjs +579 -164
  140. package/tfidf.mjs +130 -40
  141. package/tier.mjs +1 -1
  142. package/tool-schemas.mjs +381 -131
  143. package/utils.mjs +184 -36
@@ -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.prepare(`
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
- `).all(project, twoHoursAgo);
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.prepare(
104
- `SELECT obs_id, filename FROM observation_files WHERE obs_id IN (${placeholders})`
105
- ).all(...obsIds);
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 *= (1.0 + 0.3 * fileOverlap);
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', 'used', 'using', 'new', 'added', 'updated',
142
- 'file', 'files', 'code', 'change', 'changed', 'changes',
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.replace(/["()]/g, ' ').split(/\s+/)
159
- .map(t => porterStem(t.toLowerCase()))
160
- .filter(t => t.length > 1 && t !== 'or' && t !== 'and')
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); // stem -> # of top docs it appears in (the >=2 bar)
176
- const stemSurfaces = Object.create(null); // stem -> Map(surface -> total occurrences)
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.replace(/[^a-z0-9_-]/g, ' ').split(/\s+/).filter(t => t.length >= 3);
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, bestN = -1;
223
+ let best = null,
224
+ bestN = -1;
205
225
  for (const [surface, n] of stemSurfaces[stem]) {
206
- if (n > bestN) { best = surface; bestN = n; }
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.prepare(`
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
- `).all(ftsQuery, project ?? null, project ?? null);
235
- } catch (e) { debugCatch(e, 'expandQueryByConcepts-fts'); return []; }
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.replace(/["()]/g, ' ').split(/\s+/)
251
- .map(t => t.toLowerCase())
252
- .filter(t => t.length > 1 && t !== 'or' && t !== 'and')
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
- `).run(...ids);
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.prepare(`
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
- `).run(cutoff);
370
+ `,
371
+ )
372
+ .run(cutoff);
335
373
  totalMarked += marked.changes;
336
374
 
337
- const compressed = db.prepare(`
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
- `).run(cutoff);
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
- [/((?:\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, '$1***'],
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
- [/((?<![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, '$1***'],
71
- [/((?:\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, '$1***'],
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
- [/((?<![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, '$1***'],
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
- [/((?:\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, '$1***'],
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
- [/((?:\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'],
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
- [/-----BEGIN [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY(?: BLOCK)?-----/g, '***PEM_KEY***'],
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
- [/(\b(?:SUPABASE_KEY|SUPABASE_ANON_KEY|SUPABASE_SERVICE_ROLE_KEY|DATABASE_URL|REDIS_URL)\s*[=:]\s*)[^\s,;'"}\]]+/gi, '$1***'],
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
- [/\b(postgres(?:ql)?|mysql|mariadb|mssql|sqlserver|mongodb(?:\+srv)?|rediss?|amqps?):\/\/[^\s,;'"}\]]+/gi, '$1://***'],
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
- [/("(?: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'],
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
- [/("\w*(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*"\s*:\s*")[^"]{6,}(")/gi, '$1***$2'],
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
- [/(['"]\w*(?:password|passwd|passphrase|secret|api[_-]?key|auth[_-]?token|access[_-]?token|private[_-]?key)\w*['"]\s*:\s*)(['"])[^'"]{6,}\2/gi, '$1$2***$2'],
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***'],
@@ -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
- type: 'text',
21
- text: result.healthy
22
- ? 'FTS5 indexes are healthy — all integrity checks passed.'
23
- : `FTS5 issues found:\n${result.details.join('\n')}`,
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 = result.errors.length > 0
30
- ? `Rebuilt: ${result.rebuilt.join(', ')}. Errors: ${result.errors.join(', ')}`
31
- : `Successfully rebuilt: ${result.rebuilt.join(', ')}`;
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
  }