claude-mem-lite 4.0.4 → 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.
Files changed (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +0 -49
  4. package/README.zh-CN.md +0 -48
  5. package/adopt-content.mjs +0 -1
  6. package/cli/common.mjs +0 -17
  7. package/cli.mjs +19 -3
  8. package/commands/update.md +4 -11
  9. package/format-utils.mjs +0 -21
  10. package/hook-update.mjs +37 -12
  11. package/hook.mjs +5 -29
  12. package/hooks/hooks.json +0 -10
  13. package/install.mjs +24 -666
  14. package/lib/doctor-drift.mjs +0 -1
  15. package/lib/frontmatter.mjs +1 -2
  16. package/lib/hook-prune.mjs +133 -0
  17. package/lib/hook-stdin.mjs +1 -1
  18. package/lib/hook-telemetry.mjs +1 -1
  19. package/lib/metrics.mjs +2 -2
  20. package/lib/shard-gc.mjs +11 -7
  21. package/mem-cli.mjs +2 -423
  22. package/nlp.mjs +1 -1
  23. package/npm-shrinkwrap.json +2 -2
  24. package/package.json +3 -15
  25. package/schema.mjs +0 -1
  26. package/scripts/hook-launcher.mjs +2 -2
  27. package/scripts/prompt-search-utils.mjs +0 -30
  28. package/scripts/user-prompt-search.js +3 -125
  29. package/server.mjs +11 -449
  30. package/source-files.mjs +11 -28
  31. package/synonyms.mjs +2 -1
  32. package/tool-schemas.mjs +0 -80
  33. package/utils.mjs +5 -13
  34. package/commands/tools.md +0 -67
  35. package/install-metadata.mjs +0 -2193
  36. package/lib/registry-core.mjs +0 -264
  37. package/registry/preinstalled.json +0 -2419
  38. package/registry-enricher.mjs +0 -124
  39. package/registry-github.mjs +0 -86
  40. package/registry-importer.mjs +0 -569
  41. package/registry-recommend.mjs +0 -503
  42. package/registry-retriever.mjs +0 -611
  43. package/registry-scanner.mjs +0 -261
  44. package/registry.mjs +0 -665
  45. package/resource-discovery.mjs +0 -199
  46. package/scripts/pre-skill-bridge.js +0 -146
@@ -1,264 +0,0 @@
1
- // Shared body for the registry write actions — import / remove / reindex / enrich.
2
- //
3
- // These were the last un-collapsed CLI/MCP twin (audit 2026-08-22 P1-3): mem-cli.mjs and
4
- // server.mjs each wrote their own SQL for the same three actions, and had already drifted —
5
- // the CLI granted a user-initiated import `quality_tier = 'installed'` and the MCP twin did
6
- // not, so the same intent produced differently-ranked rows depending on which surface the
7
- // user reached for. That is this project's first-listed病类 ("a guard wired into one face,
8
- // missing on the other"), so the fix is a single body both faces call, not a second patch.
9
- //
10
- // Contract: these functions decide and write. They return structured results and never
11
- // format output or touch process state — rendering (and CLI-only concerns like bare-flag
12
- // rejection or the "add --capability-summary" tip) stays in the surface.
13
-
14
- import { readFileSync } from 'fs';
15
-
16
- import { upsertResource } from '../registry.mjs';
17
- import { isPathConfined } from '../utils.mjs';
18
-
19
- /**
20
- * String columns an import may set, in canonical snake_case. Single source: the CLI derives
21
- * its kebab-case flag names from this list, the MCP tool reads its args by these keys, so a
22
- * new column is added once and both surfaces pick it up.
23
- */
24
- export const IMPORT_STRING_FIELDS = [
25
- 'repo_url',
26
- 'local_path',
27
- 'invocation_name',
28
- 'intent_tags',
29
- 'domain_tags',
30
- 'trigger_patterns',
31
- 'capability_summary',
32
- 'keywords',
33
- 'tech_stack',
34
- 'use_cases',
35
- ];
36
-
37
- /**
38
- * Resolve the `source` column for an import.
39
- *
40
- * Preserve provenance on a metadata-only re-import: default to 'user' only for a genuinely
41
- * NEW resource. Re-importing an existing github/preinstalled row without an explicit source
42
- * must not flip it to 'user' (which also mis-grants the user-source rank boost).
43
- */
44
- function resolveSource(db, { type, name, source }) {
45
- if (source) return source;
46
- const existing = db.prepare('SELECT source FROM resources WHERE type = ? AND name = ?').get(type, name);
47
- return existing ? existing.source : 'user';
48
- }
49
-
50
- /**
51
- * Upsert one resource.
52
- *
53
- * @param {object} db open resource-registry.db handle
54
- * @param {object} params
55
- * @param {string} params.name
56
- * @param {string} params.type 'skill' | 'agent'
57
- * @param {string} [params.source] explicit provenance; absent = user-initiated
58
- * @param {object} [params.fields] IMPORT_STRING_FIELDS values (snake_case keys)
59
- * @returns {{id: number, source: string, installedTierGranted: boolean}}
60
- */
61
- export function importResource(db, { name, type, source, fields = {} }) {
62
- const resolvedSource = resolveSource(db, { type, name, source });
63
-
64
- const row = { name, type, status: 'active', source: resolvedSource };
65
- for (const f of IMPORT_STRING_FIELDS) row[f] = fields[f] || '';
66
-
67
- const id = upsertResource(db, row);
68
-
69
- // A user-initiated import (no explicit --source/source arg) means the user deliberately
70
- // added this resource — it gets the 'installed' quality tier, which the retriever reads as
71
- // a ranking bonus and the recommendation gate reads as a precision signal.
72
- const installedTierGranted = Boolean(id) && !source;
73
- if (installedTierGranted) {
74
- db.prepare("UPDATE resources SET quality_tier = 'installed' WHERE id = ?").run(id);
75
- }
76
-
77
- return { id, source: resolvedSource, installedTierGranted };
78
- }
79
-
80
- /**
81
- * Delete one resource.
82
- * @returns {{removed: boolean}} removed=false means nothing matched (not an error).
83
- */
84
- export function removeResource(db, { name, type }) {
85
- const result = db.prepare('DELETE FROM resources WHERE type = ? AND name = ?').run(type, name);
86
- return { removed: result.changes > 0 };
87
- }
88
-
89
- /**
90
- * Rebuild the FTS5 index over the resources table.
91
- * @returns {{activeCount: number}}
92
- */
93
- export function reindexResources(db) {
94
- db.exec("INSERT INTO resources_fts(resources_fts) VALUES('rebuild')");
95
- const row = db.prepare('SELECT COUNT(*) as c FROM resources WHERE status = ?').get('active');
96
- return { activeCount: row.c };
97
- }
98
-
99
- // ─── Enrichment (four legs, one gate) ───────────────────────────────────────
100
- //
101
- // `resources.local_path` is a filesystem path stored in a DB row, and every enrichment leg
102
- // ends in `readFileSync(local_path)`. Before audit 2026-09-02 P1-3 the confinement check
103
- // guarded exactly ONE of the four legs — the MCP `enrich` action — while the MCP
104
- // `import_url --enrich` leg, the CLI `enrich <name>` leg and the CLI `enrich --all` leg read
105
- // the path bare. That is this repo's first-listed 病类 in its narrowest form: not "CLI vs
106
- // MCP" but "one branch of one face". Counting the legs is what showed the MCP side was
107
- // itself half-guarded, so a CLI-only patch would have left the shape alive on both faces.
108
- //
109
- // Every leg now funnels through `enrichResourceRow`, so a new leg cannot be written without
110
- // passing `confineTo` — and omitting it is a visible `undefined`, not a silently absent `if`.
111
-
112
- /** Env var that disables the confinement gate (escape hatch for relocated managed dirs). */
113
- export const REGISTRY_CONFINE_ENV = 'CLAUDE_MEM_REGISTRY_CONFINE';
114
-
115
- /**
116
- * Whether the confinement gate is active. Only `off` disables it — case-insensitively and
117
- * ignoring surrounding whitespace, so `OFF` and ` off\n` also open the gate. Every OTHER
118
- * value, including an unset one and every near-miss (`of`, `0`, `false`, `disable`), keeps
119
- * the gate ON, because the failure mode of a typo must be "refuses a path it could have
120
- * read", never "reads a path it should have refused". Said precisely because the first
121
- * version of this sentence claimed "the exact string `off`" in three places, which is the
122
- * wrong description of a `.trim().toLowerCase()` — the safety property held, the wording
123
- * did not.
124
- */
125
- export function registryConfineEnabled(env = process.env) {
126
- return (
127
- String(env[REGISTRY_CONFINE_ENV] ?? '')
128
- .trim()
129
- .toLowerCase() !== 'off'
130
- );
131
- }
132
-
133
- /**
134
- * Enrich one already-resolved resource row.
135
- *
136
- * @param {object} db open resource-registry.db handle
137
- * @param {object} row must carry {name, type, local_path}
138
- * @param {object} opts
139
- * @param {string} opts.confineTo base dir `local_path` must stay within
140
- * @param {Function} opts.enrichResource injected `(db,name,type,content) => Promise<boolean>`;
141
- * injected rather than imported so this module stays a
142
- * leaf and the Anthropic SDK stays off the CLI's
143
- * cold-start path (both faces already lazy-import it)
144
- * @param {object} [opts.env] env source for the escape hatch (tests pass their own)
145
- * @returns {Promise<{status:'enriched'|'failed'|'no-path'|'denied'|'unreadable', error?:Error}>}
146
- * `error` is carried on 'unreadable' only: a stale local_path (the file was moved or
147
- * deleted after import) is the common case, and the errno is the whole diagnosis —
148
- * collapsing it into a bare status would make this refactor a diagnosability
149
- * regression on the one leg that already surfaced the message.
150
- */
151
- export async function enrichResourceRow(db, row, { confineTo, enrichResource, env } = {}) {
152
- // `confineTo` is REQUIRED, and a missing one throws rather than defaulting to "no gate".
153
- // The defect this replaces was three call sites that simply had no `if` — invisible in
154
- // review because absence has no syntax. A fifth leg written without the gate now fails
155
- // loudly on its first call instead of shipping as a silent hole; the static sweep in
156
- // tests/registry-enrich-confinement.test.mjs catches it earlier still.
157
- if (!confineTo) throw new TypeError('enrichResourceRow: confineTo is required');
158
- if (!row?.local_path) return { status: 'no-path' };
159
- if (registryConfineEnabled(env) && !isPathConfined(row.local_path, confineTo)) {
160
- return { status: 'denied' };
161
- }
162
- let content;
163
- try {
164
- content = readFileSync(row.local_path, 'utf8');
165
- } catch (e) {
166
- return { status: 'unreadable', error: e };
167
- }
168
- // Enrichment itself is an LLM round-trip; a throw here is a failed enrichment, not a
169
- // refused read, and the two must stay distinguishable in the counts the surfaces render.
170
- try {
171
- return { status: (await enrichResource(db, row.name, row.type, content)) ? 'enriched' : 'failed' };
172
- } catch (e) {
173
- return { status: 'failed', error: e };
174
- }
175
- }
176
-
177
- /**
178
- * Enrich the rows an import just produced (MCP `import_url --enrich` + CLI `import --enrich`).
179
- *
180
- * `results` carries only {id, name, type}; local_path is read back per row because
181
- * importFromGitHub writes it during the import.
182
- *
183
- * @returns {Promise<{ok:number, denied:number, total:number}>}
184
- */
185
- export async function enrichImportedResources(db, results, { confineTo, enrichResource, env } = {}) {
186
- // Checked here too, not only in the delegate: an empty `results` never reaches
187
- // enrichResourceRow, so an ungated caller would otherwise pass on the empty input a test
188
- // is most likely to use and throw only in production.
189
- if (!confineTo) throw new TypeError('enrichImportedResources: confineTo is required');
190
- let ok = 0,
191
- denied = 0;
192
- for (const r of results) {
193
- const row = db.prepare('SELECT name, type, local_path FROM resources WHERE id = ?').get(r.id);
194
- const { status } = await enrichResourceRow(db, row, { confineTo, enrichResource, env });
195
- if (status === 'enriched') ok++;
196
- else if (status === 'denied') denied++;
197
- }
198
- return { ok, denied, total: results.length };
199
- }
200
-
201
- /**
202
- * Resolve one active resource by name and enrich it (MCP `enrich` + CLI `enrich <name>`).
203
- * @returns {Promise<{status:'not-found'|'enriched'|'failed'|'no-path'|'denied'|'unreadable', name:string, error?:Error}>}
204
- */
205
- export async function enrichNamedResource(db, name, { confineTo, enrichResource, env } = {}) {
206
- // Same reason as enrichImportedResources: the 'not-found' return short-circuits above the
207
- // delegate, so the gate check cannot be left to it.
208
- if (!confineTo) throw new TypeError('enrichNamedResource: confineTo is required');
209
- const row = db
210
- .prepare("SELECT name, type, local_path FROM resources WHERE name = ? AND status = 'active'")
211
- .get(name);
212
- if (!row) return { status: 'not-found', name };
213
- const { status, error } = await enrichResourceRow(db, row, { confineTo, enrichResource, env });
214
- return { status, name: row.name, error };
215
- }
216
-
217
- /**
218
- * How a registry search hit should be invoked, and the path worth showing for it.
219
- *
220
- * Audit 2026-09-02 P2-6: this ran to ~15 lines in `mem-cli.mjs cmdRegistry` and again in
221
- * `server.mjs mem_registry`, and the copies had already diverged on `portablePath`:
222
- *
223
- * MCP `isManaged ? toPortable(local_path) : ''`
224
- * CLI `isManaged && local_path.startsWith(home) ? '~'+… : (local_path || '')`
225
- *
226
- * For a NON-managed resource the CLI therefore printed `Path: /home/<user>/…` — an absolute
227
- * path, un-tilde'd, for a row whose `Use:` line is `Skill("x")` or `mem_use(name=…)` and
228
- * never mentions the path at all. MCP printed nothing. The MCP behaviour is the one kept:
229
- * the line carried no action for the reader and spelled out a home directory to do it.
230
- *
231
- * Returns DATA, not a rendered line. The two faces genuinely differ in dialect — the CLI
232
- * indents four spaces and prints through `out()`, MCP indents two and emits Markdown bold —
233
- * and collapsing that would replace a real duplicate with a formatting flag.
234
- *
235
- * @param {{name:string,type:string,local_path?:string,invocation_name?:string}} r
236
- * @param {{home:string, managedPrefix:string}} ctx `managedPrefix` is `<dataDir>/managed/`,
237
- * already including the trailing separator, so the caller owns CLAUDE_MEM_DIR resolution.
238
- * @returns {{isManaged:boolean, portablePath:string, howToUse:string}}
239
- */
240
- export function resourceUseHint(r, { home, managedPrefix }) {
241
- const isManaged = Boolean(r.local_path && r.local_path.includes(managedPrefix));
242
- const portablePath = isManaged
243
- ? r.local_path.startsWith(home)
244
- ? '~' + r.local_path.slice(home.length)
245
- : r.local_path
246
- : '';
247
- const agentArg = r.type === 'agent' ? ', type="agent"' : '';
248
-
249
- let howToUse;
250
- if (isManaged) {
251
- // Managed: Read(path) or mem_use — Skill() does not resolve managed resources.
252
- // Agents always carry a complete .md path; only skills can be a directory, which
253
- // resolves to its SKILL.md.
254
- const resolvedPath = portablePath.endsWith('.md') ? portablePath : `${portablePath}/SKILL.md`;
255
- howToUse = `Read("${resolvedPath}") or mem_use(name="${r.name}"${agentArg})`;
256
- } else if (r.invocation_name) {
257
- // Native plugin/user resource: invoke by its full invocation name.
258
- howToUse =
259
- r.type === 'skill' ? `Skill("${r.invocation_name}")` : `Agent(subagent_type="${r.invocation_name}")`;
260
- } else {
261
- howToUse = `mem_use(name="${r.name}"${agentArg})`;
262
- }
263
- return { isManaged, portablePath, howToUse };
264
- }