claude-mem-lite 2.65.0 → 2.66.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "2.65.0",
13
+ "version": "2.66.0",
14
14
  "source": "./",
15
15
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall"
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.65.0",
3
+ "version": "2.66.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code — FTS5 search, episode batching, error-triggered recall",
5
5
  "author": {
6
6
  "name": "sdsrss"
package/install.mjs CHANGED
@@ -1438,6 +1438,11 @@ async function doctor() {
1438
1438
  }
1439
1439
 
1440
1440
  console.log(`\n ${buildDoctorSummary(issues, warnings)}\n`);
1441
+ // Diagnostic-tool exit-code contract: any ✗-level finding must propagate non-zero
1442
+ // so CI / wrapper scripts (`claude-mem-lite doctor || alert`) actually trip. Keeps
1443
+ // ⚠-only states at exit 0 (#8268 already established the visual ⚠ vs counted-issue
1444
+ // separation; this propagates that count to the shell).
1445
+ if (issues > 0) process.exitCode = 1;
1441
1446
  }
1442
1447
 
1443
1448
  // ─── Settings helpers ───────────────────────────────────────────────────────
package/mem-cli.mjs CHANGED
@@ -1366,7 +1366,13 @@ function cmdExport(db, args) {
1366
1366
  `).all(...params, limit);
1367
1367
 
1368
1368
  if (rows.length === 0) {
1369
- out('[mem] No observations found matching criteria');
1369
+ // Empty result must respect the requested format so `export … | jq` works:
1370
+ // json → "[]" (valid empty array)
1371
+ // jsonl → 0 lines (valid empty file)
1372
+ // The friendly note goes to stderr so it doesn't poison stdout for callers
1373
+ // piping to a parser.
1374
+ if (format === 'json') out('[]');
1375
+ process.stderr.write('[mem] No observations found matching criteria\n');
1370
1376
  return;
1371
1377
  }
1372
1378
 
@@ -2320,6 +2326,17 @@ export async function run(argv) {
2320
2326
  return;
2321
2327
  }
2322
2328
 
2329
+ // --json contract surfacing: only `search` and `context` actually emit JSON;
2330
+ // historically `recent --json | jq` etc. silently produced text, breaking
2331
+ // automation. Emit a one-line stderr note when --json is passed to a command
2332
+ // that doesn't honor it. Stdout output and exit code are unchanged so existing
2333
+ // text-parsing callers keep working — the note lives in stderr for scripts to
2334
+ // detect the gap.
2335
+ const JSON_SUPPORTED_CMDS = new Set(['search', 'context']);
2336
+ if (cmdArgs.includes('--json') && !JSON_SUPPORTED_CMDS.has(cmd)) {
2337
+ process.stderr.write(`[mem] Note: --json is supported only on: ${[...JSON_SUPPORTED_CMDS].join(', ')}. "${cmd}" outputs text.\n`);
2338
+ }
2339
+
2323
2340
  try {
2324
2341
  switch (cmd) {
2325
2342
  case 'search': cmdSearch(db, cmdArgs); break;
package/nlp.mjs CHANGED
@@ -117,10 +117,17 @@ export function extractCjkKeywords(text) {
117
117
  export function extractCjkLikePatterns(query) {
118
118
  if (!query || !/[\u4e00-\u9fff\u3400-\u4dbf]{2,}/.test(query)) return [];
119
119
  const keywords = extractCjkKeywords(query);
120
- // Bigrams for unmatched CJK portions
120
+ // Bigrams for unmatched CJK portions \u2014 but only from pure-CJK whitespace tokens.
121
+ // Mixed-script tokens (e.g. "xyzAbc\u4e0d\u5b58\u5728neverhit") behave as identifier-like
122
+ // literals; LIKE-OR'ing the CJK-suffix bigrams matches unrelated docs containing
123
+ // common fragments. Mirrors the FTS-side guard in sanitizeFtsQuery.
121
124
  let remainder = query;
122
125
  for (const w of keywords) remainder = remainder.split(w).join(' ');
123
- const bigrams = cjkBigrams(remainder).split(' ').filter(Boolean);
126
+ const pureCjkOnly = remainder
127
+ .split(/\s+/)
128
+ .filter(t => /[\u4e00-\u9fff\u3400-\u4dbf]/.test(t) && !/[A-Za-z0-9]/.test(t))
129
+ .join(' ');
130
+ const bigrams = pureCjkOnly ? cjkBigrams(pureCjkOnly).split(' ').filter(Boolean) : [];
124
131
  return [...new Set([...keywords, ...bigrams])];
125
132
  }
126
133
 
@@ -255,7 +262,16 @@ export function sanitizeFtsQuery(query) {
255
262
  // Individual CJK chars ("系","统") are too noisy; bigrams ("系统") capture compound words.
256
263
  // Skip bigrams when CJK synonym extraction already produced meaningful tokens —
257
264
  // bigrams joined with AND would make the query too restrictive.
258
- const bigrams = cjkExtracted ? null : cjkBigrams(cleaned);
265
+ // Also skip for mixed-script tokens (e.g. "xyzAbc不存在neverhit"): the latin portion
266
+ // is already a strong literal anchor; bigramming the CJK suffix lets short fragments
267
+ // like "存在" match alone after AND→OR fallback, exploding recall onto unrelated docs.
268
+ let bigrams = null;
269
+ if (!cjkExtracted) {
270
+ const pureCjkTokens = tokens.filter(t =>
271
+ /[一-鿿㐀-䶿]/.test(t) && !/[A-Za-z0-9]/.test(t)
272
+ );
273
+ if (pureCjkTokens.length > 0) bigrams = cjkBigrams(pureCjkTokens.join(' '));
274
+ }
259
275
  const bigramSet = new Set(bigrams ? bigrams.split(' ').filter(b => b && !CJK_STOP_WORDS.has(b)) : []);
260
276
  const hasBigrams = bigramSet.size > 0;
261
277
  const finalTokens = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "2.65.0",
3
+ "version": "2.66.0",
4
4
  "description": "Lightweight persistent memory system for Claude Code",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",
package/secret-scrub.mjs CHANGED
@@ -7,7 +7,19 @@ export const SECRET_PATTERNS = [
7
7
  // Key-value assignments: password=xxx, token=xxx, api_key=xxx, secret=xxx, etc.
8
8
  // Excludes code-like values: null, undefined, true, false, None, empty, function calls (word()),
9
9
  // and short values (<6 chars) that are typically variable names not secrets.
10
- [/(\b(?:password|passwd|token|api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token|bearer)\s*[=:]\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi, '$1***'],
10
+ //
11
+ // Split into two patterns so prose mentions don't get scrubbed:
12
+ // 1. Bare credential nouns (password|passwd|token|bearer) commonly appear in
13
+ // English prose — "Marker token: xyzpdq", "the bearer: alice". We require
14
+ // the keyword NOT to be preceded by an English-word + horizontal-space
15
+ // (the prose mention shape). Code/config has the keyword at start-of-line,
16
+ // after a separator, or in object-literal context — none of which match
17
+ // "letter-then-space" preceding the keyword.
18
+ // 2. Structured keys (api_key, auth_token, …) keep the original behavior —
19
+ // a separator/compound key is unambiguous config syntax even when
20
+ // preceded by prose ("see auth_token: shhhhhh").
21
+ [/((?<![A-Za-z][ \t])\b(?:password|passwd|token|bearer)\s*[=:]\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi, '$1***'],
22
+ [/(\b(?:api[_-]?key|api[_-]?secret|secret[_-]?key|access[_-]?key|private[_-]?key|client[_-]?secret|auth[_-]?token)\s*[=:]\s*)(?!process\.env\.)(?!new\s)(?!\w+\()(?!(?:null|undefined|true|false|None|nil|empty|""|''|0)\b)[^\s,;'"}\]]{6,}/gi, '$1***'],
11
23
  // AWS access keys (AKIA...)
12
24
  [/\bAKIA[A-Z0-9]{16}\b/g, '***'],
13
25
  // OpenAI / Anthropic keys (sk-...) — specific prefixes have lower length threshold