claude-mem-lite 3.35.0 → 3.35.2
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.mjs +4 -2
- package/hook-episode.mjs +33 -0
- package/hook.mjs +88 -57
- package/install.mjs +2 -1
- package/lib/resolve-data-dir.mjs +35 -0
- package/package.json +2 -1
- package/registry-recommend.mjs +2 -2
- package/schema.mjs +2 -1
- package/scripts/hook-launcher.mjs +9 -4
- package/scripts/post-tool-recall.js +2 -2
- package/scripts/pre-skill-bridge.js +2 -1
- package/scripts/pre-tool-recall.js +2 -2
- package/source-files.mjs +6 -0
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"plugins": [
|
|
11
11
|
{
|
|
12
12
|
"name": "claude-mem-lite",
|
|
13
|
-
"version": "3.35.
|
|
13
|
+
"version": "3.35.2",
|
|
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.35.
|
|
3
|
+
"version": "3.35.2",
|
|
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.mjs
CHANGED
|
@@ -33,8 +33,10 @@ if (cmd === '--version' || cmd === '-v') {
|
|
|
33
33
|
const { existsSync } = await import('fs');
|
|
34
34
|
const { join } = await import('path');
|
|
35
35
|
// D#29: honor CLAUDE_MEM_DIR so the install-vs-CLI help routing is correct on
|
|
36
|
-
// relocated installs (matches schema.mjs DB_DIR
|
|
37
|
-
|
|
36
|
+
// relocated installs (matches schema.mjs DB_DIR via the shared resolver, which
|
|
37
|
+
// also fixes the HOME-unset relative-path fallback this branch used to have).
|
|
38
|
+
const { resolveDataDir } = await import('./lib/resolve-data-dir.mjs');
|
|
39
|
+
const dataDir = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
38
40
|
const dbPath = join(dataDir, 'claude-mem-lite.db');
|
|
39
41
|
if (existsSync(dbPath)) {
|
|
40
42
|
const { run } = await import('./mem-cli.mjs');
|
package/hook-episode.mjs
CHANGED
|
@@ -136,6 +136,39 @@ export function createEpisode(sessionId, project) {
|
|
|
136
136
|
};
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Split an episode's entries by originating CC session so each concurrent
|
|
141
|
+
* session flushes as its own observation. Common path (single session, or all
|
|
142
|
+
* untagged/legacy entries) returns [episode] BY REFERENCE — behavior identical
|
|
143
|
+
* to pre-grouping. When >=2 sessions interleaved in one buffer, returns one
|
|
144
|
+
* sub-episode per session with its own entries + recomputed file union;
|
|
145
|
+
* filesRead (untagged bash reads-file) is inherited by every sub. Each sub
|
|
146
|
+
* resets savedId so it receives its own from its immediate-save (savedId is
|
|
147
|
+
* load-bearing: llm-episode upgrades the pre-saved obs by it).
|
|
148
|
+
* @param {object} episode
|
|
149
|
+
* @returns {object[]}
|
|
150
|
+
*/
|
|
151
|
+
export function planEpisodeFlush(episode) {
|
|
152
|
+
const groups = new Map();
|
|
153
|
+
for (const e of episode.entries) {
|
|
154
|
+
const key = e.ccSession ?? '__none__';
|
|
155
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
156
|
+
groups.get(key).push(e);
|
|
157
|
+
}
|
|
158
|
+
if (groups.size <= 1) return [episode];
|
|
159
|
+
const subs = [];
|
|
160
|
+
for (const [, entries] of groups) {
|
|
161
|
+
subs.push({
|
|
162
|
+
...episode,
|
|
163
|
+
entries,
|
|
164
|
+
files: [...new Set(entries.flatMap(e => e.files || []))],
|
|
165
|
+
filesRead: episode.filesRead,
|
|
166
|
+
savedId: undefined,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return subs;
|
|
170
|
+
}
|
|
171
|
+
|
|
139
172
|
/**
|
|
140
173
|
* Add file paths to an episode's file tracking set (deduped).
|
|
141
174
|
* @param {object} episode The episode to update
|
package/hook.mjs
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
import {
|
|
32
32
|
readEpisodeRaw, episodeFile,
|
|
33
33
|
acquireLock, releaseLock, readEpisode, writeEpisode,
|
|
34
|
-
createEpisode, addFileToEpisode,
|
|
34
|
+
createEpisode, addFileToEpisode, planEpisodeFlush,
|
|
35
35
|
writePendingEntry, mergePendingEntries, episodeHasSignificantContent,
|
|
36
36
|
} from './hook-episode.mjs';
|
|
37
37
|
import { cleanupClaudeMdLegacyBlock, buildSessionContextLines } from './hook-context.mjs';
|
|
@@ -162,77 +162,104 @@ function flushEpisode(episode, hookEventName = 'PostToolUse') {
|
|
|
162
162
|
episode.filesRead = episode.filesRead || [];
|
|
163
163
|
}
|
|
164
164
|
|
|
165
|
-
|
|
165
|
+
// Split by CC session so concurrent same-project sessions flush as separate
|
|
166
|
+
// observations. planEpisodeFlush returns [episode] BY REFERENCE for the common
|
|
167
|
+
// single-session (or all-legacy) case → flushEpisodeGroup(episode) is identical
|
|
168
|
+
// to pre-grouping. Two+ interleaved sessions each get their own sub-episode.
|
|
169
|
+
const subs = planEpisodeFlush(episode);
|
|
170
|
+
let anySignificant = false;
|
|
171
|
+
for (const sub of subs) {
|
|
172
|
+
const r = flushEpisodeGroup(sub);
|
|
173
|
+
if (r === 'writefail') {
|
|
174
|
+
// Single-group: preserve the original early return — buffer left un-unlinked
|
|
175
|
+
// for a later retry, no receipt. Multi-group: skip only the failed group and
|
|
176
|
+
// keep the rest. The asymmetry is safe: each group's immediate obs is persisted
|
|
177
|
+
// BEFORE its flush-file write, so re-flushing the whole buffer would re-emit
|
|
178
|
+
// already-saved groups as duplicate observations.
|
|
179
|
+
if (subs.length === 1) return;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
if (r === 'significant') anySignificant = true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Aggregate receipt over the whole episode, gated exactly as before
|
|
186
|
+
// (isSignificant → anySignificant). v2.33.4: Stop rejects hookSpecificOutput.
|
|
187
|
+
if (anySignificant && RECEIPT_EVENTS.has(hookEventName)) {
|
|
188
|
+
try {
|
|
189
|
+
const entries = episode.entries || [];
|
|
190
|
+
const toolCounts = {};
|
|
191
|
+
for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
192
|
+
const toolSummary = Object.entries(toolCounts)
|
|
193
|
+
.sort((a, b) => b[1] - a[1])
|
|
194
|
+
.slice(0, 3)
|
|
195
|
+
.map(([t, n]) => `${t}×${n}`)
|
|
196
|
+
.join(', ');
|
|
197
|
+
const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
|
|
198
|
+
// v2.83: error→fix nudge lifted to lib/cite-back-hint.mjs::buildUnsavedBugfixHint
|
|
199
|
+
// so the wording (count + "Save now" verb) stays in sync with cite-back.
|
|
200
|
+
const bugfixHint = buildUnsavedBugfixHint(episode);
|
|
201
|
+
if (bugfixHint) lines.push(bugfixHint);
|
|
202
|
+
// v2.81: cite-back hint — fires when this episode edits a file that
|
|
203
|
+
// PreToolUse:Read/Edit nudged earlier in the same session. Precision
|
|
204
|
+
// signal (we know the file was warned about); orthogonal to the
|
|
205
|
+
// bugfix-shape nudge above and may co-fire.
|
|
206
|
+
const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
|
|
207
|
+
if (citeBack) lines.push(citeBack);
|
|
208
|
+
// Trailing newline is REQUIRED: when this receipt flushes at SessionStart
|
|
209
|
+
// (leftover episode after /clear or /compact), the startup dashboard writes a
|
|
210
|
+
// second hookSpecificOutput object right after. Without the '\n' the two land
|
|
211
|
+
// back-to-back as `}{` on one line and Claude Code's line-based JSON parser
|
|
212
|
+
// drops both — losing the episode-flush / cite-back context exactly at the
|
|
213
|
+
// session boundary. Every other hookSpecificOutput write appends '\n'; this
|
|
214
|
+
// was the lone exception.
|
|
215
|
+
process.stdout.write(JSON.stringify({
|
|
216
|
+
suppressOutput: true,
|
|
217
|
+
hookSpecificOutput: {
|
|
218
|
+
hookEventName,
|
|
219
|
+
additionalContext: lines.join('\n'),
|
|
220
|
+
},
|
|
221
|
+
}) + '\n');
|
|
222
|
+
} catch { /* never block on receipt */ }
|
|
223
|
+
}
|
|
166
224
|
|
|
167
|
-
//
|
|
168
|
-
|
|
225
|
+
// Remove episode buffer AFTER spawning background workers to prevent concurrent overwrites
|
|
226
|
+
try { unlinkSync(episodeFile()); } catch {}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Save one episode-shaped object: immediate rule-based observation (if
|
|
230
|
+
// significant) + flush file + llm-episode enrichment spawn. Extracted from
|
|
231
|
+
// flushEpisode so each CC-session slice (from planEpisodeFlush) flushes
|
|
232
|
+
// independently and carries its OWN savedId into its OWN flush file — the
|
|
233
|
+
// llm-episode worker upgrades the pre-saved obs by that id. Returns
|
|
234
|
+
// 'significant' | 'insignificant' | 'writefail'. CLAUDE_MEM_SKIP_EPISODE_LLM
|
|
235
|
+
// suppresses the detached enrichment spawn (test determinism; sibling of
|
|
236
|
+
// CLAUDE_MEM_SKIP_COMPRESS / _OPTIMIZE) — the synchronous immediate obs still lands.
|
|
237
|
+
function flushEpisodeGroup(ep) {
|
|
238
|
+
const isSignificant = episodeHasSignificantContent(ep);
|
|
239
|
+
|
|
240
|
+
// Immediate save: rule-based observation for instant visibility; the LLM
|
|
241
|
+
// background worker upgrades title/narrative/importance later.
|
|
169
242
|
if (isSignificant) {
|
|
170
243
|
try {
|
|
171
|
-
const obs = buildImmediateObservation(
|
|
172
|
-
const id = saveObservation(obs,
|
|
173
|
-
if (id)
|
|
244
|
+
const obs = buildImmediateObservation(ep);
|
|
245
|
+
const id = saveObservation(obs, ep.project, ep.sessionId);
|
|
246
|
+
if (id) ep.savedId = id;
|
|
174
247
|
} catch (e) { debugCatch(e, 'flushEpisode-immediateSave'); }
|
|
175
248
|
}
|
|
176
249
|
|
|
177
|
-
// Write episode to flush file, then remove buffer AFTER spawn to prevent race
|
|
178
250
|
const flushFile = join(RUNTIME_DIR, `ep-flush-${Date.now()}-${randomUUID().slice(0, 8)}.json`);
|
|
179
251
|
try {
|
|
180
|
-
writeFileSync(flushFile, JSON.stringify(
|
|
252
|
+
writeFileSync(flushFile, JSON.stringify(ep));
|
|
181
253
|
} catch {
|
|
182
|
-
return;
|
|
254
|
+
return 'writefail';
|
|
183
255
|
}
|
|
184
256
|
|
|
185
|
-
if (isSignificant) {
|
|
257
|
+
if (isSignificant && !process.env.CLAUDE_MEM_SKIP_EPISODE_LLM) {
|
|
186
258
|
spawnBackground('llm-episode', flushFile);
|
|
187
|
-
|
|
188
|
-
// v2.33.1: structured flush receipt so Claude sees what mem just captured
|
|
189
|
-
// and the legacy error→fix nudge consolidates here. PostToolUse JSON with
|
|
190
|
-
// hookSpecificOutput.additionalContext reliably renders across CC variants;
|
|
191
|
-
// the old plain-text stdout write was invisible on some variants.
|
|
192
|
-
// v2.33.4: Stop event rejects hookSpecificOutput entirely — skip receipt.
|
|
193
|
-
if (RECEIPT_EVENTS.has(hookEventName)) {
|
|
194
|
-
try {
|
|
195
|
-
const entries = episode.entries || [];
|
|
196
|
-
const toolCounts = {};
|
|
197
|
-
for (const e of entries) toolCounts[e.tool] = (toolCounts[e.tool] || 0) + 1;
|
|
198
|
-
const toolSummary = Object.entries(toolCounts)
|
|
199
|
-
.sort((a, b) => b[1] - a[1])
|
|
200
|
-
.slice(0, 3)
|
|
201
|
-
.map(([t, n]) => `${t}×${n}`)
|
|
202
|
-
.join(', ');
|
|
203
|
-
const lines = [`[mem] episode flushed: ${entries.length} entries (${toolSummary})`];
|
|
204
|
-
// v2.83: error→fix nudge lifted to lib/cite-back-hint.mjs::buildUnsavedBugfixHint
|
|
205
|
-
// so the wording (count + "Save now" verb) stays in sync with cite-back.
|
|
206
|
-
const bugfixHint = buildUnsavedBugfixHint(episode);
|
|
207
|
-
if (bugfixHint) lines.push(bugfixHint);
|
|
208
|
-
// v2.81: cite-back hint — fires when this episode edits a file that
|
|
209
|
-
// PreToolUse:Read/Edit nudged earlier in the same session. Precision
|
|
210
|
-
// signal (we know the file was warned about); orthogonal to the
|
|
211
|
-
// bugfix-shape nudge above and may co-fire.
|
|
212
|
-
const citeBack = loadCiteBackForEpisode(episode, RUNTIME_DIR);
|
|
213
|
-
if (citeBack) lines.push(citeBack);
|
|
214
|
-
// Trailing newline is REQUIRED: when this receipt flushes at SessionStart
|
|
215
|
-
// (leftover episode after /clear or /compact), the startup dashboard writes a
|
|
216
|
-
// second hookSpecificOutput object right after. Without the '\n' the two land
|
|
217
|
-
// back-to-back as `}{` on one line and Claude Code's line-based JSON parser
|
|
218
|
-
// drops both — losing the episode-flush / cite-back context exactly at the
|
|
219
|
-
// session boundary. Every other hookSpecificOutput write appends '\n'; this
|
|
220
|
-
// was the lone exception.
|
|
221
|
-
process.stdout.write(JSON.stringify({
|
|
222
|
-
suppressOutput: true,
|
|
223
|
-
hookSpecificOutput: {
|
|
224
|
-
hookEventName,
|
|
225
|
-
additionalContext: lines.join('\n'),
|
|
226
|
-
},
|
|
227
|
-
}) + '\n');
|
|
228
|
-
} catch { /* never block on receipt */ }
|
|
229
|
-
}
|
|
230
259
|
} else {
|
|
231
260
|
try { unlinkSync(flushFile); } catch {}
|
|
232
261
|
}
|
|
233
|
-
|
|
234
|
-
// Remove episode buffer AFTER spawning background worker to prevent concurrent overwrites
|
|
235
|
-
try { unlinkSync(episodeFile()); } catch {}
|
|
262
|
+
return isSignificant ? 'significant' : 'insignificant';
|
|
236
263
|
}
|
|
237
264
|
|
|
238
265
|
// ─── PostToolUse Handler ────────────────────────────────────────────────────
|
|
@@ -294,6 +321,10 @@ async function handlePostToolUse() {
|
|
|
294
321
|
isSignificant: EDIT_TOOLS.has(tool_name) ||
|
|
295
322
|
bashSig?.isSignificant || false,
|
|
296
323
|
bashSig: bashSig || null,
|
|
324
|
+
// CC UUID from hook stdin — lets flushEpisode split a buffer shared by
|
|
325
|
+
// concurrent same-project sessions into per-session observations. Null for
|
|
326
|
+
// legacy/stdin-less invocations (→ single __none__ group = old behavior).
|
|
327
|
+
ccSession: hookData.session_id || null,
|
|
297
328
|
};
|
|
298
329
|
|
|
299
330
|
// Episode buffer management (locked to prevent TOCTOU race)
|
package/install.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { join, resolve, dirname, isAbsolute } from 'path';
|
|
|
7
7
|
import { homedir, tmpdir } from 'os';
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
9
9
|
import { createRequire } from 'node:module';
|
|
10
|
+
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
10
11
|
|
|
11
12
|
const PROJECT_DIR = resolve(import.meta.dirname ?? dirname(fileURLToPath(import.meta.url)));
|
|
12
13
|
const SETTINGS_PATH = join(homedir(), '.claude', 'settings.json');
|
|
@@ -20,7 +21,7 @@ const DATA_DIR = join(homedir(), '.claude-mem-lite');
|
|
|
20
21
|
// the runtime/data layer READS it (pre-fix: installer wrote homedir, runtime read
|
|
21
22
|
// the relocated dir → preinstalled skills silently vanished, doctor read the wrong
|
|
22
23
|
// DB). Equals DATA_DIR when CLAUDE_MEM_DIR is unset (the common case).
|
|
23
|
-
const MEM_DATA_DIR = process.env.CLAUDE_MEM_DIR
|
|
24
|
+
const MEM_DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
24
25
|
const DB_PATH = join(MEM_DATA_DIR, 'claude-mem-lite.db');
|
|
25
26
|
const OLD_DATA_DIR = join(homedir(), '.claude-mem');
|
|
26
27
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// Single source of truth for resolving the CLAUDE_MEM_DIR data directory.
|
|
2
|
+
// Zero runtime deps (node:path + node:os only) so hot-path hook scripts can
|
|
3
|
+
// import it without pulling in better-sqlite3.
|
|
4
|
+
//
|
|
5
|
+
// The env var is a RELOCATION knob: unset → ~/.claude-mem-lite (the common,
|
|
6
|
+
// non-relocated case); set → an absolute path on a larger/faster volume.
|
|
7
|
+
// Anything else is a mistake we must reject LOUDLY rather than turn into a
|
|
8
|
+
// stray directory:
|
|
9
|
+
// - The literal strings "undefined"/"null" arise when a caller interpolates
|
|
10
|
+
// a JS nullish value into a shell env string (e.g. `CLAUDE_MEM_DIR='${x}'`
|
|
11
|
+
// with x === undefined). They are truthy, so `env || default` accepts them
|
|
12
|
+
// and better-sqlite3 then creates a relative `undefined/` dir at cwd.
|
|
13
|
+
// - A relative path silently resolves against each process's cwd, scattering
|
|
14
|
+
// state across directories.
|
|
15
|
+
// Falsy (unset/empty) is the only non-absolute value we treat as "use default".
|
|
16
|
+
import { homedir } from 'node:os';
|
|
17
|
+
import { join, isAbsolute } from 'node:path';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {string|undefined|null} raw Typically process.env.CLAUDE_MEM_DIR.
|
|
21
|
+
* @returns {string} An absolute data directory.
|
|
22
|
+
* @throws if `raw` is a non-empty, non-absolute value (incl. "undefined"/"null").
|
|
23
|
+
*/
|
|
24
|
+
export function resolveDataDir(raw) {
|
|
25
|
+
if (raw === undefined || raw === null || raw === '') {
|
|
26
|
+
return join(homedir(), '.claude-mem-lite');
|
|
27
|
+
}
|
|
28
|
+
if (typeof raw !== 'string' || raw === 'undefined' || raw === 'null' || !isAbsolute(raw)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`CLAUDE_MEM_DIR must be an absolute path; got ${JSON.stringify(raw)}. ` +
|
|
31
|
+
`Leave it unset to use ~/.claude-mem-lite.`
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return raw;
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "3.35.
|
|
3
|
+
"version": "3.35.2",
|
|
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",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"lib/id-routing.mjs",
|
|
68
68
|
"lib/err-sampler.mjs",
|
|
69
69
|
"lib/hook-telemetry.mjs",
|
|
70
|
+
"lib/resolve-data-dir.mjs",
|
|
70
71
|
"lib/file-intel.mjs",
|
|
71
72
|
"lib/reread-guard.mjs",
|
|
72
73
|
"lib/metrics.mjs",
|
package/registry-recommend.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// invocations/recommend_count. Live injection is Phase 2. `off` skips all work.
|
|
6
6
|
import { readFileSync, writeFileSync, renameSync, existsSync, mkdirSync, appendFileSync, readdirSync, unlinkSync } from 'fs';
|
|
7
7
|
import { join } from 'path';
|
|
8
|
-
import {
|
|
8
|
+
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
9
9
|
import { searchResources, cjkIntentTokens } from './registry-retriever.mjs';
|
|
10
10
|
|
|
11
11
|
const VALID_MODES = new Set(['shadow', 'live', 'off']);
|
|
@@ -26,7 +26,7 @@ const RECO_COOLDOWN_MS = 300_000; // 5 min, mirrors T4 SKILL_COOLDOWN_MS
|
|
|
26
26
|
// DB_DIR formula) so tests sandbox via env without ESM-cache gymnastics, and prod reads
|
|
27
27
|
// the same dir as the rest of the app.
|
|
28
28
|
function recoRuntimeDir() {
|
|
29
|
-
return join(process.env.CLAUDE_MEM_DIR
|
|
29
|
+
return join(resolveDataDir(process.env.CLAUDE_MEM_DIR), 'runtime');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const TOKEN_SPLIT = /[^a-z0-9一-鿿]+/;
|
package/schema.mjs
CHANGED
|
@@ -7,10 +7,11 @@ import { homedir } from 'os';
|
|
|
7
7
|
import { join } from 'path';
|
|
8
8
|
import { existsSync, mkdirSync, readdirSync, renameSync, rmSync, chmodSync } from 'fs';
|
|
9
9
|
import { OBS_FTS_COLUMNS, debugCatch } from './utils.mjs';
|
|
10
|
+
import { resolveDataDir } from './lib/resolve-data-dir.mjs';
|
|
10
11
|
|
|
11
12
|
// DATA location — DB, managed resources, registry DB, runtime/. Honors
|
|
12
13
|
// CLAUDE_MEM_DIR so users can relocate state to a larger/faster volume.
|
|
13
|
-
export const DB_DIR = process.env.CLAUDE_MEM_DIR
|
|
14
|
+
export const DB_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
14
15
|
export const DB_PATH = join(DB_DIR, 'claude-mem-lite.db');
|
|
15
16
|
export const REGISTRY_DB_PATH = join(DB_DIR, 'resource-registry.db');
|
|
16
17
|
// CODE / install location — server.mjs, hook.mjs, cli.mjs, package.json live
|
|
@@ -26,15 +26,20 @@
|
|
|
26
26
|
|
|
27
27
|
import { existsSync, mkdirSync, writeFileSync, statSync, unlinkSync, readFileSync } from 'node:fs';
|
|
28
28
|
import { spawnSync } from 'node:child_process';
|
|
29
|
-
import { dirname, join } from 'node:path';
|
|
29
|
+
import { dirname, join, isAbsolute } from 'node:path';
|
|
30
30
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
31
31
|
import { homedir } from 'node:os';
|
|
32
32
|
|
|
33
33
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const INSTALL_DIR = join(__dirname, '..');
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// A bogus CLAUDE_MEM_DIR ("undefined"/"null"/relative from a mis-quoted env
|
|
36
|
+
// interpolation) must degrade to the default here, not become a stray dir. The
|
|
37
|
+
// launcher's pure-node charter (above) forbids importing lib/resolve-data-dir.mjs,
|
|
38
|
+
// and its fail-open duty forbids throwing, so mirror that guard inline + lenient:
|
|
39
|
+
// non-absolute → default. Data-writing paths import that module and throw instead.
|
|
40
|
+
const MEM_DIR = process.env.CLAUDE_MEM_DIR;
|
|
41
|
+
const DATA_DIR = MEM_DIR && isAbsolute(MEM_DIR) ? MEM_DIR : join(homedir(), '.claude-mem-lite');
|
|
42
|
+
const RUNTIME_DIR = join(DATA_DIR, 'runtime');
|
|
38
43
|
const HEAL_MARKER = join(RUNTIME_DIR, 'hook-launcher-lastheal');
|
|
39
44
|
const HEAL_COOLDOWN_MS = 6 * 60 * 60 * 1000;
|
|
40
45
|
// Observable breakage state: written when the launcher degrades a broken install
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import { existsSync, readFileSync } from 'fs';
|
|
17
17
|
import { basename, join } from 'path';
|
|
18
|
-
import {
|
|
18
|
+
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
19
19
|
|
|
20
20
|
const SALIENCE_BIND = process.env.CLAUDE_MEM_SALIENCE === 'bind';
|
|
21
21
|
|
|
22
|
-
const DATA_DIR = process.env.CLAUDE_MEM_DIR
|
|
22
|
+
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
23
23
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
|
|
24
24
|
const LEGACY_COOLDOWN_PATH = join(RUNTIME_DIR, 'pre-recall-cooldown.json');
|
|
25
25
|
|
|
@@ -7,9 +7,10 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
7
7
|
import { join, resolve, sep } from 'path';
|
|
8
8
|
import { homedir } from 'os';
|
|
9
9
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
10
|
+
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
10
11
|
|
|
11
12
|
// CLAUDE_MEM_DIR mirrors pre-tool-recall.js — one env var sandboxes everything.
|
|
12
|
-
const DATA_DIR = process.env.CLAUDE_MEM_DIR
|
|
13
|
+
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
13
14
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
|
|
14
15
|
// D#29: all data artifacts follow DATA_DIR (CLAUDE_MEM_DIR-aware), not a hardcoded
|
|
15
16
|
// homedir — previously REGISTRY_DB_PATH/MANAGED_BASE/MARKER pinned homedir while line 12
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
8
8
|
import { basename, join } from 'path';
|
|
9
|
-
import {
|
|
9
|
+
import { resolveDataDir } from '../lib/resolve-data-dir.mjs';
|
|
10
10
|
import { buildNotLowSignalSql } from '../lib/low-signal-patterns.mjs';
|
|
11
11
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
12
12
|
import { citeFactorClause } from '../scoring-sql.mjs';
|
|
@@ -18,7 +18,7 @@ import { presentIdents } from '../lib/lesson-idents.mjs';
|
|
|
18
18
|
// CLAUDE_MEM_DIR matches schema.mjs / main CLI — one env var sandboxes the
|
|
19
19
|
// whole system. CLAUDE_MEM_DB_PATH / CLAUDE_MEM_RUNTIME_DIR remain as
|
|
20
20
|
// per-component overrides for tests that mix isolated + real paths.
|
|
21
|
-
const DATA_DIR = process.env.CLAUDE_MEM_DIR
|
|
21
|
+
const DATA_DIR = resolveDataDir(process.env.CLAUDE_MEM_DIR);
|
|
22
22
|
const DB_PATH = process.env.CLAUDE_MEM_DB_PATH || join(DATA_DIR, 'claude-mem-lite.db');
|
|
23
23
|
const RUNTIME_DIR = process.env.CLAUDE_MEM_RUNTIME_DIR || join(DATA_DIR, 'runtime');
|
|
24
24
|
// A3 (v2.83): cross-hook dedup window — must mirror DEDUP_STALE_MS in
|
package/source-files.mjs
CHANGED
|
@@ -29,6 +29,12 @@ export const SOURCE_FILES = [
|
|
|
29
29
|
'tier.mjs', 'tfidf.mjs',
|
|
30
30
|
'nlp.mjs', 'synonyms.mjs', 'scoring-sql.mjs', 'stop-words.mjs', 'project-utils.mjs',
|
|
31
31
|
'secret-scrub.mjs', 'format-utils.mjs', 'hash-utils.mjs', 'bash-utils.mjs',
|
|
32
|
+
// Single source of truth for the CLAUDE_MEM_DIR → data-dir resolver (rejects a
|
|
33
|
+
// stringified "undefined"/"null"/relative env instead of creating a stray dir).
|
|
34
|
+
// Statically imported by schema.mjs / cli.mjs / install.mjs / registry-recommend.mjs
|
|
35
|
+
// AND hook scripts (pre-tool-recall / post-tool-recall / pre-skill-bridge) — ship it
|
|
36
|
+
// or auto-update leaves schema + every hook with ERR_MODULE_NOT_FOUND on each fire.
|
|
37
|
+
'lib/resolve-data-dir.mjs',
|
|
32
38
|
// lib/ — statically imported by hook-llm.mjs (activity) + hook-handoff.mjs (git-state, task-reader);
|
|
33
39
|
// dynamically imported by hook.mjs (startup-dashboard) + mem-cli.mjs (doctor-benchmark, plan-reader).
|
|
34
40
|
'lib/activity.mjs',
|