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.
- 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-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/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/lib/registry-core.mjs
DELETED
|
@@ -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
|
-
}
|