claude-mem-lite 6.8.3 → 6.9.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/README.md +2 -2
- package/cli/common.mjs +114 -1
- package/cli.mjs +23 -4
- package/format-utils.mjs +30 -0
- package/hook-episode.mjs +11 -2
- package/hook-llm.mjs +1 -1
- package/hook.mjs +6 -1
- package/install.mjs +43 -6
- package/lib/doctor-modes.mjs +31 -0
- package/lib/install-shape.mjs +28 -0
- package/mem-cli.mjs +83 -13
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
- package/scripts/post-tool-use.sh +19 -1
- package/scripts/setup.sh +108 -22
- package/server.mjs +9 -4
- package/source-files.mjs +1 -0
- package/tfidf.mjs +3 -47
- package/utils.mjs +15 -18
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.
|
|
12
|
+
"version": "6.9.0",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.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/README.md
CHANGED
|
@@ -128,7 +128,7 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
|
|
|
128
128
|
- **FTS integrity management** -- `mem_fts_check` tool verifies FTS5 index health or rebuilds indexes on demand, useful after database recovery or when search results seem wrong
|
|
129
129
|
- **Atomic multi-table writes** -- `saveObservation` wraps the observations + observation_files INSERTs in a single `db.transaction()`, preventing orphaned rows on crash
|
|
130
130
|
- **Modular NLP pipeline** -- Synonym maps, stop words, scoring constants, and query building extracted into focused modules (`synonyms.mjs`, `stop-words.mjs`, `scoring-sql.mjs`, `nlp.mjs`) for independent testing and maintenance
|
|
131
|
-
- **
|
|
131
|
+
- **Surface-form PRF expansion** -- `observations_fts` is built on FTS5's default `unicode61` tokenizer, so the index is **not stemmed**: a query term matches the word forms actually stored, and `crash` does not match a row that only contains `crashes`. Pseudo-relevance feedback therefore uses the Porter stemmer only to *bucket* morphological variants when judging which candidate terms are discriminative, and emits the most frequent **surface** form of each — emitting a bare stem (`cach`) would match nothing and kill expansion recall
|
|
132
132
|
|
|
133
133
|
## Platform Support
|
|
134
134
|
|
|
@@ -735,7 +735,7 @@ claude-mem-lite/
|
|
|
735
735
|
hook-semaphore.mjs # LLM concurrency control: file-based semaphore for background workers
|
|
736
736
|
schema.mjs # Database schema: single source of truth for tables, migrations, FTS5
|
|
737
737
|
tool-schemas.mjs # Shared Zod schemas for MCP tool validation
|
|
738
|
-
tfidf.mjs #
|
|
738
|
+
tfidf.mjs # the Porter stemmer (name is historical: the TF-IDF vector engine it held was removed)
|
|
739
739
|
tier.mjs # Temporal tier system: activity-based time window classification
|
|
740
740
|
utils.mjs # Re-export hub: backward-compatible surface for all utility modules
|
|
741
741
|
nlp.mjs # FTS5 query building: synonym expansion, CJK bigrams, sanitization
|
package/cli/common.mjs
CHANGED
|
@@ -83,7 +83,120 @@ export function parseArgs(argv) {
|
|
|
83
83
|
i++;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
return { positional, flags };
|
|
86
|
+
return { positional, flags: trackFlagReads(flags) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ─── Inert selection flags ───────────────────────────────────────────────────
|
|
90
|
+
//
|
|
91
|
+
// The third cause of the harm parseArgs' docblock names twice. `--include_noise` (underscore
|
|
92
|
+
// spelling) and `--obs_type` (an MCP field name) both parsed, matched no reader, and let the
|
|
93
|
+
// command answer the unfiltered question; both are fixed above. `suggestUnknownFlags` fixes a
|
|
94
|
+
// third case, flags unknown to the whole CLI. What is left is the case where NOTHING is
|
|
95
|
+
// misspelled: `--type` is canonical and real — on `search`, `save` and `export` — so it clears
|
|
96
|
+
// the global known-flag set, and `browse` simply never reads it. Measured before the fix:
|
|
97
|
+
// `browse --type bugfix` printed rows of every type, exit 0, not a word.
|
|
98
|
+
//
|
|
99
|
+
// The check is read-tracking, not a per-command flag manifest, and that is the whole design.
|
|
100
|
+
// A manifest rots, and worse, it cannot see a flag that a command forwards wholesale into a
|
|
101
|
+
// core helper (`cmdSearch` hands the object to the pipeline, `cmdExport` to the writer) —
|
|
102
|
+
// deriving "which flags does this command read" from its own body would fire on every one of
|
|
103
|
+
// those. Watching what the object is actually ASKED for is exact in both directions.
|
|
104
|
+
//
|
|
105
|
+
// Scope is SELECTION-shaped flags only. Those are the ones whose silent drop hands back a
|
|
106
|
+
// wider set that reads as the answer, which is the harm. A `--confirm` that a short-circuited
|
|
107
|
+
// branch never reached is deliberately out of scope: nothing there is wrong, and a warning on
|
|
108
|
+
// correct usage is worse than the silence it replaces.
|
|
109
|
+
//
|
|
110
|
+
// `prompts-limit` is NOT here, and the reason generalises: `doctor --benchmark --prompts-limit`
|
|
111
|
+
// is read off raw `process.argv` in cli/doctor.mjs and never touches a flags object, so
|
|
112
|
+
// read-tracking would call a working flag inert. Anything read off argv must stay out.
|
|
113
|
+
export const FILTER_FLAGS = new Set([
|
|
114
|
+
'type',
|
|
115
|
+
'source',
|
|
116
|
+
'project',
|
|
117
|
+
'tier',
|
|
118
|
+
'since',
|
|
119
|
+
'from',
|
|
120
|
+
'to',
|
|
121
|
+
'branch',
|
|
122
|
+
'scope',
|
|
123
|
+
'importance',
|
|
124
|
+
'limit',
|
|
125
|
+
'offset',
|
|
126
|
+
'sort',
|
|
127
|
+
'days',
|
|
128
|
+
'age-days',
|
|
129
|
+
// Inclusion toggles, added after the same correct-usage sweep the first batch passed. They
|
|
130
|
+
// widen or narrow the SET rather than filter within it, and the harm runs the other way:
|
|
131
|
+
// an ignored `--include-noise` hands back FEWER rows than asked for, and "I searched and it
|
|
132
|
+
// was not there" is the worst answer a memory tool can give. Held back at first only
|
|
133
|
+
// because booleans are read inside branches and were the likelier false-alarm shape; the
|
|
134
|
+
// exit-code gate below turned out to cover that class, and the sweep reads zero.
|
|
135
|
+
'all',
|
|
136
|
+
'include-noise',
|
|
137
|
+
'include-compressed',
|
|
138
|
+
'deep',
|
|
139
|
+
'no-deep',
|
|
140
|
+
'or',
|
|
141
|
+
'rerank',
|
|
142
|
+
]);
|
|
143
|
+
|
|
144
|
+
let suppliedFlags = new Set();
|
|
145
|
+
let readFlags = new Set();
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wrap a parsed flags object so every lookup is recorded.
|
|
149
|
+
*
|
|
150
|
+
* `get` and `has` are both trapped: a reader spelled `if ('tier' in flags)` must count as a
|
|
151
|
+
* read exactly like `flags.tier`. `ownKeys` deliberately is NOT — `Object.keys(flags)` is
|
|
152
|
+
* enumeration, not consumption, and `suggestUnknownFlags` does exactly that on its own parse.
|
|
153
|
+
*/
|
|
154
|
+
function trackFlagReads(flags) {
|
|
155
|
+
for (const k of Object.keys(flags)) suppliedFlags.add(k);
|
|
156
|
+
return new Proxy(flags, {
|
|
157
|
+
get(target, prop, recv) {
|
|
158
|
+
if (typeof prop === 'string') readFlags.add(prop);
|
|
159
|
+
return Reflect.get(target, prop, recv);
|
|
160
|
+
},
|
|
161
|
+
has(target, prop) {
|
|
162
|
+
if (typeof prop === 'string') readFlags.add(prop);
|
|
163
|
+
return Reflect.has(target, prop);
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Start a fresh recording. `run()` calls this per invocation so tests can drive it in a loop. */
|
|
169
|
+
export function resetFlagTracking() {
|
|
170
|
+
suppliedFlags = new Set();
|
|
171
|
+
readFlags = new Set();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Selection flags the user supplied that nothing looked at, sorted.
|
|
176
|
+
*
|
|
177
|
+
* Read AFTER the command has finished — a flag consumed late (inside a branch, or by a helper
|
|
178
|
+
* the command awaits) has still been read, and reporting it early would be a false alarm.
|
|
179
|
+
* @returns {string[]}
|
|
180
|
+
*/
|
|
181
|
+
export function inertFilterFlags() {
|
|
182
|
+
return [...suppliedFlags].filter((f) => FILTER_FLAGS.has(f) && !readFlags.has(f)).sort();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* The sentence the user gets. Says what happened to their results, not what the parser did:
|
|
187
|
+
* "ignored" alone leaves them to work out whether the output is still the answer they asked
|
|
188
|
+
* for. It is not.
|
|
189
|
+
* @param {string} cmd
|
|
190
|
+
* @param {string[]} inert
|
|
191
|
+
* @returns {string}
|
|
192
|
+
*/
|
|
193
|
+
export function inertFilterFlagNotice(cmd, inert) {
|
|
194
|
+
const names = inert.map((f) => `--${f}`).join(' and ');
|
|
195
|
+
const verb = inert.length > 1 ? 'were' : 'was';
|
|
196
|
+
return (
|
|
197
|
+
`[mem] ${names} ${verb} ignored — \`${cmd}\` does not filter on ${inert.length > 1 ? 'them' : 'it'}, ` +
|
|
198
|
+
`so the results above are UNFILTERED. Run "claude-mem-lite help" for the flags this command reads.`
|
|
199
|
+
);
|
|
87
200
|
}
|
|
88
201
|
|
|
89
202
|
// ─── Output Helpers ──────────────────────────────────────────────────────────
|
package/cli.mjs
CHANGED
|
@@ -81,6 +81,28 @@ function fileFromError(e) {
|
|
|
81
81
|
return quoted ? quoted[1] : null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Is this `doctor` invocation one of the DB-layer modes?
|
|
86
|
+
*
|
|
87
|
+
* DYNAMIC on purpose. A static import here would put lib/doctor-modes.mjs in the LAUNCHER's
|
|
88
|
+
* load graph, and a missing file there kills cli.mjs before any of its own error handling
|
|
89
|
+
* exists — the user gets a raw ERR_MODULE_NOT_FOUND instead of "this install is incomplete,
|
|
90
|
+
* run repair". tests/doctor-startup-closure.test.mjs caught exactly that when the import was
|
|
91
|
+
* static. Same rule as "a recovery path must not import the thing it recovers", applied to
|
|
92
|
+
* the entry point: nothing the launcher needs before it can speak may be a hard edge.
|
|
93
|
+
* install.mjs may import it statically — that failure is caught by loadInstaller() below.
|
|
94
|
+
*/
|
|
95
|
+
async function isDoctorDbMode() {
|
|
96
|
+
try {
|
|
97
|
+
const { DOCTOR_DB_MODES } = await import('./lib/doctor-modes.mjs');
|
|
98
|
+
return process.argv.slice(3).some((a) => DOCTOR_DB_MODES.some((m) => a === `--${m}`));
|
|
99
|
+
} catch {
|
|
100
|
+
// Unreadable module → treat as the plain install health check, which is the branch that
|
|
101
|
+
// can still explain a broken install.
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
async function loadInstaller() {
|
|
85
107
|
let mod;
|
|
86
108
|
try {
|
|
@@ -129,10 +151,7 @@ if (cmd === '--version' || cmd === '-v' || cmd === '-V' || cmd === 'version') {
|
|
|
129
151
|
} else if (cmd === '--help' || cmd === '-h') {
|
|
130
152
|
const { run } = await import('./mem-cli.mjs');
|
|
131
153
|
await run(['help']);
|
|
132
|
-
} else if (
|
|
133
|
-
cmd === 'doctor' &&
|
|
134
|
-
process.argv.slice(3).some((a) => a === '--benchmark' || a === '--metrics' || a === '--session-audit')
|
|
135
|
-
) {
|
|
154
|
+
} else if (cmd === 'doctor' && (await isDoctorDbMode())) {
|
|
136
155
|
// Per #8217: the DB-layer doctor modes (--benchmark / --metrics / --session-audit,
|
|
137
156
|
// each implemented in cli/doctor.mjs) route to mem-cli. Everything else — plain
|
|
138
157
|
// `doctor`, `doctor --` (POSIX end-of-options), and `doctor --json` — stays with
|
package/format-utils.mjs
CHANGED
|
@@ -25,6 +25,36 @@ export function truncate(str, max = 80) {
|
|
|
25
25
|
return str.slice(0, end) + '\u2026';
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Longest query echoed back verbatim in a result label. Long enough that no query a human
|
|
30
|
+
* or an agent actually types is touched — the shapes that exceed it are pasted stack traces,
|
|
31
|
+
* file dumps and multi-paragraph questions.
|
|
32
|
+
*/
|
|
33
|
+
export const QUERY_LABEL_MAX = 200;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A query as it should appear in output handed back to whoever asked.
|
|
37
|
+
*
|
|
38
|
+
* Every search surface labels its answer with the query (`Found N result(s) for "<query>"`),
|
|
39
|
+
* which is how a caller confirms what was actually searched. Unbounded, that made the size
|
|
40
|
+
* of the answer track the size of the question: a 50,000-character query produced 50,024
|
|
41
|
+
* characters of CLI output, and the MCP face — with no argv ceiling — carried the whole
|
|
42
|
+
* thing back into the model's context. For a tool whose purpose is to spend context
|
|
43
|
+
* carefully, returning several KB of the caller's own input is the budget it was invoked to
|
|
44
|
+
* protect.
|
|
45
|
+
*
|
|
46
|
+
* Bounded, not silently truncated: the real length travels with the prefix, so the label
|
|
47
|
+
* still answers the question it exists for. `truncate` handles the surrogate-pair boundary.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} query
|
|
50
|
+
* @returns {string}
|
|
51
|
+
*/
|
|
52
|
+
export function queryLabel(query) {
|
|
53
|
+
if (typeof query !== 'string') return '';
|
|
54
|
+
if (query.length <= QUERY_LABEL_MAX) return query;
|
|
55
|
+
return `${truncate(query, QUERY_LABEL_MAX)} [query truncated; ${query.length} chars]`;
|
|
56
|
+
}
|
|
57
|
+
|
|
28
58
|
// Two delimiter classes are defanged here:
|
|
29
59
|
// 1. The blocks claude-mem-lite wraps injected context in (claude-mem-context /
|
|
30
60
|
// memory-context / session-handoff). User-derived text containing one LITERALLY
|
package/hook-episode.mjs
CHANGED
|
@@ -18,12 +18,21 @@ import { inferProject, EDIT_TOOLS } from './utils.mjs';
|
|
|
18
18
|
import { RUNTIME_DIR } from './hook-shared.mjs';
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
|
-
* Read episode
|
|
21
|
+
* Read the episode buffer WITHOUT holding the lock: the dying-process salvage in hook.mjs's
|
|
22
|
+
* signal handler, and the snapshot Stop / SessionStart take before they flush.
|
|
23
|
+
*
|
|
24
|
+
* Same body as `readEpisode` on purpose — the two names record which locking contract the
|
|
25
|
+
* CALLER is under, and neither function takes a lock itself. What must not differ is the
|
|
26
|
+
* PATH, so both go through `episodeFile()`. This one used to re-spell it inline, which put
|
|
27
|
+
* the buffer's name in three places (here, `episodeFile`, and the signal handler's unlink)
|
|
28
|
+
* and left the salvage path — the one that runs while the process is dying, and the hardest
|
|
29
|
+
* to notice when it is wrong — as the only one not reading the accessor.
|
|
30
|
+
*
|
|
22
31
|
* @returns {object|null} Parsed episode or null on failure
|
|
23
32
|
*/
|
|
24
33
|
export function readEpisodeRaw() {
|
|
25
34
|
try {
|
|
26
|
-
return JSON.parse(readFileSync(
|
|
35
|
+
return JSON.parse(readFileSync(episodeFile(), 'utf8'));
|
|
27
36
|
} catch {
|
|
28
37
|
return null;
|
|
29
38
|
}
|
package/hook-llm.mjs
CHANGED
|
@@ -924,7 +924,7 @@ export async function handleLLMEpisode() {
|
|
|
924
924
|
type: pick by strongest signal. decision = explicit tradeoff / "chose X over Y because Z" / rejected an approach (e.g. "Rejected schema migration — single-source module + sync test instead"; "Heterogeneous hook events → heterogeneous context budgets"). bugfix = prior-failing path fixed with a named root cause. feature = new user-visible capability. refactor = behavior unchanged but structure improved. discovery = learned how a system works (read-heavy, no writes). change = routine edit with no new principle (default if unsure and nothing else fits).
|
|
925
925
|
Facts: each MUST be (1) atomic—one claim, (2) self-contained—no pronouns, include file/function name, (3) specific—"refreshToken() in auth.ts:45 uses 1h TTL" not "handles tokens"
|
|
926
926
|
importance: Be strict — default to 1. 0=pure browsing with zero learning value. 1=routine file edits, standard changes, normal workflow (MOST episodes). 2=notable ONLY if it reveals something non-obvious: error fix with discovered root cause, architectural decision with explicit tradeoff, config change with unexpected side effects. 3=critical: breaking change affecting users, security vulnerability fix, data migration. Ask yourself: "would a future session benefit from knowing this?" — if not, it's importance=1.
|
|
927
|
-
lesson_learned: The non-obvious insight a future session would benefit from. Examples: "FTS5
|
|
927
|
+
lesson_learned: The non-obvious insight a future session would benefit from. Examples: "FTS5's default tokenizer doesn't split CJK — need bigram workaround", "vitest --reporter=verbose hangs on large test suites, use default reporter". Look hard before giving up — most coding episodes contain at least one micro-lesson (an undocumented flag, a surprising default, a debugging shortcut, an unexpected interaction). If literally no insight worth teaching (e.g. version bump, whitespace fix, file rename), output JSON null. Do NOT invent a lesson, do NOT write the strings "none"/"n/a"/"todo"/"tbd"/"-" — those will be discarded as noise.
|
|
928
928
|
scope: ${SCOPE_PROMPT_LEGEND}
|
|
929
929
|
search_aliases: 2-6 alternative search terms someone might use to find this memory later (include CJK if project uses Chinese)`;
|
|
930
930
|
|
package/hook.mjs
CHANGED
|
@@ -288,8 +288,13 @@ for (const sig of ['SIGTERM', 'SIGINT']) {
|
|
|
288
288
|
if (db) {
|
|
289
289
|
try {
|
|
290
290
|
for (const sub of planEpisodeFlush(ep)) saveEpisodeImmediate(sub, db);
|
|
291
|
+
// episodeFile(), not a second spelling of `ep-<project>.json`: this is a
|
|
292
|
+
// DESTRUCTIVE step on the buffer the salvage just persisted, and a path that
|
|
293
|
+
// drifts from the accessor deletes nothing while reporting success — the next
|
|
294
|
+
// fire then salvages the same entries again. Every other flush path
|
|
295
|
+
// (PostToolUse, Stop, SessionStart) already unlinks through the accessor.
|
|
291
296
|
try {
|
|
292
|
-
unlinkSync(
|
|
297
|
+
unlinkSync(episodeFile());
|
|
293
298
|
} catch {}
|
|
294
299
|
} finally {
|
|
295
300
|
try {
|
package/install.mjs
CHANGED
|
@@ -55,6 +55,7 @@ const HOOK_PATH = join(INSTALL_DIR, 'hook.mjs');
|
|
|
55
55
|
// P2-7: both constants and the predicate come from lib/plugin-key.mjs, which hook.mjs also
|
|
56
56
|
// imports — this pair used to be typed out in each.
|
|
57
57
|
import { MARKETPLACE_KEY, PLUGIN_KEY, PLUGIN_NAME, isPluginExplicitlyDisabled } from './lib/plugin-key.mjs';
|
|
58
|
+
import { doctorDbModeHint } from './lib/doctor-modes.mjs';
|
|
58
59
|
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
59
60
|
|
|
60
61
|
import {
|
|
@@ -70,7 +71,7 @@ import {
|
|
|
70
71
|
nativeBindingRepairHint,
|
|
71
72
|
isNativeBindingError,
|
|
72
73
|
} from './lib/binding-probe.mjs';
|
|
73
|
-
import { detectInstallShape, probeRuntimeRoots } from './lib/install-shape.mjs';
|
|
74
|
+
import { detectInstallShape, probeRuntimeRoots, hasAnyManagedCode } from './lib/install-shape.mjs';
|
|
74
75
|
import { probeSchemaCompat, schemaSkewRemedy } from './lib/schema-skew.mjs';
|
|
75
76
|
import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/native-binding-hint.mjs';
|
|
76
77
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
@@ -2366,6 +2367,24 @@ async function doctor() {
|
|
|
2366
2367
|
// was ever deployed there, so every entry reads as "missing" and this reported
|
|
2367
2368
|
// `⚠ Managed files: 121 missing` + an issue on a correct install — prescribing
|
|
2368
2369
|
// a repair against a path that does not exist.
|
|
2370
|
+
// ...and a THIRD state under the same `!shape.managed`: nothing was ever deployed here.
|
|
2371
|
+
// Both checks below otherwise prescribe `repair`, which re-syncs an install from the signed
|
|
2372
|
+
// release and runs from `<INSTALL_DIR>/cli.mjs` — one of the very entry points whose absence
|
|
2373
|
+
// produced the verdict, so on this shape it hands the reader a command that cannot start.
|
|
2374
|
+
// Damaged (some of the managed files survive) and never-deployed (none do) are different
|
|
2375
|
+
// populations with opposite commands, the same conflation the plugin-only branch above fixed
|
|
2376
|
+
// once already. Declared out here because the hook-script check needs it too and a `const`
|
|
2377
|
+
// inside the try below is not in scope there.
|
|
2378
|
+
//
|
|
2379
|
+
// The population is SOURCE_FILES, not the two entry points. Asking about the entry points
|
|
2380
|
+
// alone made this verdict a claim about two files while the message it gates says "none
|
|
2381
|
+
// present" about all of them: an install holding cli.mjs and every lib/ module, with only
|
|
2382
|
+
// server.mjs and hook.mjs gone, was reported as a data directory with no install behind it
|
|
2383
|
+
// — and sent to re-`install` instead of `repair`, which was runnable from the cli.mjs
|
|
2384
|
+
// already there.
|
|
2385
|
+
const noCodeInstall =
|
|
2386
|
+
!shape.managed && !shape.activePluginVersion && !hasAnyManagedCode(INSTALL_DIR, SOURCE_FILES);
|
|
2387
|
+
const installRemedy = `node ${join(PROJECT_DIR, 'install.mjs')} install`;
|
|
2369
2388
|
try {
|
|
2370
2389
|
const skipDrift = !shape.managed && !!shape.activePluginVersion;
|
|
2371
2390
|
const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
|
|
@@ -2436,9 +2455,13 @@ async function doctor() {
|
|
|
2436
2455
|
// self-updater is `self-update`. Naming the wrong one sent the user to a
|
|
2437
2456
|
// usage error at the exact moment their install was incomplete.
|
|
2438
2457
|
issueWarn(
|
|
2439
|
-
|
|
2440
|
-
`
|
|
2441
|
-
|
|
2458
|
+
noCodeInstall
|
|
2459
|
+
? `Managed files: no claude-mem-lite code is deployed in ${INSTALL_DIR} (${r.missingCount} ` +
|
|
2460
|
+
`file(s) absent, none present) — this is a data directory with no install behind it, not ` +
|
|
2461
|
+
`a damaged one. Fix: ${installRemedy}`
|
|
2462
|
+
: `Managed files: ${r.missingCount} missing (${parts.join('; ')}) — a copy install resolves ` +
|
|
2463
|
+
`imports against the install dir, so these throw at hook time. Fix: claude-mem-lite self-update ` +
|
|
2464
|
+
`(or: node ${join(INSTALL_DIR, 'cli.mjs')} repair)`,
|
|
2442
2465
|
);
|
|
2443
2466
|
}
|
|
2444
2467
|
// Complete copy install: no message — drift is a dev-install concern.
|
|
@@ -2462,7 +2485,11 @@ async function doctor() {
|
|
|
2462
2485
|
// missing files, and install.mjs is the one entry that cannot survive that —
|
|
2463
2486
|
// its static imports resolve before its first statement. cli.mjs has no static
|
|
2464
2487
|
// local imports and catches the failure (D#26). Same route, same command.
|
|
2465
|
-
|
|
2488
|
+
// Never-deployed gets the install command instead, for the reason spelled out at
|
|
2489
|
+
// `noCodeInstall` above: the `repair` route runs from an entry point that is itself absent.
|
|
2490
|
+
const scriptRemedy = noCodeInstall
|
|
2491
|
+
? installRemedy
|
|
2492
|
+
: `claude-mem-lite self-update (or: node ${join(INSTALL_DIR, 'cli.mjs')} repair)`;
|
|
2466
2493
|
if (skipScripts) {
|
|
2467
2494
|
ok('Hook scripts: n/a (plugin-only install — hooks run from the plugin cache)');
|
|
2468
2495
|
} else if (!h.present) {
|
|
@@ -2671,7 +2698,17 @@ async function doctor() {
|
|
|
2671
2698
|
),
|
|
2672
2699
|
);
|
|
2673
2700
|
} else {
|
|
2674
|
-
console.log(`\n ${buildDoctorSummary(issues, warnings)}
|
|
2701
|
+
console.log(`\n ${buildDoctorSummary(issues, warnings)}`);
|
|
2702
|
+
// This run checked the INSTALL. The DB-layer modes are a different implementation reached
|
|
2703
|
+
// through the same command name, and nothing else told the user they exist -- a healthy
|
|
2704
|
+
// install with bad retrieval read "All checks passed!" and ended there. Derived from
|
|
2705
|
+
// DOCTOR_DB_MODES so it cannot become a second list to forget. Text only: the exit-code
|
|
2706
|
+
// contract `claude-mem-lite doctor || alert` depends on is untouched.
|
|
2707
|
+
// Phrased as prose, not as `doctor a | b | c`: a line that looks like a command gets
|
|
2708
|
+
// copy-pasted, and `|` is a shell pipe. See doctorDbModeHint()'s note.
|
|
2709
|
+
console.log(
|
|
2710
|
+
` Deeper checks (database layer): run \`claude-mem-lite doctor\` with ${doctorDbModeHint()}\n`,
|
|
2711
|
+
);
|
|
2675
2712
|
}
|
|
2676
2713
|
// Diagnostic-tool exit-code contract: any ✗-level finding must propagate non-zero
|
|
2677
2714
|
// so CI / wrapper scripts (`claude-mem-lite doctor || alert`) actually trip. Keeps
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// lib/doctor-modes.mjs — the DB-layer `doctor` modes, in one place.
|
|
2
|
+
//
|
|
3
|
+
// `doctor` is one command name with two implementations: install.mjs's health check, which
|
|
4
|
+
// owns `--json`, and cli/doctor.mjs's DB-layer modes. cli.mjs decides between them by looking
|
|
5
|
+
// for one of these flags, and its comment used to say so with a warning attached — "Adding a
|
|
6
|
+
// NEW DB-layer mode requires extending this list — a deliberate trade for a working --json".
|
|
7
|
+
// A mode added to cli/doctor.mjs and not to that list is answered silently by the install
|
|
8
|
+
// check instead, which is a command answering as a different command.
|
|
9
|
+
//
|
|
10
|
+
// Three consumers now read this instead of spelling the list:
|
|
11
|
+
// cli.mjs — the router condition
|
|
12
|
+
// install.mjs — the pointer plain `doctor` prints, so the modes are discoverable at all
|
|
13
|
+
// tests/doctor-mode-router-sync.test.mjs — pins it against what cli/doctor.mjs implements
|
|
14
|
+
//
|
|
15
|
+
// A zero-dependency leaf on purpose. install.mjs is a recovery path and must not import
|
|
16
|
+
// anything that drags a load graph behind it (the lesson lib/data-paths.mjs exists for).
|
|
17
|
+
export const DOCTOR_DB_MODES = ['benchmark', 'metrics', 'session-audit'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The modes as PROSE — `--benchmark, --metrics or --session-audit`.
|
|
21
|
+
*
|
|
22
|
+
* Not `a | b | c`. A line shaped like a command invites a copy-paste, and `|` is a shell
|
|
23
|
+
* pipe: pasting the first draft produced `bash: --metrics: command not found`. That is the
|
|
24
|
+
* same defect 32c8923 fixed one commit earlier in this branch (a remedy that named a binary
|
|
25
|
+
* which could not run), reintroduced two commits later in a different spelling. The wording
|
|
26
|
+
* around it has to make clear this is a list of options, not a command line.
|
|
27
|
+
*/
|
|
28
|
+
export function doctorDbModeHint() {
|
|
29
|
+
const flags = DOCTOR_DB_MODES.map((m) => `--${m}`);
|
|
30
|
+
return `${flags.slice(0, -1).join(', ')} or ${flags[flags.length - 1]}`;
|
|
31
|
+
}
|
package/lib/install-shape.mjs
CHANGED
|
@@ -116,6 +116,34 @@ export function hasManagedCodeInstall(installDir) {
|
|
|
116
116
|
return MANAGED_ENTRY_POINTS.every((f) => existsSync(join(installDir, f)));
|
|
117
117
|
}
|
|
118
118
|
|
|
119
|
+
/**
|
|
120
|
+
* True when `installDir` holds ANY of `files` — i.e. code was deployed here at some point,
|
|
121
|
+
* even if it is now incomplete.
|
|
122
|
+
*
|
|
123
|
+
* The complement of `hasManagedCodeInstall` above is not one population but two, and they
|
|
124
|
+
* take OPPOSITE remedies: an install that is DAMAGED (some of it survives) is what `repair`
|
|
125
|
+
* exists for, while one that was never deployed here needs `install`. `repair` runs from
|
|
126
|
+
* `<installDir>/cli.mjs`, so prescribing it where nothing was deployed names a binary that
|
|
127
|
+
* cannot start.
|
|
128
|
+
*
|
|
129
|
+
* `files` is a PARAMETER and the caller passes the whole managed list, because this answers
|
|
130
|
+
* a question about a WIDER population than `hasManagedCodeInstall` does. Pinning both to
|
|
131
|
+
* MANAGED_ENTRY_POINTS looked tidy — one array, `every` vs `some` — and made the verdict a
|
|
132
|
+
* claim about two files while the message it gates says "none present" about all of them.
|
|
133
|
+
* An install holding cli.mjs and every lib/ module but neither entry point was reported as
|
|
134
|
+
* a data directory with no install behind it, and told to re-`install` rather than
|
|
135
|
+
* `repair` — which was runnable, sitting right there.
|
|
136
|
+
*
|
|
137
|
+
* @param {string} installDir
|
|
138
|
+
* @param {string[]} files The managed file list to test against (SOURCE_FILES at the only
|
|
139
|
+
* call site; a narrower list answers a narrower question).
|
|
140
|
+
* @returns {boolean}
|
|
141
|
+
*/
|
|
142
|
+
export function hasAnyManagedCode(installDir, files) {
|
|
143
|
+
if (!installDir || !existsSync(installDir)) return false;
|
|
144
|
+
return files.some((f) => existsSync(join(installDir, f)));
|
|
145
|
+
}
|
|
146
|
+
|
|
119
147
|
/**
|
|
120
148
|
* Plugin-cache version dirs that carry runnable code, newest first.
|
|
121
149
|
*
|
package/mem-cli.mjs
CHANGED
|
@@ -6,7 +6,14 @@ import { homedir } from 'os';
|
|
|
6
6
|
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
|
|
7
7
|
import { resolveRuntimeDir } from './lib/resolve-data-dir.mjs';
|
|
8
8
|
import { isFtsCorruptionError, FTS_CORRUPTION_REMEDY } from './lib/db-unusable.mjs';
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
truncate,
|
|
11
|
+
queryLabel,
|
|
12
|
+
typeIcon,
|
|
13
|
+
inferProject,
|
|
14
|
+
scrubSecrets,
|
|
15
|
+
COMPRESSED_PENDING_PURGE,
|
|
16
|
+
} from './utils.mjs';
|
|
10
17
|
import { resolveProject } from './project-utils.mjs';
|
|
11
18
|
// READ commands resolve the project DB-aware: a subdirectory whose own name holds no rows
|
|
12
19
|
// falls back to the enclosing work-tree root, so `cd src/auth && … recent` reads what the
|
|
@@ -99,6 +106,9 @@ import {
|
|
|
99
106
|
rejectBareStringFlags,
|
|
100
107
|
resolvePositionalAlias,
|
|
101
108
|
suggestUnknownFlags,
|
|
109
|
+
resetFlagTracking,
|
|
110
|
+
inertFilterFlags,
|
|
111
|
+
inertFilterFlagNotice,
|
|
102
112
|
OBS_TIME_FIELDS,
|
|
103
113
|
formatObsFieldValue,
|
|
104
114
|
obsFieldLabel,
|
|
@@ -191,7 +201,7 @@ function emitRecallHint(db, query) {
|
|
|
191
201
|
const n = countRecallableByFile(db, q);
|
|
192
202
|
if (n > 0) {
|
|
193
203
|
out(`[mem] ${n} observation(s) are linked to that file — search indexes text, not file paths.`);
|
|
194
|
-
out(`[mem] Try: claude-mem-lite recall "${q}"`);
|
|
204
|
+
out(`[mem] Try: claude-mem-lite recall "${queryLabel(q)}"`);
|
|
195
205
|
}
|
|
196
206
|
} catch {
|
|
197
207
|
/* hint is best-effort; never break search */
|
|
@@ -273,12 +283,13 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
273
283
|
// Haiku call + N hybrid searches; observations-only. NOT the passive path — this
|
|
274
284
|
// is the explicit "search harder" lever for vocabulary-mismatch recall misses.
|
|
275
285
|
// --deep forces deep; --no-deep forces normal; neither = unset (env/default decide).
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
286
|
+
// Both are read before the precedence contest, deliberately. The ternary short-circuited,
|
|
287
|
+
// so `search --deep --no-deep` never touched `flags['no-deep']` and the inert-flag notice
|
|
288
|
+
// called it a flag `search` "does not filter on" with "results above are UNFILTERED" — both
|
|
289
|
+
// false. A flag that LOST a precedence contest was read; it just did not win.
|
|
290
|
+
const wantsDeep = flags.deep === true || flags.deep === 'true';
|
|
291
|
+
const wantsNoDeep = flags['no-deep'] === true || flags['no-deep'] === 'true';
|
|
292
|
+
const explicitDeep = wantsDeep ? true : wantsNoDeep ? false : undefined;
|
|
282
293
|
const deepMode = resolveDeepMode(explicitDeep, { surface: 'cli' });
|
|
283
294
|
|
|
284
295
|
// --rerank: opt-in LLM rerank of the fused top-20 (option C, deep-search.mjs).
|
|
@@ -326,7 +337,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
326
337
|
out(JSON.stringify({ query, total: 0, returned: 0, offset, limit, deep: false, results: [] }));
|
|
327
338
|
} else {
|
|
328
339
|
emitDeferredTrailer();
|
|
329
|
-
fail(`[mem] No valid search terms in "${query}"`);
|
|
340
|
+
fail(`[mem] No valid search terms in "${queryLabel(query)}"`);
|
|
330
341
|
}
|
|
331
342
|
return;
|
|
332
343
|
}
|
|
@@ -477,7 +488,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
477
488
|
}),
|
|
478
489
|
);
|
|
479
490
|
} else {
|
|
480
|
-
out(`[mem] No results for "${query}"`);
|
|
491
|
+
out(`[mem] No results for "${queryLabel(query)}"`);
|
|
481
492
|
emitRecallHint(db, query);
|
|
482
493
|
// The zero-result path is where the trailer earns its keep — the D#92
|
|
483
494
|
// failure chain was exactly "searched, found nothing, item was deferred".
|
|
@@ -515,7 +526,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
515
526
|
}),
|
|
516
527
|
);
|
|
517
528
|
} else {
|
|
518
|
-
out(`[mem] No results for "${query}" at offset ${offset}`);
|
|
529
|
+
out(`[mem] No results for "${queryLabel(query)}" at offset ${offset}`);
|
|
519
530
|
}
|
|
520
531
|
return;
|
|
521
532
|
}
|
|
@@ -576,7 +587,7 @@ async function cmdSearch(db, args, { llm } = {}) {
|
|
|
576
587
|
// Pluralize on total — "Found 1 of 44 result" reads wrong; the population (44) drives
|
|
577
588
|
// grammatical number, not the page slice (1).
|
|
578
589
|
out(
|
|
579
|
-
`[mem] Found ${countLabel} result${total !== 1 ? 's' : ''} for "${query}"${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt, E# event)' : ''}`,
|
|
590
|
+
`[mem] Found ${countLabel} result${total !== 1 ? 's' : ''} for "${queryLabel(query)}"${fallbackHint}:${hasMixed ? ' (# observation, S# session, P# prompt, E# event)' : ''}`,
|
|
580
591
|
);
|
|
581
592
|
// `~Nt` = est. tokens to fetch this row's full body via mem_get (attachBodyTokens, paired with
|
|
582
593
|
// MCP). Conditional so a row that skipped enrichment renders cleanly, not "~undefinedt".
|
|
@@ -949,7 +960,16 @@ function cmdGet(db, args) {
|
|
|
949
960
|
sections.push(dRows.map(formatDeferredDetail).join('\n\n'));
|
|
950
961
|
totalFound += dRows.length;
|
|
951
962
|
}
|
|
952
|
-
|
|
963
|
+
// This note exists for a MIXED request: other sections still render, so without it the
|
|
964
|
+
// output looks complete while an id the caller asked for silently produced nothing.
|
|
965
|
+
// A deferred-only request that matched nothing is not that shape — the terminal branch
|
|
966
|
+
// below prints the same list plus the `defer list` hint, and running both made
|
|
967
|
+
// `get D#99` say it twice, the second line a superset of the first. Conditions checked
|
|
968
|
+
// rather than the note dropped: silencing it outright would blind the case it is for.
|
|
969
|
+
const deferredOnlyMiss =
|
|
970
|
+
dRows.length === 0 &&
|
|
971
|
+
bySrc.obs.length + bySrc.session.length + bySrc.prompt.length + bySrc.event.length === 0;
|
|
972
|
+
if (deferredMissing.length > 0 && !deferredOnlyMiss) {
|
|
953
973
|
process.stderr.write(
|
|
954
974
|
`[mem] Deferred item(s) not found: ${deferredMissing.map((i) => `D#${i}`).join(', ')}\n`,
|
|
955
975
|
);
|
|
@@ -3593,7 +3613,57 @@ import { cmdActivity } from './cli/activity.mjs';
|
|
|
3593
3613
|
import { DAY_MS } from './lib/time-constants.mjs';
|
|
3594
3614
|
// ─── Main Entry Point ────────────────────────────────────────────────────────
|
|
3595
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Commands that read their own raw argv instead of the object parseArgs returns, so the
|
|
3618
|
+
* inert-flag notice below has no way to observe a flag being consumed and must stay quiet.
|
|
3619
|
+
* `adopt` / `unadopt` hand `cmdArgs` straight to adopt-cli.mjs; `doctor` selects its mode
|
|
3620
|
+
* with `args.includes('--x')`; `cmdOptimize` reads `--project` and `--scope` positionally
|
|
3621
|
+
* (`args.indexOf('--scope')`, and see its own note about parsing flags that way). Adding a
|
|
3622
|
+
* command here is the per-command twin of leaving a flag out of FILTER_FLAGS.
|
|
3623
|
+
*
|
|
3624
|
+
* Do not extend this by hand alone. `tests/cli-argv-parsed-commands-complete.test.mjs`
|
|
3625
|
+
* DERIVES the answer — it walks the dispatcher's switch, reads each handler's body, and
|
|
3626
|
+
* fails if a handler indexes raw argv for a FILTER_FLAGS name without being listed here.
|
|
3627
|
+
* `optimize` was the second miss on this list in one branch (`unadopt --all` was the first),
|
|
3628
|
+
* both found by sweeping invocations, and a sweep only ever covers the population somebody
|
|
3629
|
+
* remembered to enumerate.
|
|
3630
|
+
*/
|
|
3631
|
+
const ARGV_PARSED_COMMANDS = new Set(['adopt', 'unadopt', 'doctor', 'optimize']);
|
|
3632
|
+
|
|
3596
3633
|
export async function run(argv) {
|
|
3634
|
+
// The inert-selection-flag notice is emitted HERE rather than inside the dispatcher because
|
|
3635
|
+
// `runDispatch` returns from a dozen places (adopt / unadopt / memdir-audit before the DB is
|
|
3636
|
+
// even opened, plus every `fail()` path), and a notice wired at some of them is a notice that
|
|
3637
|
+
// reports a dropped filter on some commands and not others. `fail()` sets `process.exitCode`
|
|
3638
|
+
// and RETURNS rather than calling process.exit, so this `finally` covers the failure paths too.
|
|
3639
|
+
resetFlagTracking();
|
|
3640
|
+
try {
|
|
3641
|
+
return await runDispatch(argv);
|
|
3642
|
+
} finally {
|
|
3643
|
+
// Silent for commands that never hand their flags to parseArgs. `adopt` / `unadopt` pass
|
|
3644
|
+
// raw `cmdArgs` straight to adopt-cli.mjs, which reads the array itself, and `doctor`
|
|
3645
|
+
// selects its mode with `args.includes('--x')` — so read-tracking cannot see a flag being
|
|
3646
|
+
// consumed there and would call a documented, working flag inert. `unadopt --all` is
|
|
3647
|
+
// exactly that: adopt-cli.mjs's own header documents it and `unadoptAll()` implements it,
|
|
3648
|
+
// and the first version of this notice reported it as ignored. Same class as
|
|
3649
|
+
// `prompts-limit`, which FILTER_FLAGS excludes by name for the same reason — this is the
|
|
3650
|
+
// per-COMMAND half of that rule. Blindness must present as silence, never as a warning.
|
|
3651
|
+
const argvParsed = ARGV_PARSED_COMMANDS.has(argv[0]);
|
|
3652
|
+
// Only when the command SUCCEEDED. The notice is about an answer that is wider than the
|
|
3653
|
+
// one asked for; a command that failed has no answer for it to qualify, and saying "the
|
|
3654
|
+
// results above are UNFILTERED" under an error message describes results that do not
|
|
3655
|
+
// exist. Found by sweeping correct usage for false alarms: `activity search --limit 3`
|
|
3656
|
+
// (no query) fails its own usage check BEFORE anything reads `flags.limit`, so the flag
|
|
3657
|
+
// is genuinely unread and the notice was genuinely wrong. The error IS the feedback there.
|
|
3658
|
+
const inert = process.exitCode || argvParsed ? [] : inertFilterFlags();
|
|
3659
|
+
// stderr only: stdout is a data channel (`search --json | jq`, and commands/mem.md pipes
|
|
3660
|
+
// these outputs into model context), so the notice must not enter it. Exit code untouched —
|
|
3661
|
+
// the command did run, and its answer is real, just wider than the user asked for.
|
|
3662
|
+
if (inert.length > 0) process.stderr.write(inertFilterFlagNotice(argv[0], inert) + '\n');
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3665
|
+
|
|
3666
|
+
async function runDispatch(argv) {
|
|
3597
3667
|
const cmd = argv[0];
|
|
3598
3668
|
const cmdArgs = argv.slice(1);
|
|
3599
3669
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.0",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.
|
|
9
|
+
"version": "6.9.0",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.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",
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
"lib/hook-telemetry.mjs",
|
|
92
92
|
"lib/resolve-data-dir.mjs",
|
|
93
93
|
"lib/data-paths.mjs",
|
|
94
|
+
"lib/doctor-modes.mjs",
|
|
94
95
|
"lib/export-columns.mjs",
|
|
95
96
|
"lib/file-intel.mjs",
|
|
96
97
|
"lib/reread-guard.mjs",
|
package/scripts/post-tool-use.sh
CHANGED
|
@@ -107,7 +107,25 @@ if [[ "$tool" == "Read" ]]; then
|
|
|
107
107
|
fi
|
|
108
108
|
fi
|
|
109
109
|
fi
|
|
110
|
-
|
|
110
|
+
# Mirror resolveRuntimeDir() (lib/resolve-data-dir.mjs): CLAUDE_MEM_RUNTIME_DIR wins when
|
|
111
|
+
# non-empty, and a relative value resolves against cwd. Without this the two sides of the
|
|
112
|
+
# channel disagreed whenever that override was set — bash appended to
|
|
113
|
+
# $CLAUDE_MEM_DIR/runtime while hook.mjs read (and hook-shared.mjs reaped) the override
|
|
114
|
+
# dir, which is both harms named at the top of this branch: every Read dropped from the
|
|
115
|
+
# episode, and an orphaned file nothing ever collects.
|
|
116
|
+
#
|
|
117
|
+
# The override deliberately wins over the test-containment redirect above, because the
|
|
118
|
+
# Node resolver ignores dataDir entirely once it is set. Mirroring it is the whole point;
|
|
119
|
+
# a "safer" bash rule here would be a second policy nobody reviewed.
|
|
120
|
+
#
|
|
121
|
+
# Builtins only. This is the ~5ms pre-filter — a `node -e` resolver of the kind setup.sh
|
|
122
|
+
# can afford at SessionStart costs ~27ms measured, on every tool call.
|
|
123
|
+
if [[ -n "${CLAUDE_MEM_RUNTIME_DIR:-}" ]]; then
|
|
124
|
+
runtime_dir="$CLAUDE_MEM_RUNTIME_DIR"
|
|
125
|
+
[[ "$runtime_dir" == /* ]] || runtime_dir="${PWD}/${runtime_dir}"
|
|
126
|
+
else
|
|
127
|
+
runtime_dir="${_data_dir}/runtime"
|
|
128
|
+
fi
|
|
111
129
|
# Owner-only (0700 dir / 0600 file): reads-<project>.txt lists captured file
|
|
112
130
|
# paths, so on a shared host the default umask leaked them to every local user.
|
|
113
131
|
# umask is a shell builtin — no extra process on this ~5ms per-tool-call path
|
package/scripts/setup.sh
CHANGED
|
@@ -13,7 +13,51 @@ else
|
|
|
13
13
|
ROOT="$CLAUDE_PLUGIN_ROOT"
|
|
14
14
|
fi
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
# The same three locations lib/data-paths.mjs defines, under the same names. This script
|
|
17
|
+
# carried ONE variable for all three, and under CLAUDE_MEM_DIR that variable is the CODE
|
|
18
|
+
# dir — so every question about the DATABASE was asked of a directory holding none. This
|
|
19
|
+
# repo has now had that confusion three times (v6.3.0, the same fix reintroduced with the
|
|
20
|
+
# halves swapped, and here), which is why the names are spelled out rather than inferred.
|
|
21
|
+
#
|
|
22
|
+
# CODE_DIR — ALWAYS homedir. settings.json and the MCP registration bake absolute
|
|
23
|
+
# paths to server.mjs / hook.mjs under it, so it must not follow the
|
|
24
|
+
# relocation env var. Owns node_modules and the run-once install markers.
|
|
25
|
+
# DB_DIR — follows CLAUDE_MEM_DIR. Owns claude-mem-lite.db and its sidecars.
|
|
26
|
+
# RUNTIME_DIR — follows CLAUDE_MEM_RUNTIME_DIR, else DB_DIR/runtime. Owns state a hook
|
|
27
|
+
# writes and another component reads back (see .deps-broken below).
|
|
28
|
+
#
|
|
29
|
+
# ASK the shared resolver rather than re-deriving its rules (absolute-only, "undefined" /
|
|
30
|
+
# "null" rejected) in a second language. lib/resolve-data-dir.mjs is a leaf module — node:
|
|
31
|
+
# builtins only — so it still loads with node_modules missing, which is the state the
|
|
32
|
+
# .deps-broken flag exists to describe. Gated on an override actually being set: with
|
|
33
|
+
# neither var the resolver returns exactly these defaults, and SessionStart should not pay a
|
|
34
|
+
# node spawn to be told that. A resolver that is missing (truncated tree) or that throws
|
|
35
|
+
# (invalid override) leaves the defaults in place; the Node side rejects a bad value loudly
|
|
36
|
+
# enough on its own, and aborting here would fail the user's session start.
|
|
37
|
+
CODE_DIR="$HOME/.claude-mem-lite"
|
|
38
|
+
DB_DIR="$CODE_DIR"
|
|
39
|
+
RUNTIME_DIR="$CODE_DIR/runtime"
|
|
40
|
+
if [[ -n "${CLAUDE_MEM_DIR:-}" || -n "${CLAUDE_MEM_RUNTIME_DIR:-}" ]] && [[ -f "$ROOT/lib/resolve-data-dir.mjs" ]]; then
|
|
41
|
+
# shellcheck disable=SC2016 # node script single-quoted on purpose; path passed via env, not shell expansion
|
|
42
|
+
_resolved="$(RESOLVER_MOD="$ROOT/lib/resolve-data-dir.mjs" node -e '
|
|
43
|
+
const { pathToFileURL } = require("node:url");
|
|
44
|
+
import(pathToFileURL(process.env.RESOLVER_MOD).href)
|
|
45
|
+
.then((m) => {
|
|
46
|
+
const db = m.resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
47
|
+
process.stdout.write(`${db}\n${m.resolveRuntimeDir(db)}\n`);
|
|
48
|
+
})
|
|
49
|
+
.catch(() => process.exit(1));
|
|
50
|
+
' 2>/dev/null)" || _resolved=""
|
|
51
|
+
_db="$(printf '%s\n' "$_resolved" | sed -n 1p)"
|
|
52
|
+
_rt="$(printf '%s\n' "$_resolved" | sed -n 2p)"
|
|
53
|
+
# Both or neither: a half-applied override is the split this whole block exists to close.
|
|
54
|
+
if [[ -n "$_db" && -n "$_rt" ]]; then
|
|
55
|
+
DB_DIR="$_db"
|
|
56
|
+
RUNTIME_DIR="$_rt"
|
|
57
|
+
fi
|
|
58
|
+
unset _resolved _db _rt
|
|
59
|
+
fi
|
|
60
|
+
|
|
17
61
|
OLD_UNHIDDEN_DIR="$HOME/claude-mem-lite"
|
|
18
62
|
|
|
19
63
|
# Colors
|
|
@@ -36,23 +80,38 @@ log_warn() { echo -e "${YELLOW}⚠${NC} $*" >&2; }
|
|
|
36
80
|
log_err() { echo -e "${RED}✗${NC} $*" >&2; }
|
|
37
81
|
|
|
38
82
|
# 1. Migrate unhidden dir (~/claude-mem-lite/ → ~/.claude-mem-lite/)
|
|
39
|
-
|
|
40
|
-
|
|
83
|
+
# CODE_DIR, not DB_DIR, and deliberately: the pre-v0.5 unhidden directory held the
|
|
84
|
+
# INSTALL — server.mjs, hook.mjs, package.json — and CODE_DIR is the one location that
|
|
85
|
+
# must never follow the relocation env var. Moving it into a relocated DB_DIR would
|
|
86
|
+
# strand every absolute path settings.json and the MCP registration baked.
|
|
87
|
+
if [[ -d "$OLD_UNHIDDEN_DIR" && ! -d "$CODE_DIR" ]]; then
|
|
88
|
+
mv "$OLD_UNHIDDEN_DIR" "$CODE_DIR"
|
|
41
89
|
log_ok "Migrated ~/claude-mem-lite/ → ~/.claude-mem-lite/"
|
|
42
90
|
fi
|
|
43
91
|
|
|
44
|
-
# 2. Ensure
|
|
45
|
-
mkdir -p "$
|
|
46
|
-
|
|
92
|
+
# 2. Ensure both locations exist (runtime created after migration check)
|
|
93
|
+
mkdir -p "$CODE_DIR"
|
|
94
|
+
mkdir -p "$DB_DIR"
|
|
95
|
+
if [[ "$DB_DIR" == "$CODE_DIR" ]]; then
|
|
96
|
+
log_ok "Data directory: $DB_DIR"
|
|
97
|
+
else
|
|
98
|
+
log_ok "Data directory: $DB_DIR (code: $CODE_DIR)"
|
|
99
|
+
fi
|
|
47
100
|
|
|
48
101
|
# 3. Legacy ~/.claude-mem/ DB is schema-v16 (no memory_session_id) with no migration bridge to
|
|
49
102
|
# the current schema — activating it FATALs on first launch ("no such column: memory_session_id")
|
|
50
103
|
# and the "! -f claude-mem-lite.db" guard would re-copy it every time the user deletes the broken
|
|
51
104
|
# DB (recovery loop). Mirror install.mjs migrateLegacyClaudeMemData: back it up (don't activate)
|
|
52
105
|
# and let a fresh DB be created. Source ~/.claude-mem/ is left intact.
|
|
106
|
+
#
|
|
107
|
+
# DB_DIR, because that guard is the whole convergence argument: it closes when the product
|
|
108
|
+
# creates claude-mem-lite.db, and the product creates it in DB_DIR. Asked of CODE_DIR under
|
|
109
|
+
# a relocation it never closed — nothing ever writes a database THERE — so this block
|
|
110
|
+
# copied the legacy database again on every single SessionStart, without bound. Measured
|
|
111
|
+
# 2026-09-14: control arm stable at 1 backup across three runs, relocated arm 1 → 3.
|
|
53
112
|
OLD_DIR="$HOME/.claude-mem"
|
|
54
|
-
if [[ -f "$OLD_DIR/claude-mem.db" && ! -f "$
|
|
55
|
-
BACKUP="$
|
|
113
|
+
if [[ -f "$OLD_DIR/claude-mem.db" && ! -f "$DB_DIR/claude-mem-lite.db" && ! -f "$DB_DIR/claude-mem.db" ]]; then
|
|
114
|
+
BACKUP="$DB_DIR/claude-mem-lite.db.legacy-backup-$(date +%s)"
|
|
56
115
|
if cp "$OLD_DIR/claude-mem.db" "$BACKUP" 2>/dev/null; then
|
|
57
116
|
log_info "Legacy ~/.claude-mem/ DB is schema-incompatible; backed up to $(basename "$BACKUP") (a fresh DB will be created). Old ~/.claude-mem/ preserved."
|
|
58
117
|
else
|
|
@@ -60,16 +119,20 @@ if [[ -f "$OLD_DIR/claude-mem.db" && ! -f "$DATA_DIR/claude-mem-lite.db" && ! -f
|
|
|
60
119
|
fi
|
|
61
120
|
fi
|
|
62
121
|
|
|
63
|
-
# 4. Rename claude-mem.db → claude-mem-lite.db in same directory
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
mv "$
|
|
67
|
-
mv "$
|
|
122
|
+
# 4. Rename claude-mem.db → claude-mem-lite.db in same directory (DB_DIR: a relocated user's
|
|
123
|
+
# pre-rename database sits there, and asking CODE_DIR left it unrenamed and unopened).
|
|
124
|
+
if [[ -f "$DB_DIR/claude-mem.db" && ! -f "$DB_DIR/claude-mem-lite.db" ]]; then
|
|
125
|
+
mv "$DB_DIR/claude-mem.db" "$DB_DIR/claude-mem-lite.db"
|
|
126
|
+
mv "$DB_DIR/claude-mem.db-wal" "$DB_DIR/claude-mem-lite.db-wal" 2>/dev/null || true
|
|
127
|
+
mv "$DB_DIR/claude-mem.db-shm" "$DB_DIR/claude-mem-lite.db-shm" 2>/dev/null || true
|
|
68
128
|
log_ok "Database renamed: claude-mem.db → claude-mem-lite.db"
|
|
69
129
|
fi
|
|
70
130
|
|
|
71
|
-
# 5. Ensure runtime
|
|
72
|
-
|
|
131
|
+
# 5. Ensure runtime directories exist (after migration to not mask migration check).
|
|
132
|
+
# Both: RUNTIME_DIR carries the cross-component flag below, while CODE_DIR/runtime keeps
|
|
133
|
+
# the two run-once install markers at the bottom of this file.
|
|
134
|
+
mkdir -p "$RUNTIME_DIR"
|
|
135
|
+
mkdir -p "$CODE_DIR/runtime"
|
|
73
136
|
|
|
74
137
|
# 6. Ensure native dependencies available for hooks (ESM import needs node_modules in resolution chain)
|
|
75
138
|
# Plugin cache doesn't include node_modules — symlink from data dir or npm install on first run
|
|
@@ -81,8 +144,25 @@ mkdir -p "$DATA_DIR/runtime"
|
|
|
81
144
|
# and exit on the require() error. v2.79: write a JSON flag to runtime/.deps-broken
|
|
82
145
|
# and hook.mjs SessionStart surfaces it in the Claude context as a HIGH-VISIBILITY
|
|
83
146
|
# block; success branches remove the flag so a self-heal stays visible too.
|
|
84
|
-
|
|
85
|
-
|
|
147
|
+
#
|
|
148
|
+
# ...and that contract is a two-directory agreement, not a filename. hook.mjs renders the
|
|
149
|
+
# flag from `join(RUNTIME_DIR, '.deps-broken')`, where RUNTIME_DIR is
|
|
150
|
+
# `resolveRuntimeDir(resolveDataDir(CLAUDE_MEM_DIR))` (hook-shared.mjs). Hardcoding a
|
|
151
|
+
# homedir path here meant that under CLAUDE_MEM_DIR / CLAUDE_MEM_RUNTIME_DIR the writer and
|
|
152
|
+
# the reader named two different directories, so the one surface that says "your hooks are
|
|
153
|
+
# degraded" rendered nothing on exactly the installs that had relocated. Measured
|
|
154
|
+
# 2026-09-14: flag planted where this script wrote it → banner 0 times; planted where
|
|
155
|
+
# hook.mjs reads → 1. Same SPLIT shape lib/resolve-data-dir.mjs documents; it survived
|
|
156
|
+
# because tests/runtime-dir-single-home.test.mjs sweeps `walkShipped`, which is every
|
|
157
|
+
# shipped .mjs/.js — a bash hook is structurally outside that population. RUNTIME_DIR is
|
|
158
|
+
# resolved once at the top of this file.
|
|
159
|
+
#
|
|
160
|
+
# ONLY this marker follows the override. `.mcp-dedup-v2.78` and `.residue-warned-v2.55`
|
|
161
|
+
# below are one-shot state about THIS MACHINE's install — a ~/.claude.json edit and a
|
|
162
|
+
# settings.json warning, not state a hook hands to another component — so they stay under
|
|
163
|
+
# CODE_DIR. Read lib/resolve-data-dir.mjs's MOVES/STAYS list before relocating either:
|
|
164
|
+
# moving a run-once marker re-runs what it gated.
|
|
165
|
+
DEPS_FLAG="$RUNTIME_DIR/.deps-broken"
|
|
86
166
|
|
|
87
167
|
mark_deps_broken() {
|
|
88
168
|
local reason="$1"
|
|
@@ -112,9 +192,11 @@ mark_deps_ok() {
|
|
|
112
192
|
|
|
113
193
|
if [[ ! -d "$ROOT/node_modules/better-sqlite3" ]]; then
|
|
114
194
|
# Fast path: symlink from data dir (instant, no network needed)
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
195
|
+
# CODE_DIR: node_modules belongs to the install, not to the data, and must not follow
|
|
196
|
+
# CLAUDE_MEM_DIR — install.mjs writes it under the homedir install location.
|
|
197
|
+
if [[ -d "$CODE_DIR/node_modules/better-sqlite3" ]]; then
|
|
198
|
+
if ln -sfn "$CODE_DIR/node_modules" "$ROOT/node_modules" 2>/dev/null; then
|
|
199
|
+
log_ok "Dependencies linked from $CODE_DIR"
|
|
118
200
|
fi
|
|
119
201
|
fi
|
|
120
202
|
# Slow path: npm install (first-time only, ~10-20s for native addon)
|
|
@@ -219,7 +301,9 @@ fi
|
|
|
219
301
|
# pre-v2.79.1 — extra node spawn + JSON parse on every SessionStart for a
|
|
220
302
|
# near-always no-op). Bump MCP_MIGRATION name to re-run cleanup in future
|
|
221
303
|
# versions; same shape as the .deps-broken self-heal pattern.
|
|
222
|
-
|
|
304
|
+
# CODE_DIR/runtime, not RUNTIME_DIR: this marker gates a one-shot edit of ~/.claude.json,
|
|
305
|
+
# which is machine state, not per-data-dir state. Relocating it would re-run that edit.
|
|
306
|
+
MCP_MIGRATION="$CODE_DIR/runtime/.mcp-dedup-v2.78"
|
|
223
307
|
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$MCP_MIGRATION" ]]; then
|
|
224
308
|
# shellcheck disable=SC2016 # node script single-quoted on purpose; CLAUDE_JSON passed via env, not shell expansion
|
|
225
309
|
CLAUDE_JSON="$HOME/.claude.json" node -e '
|
|
@@ -298,7 +382,9 @@ fi
|
|
|
298
382
|
# will run every hook twice (direct settings.json hooks AND plugin hooks)
|
|
299
383
|
# until they run `claude-mem-lite uninstall` to clear the settings.json
|
|
300
384
|
# entries. /plugin uninstall does not touch settings.json.
|
|
301
|
-
|
|
385
|
+
# CODE_DIR/runtime, same reason: the residue it warns about is stale hook entries in
|
|
386
|
+
# ~/.claude/settings.json — one machine, one warning, regardless of where the data lives.
|
|
387
|
+
RESIDUE_MARKER="$CODE_DIR/runtime/.residue-warned-v2.55"
|
|
302
388
|
if [[ -n "${CLAUDE_PLUGIN_ROOT:-}" && ! -f "$RESIDUE_MARKER" ]]; then
|
|
303
389
|
SETTINGS="$HOME/.claude/settings.json"
|
|
304
390
|
if [[ -f "$SETTINGS" ]]; then
|
package/server.mjs
CHANGED
|
@@ -76,7 +76,7 @@ import { formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cl
|
|
|
76
76
|
// The partial-export warning points the caller at the CLI twin, which exports the complete
|
|
77
77
|
// set by default — the invocation has to be the one that actually works on this install.
|
|
78
78
|
import { CLI_INVOKE } from './cli-path.mjs';
|
|
79
|
-
import { neutralizeContextDelimiters, neutralizeSkillDelimiters } from './format-utils.mjs';
|
|
79
|
+
import { neutralizeContextDelimiters, neutralizeSkillDelimiters, queryLabel } from './format-utils.mjs';
|
|
80
80
|
import {
|
|
81
81
|
memSearchSchema,
|
|
82
82
|
memRecentSchema,
|
|
@@ -420,13 +420,18 @@ function formatSearchOutput(
|
|
|
420
420
|
'This is a recall miss (the rewrite ran), not a query-syntax issue; the memory likely has no related observations.',
|
|
421
421
|
);
|
|
422
422
|
} else if (args.query && !ftsQuery) {
|
|
423
|
-
hint.push(`Query "${args.query}" was filtered (FTS5 keywords/special chars only).`);
|
|
423
|
+
hint.push(`Query "${queryLabel(args.query)}" was filtered (FTS5 keywords/special chars only).`);
|
|
424
424
|
hint.push('Tip: use content words instead of operators (AND, OR, NOT, NEAR).');
|
|
425
425
|
} else {
|
|
426
426
|
hint.push('No results found.');
|
|
427
427
|
if (args.query) {
|
|
428
428
|
const expanded = ftsQuery || args.query;
|
|
429
|
-
|
|
429
|
+
// Bounded like the other two echo sites. This one is the worst of the three: it
|
|
430
|
+
// prints the EXPANDED query, and synonym expansion makes it LARGER than the input
|
|
431
|
+
// (measured 13,691 chars out for 10,329 in, 1.33x). The first pass of this fix
|
|
432
|
+
// skipped the branch on the reasoning that it "never renders the label" — true of
|
|
433
|
+
// the label, false of the echo.
|
|
434
|
+
if (expanded !== args.query) hint.push(`Searched as: ${queryLabel(expanded)}`);
|
|
430
435
|
hint.push('Tip: check spelling, try broader terms, or use mem_stats to see available data.');
|
|
431
436
|
}
|
|
432
437
|
}
|
|
@@ -449,7 +454,7 @@ function formatSearchOutput(
|
|
|
449
454
|
);
|
|
450
455
|
// P2-6: empty/omitted query falls through to a "listing recent" path — label it explicitly
|
|
451
456
|
// so callers don't mistake BM25-less results for relevance-ranked ones.
|
|
452
|
-
const qLabel = args.query ? ` for "${args.query}"` : ' (no query — listing recent)';
|
|
457
|
+
const qLabel = args.query ? ` for "${queryLabel(args.query)}"` : ' (no query — listing recent)';
|
|
453
458
|
// Surface AND→OR fallback so callers (incl. Claude) know a strict multi-term
|
|
454
459
|
// query actually matched only a subset of the terms. Suppressed when the caller
|
|
455
460
|
// explicitly requested OR semantics — there's no "fallback" in that path.
|
package/source-files.mjs
CHANGED
|
@@ -60,6 +60,7 @@ export const SOURCE_FILES = [
|
|
|
60
60
|
// stays loadable without better-sqlite3. Missing from the manifest → auto-update leaves
|
|
61
61
|
// schema.mjs and the repair path with ERR_MODULE_NOT_FOUND on every fire.
|
|
62
62
|
'lib/data-paths.mjs',
|
|
63
|
+
'lib/doctor-modes.mjs',
|
|
63
64
|
// lib/ — statically imported by hook-llm.mjs (activity) + hook-handoff.mjs (git-state, task-reader);
|
|
64
65
|
// dynamically imported by hook.mjs (startup-dashboard) + mem-cli.mjs (doctor-benchmark, plan-reader).
|
|
65
66
|
'lib/activity.mjs',
|
package/tfidf.mjs
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
|
-
// tfidf.mjs —
|
|
1
|
+
// tfidf.mjs — the Porter stemmer.
|
|
2
2
|
//
|
|
3
3
|
// NAME IS HISTORICAL. This module was the TF-IDF vector search engine (vocabulary, vectors,
|
|
4
4
|
// cosine similarity, vector search, RRF merge). Phase-1 (v3.17.0, 2026-06-27) gated that arm
|
|
5
5
|
// off; Phase-2 removed it. What is left is the text-normalization half, which was never part
|
|
6
6
|
// of the vector arm and has live consumers on the DEFAULT retrieval path:
|
|
7
7
|
// - porterStem -> search-scoring.mjs (PRF term extraction)
|
|
8
|
-
//
|
|
8
|
+
// `tokenize` lived here too until it was moved to benchmark/adoption-cosine.mjs, beside the
|
|
9
|
+
// only callers it had left; this module is now one function under a two-word historical name.
|
|
9
10
|
// RRF_K moved to lib/rrf.mjs, its actual home. See tests/vector-arm-removed.test.mjs for the
|
|
10
11
|
// removal contract and tasks/specs/vector-arm-removal.md for the measurements behind it.
|
|
11
12
|
|
|
12
|
-
import { cjkBigrams } from './utils.mjs';
|
|
13
|
-
|
|
14
13
|
// ─── Porter Stemmer ──────────────────────────────────────────────────────────
|
|
15
14
|
// Minimal Porter stemmer (1980). It used to normalize tokens for the TF-IDF vocabulary,
|
|
16
15
|
// where query and document were both stemmed so the vector arm stayed internally
|
|
@@ -188,46 +187,3 @@ export function porterStem(w) {
|
|
|
188
187
|
|
|
189
188
|
return word;
|
|
190
189
|
}
|
|
191
|
-
|
|
192
|
-
// ─── Tokenization ───────────────────────────────────────────────────────────
|
|
193
|
-
|
|
194
|
-
const CJK_RANGE = /[\u4e00-\u9fff\u3400-\u4dbf]/;
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* Tokenize text into stemmed terms.
|
|
198
|
-
* ASCII: lowercase + split + Porter stem.
|
|
199
|
-
* CJK: reuse cjkBigrams() for consistency with FTS5.
|
|
200
|
-
*
|
|
201
|
-
* Built for the TF-IDF vocabulary, which is gone; the surviving consumer is
|
|
202
|
-
* benchmark/adoption-cosine.mjs, which builds its own bags. The "aligned with FTS5's
|
|
203
|
-
* porter tokenizer" claim the old docblock made here is NOT true and was already
|
|
204
|
-
* contradicted by the stemmer's own note above — observations_fts uses unicode61 with no
|
|
205
|
-
* stemming, so these terms are stems and FTS5's are surface forms.
|
|
206
|
-
*/
|
|
207
|
-
export function tokenize(text) {
|
|
208
|
-
if (!text) return [];
|
|
209
|
-
text = String(text).toLowerCase();
|
|
210
|
-
|
|
211
|
-
const tokens = [];
|
|
212
|
-
|
|
213
|
-
// Split into ASCII and CJK segments
|
|
214
|
-
const parts = text.split(/([\u4e00-\u9fff\u3400-\u4dbf]+)/);
|
|
215
|
-
for (const part of parts) {
|
|
216
|
-
if (CJK_RANGE.test(part)) {
|
|
217
|
-
// CJK: use bigrams for consistency with FTS5 indexing
|
|
218
|
-
const bigrams = cjkBigrams(part);
|
|
219
|
-
if (bigrams) {
|
|
220
|
-
for (const t of bigrams.split(/\s+/)) {
|
|
221
|
-
if (t.length >= 2) tokens.push(t);
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
} else {
|
|
225
|
-
// ASCII: split on non-alphanumeric, then Porter stem
|
|
226
|
-
for (const t of part.split(/[^a-z0-9]+/)) {
|
|
227
|
-
if (t.length >= 2) tokens.push(porterStem(t));
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
return tokens;
|
|
233
|
-
}
|
package/utils.mjs
CHANGED
|
@@ -9,37 +9,35 @@ import { buildLowSignalRegex } from './lib/low-signal-patterns.mjs';
|
|
|
9
9
|
import { scrubSecrets as _scrubSecrets } from './secret-scrub.mjs';
|
|
10
10
|
|
|
11
11
|
// ─── Re-exports from extracted modules ──────────────────────────────────────
|
|
12
|
-
// Backward compatibility:
|
|
13
|
-
|
|
12
|
+
// Backward compatibility: consumers that predate the extraction import from utils.mjs.
|
|
13
|
+
//
|
|
14
|
+
// The barrel carries what in-tree code actually imports THROUGH it, and nothing else.
|
|
15
|
+
// "Backward compatible" here means compatible with this repo's own call sites: there is no
|
|
16
|
+
// `main`/`exports` in package.json, so nothing outside the tree can import this module, and
|
|
17
|
+
// a re-export with no importer is not a compatibility guarantee — it is a name knip has to
|
|
18
|
+
// keep reporting. Eleven were dropped 2026-09-14 after both knip (whose entry set includes
|
|
19
|
+
// tests, so a test-only consumer would have kept them) and a per-name import scan covering
|
|
20
|
+
// named, namespace, dynamic and re-export forms agreed nothing imports them from here.
|
|
21
|
+
// Each one is still exported by its own leaf module, where its real callers import it.
|
|
22
|
+
// Same disposal the `extractResponseFromError` docblock records for the same reason.
|
|
14
23
|
export {
|
|
15
24
|
DECAY_HALF_LIFE_BY_TYPE,
|
|
16
25
|
DEFAULT_DECAY_HALF_LIFE_MS,
|
|
17
26
|
OBS_BM25,
|
|
18
27
|
SESS_BM25,
|
|
19
28
|
EVT_BM25,
|
|
20
|
-
TYPE_DECAY_CASE,
|
|
21
29
|
TYPE_QUALITY_CASE,
|
|
22
30
|
OBS_FTS_COLUMNS,
|
|
23
31
|
notLowSignalTitleClause,
|
|
24
32
|
noisePenaltyClause,
|
|
25
33
|
} from './scoring-sql.mjs';
|
|
26
|
-
export {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
extractCjkLikePatterns,
|
|
30
|
-
SYNONYM_MAP,
|
|
31
|
-
expandToken,
|
|
32
|
-
sanitizeFtsQuery,
|
|
33
|
-
ftsQueryTokens,
|
|
34
|
-
relaxFtsQueryToOr,
|
|
35
|
-
FTS_STOP_WORDS,
|
|
36
|
-
CJK_COMPOUNDS,
|
|
37
|
-
} from './nlp.mjs';
|
|
38
|
-
export { inferProject, resolveProject, _resetProjectCache } from './project-utils.mjs';
|
|
39
|
-
export { scrubSecrets, SECRET_PATTERNS } from './secret-scrub.mjs';
|
|
34
|
+
export { cjkBigrams, sanitizeFtsQuery, ftsQueryTokens, relaxFtsQueryToOr } from './nlp.mjs';
|
|
35
|
+
export { inferProject } from './project-utils.mjs';
|
|
36
|
+
export { scrubSecrets } from './secret-scrub.mjs';
|
|
40
37
|
export { stripPrivate } from './lib/private-strip.mjs';
|
|
41
38
|
export {
|
|
42
39
|
truncate,
|
|
40
|
+
queryLabel,
|
|
43
41
|
typeIcon,
|
|
44
42
|
fmtDate,
|
|
45
43
|
fmtTime,
|
|
@@ -51,7 +49,6 @@ export { computeMinHash, estimateJaccardFromMinHash, jaccardSimilarity } from '.
|
|
|
51
49
|
export {
|
|
52
50
|
detectBashSignificance,
|
|
53
51
|
extractErrorKeywords,
|
|
54
|
-
planErrorRecall,
|
|
55
52
|
extractFilePaths,
|
|
56
53
|
stripTestSuffix,
|
|
57
54
|
} from './bash-utils.mjs';
|