claude-mem-lite 6.2.0 → 6.4.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 +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +22 -2
- package/README.zh-CN.md +19 -2
- package/hook-shared.mjs +54 -1
- package/hook-update.mjs +11 -2
- package/hook.mjs +75 -2
- package/install.mjs +371 -34
- package/lib/data-paths.mjs +33 -0
- package/lib/db-backup.mjs +42 -17
- package/lib/plugin-key.mjs +4 -1
- package/lib/schema-skew.mjs +379 -0
- package/mem-cli.mjs +42 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -1
- package/schema.mjs +26 -13
- package/scripts/hook-launcher.mjs +25 -4
- package/scripts/launch.mjs +72 -14
- package/scripts/user-prompt-search.js +20 -0
- package/server.mjs +57 -1
- package/source-files.mjs +6 -0
- package/tool-schemas.mjs +15 -2
package/scripts/launch.mjs
CHANGED
|
@@ -209,26 +209,84 @@ try {
|
|
|
209
209
|
useDevServer = existsSync(devServer) && lstatSync(devServer).isSymbolicLink();
|
|
210
210
|
} catch {}
|
|
211
211
|
|
|
212
|
+
// The MCP server opens the DB while it is being imported, so a forward-incompat store
|
|
213
|
+
// (schema.mjs's "DB schema is vN but this binary supports up to vN-1") throws right here
|
|
214
|
+
// and kills the process before the stdio handshake. All the host can say about that is
|
|
215
|
+
// `-32000 Connection closed`, which names nothing — measured 2026-09-08, a full day of it
|
|
216
|
+
// with the real cause visible only in a JSONL file the user has no reason to open.
|
|
217
|
+
//
|
|
218
|
+
// stderr is the one channel a launcher still has at that point. It reaches the plugin's own
|
|
219
|
+
// log rather than the transcript, so this is a diagnosis for whoever goes looking, not a
|
|
220
|
+
// substitute for the SessionStart notice — which is why both exist.
|
|
221
|
+
async function importServerOrExplain(run, { dev = false } = {}) {
|
|
222
|
+
try {
|
|
223
|
+
await run();
|
|
224
|
+
} catch (e) {
|
|
225
|
+
// The classifier is loaded INSIDE its own try and any failure rethrows the ORIGINAL
|
|
226
|
+
// error. Importing it unconditionally destroyed `e`: this path exists to diagnose an
|
|
227
|
+
// install whose files are missing (issue #15), lib/schema-skew.mjs is a brand-new file,
|
|
228
|
+
// and resolveLaunchEntry can serve the server from dataDir while `../lib/…` still
|
|
229
|
+
// resolves against ROOT. Proven by review — with the module moved aside the process died
|
|
230
|
+
// naming ERR_MODULE_NOT_FOUND for the classifier while the real boot failure never
|
|
231
|
+
// appeared anywhere in the output.
|
|
232
|
+
let skewMod;
|
|
233
|
+
try {
|
|
234
|
+
skewMod = await import('../lib/schema-skew.mjs');
|
|
235
|
+
} catch {
|
|
236
|
+
throw e;
|
|
237
|
+
}
|
|
238
|
+
if (!skewMod.isSchemaSkewError(e)) throw e;
|
|
239
|
+
let shape = { managed: false, activePluginVersion: null };
|
|
240
|
+
try {
|
|
241
|
+
({ ...shape } = await import('../lib/install-shape.mjs').then((m) =>
|
|
242
|
+
m.detectInstallShape({ installDir: dataDir }),
|
|
243
|
+
));
|
|
244
|
+
} catch {
|
|
245
|
+
/* shape unknown → schemaSkewRemedy answers 'unknown', which is its job */
|
|
246
|
+
}
|
|
247
|
+
const skew = skewMod.schemaSkewFromError(e) || { dbVersion: null, binaryVersion: null };
|
|
248
|
+
process.stderr.write(
|
|
249
|
+
skewMod.formatSchemaSkewNotice({
|
|
250
|
+
dbVersion: skew.dbVersion,
|
|
251
|
+
binaryVersion: skew.binaryVersion,
|
|
252
|
+
// `dev` is passed because the useDevServer branch IS the dev install by definition —
|
|
253
|
+
// omitting it told a checkout to `npm i -g` over its own working tree. `root: ROOT`
|
|
254
|
+
// so a mixed managed+plugin machine gets the remedy for the tree that is behind.
|
|
255
|
+
remedy: skewMod.schemaSkewRemedy({
|
|
256
|
+
managed: shape.managed,
|
|
257
|
+
activePluginVersion: shape.activePluginVersion,
|
|
258
|
+
dev,
|
|
259
|
+
root: ROOT,
|
|
260
|
+
}),
|
|
261
|
+
codeHome: ROOT,
|
|
262
|
+
}) + '\n',
|
|
263
|
+
);
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
212
268
|
if (useDevServer) {
|
|
213
|
-
await import(pathToFileURL(devServer).href);
|
|
269
|
+
await importServerOrExplain(() => import(pathToFileURL(devServer).href), { dev: true });
|
|
214
270
|
} else {
|
|
215
271
|
// Preflight: detect incomplete primary install (issue #15) — if relative
|
|
216
272
|
// imports referenced by server.mjs are missing on disk, fall back to the
|
|
217
273
|
// hook-update.mjs-maintained ~/.claude-mem-lite/ copy when healthy, or exit
|
|
218
274
|
// with a clear repair command instead of a Node ERR_MODULE_NOT_FOUND stack.
|
|
219
275
|
const { resolveLaunchEntry } = await import('./launch-preflight.mjs');
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
276
|
+
await importServerOrExplain(async () => {
|
|
277
|
+
try {
|
|
278
|
+
const entry = resolveLaunchEntry({
|
|
279
|
+
primaryRoot: ROOT,
|
|
280
|
+
fallbackRoot: dataDir,
|
|
281
|
+
warn: (msg) => process.stderr.write(msg + '\n'),
|
|
282
|
+
});
|
|
283
|
+
await import(pathToFileURL(entry.path).href);
|
|
284
|
+
} catch (e) {
|
|
285
|
+
if (e.code === 'INSTALL_INCOMPLETE') {
|
|
286
|
+
process.stderr.write(e.message + '\n');
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
throw e;
|
|
231
290
|
}
|
|
232
|
-
|
|
233
|
-
}
|
|
291
|
+
});
|
|
234
292
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
import { injectedIdsFileName, mergeInjectedMarker } from '../lib/injected-ids.mjs';
|
|
40
40
|
import { getDeferredByIds } from '../lib/deferred-work.mjs';
|
|
41
41
|
import { recordHookError } from '../lib/hook-telemetry.mjs';
|
|
42
|
+
import { isSchemaSkewError, schemaSkewFromError, shouldRecordSkew } from '../lib/schema-skew.mjs';
|
|
42
43
|
|
|
43
44
|
import { DAY_MS } from '../lib/time-constants.mjs';
|
|
44
45
|
import { envNumber } from '../lib/env-number.mjs';
|
|
@@ -773,6 +774,25 @@ async function main() {
|
|
|
773
774
|
// A failed DB open silently kills EVERY prompt-time injection while `stats`
|
|
774
775
|
// reads zero errors (audit 2026-08-14 M-5) — record before the mandatory
|
|
775
776
|
// swallow. Exact blindness class of the 2026-08-13 pre-recall:db-open outage.
|
|
777
|
+
//
|
|
778
|
+
// Schema skew is the one member of that family worth deduplicating: it persists until
|
|
779
|
+
// the user installs newer code, so it repeats on EVERY prompt. This face opens the DB
|
|
780
|
+
// itself rather than through hook-shared's openDb, so it needs the gate explicitly —
|
|
781
|
+
// it contributed 15 of one measured day's 727 identical lines, i.e. the flood was ~98%
|
|
782
|
+
// closed and not closed. Shared implementation, deliberately: a second copy of a
|
|
783
|
+
// dedup rule is this repo's twin-drift class.
|
|
784
|
+
if (isSchemaSkewError(e)) {
|
|
785
|
+
let project = '';
|
|
786
|
+
try {
|
|
787
|
+
project = inferProject();
|
|
788
|
+
} catch {
|
|
789
|
+
/* total — the marker degrades to one shared file, never a throw */
|
|
790
|
+
}
|
|
791
|
+
if (shouldRecordSkew(RUNTIME_DIR, project, schemaSkewFromError(e))) {
|
|
792
|
+
recordHookError('ups:db-open', e, RUNTIME_DIR);
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
776
796
|
recordHookError('ups:db-open', e, RUNTIME_DIR);
|
|
777
797
|
return;
|
|
778
798
|
}
|
package/server.mjs
CHANGED
|
@@ -7,7 +7,15 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
|
7
7
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
8
8
|
import { truncate, typeIcon, inferProject, fmtDate, debugLog, debugCatch } from './utils.mjs';
|
|
9
9
|
import { resolveProject as _resolveProjectShared } from './project-utils.mjs';
|
|
10
|
-
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR } from './schema.mjs';
|
|
10
|
+
import { ensureDbWithWalRecovery, DB_PATH, DB_DIR, CODE_DIR } from './schema.mjs';
|
|
11
|
+
// schema.mjs already imports this module for SCHEMA_SKEW_CODE, so it is in the graph before
|
|
12
|
+
// the DB is touched — a static import here adds no cold-start cost.
|
|
13
|
+
import {
|
|
14
|
+
isSchemaSkewError,
|
|
15
|
+
schemaSkewFromError,
|
|
16
|
+
schemaSkewRemedy,
|
|
17
|
+
formatSchemaSkewNotice,
|
|
18
|
+
} from './lib/schema-skew.mjs';
|
|
11
19
|
import { reRankWithContext, runIdleCleanup, buildServerInstructions } from './search-scoring.mjs';
|
|
12
20
|
import { searchObservationsHybrid } from './search-engine.mjs';
|
|
13
21
|
import {
|
|
@@ -153,6 +161,54 @@ try {
|
|
|
153
161
|
info: (m) => debugLog('INFO', 'server', m),
|
|
154
162
|
});
|
|
155
163
|
} catch (err) {
|
|
164
|
+
// Schema skew gets the same treatment as the CLI (mem-cli.mjs) and for the same reason:
|
|
165
|
+
// the raw message ends in `npm i -g claude-mem-lite@latest`, which repairs nothing on the
|
|
166
|
+
// plugin-cache install that actually hits this.
|
|
167
|
+
//
|
|
168
|
+
// This branch has to live HERE rather than in scripts/launch.mjs, which already knows how
|
|
169
|
+
// to format the notice. server.mjs opens the DB while it is being imported, so the throw
|
|
170
|
+
// is caught by this block and `process.exit(1)` runs before the launcher's own catch can
|
|
171
|
+
// ever see it — the launcher's handler is unreachable for this one error. Re-throwing
|
|
172
|
+
// instead would fix the plugin path and break the npm one, because install.mjs registers
|
|
173
|
+
// `claude mcp add ... -- node <SERVER_PATH>`, launching this file with no launcher above
|
|
174
|
+
// it to catch anything. Emitting from here covers both, and the launcher's copy stays as
|
|
175
|
+
// the handler for failures that happen before this module is reached.
|
|
176
|
+
if (isSchemaSkewError(err)) {
|
|
177
|
+
const skew = schemaSkewFromError(err) || { dbVersion: null, binaryVersion: null };
|
|
178
|
+
let shape = { managed: false, activePluginVersion: null };
|
|
179
|
+
let dev = false;
|
|
180
|
+
try {
|
|
181
|
+
const [shapeMod, updateMod] = await Promise.all([
|
|
182
|
+
import('./lib/install-shape.mjs'),
|
|
183
|
+
import('./hook-update.mjs'),
|
|
184
|
+
]);
|
|
185
|
+
// CODE_DIR, not DB_DIR: `hasManagedCodeInstall` looks for server.mjs + hook.mjs inside
|
|
186
|
+
// whatever it is handed, and DB_DIR follows CLAUDE_MEM_DIR. Handing it the relocated DATA
|
|
187
|
+
// dir reports `managed: false` for a machine that has a managed install, so the remedy
|
|
188
|
+
// came out as "could not identify this install" or, with any plugin cache present, the
|
|
189
|
+
// plugin commands printed under a line naming ~/.claude-mem-lite. The three sibling call
|
|
190
|
+
// sites (mem-cli.mjs, hook.mjs, scripts/launch.mjs) all pass the CODE dir.
|
|
191
|
+
shape = shapeMod.detectInstallShape({ installDir: CODE_DIR });
|
|
192
|
+
dev = updateMod.isDevMode();
|
|
193
|
+
} catch {
|
|
194
|
+
/* shape unknown → schemaSkewRemedy answers 'unknown', which is its job */
|
|
195
|
+
}
|
|
196
|
+
const codeHome = process.env.CLAUDE_PLUGIN_ROOT || import.meta.dirname;
|
|
197
|
+
console.error(
|
|
198
|
+
formatSchemaSkewNotice({
|
|
199
|
+
dbVersion: skew.dbVersion,
|
|
200
|
+
binaryVersion: skew.binaryVersion,
|
|
201
|
+
remedy: schemaSkewRemedy({
|
|
202
|
+
managed: shape.managed,
|
|
203
|
+
activePluginVersion: shape.activePluginVersion,
|
|
204
|
+
dev,
|
|
205
|
+
root: codeHome,
|
|
206
|
+
}),
|
|
207
|
+
codeHome,
|
|
208
|
+
}),
|
|
209
|
+
);
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
156
212
|
// Fatal: log and exit with descriptive message (Claude Code shows stderr)
|
|
157
213
|
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
|
|
158
214
|
if (err.walRecoveryAttempted) {
|
package/source-files.mjs
CHANGED
|
@@ -55,6 +55,11 @@ export const SOURCE_FILES = [
|
|
|
55
55
|
// AND hook scripts (pre-tool-recall / post-tool-recall) — ship it
|
|
56
56
|
// or auto-update leaves schema + every hook with ERR_MODULE_NOT_FOUND on each fire.
|
|
57
57
|
'lib/resolve-data-dir.mjs',
|
|
58
|
+
// DB_DIR / DB_PATH / CODE_DIR. Statically imported by schema.mjs (which re-exports all
|
|
59
|
+
// three) and by hook-update.mjs, which takes them from HERE so the verified repair path
|
|
60
|
+
// stays loadable without better-sqlite3. Missing from the manifest → auto-update leaves
|
|
61
|
+
// schema.mjs and the repair path with ERR_MODULE_NOT_FOUND on every fire.
|
|
62
|
+
'lib/data-paths.mjs',
|
|
58
63
|
// lib/ — statically imported by hook-llm.mjs (activity) + hook-handoff.mjs (git-state, task-reader);
|
|
59
64
|
// dynamically imported by hook.mjs (startup-dashboard) + mem-cli.mjs (doctor-benchmark, plan-reader).
|
|
60
65
|
'lib/activity.mjs',
|
|
@@ -131,6 +136,7 @@ export const SOURCE_FILES = [
|
|
|
131
136
|
// Missing from the manifest → an updated install ships a doctor that throws
|
|
132
137
|
// ERR_MODULE_NOT_FOUND on the command users run when something is already wrong.
|
|
133
138
|
'lib/install-shape.mjs',
|
|
139
|
+
'lib/schema-skew.mjs',
|
|
134
140
|
// Single-envelope stdout for hook processes — imported by hook.mjs. Claude Code
|
|
135
141
|
// parses hook stdout as ONE JSON document; missing from the manifest → an updated
|
|
136
142
|
// install throws ERR_MODULE_NOT_FOUND on every hook fire.
|
package/tool-schemas.mjs
CHANGED
|
@@ -256,7 +256,15 @@ export const memGetSchema = {
|
|
|
256
256
|
};
|
|
257
257
|
|
|
258
258
|
export const memDeleteSchema = {
|
|
259
|
-
|
|
259
|
+
// `.nonoptional()` is not a runtime change — zod already rejected an omitted `ids` here.
|
|
260
|
+
// It is what keeps the field in the PUBLISHED JSON Schema's `required` array: zod 4's
|
|
261
|
+
// toJSONSchema({io:'input'}) reads a ZodPipe's input side as accepting `undefined` and
|
|
262
|
+
// drops the key, so the advertised contract said optional while the server said required.
|
|
263
|
+
// See tests/tool-schemas.test.mjs, which grades every field against runtime ground truth.
|
|
264
|
+
ids: coerceIntArray
|
|
265
|
+
.pipe(z.array(z.number().int()).min(1).max(50))
|
|
266
|
+
.nonoptional()
|
|
267
|
+
.describe('Observation IDs to delete'),
|
|
260
268
|
confirm: coerceBool.describe('false=preview what will be deleted, true=execute deletion'),
|
|
261
269
|
};
|
|
262
270
|
|
|
@@ -433,7 +441,8 @@ export const memMaintainSchema = {
|
|
|
433
441
|
};
|
|
434
442
|
|
|
435
443
|
export const memUpdateSchema = {
|
|
436
|
-
|
|
444
|
+
// `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
|
|
445
|
+
id: coerceInt.pipe(z.number().int().positive()).nonoptional().describe('Observation ID to update'),
|
|
437
446
|
// CLI parity (cmdUpdate): empty/whitespace title would render as `(untitled)`
|
|
438
447
|
// in every listing — reject here like the CLI does, instead of persisting it.
|
|
439
448
|
title: z
|
|
@@ -541,6 +550,10 @@ export const memDeferDropSchema = {
|
|
|
541
550
|
coerceInt.pipe(z.number().int().positive()),
|
|
542
551
|
z.string().regex(/^D#\d+$/, 'expected D#N or positive integer'),
|
|
543
552
|
])
|
|
553
|
+
// `.nonoptional()` for the published-`required` reason documented on memDeleteSchema.ids.
|
|
554
|
+
// This is the CORE tool of the three — it ships in tools/list, so the drift was visible
|
|
555
|
+
// to every agent, and the tool's own description already calls the reason "required".
|
|
556
|
+
.nonoptional()
|
|
544
557
|
.describe('Deferred item id — accepts D#N (raw id) or positive integer (ordinal-within-project)'),
|
|
545
558
|
reason: z.string().min(1).max(500).describe('Why this item is being dropped (required for audit trail)'),
|
|
546
559
|
project: z.string().optional().describe('Project name (default: inferred from CWD)'),
|