claude-mem-lite 4.0.3 → 5.0.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 +0 -49
- package/README.zh-CN.md +0 -48
- package/adopt-content.mjs +0 -1
- package/cli/common.mjs +0 -17
- package/cli.mjs +19 -3
- package/commands/update.md +4 -11
- package/format-utils.mjs +0 -21
- package/hook-context.mjs +6 -1
- package/hook-handoff.mjs +9 -0
- package/hook-update.mjs +37 -12
- package/hook.mjs +5 -29
- package/hooks/hooks.json +0 -10
- package/install.mjs +24 -666
- package/lib/doctor-drift.mjs +0 -1
- package/lib/fast-summary.mjs +11 -0
- package/lib/frontmatter.mjs +1 -2
- package/lib/hook-prune.mjs +133 -0
- package/lib/hook-stdin.mjs +1 -1
- package/lib/hook-telemetry.mjs +1 -1
- package/lib/metrics.mjs +2 -2
- package/lib/shard-gc.mjs +11 -7
- package/mem-cli.mjs +2 -423
- package/nlp.mjs +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +3 -15
- package/schema.mjs +0 -1
- package/scripts/hook-launcher.mjs +2 -2
- package/scripts/prompt-search-utils.mjs +0 -30
- package/scripts/user-prompt-search.js +3 -125
- package/server.mjs +11 -449
- package/source-files.mjs +11 -28
- package/synonyms.mjs +2 -1
- package/tool-schemas.mjs +0 -80
- package/utils.mjs +5 -13
- package/commands/tools.md +0 -67
- package/install-metadata.mjs +0 -2193
- package/lib/registry-core.mjs +0 -264
- package/registry/preinstalled.json +0 -2419
- package/registry-enricher.mjs +0 -124
- package/registry-github.mjs +0 -86
- package/registry-importer.mjs +0 -569
- package/registry-recommend.mjs +0 -503
- package/registry-retriever.mjs +0 -611
- package/registry-scanner.mjs +0 -261
- package/registry.mjs +0 -665
- package/resource-discovery.mjs +0 -199
- package/scripts/pre-skill-bridge.js +0 -146
package/install.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
mkdirSync,
|
|
11
11
|
mkdtempSync,
|
|
12
12
|
copyFileSync,
|
|
13
|
-
cpSync,
|
|
14
13
|
renameSync,
|
|
15
14
|
symlinkSync,
|
|
16
15
|
unlinkSync,
|
|
@@ -18,7 +17,7 @@ import {
|
|
|
18
17
|
statSync,
|
|
19
18
|
lstatSync,
|
|
20
19
|
} from 'fs';
|
|
21
|
-
import { join, resolve, dirname,
|
|
20
|
+
import { join, resolve, dirname, basename } from 'path';
|
|
22
21
|
import { homedir, tmpdir } from 'os';
|
|
23
22
|
import { fileURLToPath, pathToFileURL } from 'url';
|
|
24
23
|
import { createRequire } from 'node:module';
|
|
@@ -58,7 +57,6 @@ const HOOK_PATH = join(INSTALL_DIR, 'hook.mjs');
|
|
|
58
57
|
import { MARKETPLACE_KEY, PLUGIN_KEY, isPluginExplicitlyDisabled } from './lib/plugin-key.mjs';
|
|
59
58
|
const NPM_INSTALL_CMD = 'npm install --omit=dev --no-audit --no-fund';
|
|
60
59
|
|
|
61
|
-
import { RESOURCE_METADATA } from './install-metadata.mjs';
|
|
62
60
|
import {
|
|
63
61
|
scanPluginCacheHookPollution,
|
|
64
62
|
hasInstallManagedHooks,
|
|
@@ -75,6 +73,7 @@ import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/nat
|
|
|
75
73
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
76
74
|
import { acquireLock } from './lib/proc-lock.mjs';
|
|
77
75
|
import { atomicWriteFileSync } from './lib/atomic-write.mjs';
|
|
76
|
+
import { isMemHook, launcherEntryPath } from './lib/hook-prune.mjs';
|
|
78
77
|
|
|
79
78
|
// Re-export for backward compatibility — tests/install-hook-scripts.test.mjs
|
|
80
79
|
// and any external consumers still import HOOK_SCRIPT_FILES from install.mjs.
|
|
@@ -126,177 +125,6 @@ export function migrateLegacyClaudeMemData(oldDir, newDir, opts = {}) {
|
|
|
126
125
|
return { action: 'backed-up', backupPath };
|
|
127
126
|
}
|
|
128
127
|
|
|
129
|
-
/**
|
|
130
|
-
* Derive invocation_name from resource name when metadata doesn't provide one.
|
|
131
|
-
* Rules:
|
|
132
|
-
* "parent/child" → "parent:child" (plugin:resource format)
|
|
133
|
-
* "simple-name" → "simple-name" (standalone resource)
|
|
134
|
-
* @param {string} name Resource name
|
|
135
|
-
* @returns {string} Derived invocation name
|
|
136
|
-
*/
|
|
137
|
-
function deriveInvocationName(name) {
|
|
138
|
-
if (name.includes('/')) return name.replace('/', ':');
|
|
139
|
-
return name;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
/**
|
|
143
|
-
* Apply curated metadata to existing resource DB entries.
|
|
144
|
-
* Fixes existing installs that have generic name-echo metadata.
|
|
145
|
-
* Also syncs keywords, tech_stack, use_cases and auto-derives invocation_name.
|
|
146
|
-
* @param {Database} rdb Registry database handle
|
|
147
|
-
*/
|
|
148
|
-
function reindexKnownResources(rdb) {
|
|
149
|
-
const update = rdb.prepare(`
|
|
150
|
-
UPDATE resources SET
|
|
151
|
-
intent_tags = ?, domain_tags = ?,
|
|
152
|
-
capability_summary = ?, trigger_patterns = ?,
|
|
153
|
-
invocation_name = CASE WHEN ? != '' THEN ? ELSE invocation_name END,
|
|
154
|
-
recommendation_mode = CASE WHEN ? != '' THEN ? ELSE recommendation_mode END,
|
|
155
|
-
keywords = CASE WHEN ? != '' THEN ? ELSE keywords END,
|
|
156
|
-
tech_stack = CASE WHEN ? != '' THEN ? ELSE tech_stack END,
|
|
157
|
-
use_cases = CASE WHEN ? != '' THEN ? ELSE use_cases END,
|
|
158
|
-
updated_at = datetime('now')
|
|
159
|
-
WHERE type = ? AND name = ?
|
|
160
|
-
`);
|
|
161
|
-
|
|
162
|
-
rdb.transaction(() => {
|
|
163
|
-
for (const [key, meta] of Object.entries(RESOURCE_METADATA)) {
|
|
164
|
-
const sep = key.indexOf(':');
|
|
165
|
-
if (sep < 0) continue; // skip malformed keys without type:name separator
|
|
166
|
-
const type = key.slice(0, sep);
|
|
167
|
-
const name = key.slice(sep + 1);
|
|
168
|
-
const invName = meta.invocation_name || deriveInvocationName(name);
|
|
169
|
-
const recMode = meta.recommendation_mode || '';
|
|
170
|
-
const kw = meta.keywords || '';
|
|
171
|
-
const ts = meta.tech_stack || '';
|
|
172
|
-
const uc = meta.use_cases || '';
|
|
173
|
-
update.run(
|
|
174
|
-
meta.intent_tags,
|
|
175
|
-
meta.domain_tags,
|
|
176
|
-
meta.capability_summary,
|
|
177
|
-
meta.trigger_patterns,
|
|
178
|
-
invName,
|
|
179
|
-
invName,
|
|
180
|
-
recMode,
|
|
181
|
-
recMode,
|
|
182
|
-
kw,
|
|
183
|
-
kw,
|
|
184
|
-
ts,
|
|
185
|
-
ts,
|
|
186
|
-
uc,
|
|
187
|
-
uc,
|
|
188
|
-
type,
|
|
189
|
-
name,
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
})();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/**
|
|
196
|
-
* Register plugin resources that have no local files (virtual resources).
|
|
197
|
-
* These are skills/agents from other installed plugins that the dispatch
|
|
198
|
-
* system should know about for intelligent recommendation.
|
|
199
|
-
* Only inserts entries that don't already exist in the resources table.
|
|
200
|
-
* @param {Database} rdb Registry database handle
|
|
201
|
-
*/
|
|
202
|
-
function registerVirtualResources(rdb) {
|
|
203
|
-
const insert = rdb.prepare(`
|
|
204
|
-
INSERT OR IGNORE INTO resources (name, type, status, source, local_path, invocation_name,
|
|
205
|
-
intent_tags, domain_tags, capability_summary, trigger_patterns,
|
|
206
|
-
keywords, tech_stack, use_cases, recommendation_mode,
|
|
207
|
-
created_at, updated_at)
|
|
208
|
-
VALUES (?, ?, 'active', 'preinstalled', '', ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
|
209
|
-
`);
|
|
210
|
-
|
|
211
|
-
// Backfill FTS5 fields for existing resources that have empty keywords/tech_stack/use_cases
|
|
212
|
-
const updateFts = rdb.prepare(`
|
|
213
|
-
UPDATE resources SET
|
|
214
|
-
keywords = CASE WHEN (keywords IS NULL OR keywords = '') AND ?1 != '' THEN ?1 ELSE keywords END,
|
|
215
|
-
tech_stack = CASE WHEN (tech_stack IS NULL OR tech_stack = '') AND ?2 != '' THEN ?2 ELSE tech_stack END,
|
|
216
|
-
use_cases = CASE WHEN (use_cases IS NULL OR use_cases = '') AND ?3 != '' THEN ?3 ELSE use_cases END,
|
|
217
|
-
updated_at = datetime('now')
|
|
218
|
-
WHERE type = ?4 AND name = ?5
|
|
219
|
-
AND ((keywords IS NULL OR keywords = '') OR (tech_stack IS NULL OR tech_stack = '') OR (use_cases IS NULL OR use_cases = ''))
|
|
220
|
-
`);
|
|
221
|
-
|
|
222
|
-
let count = 0;
|
|
223
|
-
rdb.transaction(() => {
|
|
224
|
-
for (const [key, meta] of Object.entries(RESOURCE_METADATA)) {
|
|
225
|
-
const sep = key.indexOf(':');
|
|
226
|
-
if (sep < 0) continue;
|
|
227
|
-
const type = key.slice(0, sep);
|
|
228
|
-
const name = key.slice(sep + 1);
|
|
229
|
-
const { changes } = insert.run(
|
|
230
|
-
name,
|
|
231
|
-
type,
|
|
232
|
-
meta.invocation_name || deriveInvocationName(name),
|
|
233
|
-
meta.intent_tags || name.replace(/-/g, ' '),
|
|
234
|
-
meta.domain_tags || '',
|
|
235
|
-
meta.capability_summary || `${type}: ${name.replace(/-/g, ' ')}`,
|
|
236
|
-
meta.trigger_patterns || `when user needs ${name.replace(/-/g, ' ')}`,
|
|
237
|
-
meta.keywords || '',
|
|
238
|
-
meta.tech_stack || '',
|
|
239
|
-
meta.use_cases || '',
|
|
240
|
-
meta.recommendation_mode || 'proactive',
|
|
241
|
-
);
|
|
242
|
-
count += changes;
|
|
243
|
-
|
|
244
|
-
// Backfill FTS5 fields for existing resources.
|
|
245
|
-
// ?N numbered placeholders REQUIRE object-form binding in better-sqlite3 —
|
|
246
|
-
// positional .run(v1, v2, …) always throws "Too many parameter values"
|
|
247
|
-
// regardless of arg count. Pre-fix this swallow-warned on every install
|
|
248
|
-
// (masked by install.mjs:785 import failure before the v2.84.2 path fix).
|
|
249
|
-
if (changes === 0) {
|
|
250
|
-
updateFts.run({
|
|
251
|
-
1: meta.keywords || '',
|
|
252
|
-
2: meta.tech_stack || '',
|
|
253
|
-
3: meta.use_cases || '',
|
|
254
|
-
4: type,
|
|
255
|
-
5: name,
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// Backfill keywords from preinstalled tags for resources still missing keywords
|
|
261
|
-
try {
|
|
262
|
-
const backfill = rdb.prepare(`
|
|
263
|
-
UPDATE resources SET keywords = (
|
|
264
|
-
SELECT GROUP_CONCAT(json_each.value, ',')
|
|
265
|
-
FROM preinstalled p, json_each(p.tags)
|
|
266
|
-
WHERE p.type = resources.type AND p.name = resources.name
|
|
267
|
-
)
|
|
268
|
-
WHERE (keywords IS NULL OR keywords = '')
|
|
269
|
-
AND EXISTS (
|
|
270
|
-
SELECT 1 FROM preinstalled p
|
|
271
|
-
WHERE p.type = resources.type AND p.name = resources.name
|
|
272
|
-
AND p.tags != '[]' AND p.tags IS NOT NULL
|
|
273
|
-
)
|
|
274
|
-
`);
|
|
275
|
-
backfill.run();
|
|
276
|
-
} catch {}
|
|
277
|
-
|
|
278
|
-
// Backfill invocation_name for resources that still have it empty
|
|
279
|
-
// Derive from name: "parent/child" → "parent:child", otherwise use name as-is
|
|
280
|
-
try {
|
|
281
|
-
const emptyInvoc = rdb
|
|
282
|
-
.prepare(
|
|
283
|
-
`
|
|
284
|
-
SELECT id, name FROM resources
|
|
285
|
-
WHERE status = 'active' AND (invocation_name IS NULL OR invocation_name = '')
|
|
286
|
-
`,
|
|
287
|
-
)
|
|
288
|
-
.all();
|
|
289
|
-
if (emptyInvoc.length > 0) {
|
|
290
|
-
const setInvoc = rdb.prepare('UPDATE resources SET invocation_name = ? WHERE id = ?');
|
|
291
|
-
for (const r of emptyInvoc) {
|
|
292
|
-
setInvoc.run(deriveInvocationName(r.name), r.id);
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
} catch {}
|
|
296
|
-
})();
|
|
297
|
-
return count;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
128
|
let cmd = process.argv[2];
|
|
301
129
|
let flags = new Set(process.argv.slice(3));
|
|
302
130
|
|
|
@@ -307,51 +135,6 @@ function ok(msg) {
|
|
|
307
135
|
console.log(` ✓ ${msg}`);
|
|
308
136
|
}
|
|
309
137
|
|
|
310
|
-
/**
|
|
311
|
-
* Recursive on-disk size of `dir`, in bytes. Bounded by `maxEntries` so a
|
|
312
|
-
* surprise-large tree can never turn a progress line into a long stat storm —
|
|
313
|
-
* returns `{ bytes, truncated }` and callers render truncated sums as "≥ N MB".
|
|
314
|
-
*
|
|
315
|
-
* @param {string} dir Directory to measure (missing dir → 0 bytes).
|
|
316
|
-
* @param {number} [maxEntries=50000] Stat budget.
|
|
317
|
-
* @returns {{bytes: number, truncated: boolean}}
|
|
318
|
-
*/
|
|
319
|
-
function dirSizeBytes(dir, maxEntries = 50000) {
|
|
320
|
-
let bytes = 0,
|
|
321
|
-
seen = 0,
|
|
322
|
-
truncated = false;
|
|
323
|
-
const stack = [dir];
|
|
324
|
-
while (stack.length > 0) {
|
|
325
|
-
const cur = stack.pop();
|
|
326
|
-
let entries;
|
|
327
|
-
try {
|
|
328
|
-
entries = readdirSync(cur, { withFileTypes: true });
|
|
329
|
-
} catch {
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
for (const e of entries) {
|
|
333
|
-
if (++seen > maxEntries) {
|
|
334
|
-
truncated = true;
|
|
335
|
-
return { bytes, truncated };
|
|
336
|
-
}
|
|
337
|
-
const p = join(cur, e.name);
|
|
338
|
-
if (e.isDirectory()) stack.push(p);
|
|
339
|
-
else if (e.isFile()) {
|
|
340
|
-
try {
|
|
341
|
-
bytes += statSync(p).size;
|
|
342
|
-
} catch {
|
|
343
|
-
/* raced away */
|
|
344
|
-
}
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
return { bytes, truncated };
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Render a byte count as a short human string ("148 MB"). */
|
|
352
|
-
function fmtMB(bytes, truncated = false) {
|
|
353
|
-
return `${truncated ? '≥' : ''}${Math.round(bytes / 1048576)} MB`;
|
|
354
|
-
}
|
|
355
138
|
function warn(msg) {
|
|
356
139
|
console.log(` ⚠ ${msg}`);
|
|
357
140
|
}
|
|
@@ -494,50 +277,6 @@ function isDevInstall() {
|
|
|
494
277
|
}
|
|
495
278
|
}
|
|
496
279
|
|
|
497
|
-
// Decide what to check out of a registry repo. We only ever copy the manifest's
|
|
498
|
-
// `entry.path` subdirs into managed/skills|agents, so the working tree never
|
|
499
|
-
// needs the rest of the repo — a partial+sparse clone fetches just those subtrees
|
|
500
|
-
// (e.g. davila7/claude-code-templates: 197MB whole repo → a few MB of 3 paths).
|
|
501
|
-
// Returns { full, paths }: `full` forces a normal checkout when any entry maps to
|
|
502
|
-
// the repo root ('.') — sparse buys nothing there. Unsafe paths ('..'/absolute)
|
|
503
|
-
// are dropped here exactly as the copy loop drops them, so they never reach
|
|
504
|
-
// `sparse-checkout set`. Pure + exported for unit testing.
|
|
505
|
-
export function planRepoSparsePaths(entries) {
|
|
506
|
-
let needsFull = false;
|
|
507
|
-
const paths = [];
|
|
508
|
-
for (const e of entries || []) {
|
|
509
|
-
const p = e && e.path;
|
|
510
|
-
if (!p || p === '.' || p === './') {
|
|
511
|
-
needsFull = true;
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (isAbsolute(p) || String(p).includes('..')) continue; // unsafe — skipped at copy too
|
|
515
|
-
const norm = String(p).replace(/^\.\//, '').replace(/\/+$/, '');
|
|
516
|
-
if (norm && !paths.includes(norm)) paths.push(norm);
|
|
517
|
-
}
|
|
518
|
-
// No usable sparse paths (all root or all unsafe) → a full checkout is the only
|
|
519
|
-
// thing that can produce content; sparse would be an empty, pointless tree.
|
|
520
|
-
return { full: needsFull || paths.length === 0, paths };
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// True only for a clone this code produced (partial-clone promisor + sparse-checkout
|
|
524
|
-
// both on). A legacy full clone returns false → caller re-clones it slim. Detection
|
|
525
|
-
// erring false only costs a one-time re-clone (the dir is a rebuildable cache), so
|
|
526
|
-
// over-eager migration is safe; under-eager just keeps a fat clone one more cycle.
|
|
527
|
-
function isPartialSparseClone(clonePath) {
|
|
528
|
-
const cfg = (key) => {
|
|
529
|
-
try {
|
|
530
|
-
return execFileSync('git', ['-C', clonePath, 'config', '--get', key], {
|
|
531
|
-
encoding: 'utf8',
|
|
532
|
-
stdio: 'pipe',
|
|
533
|
-
}).trim();
|
|
534
|
-
} catch {
|
|
535
|
-
return '';
|
|
536
|
-
} // missing key → git exits non-zero → treat as unset
|
|
537
|
-
};
|
|
538
|
-
return cfg('remote.origin.promisor') === 'true' && cfg('core.sparseCheckout') === 'true';
|
|
539
|
-
}
|
|
540
|
-
|
|
541
280
|
// ─── Install ────────────────────────────────────────────────────────────────
|
|
542
281
|
|
|
543
282
|
// Dynamic-import helpers, resolved against the installed copy at INSTALL_DIR
|
|
@@ -616,11 +355,6 @@ function installSourceFiles(IS_DEV) {
|
|
|
616
355
|
execFileSync('chmod', ['+x', join(scriptsDir, 'post-tool-use.sh')], { stdio: 'pipe' });
|
|
617
356
|
} catch {}
|
|
618
357
|
// commands/ is intentionally NOT copied — see dev-mode branch above.
|
|
619
|
-
// Copy registry manifest
|
|
620
|
-
const registryDir = join(DATA_DIR, 'registry');
|
|
621
|
-
if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
|
|
622
|
-
const manifestSrc = join(PROJECT_DIR, 'registry', 'preinstalled.json');
|
|
623
|
-
if (existsSync(manifestSrc)) copyFileSync(manifestSrc, join(registryDir, 'preinstalled.json'));
|
|
624
358
|
ok('Source files copied to ~/.claude-mem-lite/');
|
|
625
359
|
|
|
626
360
|
// v2.48 P1-4: prune stale top-level .mjs + 0-byte .db files left behind by
|
|
@@ -1049,17 +783,6 @@ function configureHooks() {
|
|
|
1049
783
|
],
|
|
1050
784
|
};
|
|
1051
785
|
|
|
1052
|
-
const memPreSkillBridge = {
|
|
1053
|
-
matcher: 'Skill',
|
|
1054
|
-
hooks: [
|
|
1055
|
-
{
|
|
1056
|
-
type: 'command',
|
|
1057
|
-
command: nodeHook('scripts/pre-skill-bridge.js'),
|
|
1058
|
-
timeout: 3,
|
|
1059
|
-
},
|
|
1060
|
-
],
|
|
1061
|
-
};
|
|
1062
|
-
|
|
1063
786
|
// P0 subagent dispatch-time injection (default off — CLAUDE_MEM_SUBAGENT_INJECT).
|
|
1064
787
|
// Fires on the Agent/Task dispatch so a subagent (otherwise memory-blind — #8848)
|
|
1065
788
|
// can receive one relevant lesson via updatedInput. Parity with hooks/hooks.json.
|
|
@@ -1079,13 +802,13 @@ function configureHooks() {
|
|
|
1079
802
|
};
|
|
1080
803
|
|
|
1081
804
|
// Filter out existing mem hooks, then append fresh ones
|
|
1082
|
-
// PreToolUse has
|
|
805
|
+
// PreToolUse has two separate matchers, so we register both
|
|
1083
806
|
// Event set MUST stay equal to hooks/hooks.json's (minus scripts/setup.sh, which
|
|
1084
807
|
// bootstraps the plugin cache and has no settings.json counterpart) —
|
|
1085
808
|
// tests/audit-silent-20260814.test.mjs diffs a real `install --dev` run's
|
|
1086
809
|
// settings.json against the shipped manifest and reds on any new divergence.
|
|
1087
810
|
const hookConfigs = {
|
|
1088
|
-
PreToolUse: [memPreToolRecall,
|
|
811
|
+
PreToolUse: [memPreToolRecall, memPreAgentInject],
|
|
1089
812
|
PostToolUse: [memPostToolUse, memPostToolRecall],
|
|
1090
813
|
PostToolUseFailure: [memPostToolFailure],
|
|
1091
814
|
PreCompact: [memPreCompact],
|
|
@@ -1143,339 +866,6 @@ function backupLegacyClaudeMemData() {
|
|
|
1143
866
|
}
|
|
1144
867
|
}
|
|
1145
868
|
|
|
1146
|
-
async function installPreinstalledResources() {
|
|
1147
|
-
// 6. Install pre-installed resources (skills + agents)
|
|
1148
|
-
if (process.env.CLAUDE_MEM_SKIP_REPOS) {
|
|
1149
|
-
ok('Skill/agent registry: skipped (CLAUDE_MEM_SKIP_REPOS)');
|
|
1150
|
-
} else
|
|
1151
|
-
try {
|
|
1152
|
-
const manifestPath = join(INSTALL_DIR, 'registry', 'preinstalled.json');
|
|
1153
|
-
if (!existsSync(manifestPath)) {
|
|
1154
|
-
// For git-clone mode, check PROJECT_DIR
|
|
1155
|
-
const altPath = join(PROJECT_DIR, 'registry', 'preinstalled.json');
|
|
1156
|
-
if (existsSync(altPath)) {
|
|
1157
|
-
const registryDir = join(INSTALL_DIR, 'registry');
|
|
1158
|
-
if (!existsSync(registryDir)) mkdirSync(registryDir, { recursive: true });
|
|
1159
|
-
copyFileSync(altPath, manifestPath);
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (existsSync(manifestPath)) {
|
|
1164
|
-
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
1165
|
-
const resources = manifest.resources || [];
|
|
1166
|
-
|
|
1167
|
-
if (resources.length > 0) {
|
|
1168
|
-
const managedDir = join(MEM_DATA_DIR, 'managed');
|
|
1169
|
-
|
|
1170
|
-
// 6a. Git shallow clone unique repos
|
|
1171
|
-
const repos = new Map();
|
|
1172
|
-
for (const r of resources) {
|
|
1173
|
-
if (!repos.has(r.repo)) repos.set(r.repo, []);
|
|
1174
|
-
repos.get(r.repo).push(r);
|
|
1175
|
-
}
|
|
1176
|
-
|
|
1177
|
-
// Disclose the cost BEFORE spending it. This step is the single largest
|
|
1178
|
-
// thing `install` does — N shallow git clones over the network, ~150 MB on
|
|
1179
|
-
// disk for the default manifest — and it used to announce itself only after
|
|
1180
|
-
// the fact ("Repos: 15 cloned"). A first-time user on a metered link or a
|
|
1181
|
-
// small disk had no warning and no visible way out; the opt-out existed but
|
|
1182
|
-
// lived only in an env var no output ever mentioned.
|
|
1183
|
-
// Count only repos not already on disk — a re-run/update clones nothing, and
|
|
1184
|
-
// announcing "cloning 15 repos" every time would be false.
|
|
1185
|
-
const repoDirName = (repoUrl) =>
|
|
1186
|
-
repoUrl
|
|
1187
|
-
.split('/')
|
|
1188
|
-
.slice(-2)
|
|
1189
|
-
.join('-')
|
|
1190
|
-
.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1191
|
-
const pendingClones = [...repos.keys()].filter(
|
|
1192
|
-
(repoUrl) => !existsSync(join(managedDir, 'repos', repoDirName(repoUrl))),
|
|
1193
|
-
).length;
|
|
1194
|
-
if (pendingClones > 0) {
|
|
1195
|
-
log(`Skill/agent registry: cloning ${pendingClones} repo(s) — network + ~150 MB on disk.`);
|
|
1196
|
-
log(' Skip with CLAUDE_MEM_SKIP_REPOS=1 (memory features work without it).');
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
let cloned = 0,
|
|
1200
|
-
updated = 0;
|
|
1201
|
-
const deadRepos = new Set(); // repos that no longer exist (404)
|
|
1202
|
-
|
|
1203
|
-
const isRepoNotFound = (err) => {
|
|
1204
|
-
const msg = (err?.stderr ? err.stderr.toString() : '') + (err?.message || '');
|
|
1205
|
-
return /repository.*not found|404/i.test(msg);
|
|
1206
|
-
};
|
|
1207
|
-
|
|
1208
|
-
for (const [repoUrl, entries] of repos) {
|
|
1209
|
-
const repoName = repoUrl
|
|
1210
|
-
.split('/')
|
|
1211
|
-
.slice(-2)
|
|
1212
|
-
.join('-')
|
|
1213
|
-
.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
1214
|
-
const clonePath = join(managedDir, 'repos', repoName);
|
|
1215
|
-
let repoReady = false;
|
|
1216
|
-
|
|
1217
|
-
const plan = planRepoSparsePaths(entries);
|
|
1218
|
-
const cloneUrl = `${repoUrl.replace(/\.git$/, '')}.git`;
|
|
1219
|
-
// Clone only what we extract: a partial (blob:none) + sparse clone fetches
|
|
1220
|
-
// just the manifest subpaths' subtrees instead of the whole repo. Falls
|
|
1221
|
-
// back to a plain shallow clone if partial-clone/sparse-checkout is
|
|
1222
|
-
// unsupported (old git/server) — identical to the prior behavior.
|
|
1223
|
-
const cloneSlim = () => {
|
|
1224
|
-
if (plan.full) {
|
|
1225
|
-
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], {
|
|
1226
|
-
stdio: 'pipe',
|
|
1227
|
-
timeout: 30000,
|
|
1228
|
-
});
|
|
1229
|
-
return;
|
|
1230
|
-
}
|
|
1231
|
-
try {
|
|
1232
|
-
execFileSync(
|
|
1233
|
-
'git',
|
|
1234
|
-
['clone', '--depth', '1', '--filter=blob:none', '--no-checkout', cloneUrl, clonePath],
|
|
1235
|
-
{ stdio: 'pipe', timeout: 30000 },
|
|
1236
|
-
);
|
|
1237
|
-
execFileSync('git', ['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths], {
|
|
1238
|
-
stdio: 'pipe',
|
|
1239
|
-
timeout: 30000,
|
|
1240
|
-
});
|
|
1241
|
-
execFileSync('git', ['-C', clonePath, 'checkout'], { stdio: 'pipe', timeout: 30000 });
|
|
1242
|
-
} catch {
|
|
1243
|
-
try {
|
|
1244
|
-
rmSync(clonePath, { recursive: true, force: true });
|
|
1245
|
-
} catch {}
|
|
1246
|
-
execFileSync('git', ['clone', '--depth', '1', cloneUrl, clonePath], {
|
|
1247
|
-
stdio: 'pipe',
|
|
1248
|
-
timeout: 30000,
|
|
1249
|
-
});
|
|
1250
|
-
}
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
// Migrate a legacy full clone: drop it so the fresh-clone path below
|
|
1254
|
-
// rebuilds it slim. managed/repos is a rebuildable cache, so this loses
|
|
1255
|
-
// nothing and reclaims the bulk of its footprint on the next install run.
|
|
1256
|
-
if (!plan.full && existsSync(clonePath) && !isPartialSparseClone(clonePath)) {
|
|
1257
|
-
try {
|
|
1258
|
-
rmSync(clonePath, { recursive: true, force: true });
|
|
1259
|
-
} catch {}
|
|
1260
|
-
}
|
|
1261
|
-
|
|
1262
|
-
if (!existsSync(clonePath)) {
|
|
1263
|
-
// Fresh clone (also the rebuild path for a just-migrated legacy clone)
|
|
1264
|
-
try {
|
|
1265
|
-
mkdirSync(join(managedDir, 'repos'), { recursive: true });
|
|
1266
|
-
cloneSlim();
|
|
1267
|
-
cloned++;
|
|
1268
|
-
repoReady = true;
|
|
1269
|
-
} catch (err) {
|
|
1270
|
-
if (isRepoNotFound(err)) {
|
|
1271
|
-
deadRepos.add(repoUrl);
|
|
1272
|
-
warn(` Repo not found (removed?): ${repoUrl}`);
|
|
1273
|
-
} else {
|
|
1274
|
-
warn(` Clone failed: ${repoUrl}`);
|
|
1275
|
-
}
|
|
1276
|
-
continue;
|
|
1277
|
-
}
|
|
1278
|
-
} else {
|
|
1279
|
-
// Update existing: fetch latest and fast-forward
|
|
1280
|
-
try {
|
|
1281
|
-
// Re-assert the sparse set so a newer manifest that adds a subpath to
|
|
1282
|
-
// an already-slim clone checks it out (idempotent; no-op for full clones).
|
|
1283
|
-
if (!plan.full && isPartialSparseClone(clonePath)) {
|
|
1284
|
-
try {
|
|
1285
|
-
execFileSync(
|
|
1286
|
-
'git',
|
|
1287
|
-
['-C', clonePath, 'sparse-checkout', 'set', '--no-cone', ...plan.paths],
|
|
1288
|
-
{ stdio: 'pipe', timeout: 30000 },
|
|
1289
|
-
);
|
|
1290
|
-
} catch {}
|
|
1291
|
-
}
|
|
1292
|
-
const localHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'HEAD'], {
|
|
1293
|
-
encoding: 'utf8',
|
|
1294
|
-
stdio: 'pipe',
|
|
1295
|
-
}).trim();
|
|
1296
|
-
execFileSync('git', ['-C', clonePath, 'fetch', '--depth', '1', 'origin'], {
|
|
1297
|
-
stdio: 'pipe',
|
|
1298
|
-
timeout: 30000,
|
|
1299
|
-
});
|
|
1300
|
-
const remoteHash = execFileSync('git', ['-C', clonePath, 'rev-parse', 'FETCH_HEAD'], {
|
|
1301
|
-
encoding: 'utf8',
|
|
1302
|
-
stdio: 'pipe',
|
|
1303
|
-
}).trim();
|
|
1304
|
-
if (localHash !== remoteHash) {
|
|
1305
|
-
execFileSync('git', ['-C', clonePath, 'reset', '--hard', 'FETCH_HEAD'], { stdio: 'pipe' });
|
|
1306
|
-
updated++;
|
|
1307
|
-
repoReady = true; // needs re-copy
|
|
1308
|
-
}
|
|
1309
|
-
} catch (err) {
|
|
1310
|
-
if (isRepoNotFound(err)) {
|
|
1311
|
-
deadRepos.add(repoUrl);
|
|
1312
|
-
warn(` Repo not found (removed?): ${repoUrl} — cleaning up`);
|
|
1313
|
-
// Remove local clone
|
|
1314
|
-
try {
|
|
1315
|
-
rmSync(clonePath, { recursive: true, force: true });
|
|
1316
|
-
} catch {}
|
|
1317
|
-
// Remove extracted resources
|
|
1318
|
-
for (const entry of entries) {
|
|
1319
|
-
const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
|
|
1320
|
-
const destPath = join(destDir, entry.name);
|
|
1321
|
-
try {
|
|
1322
|
-
if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
|
|
1323
|
-
} catch {}
|
|
1324
|
-
}
|
|
1325
|
-
continue;
|
|
1326
|
-
}
|
|
1327
|
-
// Transient failure — use existing clone as-is
|
|
1328
|
-
}
|
|
1329
|
-
}
|
|
1330
|
-
|
|
1331
|
-
// Copy resources to managed/skills/ or managed/agents/
|
|
1332
|
-
// Re-copy if repo was freshly cloned or updated
|
|
1333
|
-
mkdirSync(join(managedDir, 'skills'), { recursive: true });
|
|
1334
|
-
mkdirSync(join(managedDir, 'agents'), { recursive: true });
|
|
1335
|
-
for (const entry of entries) {
|
|
1336
|
-
// Path traversal guard: reject entries with '..' or absolute paths
|
|
1337
|
-
if (
|
|
1338
|
-
entry.path.includes('..') ||
|
|
1339
|
-
entry.name.includes('..') ||
|
|
1340
|
-
isAbsolute(entry.path) ||
|
|
1341
|
-
isAbsolute(entry.name)
|
|
1342
|
-
)
|
|
1343
|
-
continue;
|
|
1344
|
-
const srcPath = entry.path === '.' ? clonePath : join(clonePath, entry.path);
|
|
1345
|
-
const destDir = join(managedDir, entry.type === 'skill' ? 'skills' : 'agents');
|
|
1346
|
-
const destPath = join(destDir, entry.name);
|
|
1347
|
-
if (existsSync(srcPath) && (repoReady || !existsSync(destPath))) {
|
|
1348
|
-
try {
|
|
1349
|
-
if (existsSync(destPath)) rmSync(destPath, { recursive: true, force: true });
|
|
1350
|
-
cpSync(srcPath, destPath, { recursive: true });
|
|
1351
|
-
} catch {}
|
|
1352
|
-
}
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
const managedSize = dirSizeBytes(managedDir);
|
|
1356
|
-
ok(
|
|
1357
|
-
`Repos: ${cloned} cloned, ${updated} updated, ${repos.size - deadRepos.size} active` +
|
|
1358
|
-
(deadRepos.size > 0 ? `, ${deadRepos.size} dead removed` : '') +
|
|
1359
|
-
` (${fmtMB(managedSize.bytes, managedSize.truncated)} in ${managedDir})`,
|
|
1360
|
-
);
|
|
1361
|
-
|
|
1362
|
-
// 6b. Init registry DB and record preinstalled entries
|
|
1363
|
-
const { ensureRegistryDb } = await importFromInstall('registry.mjs');
|
|
1364
|
-
const regDbPath = join(MEM_DATA_DIR, 'resource-registry.db');
|
|
1365
|
-
const rdb = ensureRegistryDb(regDbPath);
|
|
1366
|
-
|
|
1367
|
-
const insertPre = rdb.prepare(`
|
|
1368
|
-
INSERT OR REPLACE INTO preinstalled (name, type, repo_url, repo_path, tags, enabled)
|
|
1369
|
-
VALUES (?, ?, ?, ?, ?, 1)
|
|
1370
|
-
`);
|
|
1371
|
-
const activeResources =
|
|
1372
|
-
deadRepos.size > 0 ? resources.filter((r) => !deadRepos.has(r.repo)) : resources;
|
|
1373
|
-
for (const r of activeResources) {
|
|
1374
|
-
insertPre.run(r.name, r.type, r.repo, r.path, JSON.stringify(r.tags || []));
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Clean up DB entries for dead repos
|
|
1378
|
-
if (deadRepos.size > 0) {
|
|
1379
|
-
const delPre = rdb.prepare('DELETE FROM preinstalled WHERE repo_url = ?');
|
|
1380
|
-
const delRes = rdb.prepare('DELETE FROM resources WHERE repo_url = ?');
|
|
1381
|
-
for (const deadUrl of deadRepos) {
|
|
1382
|
-
try {
|
|
1383
|
-
delPre.run(deadUrl);
|
|
1384
|
-
} catch {}
|
|
1385
|
-
try {
|
|
1386
|
-
delRes.run(deadUrl);
|
|
1387
|
-
} catch {}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
ok(
|
|
1391
|
-
`Registry DB initialized (${activeResources.length} preinstalled entries` +
|
|
1392
|
-
(deadRepos.size > 0 ? `, ${deadRepos.size} dead repos purged` : '') +
|
|
1393
|
-
')',
|
|
1394
|
-
);
|
|
1395
|
-
|
|
1396
|
-
// 6c. Fetch GitHub stars (best-effort, unauthenticated)
|
|
1397
|
-
log(' Fetching GitHub stars...');
|
|
1398
|
-
const starCache = new Map();
|
|
1399
|
-
for (const [repoUrl] of repos) {
|
|
1400
|
-
if (deadRepos.has(repoUrl)) continue;
|
|
1401
|
-
const match = repoUrl.match(/github\.com\/([^/]+)\/([^/]+)/);
|
|
1402
|
-
if (match) {
|
|
1403
|
-
try {
|
|
1404
|
-
const apiUrl = `https://api.github.com/repos/${match[1]}/${match[2]}`;
|
|
1405
|
-
const res = execFileSync('curl', ['-sf', apiUrl], { encoding: 'utf8', timeout: 10000 });
|
|
1406
|
-
const data = JSON.parse(res);
|
|
1407
|
-
if (typeof data.stargazers_count === 'number') {
|
|
1408
|
-
starCache.set(repoUrl, data.stargazers_count);
|
|
1409
|
-
}
|
|
1410
|
-
} catch {}
|
|
1411
|
-
}
|
|
1412
|
-
}
|
|
1413
|
-
if (starCache.size > 0) ok(`Stars fetched (${starCache.size}/${repos.size} repos)`);
|
|
1414
|
-
|
|
1415
|
-
// 6d. Scan and index resources (fallback-only, Haiku indexing deferred to first run)
|
|
1416
|
-
log(' Scanning resources...');
|
|
1417
|
-
const { scanAllResources, diffResources } = await importFromInstall('registry-scanner.mjs');
|
|
1418
|
-
const scanned = scanAllResources({ dataDir: MEM_DATA_DIR });
|
|
1419
|
-
|
|
1420
|
-
// Attach star counts and repo URLs
|
|
1421
|
-
for (const s of scanned) {
|
|
1422
|
-
const entry = resources.find((r) => r.name === s.name && r.type === s.type);
|
|
1423
|
-
if (entry) {
|
|
1424
|
-
s.repoUrl = entry.repo;
|
|
1425
|
-
s.repoStars = starCache.get(entry.repo) || 0;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
|
|
1429
|
-
const { toIndex } = diffResources(rdb, scanned);
|
|
1430
|
-
if (toIndex.length > 0) {
|
|
1431
|
-
// Use fallback indexing at install time (no Haiku calls)
|
|
1432
|
-
// Full Haiku indexing happens on first SessionStart
|
|
1433
|
-
const { upsertResource } = await importFromInstall('registry.mjs');
|
|
1434
|
-
for (const res of toIndex) {
|
|
1435
|
-
try {
|
|
1436
|
-
const metaKey = `${res.type}:${res.name}`;
|
|
1437
|
-
const meta = RESOURCE_METADATA[metaKey];
|
|
1438
|
-
upsertResource(rdb, {
|
|
1439
|
-
name: res.name,
|
|
1440
|
-
type: res.type,
|
|
1441
|
-
status: 'active',
|
|
1442
|
-
source: res.source,
|
|
1443
|
-
repo_url: res.repoUrl || null,
|
|
1444
|
-
repo_stars: res.repoStars || 0,
|
|
1445
|
-
local_path: res.localPath,
|
|
1446
|
-
file_hash: res.fileHash,
|
|
1447
|
-
invocation_name: meta?.invocation_name || deriveInvocationName(res.name),
|
|
1448
|
-
intent_tags: meta?.intent_tags || res.name.replace(/-/g, ' '),
|
|
1449
|
-
domain_tags: meta?.domain_tags || '',
|
|
1450
|
-
trigger_patterns:
|
|
1451
|
-
meta?.trigger_patterns || `when user needs ${res.name.replace(/-/g, ' ')}`,
|
|
1452
|
-
capability_summary:
|
|
1453
|
-
meta?.capability_summary || `${res.type}: ${res.name.replace(/-/g, ' ')}`,
|
|
1454
|
-
});
|
|
1455
|
-
} catch {}
|
|
1456
|
-
}
|
|
1457
|
-
ok(`Resources registered: ${toIndex.length} indexed`);
|
|
1458
|
-
}
|
|
1459
|
-
|
|
1460
|
-
// Apply curated metadata to all known resources (fixes existing installs)
|
|
1461
|
-
reindexKnownResources(rdb);
|
|
1462
|
-
ok('Resource metadata curated (FTS5 reindexed)');
|
|
1463
|
-
|
|
1464
|
-
// Register plugin resources (skills/agents from other plugins, no local files)
|
|
1465
|
-
const virtualCount = registerVirtualResources(rdb);
|
|
1466
|
-
if (virtualCount > 0) ok(`Plugin resources registered: ${virtualCount} virtual entries`);
|
|
1467
|
-
|
|
1468
|
-
rdb.close();
|
|
1469
|
-
}
|
|
1470
|
-
} else {
|
|
1471
|
-
log(' No preinstalled manifest found, skipping');
|
|
1472
|
-
}
|
|
1473
|
-
} catch (e) {
|
|
1474
|
-
warn('Resource setup: ' + e.message);
|
|
1475
|
-
log(' Skills/agents will be indexed on first use');
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
869
|
function verifyDatabase() {
|
|
1480
870
|
// 7. Verify database
|
|
1481
871
|
if (existsSync(DB_PATH)) {
|
|
@@ -1557,7 +947,6 @@ async function install() {
|
|
|
1557
947
|
const managedHooks = configureHooks();
|
|
1558
948
|
dedupePluginCacheAndHooks({ managedHooks });
|
|
1559
949
|
backupLegacyClaudeMemData();
|
|
1560
|
-
await installPreinstalledResources();
|
|
1561
950
|
verifyDatabase();
|
|
1562
951
|
await dogfoodAutoAdopt();
|
|
1563
952
|
disableOldClaudeMemPlugin();
|
|
@@ -2480,29 +1869,6 @@ async function doctor() {
|
|
|
2480
1869
|
}
|
|
2481
1870
|
}
|
|
2482
1871
|
|
|
2483
|
-
// Env flags that are ACCEPTED but do nothing. A flag a user set and believes is
|
|
2484
|
-
// in effect is a silent lie the rest of doctor cannot see: every other check here
|
|
2485
|
-
// asks whether the install is healthy, and this install is perfectly healthy while
|
|
2486
|
-
// behaving as though the flag were unset (audit 2026-08-22 P2-5).
|
|
2487
|
-
// Dynamically imported, and the mode table is read from the module that owns it:
|
|
2488
|
-
// a static import would drag the registry retriever's dependency chain into a tool
|
|
2489
|
-
// whose whole job is to run when the tree is broken, and a local copy of the list
|
|
2490
|
-
// is the drift shape this repo keeps paying for.
|
|
2491
|
-
try {
|
|
2492
|
-
const { getRequestedRecommendMode, RECOMMEND_MODE_UNIMPLEMENTED } =
|
|
2493
|
-
await import('./registry-recommend.mjs');
|
|
2494
|
-
const requested = getRequestedRecommendMode();
|
|
2495
|
-
if (RECOMMEND_MODE_UNIMPLEMENTED.has(requested)) {
|
|
2496
|
-
dwarn(
|
|
2497
|
-
`CLAUDE_MEM_RECOMMEND_MODE=${requested}: accepted but NOT implemented — ` +
|
|
2498
|
-
'live skill-recommendation injection is Phase 2 and does not exist. The engine ' +
|
|
2499
|
-
'is running in shadow (logs only, injects nothing). Set shadow or off.',
|
|
2500
|
-
);
|
|
2501
|
-
}
|
|
2502
|
-
} catch (e) {
|
|
2503
|
-
dwarn('Env flags: check failed — ' + e.message);
|
|
2504
|
-
}
|
|
2505
|
-
|
|
2506
1872
|
// Plugin cache versions
|
|
2507
1873
|
const pluginCacheBase = join(homedir(), '.claude', 'plugins', 'cache', MARKETPLACE_KEY, 'claude-mem-lite');
|
|
2508
1874
|
if (existsSync(pluginCacheBase)) {
|
|
@@ -2551,30 +1917,6 @@ async function doctor() {
|
|
|
2551
1917
|
|
|
2552
1918
|
// ─── Settings helpers ───────────────────────────────────────────────────────
|
|
2553
1919
|
|
|
2554
|
-
// Identify a settings hook as one of OURS (to replace on install / strip on uninstall).
|
|
2555
|
-
// Must be tight: the old `hook.mjs` + event-word test matched a user's OWN generic hook
|
|
2556
|
-
// (`node ~/.config/hook.mjs session-start`) and install/uninstall silently deleted it.
|
|
2557
|
-
// The launcher marker (`hook-launcher.mjs`, which every Node hook routes through since v2.84)
|
|
2558
|
-
// replaces that clause; the product-name substring (our install-dir / legacy-direct hooks)
|
|
2559
|
-
// and the bash prefilter round out the real markers.
|
|
2560
|
-
export function isMemHook(cfg) {
|
|
2561
|
-
if (!cfg.hooks) return false;
|
|
2562
|
-
return cfg.hooks.some((h) => {
|
|
2563
|
-
const cmd = h.command || '';
|
|
2564
|
-
return (
|
|
2565
|
-
cmd.includes('claude-mem-lite') ||
|
|
2566
|
-
cmd.includes('hook-launcher.mjs') ||
|
|
2567
|
-
cmd.includes('scripts/post-tool-use.sh') ||
|
|
2568
|
-
// Same reason post-tool-use.sh is named here: a bash prefilter routes through
|
|
2569
|
-
// NO launcher, and the product-name clause only fires when the install dir
|
|
2570
|
-
// happens to contain it — which CLAUDE_MEM_DIR can relocate. Without this line
|
|
2571
|
-
// an Agent|Task hook in a relocated install survives uninstall and duplicates
|
|
2572
|
-
// on reinstall (audit 2026-08-22 P2-5 added the second prefilter).
|
|
2573
|
-
cmd.includes('scripts/pre-agent-inject.sh')
|
|
2574
|
-
);
|
|
2575
|
-
});
|
|
2576
|
-
}
|
|
2577
|
-
|
|
2578
1920
|
function hasMemHooksConfigured(settings) {
|
|
2579
1921
|
if (!settings?.hooks) return false;
|
|
2580
1922
|
return Object.values(settings.hooks).some(
|
|
@@ -2616,7 +1958,7 @@ function looksLikeHookPath(p) {
|
|
|
2616
1958
|
return HOOK_PATH_EXTS.some((ext) => p.endsWith(ext));
|
|
2617
1959
|
}
|
|
2618
1960
|
|
|
2619
|
-
export function collectOrphanHookPaths(settings) {
|
|
1961
|
+
export function collectOrphanHookPaths(settings, installDir = INSTALL_DIR) {
|
|
2620
1962
|
if (!settings?.hooks) return [];
|
|
2621
1963
|
const out = [];
|
|
2622
1964
|
for (const configs of Object.values(settings.hooks)) {
|
|
@@ -2626,6 +1968,14 @@ export function collectOrphanHookPaths(settings) {
|
|
|
2626
1968
|
for (const h of cfg.hooks || []) {
|
|
2627
1969
|
const cmd = h.command || '';
|
|
2628
1970
|
if (cmd.includes('${CLAUDE_PLUGIN_ROOT}')) continue;
|
|
1971
|
+
// The launcher's entry argument, which is unquoted and so invisible to the
|
|
1972
|
+
// quoted-token scan below. Recorded IN ADDITION to that scan, never instead of
|
|
1973
|
+
// it: a `continue` here shadowed a missing `scripts/hook-launcher.mjs` — the
|
|
1974
|
+
// half-applied-update shape where every hook fire dies with
|
|
1975
|
+
// ERR_MODULE_NOT_FOUND — and doctor went back to printing "Orphan hooks: none".
|
|
1976
|
+
// Both files have to exist for the hook to run, so both are reportable.
|
|
1977
|
+
const entry = launcherEntryPath(cmd, installDir);
|
|
1978
|
+
if (entry && !existsSync(entry) && !out.includes(entry)) out.push(entry);
|
|
2629
1979
|
// v2.80: scan ALL quoted tokens (was: only the first), prefer ones
|
|
2630
1980
|
// that look like a hook path. Fixes a footgun where a wrapper command
|
|
2631
1981
|
// like `bash -c "some inline" "/real/path.sh"` would pick "some inline"
|
|
@@ -2661,11 +2011,11 @@ export function collectOrphanHookPaths(settings) {
|
|
|
2661
2011
|
* - 0-byte `.db` files that are NOT in the protected-db allow-list
|
|
2662
2012
|
*
|
|
2663
2013
|
* Protections (never touched):
|
|
2664
|
-
* - subdirectories (
|
|
2014
|
+
* - subdirectories (runtime/, scripts/, lib/, cli/, commands/, server/, node_modules/, .claude-plugin/, etc.)
|
|
2665
2015
|
* - non-empty `.db` files — real data risk, always preserved
|
|
2666
2016
|
* - WAL/SHM (`*-wal`, `*-shm`) transients
|
|
2667
2017
|
* - files not ending in `.mjs` or `.db`
|
|
2668
|
-
* - the
|
|
2018
|
+
* - the canonical DB (`claude-mem-lite.db`) even when 0-byte (fresh-install transient state)
|
|
2669
2019
|
*
|
|
2670
2020
|
* @param {string} dataDir Absolute path, typically `~/.claude-mem-lite`
|
|
2671
2021
|
* @param {string[]} sourceFiles SOURCE_FILES manifest
|
|
@@ -2677,7 +2027,15 @@ export function pruneStaleInstallFiles(dataDir, sourceFiles) {
|
|
|
2677
2027
|
// like 'lib/activity.mjs' — those belong to a subdir and prune never touches
|
|
2678
2028
|
// subdirs anyway. For top-level entries ('server.mjs'), basename === entry.
|
|
2679
2029
|
const topLevelAllowed = new Set(sourceFiles.filter((f) => !f.includes('/')).map((f) => f));
|
|
2680
|
-
|
|
2030
|
+
// `resource-registry.db` came off this list with the registry itself. My first pass kept
|
|
2031
|
+
// it on a wrong premise — "a user upgrading from <=4.x might lose data" — but the prune
|
|
2032
|
+
// below gates on `st.size === 0`, so a registry DB with anything in it was never
|
|
2033
|
+
// reachable here in the first place. The only file the entry saved was a ZERO-BYTE one,
|
|
2034
|
+
// which holds no user data and is exactly the stale artifact this function exists to
|
|
2035
|
+
// clear. Nothing creates the file any more, so the "fresh-install transient state"
|
|
2036
|
+
// rationale that keeps `claude-mem-lite.db` here does not transfer. (R9 review F8a
|
|
2037
|
+
// corrected the premise; the CHANGELOG still tells users where the real file lives.)
|
|
2038
|
+
const PROTECTED_DBS = new Set(['claude-mem-lite.db']);
|
|
2681
2039
|
const removed = [];
|
|
2682
2040
|
let entries;
|
|
2683
2041
|
try {
|