claude-mem-lite 3.21.0 → 3.22.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/cli/common.mjs +13 -1
- package/format-utils.mjs +18 -0
- package/hook-context.mjs +6 -2
- package/hook-handoff.mjs +9 -5
- package/hook-memory.mjs +5 -2
- package/hook.mjs +22 -3
- package/lib/compress-core.mjs +5 -2
- package/lib/maintain-core.mjs +17 -9
- package/lib/search-core.mjs +1 -1
- package/mem-cli.mjs +71 -44
- package/package.json +1 -1
- package/scripts/post-tool-use.sh +5 -1
- package/scripts/user-prompt-search.js +10 -1
- package/search-engine.mjs +8 -3
- package/server.mjs +18 -7
- package/utils.mjs +1 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.
|
|
13
|
+
"version": "3.22.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.22.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/cli/common.mjs
CHANGED
|
@@ -21,7 +21,19 @@ export function parseArgs(argv) {
|
|
|
21
21
|
while (i < argv.length) {
|
|
22
22
|
const arg = argv[i];
|
|
23
23
|
if (arg.startsWith('--')) {
|
|
24
|
-
const
|
|
24
|
+
const body = arg.slice(2);
|
|
25
|
+
// `--key=value` (GNU long-option form). Split on the FIRST '=' so values that
|
|
26
|
+
// themselves contain '=' (e.g. `--from=2026-01-01`, a token with '=') stay intact.
|
|
27
|
+
// Without this, `--type=feature` parsed as a boolean flag literally named
|
|
28
|
+
// "type=feature"; the real `--type` stayed undefined and the default silently
|
|
29
|
+
// applied — a save landed in the wrong project / type with no error.
|
|
30
|
+
const eq = body.indexOf('=');
|
|
31
|
+
if (eq >= 0) {
|
|
32
|
+
flags[body.slice(0, eq)] = body.slice(eq + 1);
|
|
33
|
+
i++;
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
const key = body;
|
|
25
37
|
const next = argv[i + 1];
|
|
26
38
|
if (next !== undefined && !next.startsWith('--') && (!next.startsWith('-') || /^-\d/.test(next))) {
|
|
27
39
|
flags[key] = next;
|
package/format-utils.mjs
CHANGED
|
@@ -24,6 +24,24 @@ export function truncate(str, max = 80) {
|
|
|
24
24
|
return str.slice(0, end) + '\u2026';
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
// The block delimiters claude-mem-lite wraps injected context in. Any user-derived text
|
|
28
|
+
// (observation title / lesson, handoff body) that contains one of these LITERALLY would
|
|
29
|
+
// prematurely open or close the block it lands in, and the model then reads the rest as
|
|
30
|
+
// undelimited context. Reachable by editing files that contain these tokens \u2014 e.g.
|
|
31
|
+
// developing claude-mem-lite itself, where source/observations carry the delimiter names.
|
|
32
|
+
const CONTEXT_DELIMITER_RE = /<\/?(?:claude-mem-context|memory-context|session-handoff)>/gi;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Defang the literal context-block delimiter tags in user-derived text. Strips just the
|
|
36
|
+
* angle brackets, so `</claude-mem-context>` renders as `/claude-mem-context` \u2014 still
|
|
37
|
+
* readable, but no longer a structural delimiter. Complements `mdCell`'s pipe-escaping.
|
|
38
|
+
* @param {string} s Input string (any type; coerced)
|
|
39
|
+
* @returns {string} Text with delimiter tags defanged
|
|
40
|
+
*/
|
|
41
|
+
export function neutralizeContextDelimiters(s) {
|
|
42
|
+
return String(s ?? '').replace(CONTEXT_DELIMITER_RE, (m) => m.slice(1, -1));
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
/**
|
|
28
46
|
* Render the PostToolUse error-recall hint block (hook.mjs::triggerErrorRecall).
|
|
29
47
|
* The single most-relevant hit (rows[0]) that carries a lesson_learned gets its
|
package/hook-context.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { basename, join } from 'path';
|
|
|
5
5
|
import { existsSync, readFileSync, writeFileSync, renameSync, unlinkSync } from 'fs';
|
|
6
6
|
import {
|
|
7
7
|
estimateTokens, truncate, typeIcon, fmtTime, inferProject,
|
|
8
|
-
debugLog, debugCatch,
|
|
8
|
+
debugLog, debugCatch, neutralizeContextDelimiters,
|
|
9
9
|
DECAY_HALF_LIFE_BY_TYPE, DEFAULT_DECAY_HALF_LIFE_MS, notLowSignalTitleClause,
|
|
10
10
|
} from './utils.mjs';
|
|
11
11
|
import { STALE_SESSION_MS, FALLBACK_OBS_WINDOW_MS, RUNTIME_DIR, effectiveQuiet } from './hook-shared.mjs';
|
|
@@ -462,7 +462,11 @@ export function buildSessionContextLines(db, project, now = new Date(), currentC
|
|
|
462
462
|
}
|
|
463
463
|
}
|
|
464
464
|
|
|
465
|
-
|
|
465
|
+
// Defang any literal block-delimiter tag carried in a title/lesson/summary so a row
|
|
466
|
+
// can't prematurely close the <claude-mem-context> block it's wrapped in (mdCell does
|
|
467
|
+
// the same for `|`). One source of truth: both the SessionStart hook and the CLI
|
|
468
|
+
// `context` command consume this return.
|
|
469
|
+
return neutralizeContextDelimiters([...summaryLines, ...handoffLines, ...deferredLines, ...obsLines].join('\n'));
|
|
466
470
|
}
|
|
467
471
|
|
|
468
472
|
/**
|
package/hook-handoff.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Extracted for testability — hook.mjs has module-level side effects
|
|
3
3
|
|
|
4
4
|
import { basename } from 'path';
|
|
5
|
-
import { truncate, extractMatchKeywords, tokenizeHandoff, isSpecificTerm, scrubSecrets, LOW_SIGNAL_TITLE, EDIT_TOOLS, isMetaTriggerPrompt, notLowSignalTitleClause } from './utils.mjs';
|
|
5
|
+
import { truncate, extractMatchKeywords, tokenizeHandoff, isSpecificTerm, scrubSecrets, LOW_SIGNAL_TITLE, EDIT_TOOLS, isMetaTriggerPrompt, notLowSignalTitleClause, neutralizeContextDelimiters } from './utils.mjs';
|
|
6
6
|
import { scrubRecord } from './lib/scrub-record.mjs';
|
|
7
7
|
import {
|
|
8
8
|
HANDOFF_EXPIRY_CLEAR, HANDOFF_EXPIRY_EXIT, HANDOFF_ANCHOR_MAX_AGE,
|
|
@@ -418,11 +418,15 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
418
418
|
`<session-handoff source="${handoff.type}" age="${ageStr}" origin="hook-injected">`,
|
|
419
419
|
];
|
|
420
420
|
|
|
421
|
+
// Defang delimiter tags in the free-text fields ONLY — never the structural
|
|
422
|
+
// <session-handoff> tags in `lines`, or the block would lose its own framing. A
|
|
423
|
+
// user prompt or edit snippet carrying a literal </session-handoff> would otherwise
|
|
424
|
+
// close the block early and the rest would read as a real user message.
|
|
421
425
|
if (handoff.working_on) {
|
|
422
|
-
lines.push('## Working On', handoff.working_on, '');
|
|
426
|
+
lines.push('## Working On', neutralizeContextDelimiters(handoff.working_on), '');
|
|
423
427
|
}
|
|
424
428
|
if (handoff.completed) {
|
|
425
|
-
lines.push('## Completed', ...handoff.completed.split('\n').map(l => `- ${l}`), '');
|
|
429
|
+
lines.push('## Completed', ...neutralizeContextDelimiters(handoff.completed).split('\n').map(l => `- ${l}`), '');
|
|
426
430
|
}
|
|
427
431
|
if (handoff.unfinished) {
|
|
428
432
|
// Extract only the pending-work portion (before narrative history separator).
|
|
@@ -431,7 +435,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
431
435
|
// completeness claim the episode buffer can't support.
|
|
432
436
|
const pending = extractUnfinishedSummary(handoff.unfinished);
|
|
433
437
|
if (pending) {
|
|
434
|
-
lines.push('## Recent activity', ...pending.split('; ').map(l => `- ${l}`), '');
|
|
438
|
+
lines.push('## Recent activity', ...neutralizeContextDelimiters(pending).split('; ').map(l => `- ${l}`), '');
|
|
435
439
|
}
|
|
436
440
|
}
|
|
437
441
|
if (handoff.key_files) {
|
|
@@ -441,7 +445,7 @@ function renderHandoffFromRow(handoff, db, project) {
|
|
|
441
445
|
} catch {}
|
|
442
446
|
}
|
|
443
447
|
if (handoff.key_decisions) {
|
|
444
|
-
lines.push('## Key Decisions', ...handoff.key_decisions.split('\n').map(l => `- ${l}`), '');
|
|
448
|
+
lines.push('## Key Decisions', ...neutralizeContextDelimiters(handoff.key_decisions).split('\n').map(l => `- ${l}`), '');
|
|
445
449
|
}
|
|
446
450
|
|
|
447
451
|
lines.push('</session-handoff>');
|
package/hook-memory.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// claude-mem-lite — Semantic Memory Injection
|
|
2
2
|
// Search past observations for relevant memories to inject as context at user-prompt time.
|
|
3
3
|
|
|
4
|
-
import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords } from './utils.mjs';
|
|
4
|
+
import { sanitizeFtsQuery, relaxFtsQueryToOr, debugCatch, truncate, OBS_BM25, notLowSignalTitleClause, noisePenaltyClause, tokenizeHandoff, HANDOFF_STOP_WORDS, extractCjkKeywords, neutralizeContextDelimiters } from './utils.mjs';
|
|
5
5
|
import { citeFactorJs } from './scoring-sql.mjs';
|
|
6
6
|
import { recordMetric } from './lib/metrics.mjs';
|
|
7
7
|
import { DB_DIR } from './schema.mjs';
|
|
@@ -123,7 +123,10 @@ export function formatMemoryLine(obs) {
|
|
|
123
123
|
&& hasFilePaths(obs.files_modified)) {
|
|
124
124
|
staleHint = ' [verify-before-use]';
|
|
125
125
|
}
|
|
126
|
-
|
|
126
|
+
// Defang any literal block-delimiter tag in title/lesson so it can't prematurely close
|
|
127
|
+
// the <memory-context> block this line is injected into (parity with hook-context's
|
|
128
|
+
// <claude-mem-context> defense).
|
|
129
|
+
return neutralizeContextDelimiters(`- [${obs.type}] ${truncate(obs.title, 80)}${lessonTag} (#${obs.id})${staleHint}`);
|
|
127
130
|
}
|
|
128
131
|
|
|
129
132
|
function hasFilePaths(filesModified) {
|
package/hook.mjs
CHANGED
|
@@ -39,6 +39,7 @@ import { entry as preCompactEntry } from './hook-precompact.mjs';
|
|
|
39
39
|
import {
|
|
40
40
|
RUNTIME_DIR, EPISODE_BUFFER_SIZE, EPISODE_TIME_GAP_MS,
|
|
41
41
|
SESSION_EXPIRY_MS, STALE_SESSION_MS, STALE_LOCK_MS,
|
|
42
|
+
HANDOFF_EXPIRY_CLEAR, HANDOFF_EXPIRY_EXIT,
|
|
42
43
|
sessionFile, getSessionId, createSessionId, openDb,
|
|
43
44
|
spawnBackground, sweepOrphanEpisodeFiles,
|
|
44
45
|
} from './hook-shared.mjs';
|
|
@@ -696,15 +697,19 @@ function runSessionStartDbMutations(db, { sessionId, project, prevSessionId, now
|
|
|
696
697
|
WHERE status = 'active' AND started_at_epoch < ?
|
|
697
698
|
`).run(staleSessionCutoff);
|
|
698
699
|
|
|
699
|
-
// Auto-compress: mark old low-importance observations as compressed (30+ days, importance
|
|
700
|
+
// Auto-compress: mark old low-importance observations as compressed (30+ days, importance<=1)
|
|
700
701
|
// Lightweight: only marks rows, doesn't create summaries (full compression via mem_compress)
|
|
701
702
|
// v2.56.0 #4: protect injection_count > 0 obs (proven contextually relevant
|
|
702
703
|
// via hook-memory injection, even if user never explicitly fetched). Same
|
|
703
704
|
// protection applied symmetrically in auto-maintain decay/mark-idle below.
|
|
705
|
+
// `<= 1` (was `= 1`): citation-decay floors importance at 0 (added v2.73.2, after this
|
|
706
|
+
// predicate was written) and the LLM low-signal filter saves at imp=0 — those rows are
|
|
707
|
+
// STRICTLY lower value than imp=1 yet escaped GC, accumulating to ~40% of a mature DB
|
|
708
|
+
// (immortal: hidden from injection by the imp>=1 floor, but visible as explicit-search noise).
|
|
704
709
|
const compressed = db.prepare(`
|
|
705
710
|
UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
|
|
706
711
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
707
|
-
AND importance
|
|
712
|
+
AND COALESCE(importance, 1) <= 1
|
|
708
713
|
AND COALESCE(injection_count, 0) = 0
|
|
709
714
|
AND created_at_epoch < ?
|
|
710
715
|
AND project = ?
|
|
@@ -722,7 +727,7 @@ function runSessionStartDbMutations(db, { sessionId, project, prevSessionId, now
|
|
|
722
727
|
const noiseCompressed = db.prepare(`
|
|
723
728
|
UPDATE observations SET compressed_into = ${COMPRESSED_AUTO}
|
|
724
729
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
725
|
-
AND importance
|
|
730
|
+
AND COALESCE(importance, 1) <= 1
|
|
726
731
|
AND (lesson_learned IS NULL OR lesson_learned = '' OR lesson_learned = 'none')
|
|
727
732
|
AND (facts IS NULL OR facts = '' OR facts = '[]')
|
|
728
733
|
AND (
|
|
@@ -848,6 +853,20 @@ function runSessionStartAutoMaintain(db) {
|
|
|
848
853
|
if (swept > 0) debugLog('DEBUG', 'auto-maintain', `swept ${swept} orphan ep-flush/pending file(s)`);
|
|
849
854
|
} catch (e) { debugCatch(e, 'auto-maintain-orphan-sweep'); }
|
|
850
855
|
|
|
856
|
+
// GC expired session_handoffs: the consume-DELETE (handleSessionStart) only removes
|
|
857
|
+
// the single handoff a continuation reads back; an 'exit'/'compact' that is never
|
|
858
|
+
// resumed (and every superseded 'clear') lingers forever — read paths filter by
|
|
859
|
+
// expiry but nothing reaped the rows. Delete past-expiry rows with a +1d margin so a
|
|
860
|
+
// still-readable handoff is never raced away. 'clear' 6h+1d, 'exit'/other 7d+1d.
|
|
861
|
+
try {
|
|
862
|
+
const gc = db.prepare(`
|
|
863
|
+
DELETE FROM session_handoffs
|
|
864
|
+
WHERE (type = 'clear' AND created_at_epoch < ?)
|
|
865
|
+
OR (type != 'clear' AND created_at_epoch < ?)
|
|
866
|
+
`).run(Date.now() - HANDOFF_EXPIRY_CLEAR - 86400000, Date.now() - HANDOFF_EXPIRY_EXIT - 86400000);
|
|
867
|
+
if (gc.changes > 0) debugLog('DEBUG', 'auto-maintain', `gc'd ${gc.changes} expired session_handoffs`);
|
|
868
|
+
} catch (e) { debugCatch(e, 'auto-maintain-handoff-gc'); }
|
|
869
|
+
|
|
851
870
|
// Mark maintenance as done (24h gate) — even though compression runs in background
|
|
852
871
|
writeFileSync(maintainFile, JSON.stringify({ epoch: Date.now() }));
|
|
853
872
|
// Weekly summary grouping runs in background to avoid blocking SessionStart
|
package/lib/compress-core.mjs
CHANGED
|
@@ -22,9 +22,12 @@ import { getVocabulary, computeVector } from '../tfidf.mjs';
|
|
|
22
22
|
import { scrubRecord } from './scrub-record.mjs';
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
* Low-value compression candidates: importance
|
|
25
|
+
* Low-value compression candidates: importance<=1, never accessed, older than
|
|
26
26
|
* `cutoff`, not already compressed. `includeAutoMarked` also folds in rows the
|
|
27
27
|
* hook lightweight-marked as COMPRESSED_AUTO (the hook re-summarizes those).
|
|
28
|
+
* `<= 1` (was `= 1`): citation-decay floors importance at 0 and the LLM low-signal
|
|
29
|
+
* filter saves at imp=0; those rows are strictly lower value than imp=1 and must be
|
|
30
|
+
* GC-eligible too, or they accumulate forever (parity with hook.mjs auto-compress).
|
|
28
31
|
*/
|
|
29
32
|
export function selectCompressionCandidates(db, { cutoff, project = null, includeAutoMarked = false }) {
|
|
30
33
|
const compressedFilter = includeAutoMarked
|
|
@@ -35,7 +38,7 @@ export function selectCompressionCandidates(db, { cutoff, project = null, includ
|
|
|
35
38
|
return db.prepare(`
|
|
36
39
|
SELECT id, project, type, title, created_at, created_at_epoch
|
|
37
40
|
FROM observations
|
|
38
|
-
WHERE COALESCE(importance, 1)
|
|
41
|
+
WHERE COALESCE(importance, 1) <= 1
|
|
39
42
|
AND COALESCE(access_count, 0) = 0
|
|
40
43
|
AND created_at_epoch < ?
|
|
41
44
|
${compressedFilter}
|
package/lib/maintain-core.mjs
CHANGED
|
@@ -112,17 +112,25 @@ export function cleanupBroken(db, { projectFilter, baseParams, opCap = OP_CAP })
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
/**
|
|
115
|
-
* Decay importance of old, never-accessed, NEVER-INJECTED observations
|
|
116
|
-
*
|
|
117
|
-
*
|
|
115
|
+
* Decay importance of old, never-accessed, NEVER-INJECTED observations and mark the
|
|
116
|
+
* importance-1 idle ones as pending-purge. injection_count>0 is protected as first-class
|
|
117
|
+
* engagement alongside access_count (unified across all three paths).
|
|
118
|
+
*
|
|
119
|
+
* MARK-IDLE RUNS BEFORE DECAY (audit MED-1): if decay ran first, an imp-2 row would be
|
|
120
|
+
* decayed 2→1 and then re-selected by the same call's mark-idle pass → hidden as
|
|
121
|
+
* pending-purge in ONE pass, collapsing the per-tier grace cycle and over-marking vs what
|
|
122
|
+
* `maintain scan` (stale = imp-1 only) forecasts. Marking first means each call only marks
|
|
123
|
+
* rows that were ALREADY imp-1; a freshly-decayed imp-2→1 row waits for the next call,
|
|
124
|
+
* so importance tiers each buy a grace cycle (imp3→2→1→pending across runs) and the scan
|
|
125
|
+
* forecast matches what decay actually marks.
|
|
118
126
|
*/
|
|
119
127
|
export function decayAndMarkIdle(db, { projectFilter, baseParams, staleAge, opCap = OP_CAP }) {
|
|
120
|
-
const
|
|
121
|
-
UPDATE observations SET
|
|
128
|
+
const idleMarked = db.prepare(`
|
|
129
|
+
UPDATE observations SET compressed_into = ${COMPRESSED_PENDING_PURGE}
|
|
122
130
|
WHERE id IN (
|
|
123
131
|
SELECT id FROM observations
|
|
124
132
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
125
|
-
AND COALESCE(importance, 1)
|
|
133
|
+
AND COALESCE(importance, 1) = 1
|
|
126
134
|
AND COALESCE(access_count, 0) = 0
|
|
127
135
|
AND COALESCE(injection_count, 0) = 0
|
|
128
136
|
AND created_at_epoch < ?
|
|
@@ -130,12 +138,12 @@ export function decayAndMarkIdle(db, { projectFilter, baseParams, staleAge, opCa
|
|
|
130
138
|
)
|
|
131
139
|
`).run(staleAge, ...baseParams).changes;
|
|
132
140
|
|
|
133
|
-
const
|
|
134
|
-
UPDATE observations SET
|
|
141
|
+
const decayed = db.prepare(`
|
|
142
|
+
UPDATE observations SET importance = MAX(1, COALESCE(importance, 1) - 1)
|
|
135
143
|
WHERE id IN (
|
|
136
144
|
SELECT id FROM observations
|
|
137
145
|
WHERE COALESCE(compressed_into, 0) = 0
|
|
138
|
-
AND COALESCE(importance, 1)
|
|
146
|
+
AND COALESCE(importance, 1) > 1
|
|
139
147
|
AND COALESCE(access_count, 0) = 0
|
|
140
148
|
AND COALESCE(injection_count, 0) = 0
|
|
141
149
|
AND created_at_epoch < ?
|
package/lib/search-core.mjs
CHANGED
|
@@ -102,7 +102,7 @@ export function searchSessionsFts(db, { ftsQuery, project = null, projectBoost =
|
|
|
102
102
|
return db.prepare(`
|
|
103
103
|
SELECT s.id, s.request, s.completed, s.project, s.created_at, s.created_at_epoch,
|
|
104
104
|
${SESS_BM25}
|
|
105
|
-
* (1.0 + EXP(-0.693 * (? - s.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
|
|
105
|
+
* (1.0 + EXP(-0.693 * MAX(0, ? - s.created_at_epoch) / ${DEFAULT_DECAY_HALF_LIFE_MS}.0))
|
|
106
106
|
* (CASE WHEN ? IS NOT NULL AND s.project = ? THEN 2.0 ELSE 1.0 END) as score
|
|
107
107
|
FROM session_summaries_fts
|
|
108
108
|
JOIN session_summaries s ON session_summaries_fts.rowid = s.id
|
package/mem-cli.mjs
CHANGED
|
@@ -324,7 +324,10 @@ function cmdRecent(db, args) {
|
|
|
324
324
|
// accepted silently; the positional path must reject garbage like the --limit flag does.
|
|
325
325
|
const isValid = rawArg !== undefined && isNumericToken(rawArg) && Number.isInteger(rawLimit) && rawLimit > 0 && rawLimit <= RECENT_MAX;
|
|
326
326
|
if (rawArg !== undefined && !isValid) {
|
|
327
|
-
|
|
327
|
+
// Name the ACTUAL fallback: a present --limit overrides the positional below, so
|
|
328
|
+
// claiming "default 10" when `recent abc --limit 5` returns 5 misled the user.
|
|
329
|
+
const fallbackLabel = flags.limit !== undefined ? '--limit' : 'default 10';
|
|
330
|
+
process.stderr.write(`[mem] Invalid count "${rawArg}" (must be an integer between 1 and ${RECENT_MAX}); using ${fallbackLabel}\n`);
|
|
328
331
|
}
|
|
329
332
|
// Positional [N] wins for backward-compat; --limit is sibling-parity alias
|
|
330
333
|
// (search/recall/browse/stats all accept --limit). Pre-2.69 `recent --limit N`
|
|
@@ -1423,6 +1426,12 @@ function cmdDelete(db, args) {
|
|
|
1423
1426
|
return;
|
|
1424
1427
|
}
|
|
1425
1428
|
|
|
1429
|
+
// Snapshot before the irreversible hard-delete so a wrong-id delete has a pre-image,
|
|
1430
|
+
// matching the maintain purge/cleanup hard-delete paths (audit MED-2). Best-effort
|
|
1431
|
+
// (never throws, skips :memory:); rows here are confirmed + non-empty, so the delete
|
|
1432
|
+
// always removes something worth backing up. Must run OUTSIDE the transaction (VACUUM).
|
|
1433
|
+
snapshotDb(db, { tag: 'pre-delete' });
|
|
1434
|
+
|
|
1426
1435
|
// Transaction: clean up related_ids references + delete (aligned with MCP mem_delete)
|
|
1427
1436
|
const deletedIds = new Set(ids);
|
|
1428
1437
|
const deleteTx = db.transaction(() => {
|
|
@@ -1552,6 +1561,12 @@ function cmdUpdate(db, args) {
|
|
|
1552
1561
|
|
|
1553
1562
|
function cmdExport(db, args) {
|
|
1554
1563
|
const { flags } = parseArgs(args);
|
|
1564
|
+
// Guard value-less string flags. Bare `--to` parsed to boolean `true`, and
|
|
1565
|
+
// `new Date(true).getTime()` is 1 (NOT NaN), so the isNaN guard below missed it and
|
|
1566
|
+
// the filter became `created_at_epoch <= 1` → an EMPTY export with exit 0. A backup
|
|
1567
|
+
// script (`export --to "$END" > backup.json`) with an unset `$END` would silently
|
|
1568
|
+
// write an empty backup and report success. Reject like cmdSearch does.
|
|
1569
|
+
if (rejectBareStringFlags(flags, ['project', 'type', 'from', 'to'])) return;
|
|
1555
1570
|
const wheres = [];
|
|
1556
1571
|
const params = [];
|
|
1557
1572
|
// --include-compressed: include compressed observations (aligned with MCP mem_export)
|
|
@@ -1743,8 +1758,10 @@ function cmdCompress(db, args) {
|
|
|
1743
1758
|
// got the 30-day cutoff without knowing their input was discarded.
|
|
1744
1759
|
let ageDays = 30;
|
|
1745
1760
|
if (flags['age-days'] !== undefined) {
|
|
1746
|
-
|
|
1747
|
-
|
|
1761
|
+
// isNumericToken (not bare parseInt) so "1e5"→1 and "30x"→30 are rejected rather than
|
|
1762
|
+
// silently mis-parsed into a far-too-broad cutoff — parity with recent/search/maintain.
|
|
1763
|
+
const parsed = Number(flags['age-days']);
|
|
1764
|
+
if (!isNumericToken(flags['age-days']) || !Number.isInteger(parsed) || parsed < 1) {
|
|
1748
1765
|
fail(`[mem] Invalid --age-days "${flags['age-days']}". Must be a positive integer.`);
|
|
1749
1766
|
return;
|
|
1750
1767
|
}
|
|
@@ -1800,6 +1817,11 @@ function cmdMaintain(db, args) {
|
|
|
1800
1817
|
fail("[mem] Usage: claude-mem-lite maintain <scan|execute> [--ops cleanup,decay,boost,demote_pinned,dedup,purge_stale,rebuild_vectors,vacuum] [--project P] [--retain-days N] [--merge-ids keepId:removeId,...] — 'scan' previews, 'execute' applies.");
|
|
1801
1818
|
return;
|
|
1802
1819
|
}
|
|
1820
|
+
// Guard value-less string flags before any `.split()` / resolveProject runs. A bare
|
|
1821
|
+
// `--merge-ids` parsed to boolean `true`, and `true.split(',')` (line ~1911) crashed
|
|
1822
|
+
// with a raw stack trace — the one string-flag path that lacked this #8470 guard, and
|
|
1823
|
+
// the exact form the `scan` output suggests copy-pasting (`--merge-ids <pairs>`).
|
|
1824
|
+
if (rejectBareStringFlags(flags, ['ops', 'project', 'merge-ids', 'retain-days'])) return;
|
|
1803
1825
|
|
|
1804
1826
|
const project = flags.project ? resolveProject(db, flags.project) : null;
|
|
1805
1827
|
const projectFilter = project ? 'AND project = ?' : '';
|
|
@@ -1868,15 +1890,57 @@ function cmdMaintain(db, args) {
|
|
|
1868
1890
|
// T2-P1-B: surface the OP_CAP hit so users know to re-run, matching MCP mem_maintain.
|
|
1869
1891
|
const capHint = (changes) => (changes >= OP_CAP ? ' (cap reached, re-run for more)' : '');
|
|
1870
1892
|
|
|
1871
|
-
//
|
|
1872
|
-
//
|
|
1873
|
-
// (
|
|
1874
|
-
|
|
1893
|
+
// Parse + validate --retain-days BEFORE the transaction so an invalid value rejects the
|
|
1894
|
+
// whole command atomically. The old code validated inside db.transaction() with a bare
|
|
1895
|
+
// `return`, so an earlier op (cleanup hard-delete / decay / boost) had already mutated and
|
|
1896
|
+
// the transaction COMMITTED despite the exit-1 error (audit MED-2 atomicity).
|
|
1897
|
+
let retainDays = 30;
|
|
1898
|
+
if (ops.includes('purge_stale') && flags['retain-days'] !== undefined) {
|
|
1899
|
+
// Number + isInteger (not parseInt) so "7.5"/"30x" are rejected rather than silently
|
|
1900
|
+
// truncated — parity with the mem_maintain zod .int().min(7).max(365) bound.
|
|
1901
|
+
const parsed = Number(flags['retain-days']);
|
|
1902
|
+
if (!Number.isInteger(parsed) || parsed < 7 || parsed > 365) {
|
|
1903
|
+
fail(`[mem] --retain-days must be an integer in [7, 365] (got "${flags['retain-days']}")`);
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
retainDays = parsed;
|
|
1907
|
+
}
|
|
1908
|
+
const retainCutoff = Date.now() - retainDays * 86400000;
|
|
1909
|
+
// purge_stale is the only DELETE here — require --confirm so a mis-typed run can't wipe rows.
|
|
1910
|
+
const confirmed = flags.confirm === true || flags.confirm === 'true';
|
|
1911
|
+
|
|
1912
|
+
// Snapshot the DB before the irreversible cleanup/purge hard-deletes — only when rows will
|
|
1913
|
+
// actually be removed, and OUTSIDE the transaction below (VACUUM cannot run inside one).
|
|
1914
|
+
// Best-effort; snapshotDb never throws.
|
|
1915
|
+
const willPurge = ops.includes('purge_stale') && confirmed;
|
|
1875
1916
|
if (hardDeleteCandidateCount(db, mctx, { cleanup: ops.includes('cleanup'), purge: willPurge }) > 0) {
|
|
1876
1917
|
snapshotDb(db, { tag: 'pre-maintain' });
|
|
1877
1918
|
}
|
|
1878
1919
|
|
|
1879
1920
|
db.transaction(() => {
|
|
1921
|
+
// PURGE FIRST — matches the auto-maintain hook order (hook.mjs:766). Running decay BEFORE
|
|
1922
|
+
// purge in one transaction marked a stale row pending-purge AND deleted it in the SAME call
|
|
1923
|
+
// (zero grace), and the pre-txn snapshot guard counts only PRE-EXISTING pending rows so it
|
|
1924
|
+
// skipped the backup → permanent, unrecoverable loss of notable imp-2/3 memories (audit
|
|
1925
|
+
// HIGH-1). Purging first deletes only rows a PRIOR run marked (which the guard saw + backed
|
|
1926
|
+
// up); rows decay marks below wait for the next maintain run, regaining the grace cycle.
|
|
1927
|
+
if (ops.includes('purge_stale')) {
|
|
1928
|
+
if (!confirmed) {
|
|
1929
|
+
const previewRow = purgeStalePreview(db, mctx, retainCutoff);
|
|
1930
|
+
const pushLines = [`purge_stale preview (no --confirm):`,
|
|
1931
|
+
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`];
|
|
1932
|
+
if (previewRow.candidates > 0) {
|
|
1933
|
+
pushLines.push(` Oldest: ${new Date(previewRow.oldest).toISOString().slice(0, 10)}`);
|
|
1934
|
+
pushLines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
1935
|
+
}
|
|
1936
|
+
pushLines.push(` To delete, re-run with --confirm.`);
|
|
1937
|
+
results.push(pushLines.join('\n'));
|
|
1938
|
+
} else {
|
|
1939
|
+
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1940
|
+
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)${capHint(purged)}`);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1880
1944
|
if (ops.includes('cleanup')) {
|
|
1881
1945
|
const deleted = cleanupBroken(db, mctx);
|
|
1882
1946
|
results.push(`Cleaned up ${deleted} broken observations${capHint(deleted)}`);
|
|
@@ -1926,43 +1990,6 @@ function cmdMaintain(db, args) {
|
|
|
1926
1990
|
results.push('Warning: --merge-ids provided but "dedup" not in operations — merge-ids ignored');
|
|
1927
1991
|
}
|
|
1928
1992
|
|
|
1929
|
-
if (ops.includes('purge_stale')) {
|
|
1930
|
-
// --retain-days: default 30 when absent; reject negative / 0 / NaN / out-of-range.
|
|
1931
|
-
// A negative value made retainCutoff a FUTURE timestamp → purged the entire
|
|
1932
|
-
// pending-purge backlog regardless of age; 0/garbage silently became 30 and
|
|
1933
|
-
// masked typos. Parity with the mem_maintain MCP zod bound [7, 365].
|
|
1934
|
-
let retainDays = 30;
|
|
1935
|
-
if (flags['retain-days'] !== undefined) {
|
|
1936
|
-
// Number + isInteger (not parseInt) so "7.5"/"30x" are rejected rather than
|
|
1937
|
-
// silently truncated to 7/30 — parity with the mem_maintain zod .int() bound.
|
|
1938
|
-
const parsed = Number(flags['retain-days']);
|
|
1939
|
-
if (!Number.isInteger(parsed) || parsed < 7 || parsed > 365) {
|
|
1940
|
-
// fail() only sets exitCode + writes stderr; it does NOT throw, so we
|
|
1941
|
-
// MUST return or execution falls through to the DELETE with the bad value.
|
|
1942
|
-
fail(`[mem] --retain-days must be an integer in [7, 365] (got "${flags['retain-days']}")`);
|
|
1943
|
-
return;
|
|
1944
|
-
}
|
|
1945
|
-
retainDays = parsed;
|
|
1946
|
-
}
|
|
1947
|
-
const retainCutoff = Date.now() - retainDays * 86400000;
|
|
1948
|
-
// T2-P0-A (CLI parity): purge_stale is the only DELETE in this code path — require
|
|
1949
|
-
// --confirm so a mis-typed `maintain execute --ops purge_stale` can't wipe rows silently.
|
|
1950
|
-
const confirmed = flags.confirm === true || flags.confirm === 'true';
|
|
1951
|
-
if (!confirmed) {
|
|
1952
|
-
const previewRow = purgeStalePreview(db, mctx, retainCutoff);
|
|
1953
|
-
const pushLines = [`purge_stale preview (no --confirm):`,
|
|
1954
|
-
` Candidates (pending-purge, older than ${retainDays}d): ${previewRow.candidates}`];
|
|
1955
|
-
if (previewRow.candidates > 0) {
|
|
1956
|
-
pushLines.push(` Oldest: ${new Date(previewRow.oldest).toISOString().slice(0, 10)}`);
|
|
1957
|
-
pushLines.push(` Newest: ${new Date(previewRow.newest).toISOString().slice(0, 10)}`);
|
|
1958
|
-
}
|
|
1959
|
-
pushLines.push(` To delete, re-run with --confirm.`);
|
|
1960
|
-
results.push(pushLines.join('\n'));
|
|
1961
|
-
} else {
|
|
1962
|
-
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1963
|
-
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)${capHint(purged)}`);
|
|
1964
|
-
}
|
|
1965
|
-
}
|
|
1966
1993
|
})();
|
|
1967
1994
|
|
|
1968
1995
|
// FTS optimize
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.22.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/scripts/post-tool-use.sh
CHANGED
|
@@ -32,7 +32,11 @@ if [[ "$tool" == "Read" ]]; then
|
|
|
32
32
|
# Sanitize project name to match utils.mjs inferProject()
|
|
33
33
|
project="${project//[^a-zA-Z0-9_.-]/-}"
|
|
34
34
|
project="${project:-unknown}"
|
|
35
|
-
|
|
35
|
+
# Honor CLAUDE_MEM_DIR relocation (mirrors schema.mjs DB_DIR → hook-shared RUNTIME_DIR).
|
|
36
|
+
# hook.mjs flushEpisode reads reads-<project>.txt from CLAUDE_MEM_DIR/runtime; if this
|
|
37
|
+
# bash fast-path wrote to $HOME unconditionally, a relocated install would drop all
|
|
38
|
+
# Read context from episodes AND grow an uncollected reads file in $HOME forever.
|
|
39
|
+
runtime_dir="${CLAUDE_MEM_DIR:-$HOME/.claude-mem-lite}/runtime"
|
|
36
40
|
mkdir -p "$runtime_dir" 2>/dev/null
|
|
37
41
|
# Use printf to avoid shell interpretation of special characters in file paths
|
|
38
42
|
printf '%s\n' "$file_path" >> "${runtime_dir}/reads-${project}.txt"
|
|
@@ -491,6 +491,11 @@ async function main() {
|
|
|
491
491
|
|
|
492
492
|
let hookData;
|
|
493
493
|
try { hookData = JSON.parse(raw); } catch { return; }
|
|
494
|
+
// JSON.parse('null'/'42'/'"x"') succeeds with a non-object; dereferencing .prompt on
|
|
495
|
+
// it threw a raw TypeError → unhandled rejection → exit 1 (this was the lone hook
|
|
496
|
+
// script without an exit-0 safety net, violating the "hooks never exit non-zero"
|
|
497
|
+
// invariant — exit 2 on UserPromptSubmit would even block the user's prompt).
|
|
498
|
+
if (!hookData || typeof hookData !== 'object') return;
|
|
494
499
|
|
|
495
500
|
const rawPrompt = hookData.prompt || hookData.user_prompt;
|
|
496
501
|
if (!rawPrompt || typeof rawPrompt !== 'string') return;
|
|
@@ -724,4 +729,8 @@ async function main() {
|
|
|
724
729
|
}
|
|
725
730
|
}
|
|
726
731
|
|
|
727
|
-
|
|
732
|
+
// Swallow any rejection so the hook can never surface a non-zero exit (the invariant
|
|
733
|
+
// every sibling hook script upholds). Deliberately NOT `.finally(process.exit(0))` —
|
|
734
|
+
// this hook detaches a background `claude -p` search and a forced exit would kill it;
|
|
735
|
+
// letting the loop drain naturally exits 0 once the detached child is unref'd.
|
|
736
|
+
main().catch(() => {});
|
package/search-engine.mjs
CHANGED
|
@@ -16,8 +16,13 @@ import { extractPRFTerms, expandQueryByConcepts } from './search-scoring.mjs';
|
|
|
16
16
|
|
|
17
17
|
// Scoring expressions — full adds project boost + access bonus; simple is for
|
|
18
18
|
// expansion paths where boost would over-amplify already-loose matches.
|
|
19
|
+
// `MAX(0, now - ts)` clamps the recency age to >= 0: a far-FUTURE created_at/last_accessed
|
|
20
|
+
// (reachable via restore/import-jsonl, which accept arbitrary epochs) otherwise made the
|
|
21
|
+
// exponent large-positive → EXP overflowed to +Infinity → score -Infinity → that row sorted
|
|
22
|
+
// #1 for any match AND JSON.stringify emitted `"score": null` (numeric-contract break). A
|
|
23
|
+
// future row now reads as age 0 = max (finite) recency, not Infinity.
|
|
19
24
|
const FULL_SCORE = `${OBS_BM25}
|
|
20
|
-
* (1.0 + EXP(-0.693 * (? - MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))) / ${TYPE_DECAY_CASE}))
|
|
25
|
+
* (1.0 + EXP(-0.693 * MAX(0, ? - MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))) / ${TYPE_DECAY_CASE}))
|
|
21
26
|
* ${TYPE_QUALITY_CASE}
|
|
22
27
|
* (CASE WHEN ? IS NOT NULL AND o.project = ? THEN 2.0 ELSE 1.0 END)
|
|
23
28
|
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
@@ -25,7 +30,7 @@ const FULL_SCORE = `${OBS_BM25}
|
|
|
25
30
|
* (1.0 + 0.3 * (o.lesson_learned IS NOT NULL))`;
|
|
26
31
|
|
|
27
32
|
const SIMPLE_SCORE = `${OBS_BM25}
|
|
28
|
-
* (1.0 + EXP(-0.693 * (? - MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))) / ${TYPE_DECAY_CASE}))
|
|
33
|
+
* (1.0 + EXP(-0.693 * MAX(0, ? - MAX(o.created_at_epoch, COALESCE(o.last_accessed_at, o.created_at_epoch))) / ${TYPE_DECAY_CASE}))
|
|
29
34
|
* ${TYPE_QUALITY_CASE}
|
|
30
35
|
* (0.5 + 0.5 * COALESCE(o.importance, 1))
|
|
31
36
|
* (1.0 + 0.3 * (o.lesson_learned IS NOT NULL))`;
|
|
@@ -320,7 +325,7 @@ export function findFtsAnchor(db, { ftsQuery, project = null, nowT = null, halfL
|
|
|
320
325
|
AND (? IS NULL OR o.project = ?)
|
|
321
326
|
AND COALESCE(o.compressed_into, 0) = 0
|
|
322
327
|
ORDER BY ${OBS_BM25}
|
|
323
|
-
* (1.0 + EXP(-0.693 * (? - o.created_at_epoch) / ${halfLifeMs}.0))
|
|
328
|
+
* (1.0 + EXP(-0.693 * MAX(0, ? - o.created_at_epoch) / ${halfLifeMs}.0))
|
|
324
329
|
LIMIT 1
|
|
325
330
|
`;
|
|
326
331
|
const stmt = db.prepare(sql);
|
package/server.mjs
CHANGED
|
@@ -617,6 +617,11 @@ server.registerTool(
|
|
|
617
617
|
return { content: [{ type: 'text', text: lines.join('\n') }] };
|
|
618
618
|
}
|
|
619
619
|
|
|
620
|
+
// Snapshot before the irreversible hard-delete so a wrong-id delete has a pre-image,
|
|
621
|
+
// matching the CLI delete + maintain purge/cleanup paths (audit MED-2). Best-effort
|
|
622
|
+
// (never throws, skips :memory:). Must run OUTSIDE the transaction below (VACUUM).
|
|
623
|
+
snapshotDb(db, { tag: 'pre-delete' });
|
|
624
|
+
|
|
620
625
|
// Wrap cleanup + deletion in a transaction for consistency
|
|
621
626
|
const deletedIds = new Set(args.ids);
|
|
622
627
|
const deleteTx = db.transaction(() => {
|
|
@@ -1088,6 +1093,19 @@ server.registerTool(
|
|
|
1088
1093
|
}
|
|
1089
1094
|
|
|
1090
1095
|
db.transaction(() => {
|
|
1096
|
+
// PURGE FIRST — matches the auto-maintain hook order (hook.mjs:766) and the CLI
|
|
1097
|
+
// cmdMaintain. Running decay BEFORE purge in one transaction marked a stale row
|
|
1098
|
+
// pending-purge AND deleted it in the SAME call (zero grace), while the pre-txn
|
|
1099
|
+
// snapshot guard counts only PRE-EXISTING pending rows so it skipped the backup →
|
|
1100
|
+
// permanent loss of notable imp-2/3 memories (audit HIGH-1). Purging first deletes
|
|
1101
|
+
// only rows a PRIOR run marked (backed up); rows decay marks below wait one cycle.
|
|
1102
|
+
if (ops.includes('purge_stale')) {
|
|
1103
|
+
const retainDays = args.retain_days ?? 30;
|
|
1104
|
+
const retainCutoff = Date.now() - retainDays * 86400000;
|
|
1105
|
+
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1106
|
+
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)` + (purged >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1091
1109
|
if (ops.includes('cleanup')) {
|
|
1092
1110
|
const deleted = cleanupBroken(db, mctx);
|
|
1093
1111
|
results.push(`Cleaned up ${deleted} broken observations` + (deleted >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
@@ -1118,13 +1136,6 @@ server.registerTool(
|
|
|
1118
1136
|
if (!ops.includes('dedup') && args.merge_ids) {
|
|
1119
1137
|
results.push('Warning: merge_ids provided but "dedup" not in operations — merge_ids ignored');
|
|
1120
1138
|
}
|
|
1121
|
-
|
|
1122
|
-
if (ops.includes('purge_stale')) {
|
|
1123
|
-
const retainDays = args.retain_days ?? 30;
|
|
1124
|
-
const retainCutoff = Date.now() - retainDays * 86400000;
|
|
1125
|
-
const purged = purgeStale(db, mctx, retainCutoff);
|
|
1126
|
-
results.push(`Purged ${purged} stale observations (retained last ${retainDays} days)` + (purged >= OP_CAP ? ' (cap reached, re-run for more)' : ''));
|
|
1127
|
-
}
|
|
1128
1139
|
})();
|
|
1129
1140
|
|
|
1130
1141
|
// FTS5 optimize (outside transaction)
|
package/utils.mjs
CHANGED
|
@@ -14,7 +14,7 @@ export { cjkBigrams, extractCjkSynonymTokens, extractCjkKeywords, extractCjkLike
|
|
|
14
14
|
export { resolveProject, _resetProjectCache } from './project-utils.mjs';
|
|
15
15
|
export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
|
|
16
16
|
export { stripPrivate } from './lib/private-strip.mjs';
|
|
17
|
-
export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey, formatErrorRecallHints } from './format-utils.mjs';
|
|
17
|
+
export { truncate, typeIcon, fmtDate, fmtTime, isoWeekKey, formatErrorRecallHints, neutralizeContextDelimiters } from './format-utils.mjs';
|
|
18
18
|
export { computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from './hash-utils.mjs';
|
|
19
19
|
export { detectBashSignificance, extractErrorKeywords, extractFilePaths, stripTestSuffix } from './bash-utils.mjs';
|
|
20
20
|
|