claude-mem-lite 6.12.0 → 6.12.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/cli/common.mjs +26 -2
- package/cli/verify-apply.mjs +1 -2
- package/cli-path.mjs +11 -1
- package/commands/adopt.md +1 -1
- package/commands/bug.md +1 -1
- package/commands/lesson.md +1 -1
- package/commands/mem.md +8 -8
- package/commands/unadopt.md +1 -1
- package/hook-handoff.mjs +15 -5
- package/install.mjs +16 -12
- package/lib/binding-probe.mjs +6 -4
- package/lib/db-unusable.mjs +4 -3
- package/lib/import-jsonl.mjs +43 -29
- package/lib/install-shape.mjs +2 -1
- package/lib/native-binding-hint.mjs +2 -1
- package/mem-cli.mjs +6 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/scripts/hook-launcher.mjs +5 -2
- package/scripts/launch.mjs +5 -2
- package/scripts/pre-tool-recall.js +4 -2
- package/server.mjs +2 -2
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"plugins": [
|
|
10
10
|
{
|
|
11
11
|
"name": "claude-mem-lite",
|
|
12
|
-
"version": "6.12.
|
|
12
|
+
"version": "6.12.2",
|
|
13
13
|
"source": "./",
|
|
14
14
|
"homepage": "https://github.com/sdsrss/claude-mem-lite",
|
|
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)."
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.12.
|
|
3
|
+
"version": "6.12.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/README.md
CHANGED
|
@@ -165,7 +165,7 @@ is still outside it gets a message naming both sides of the mismatch instead of
|
|
|
165
165
|
|
|
166
166
|
Plugin mode manages its own hooks/runtime. On session start it only **checks and reports** new claude-mem-lite versions; it does **not** self-overwrite plugin files in place. Update plugin-mode installs through Claude's plugin workflow.
|
|
167
167
|
|
|
168
|
-
> **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (
|
|
168
|
+
> **The plugin install is complete on its own** — hooks, MCP tools, and the bundled slash commands (`/mem`, `/lesson`, `/bug`, `/adopt`) all run from the plugin with no second step. The slash commands invoke the bundled CLI by an absolute path resolved from the plugin directory (`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <cmd>`), so they work without anything on your `PATH`. A global `claude-mem-lite` **shell** command (for running queries yourself in a terminal) is **optional** — `npm i -g claude-mem-lite` — and is a *separate* npm install: the plugin's auto-update does **not** refresh it, so re-run `npm i -g claude-mem-lite@latest` if you want that shell command kept in sync. You do **not** need it for the plugin to be fully functional.
|
|
169
169
|
|
|
170
170
|
> **Auto-adopt writes into your project, on every SessionStart (v3.13+).** The plugin adds a slug-scoped **managed block** to your project's own **`<cwd>/CLAUDE.md`** — a file that is normally committed to git — plus a `<cwd>/.claude/plugin_claude_mem_lite.md` detail file. The block is a system-authority pointer that boosts Claude's proactive use of `mem_recall` / `mem_save`. Everything outside the block is preserved verbatim, and it coexists with other plugins' blocks in the same file ([details](#invited-memory-v232)). This happens on **every** SessionStart, not just the first: the sync is idempotent and re-applies the block if it is edited away, and refreshes it when the shipped template changes. It applies regardless of install path (npm, npx, `/plugin`, manual), so **no manual `/adopt` is needed**.
|
|
171
171
|
>
|
package/README.zh-CN.md
CHANGED
|
@@ -129,7 +129,7 @@ v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`
|
|
|
129
129
|
|
|
130
130
|
插件模式会管理自己的运行时与钩子。SessionStart 时它现在只会**检查并提示**新版本,不会直接覆盖插件目录中的文件。插件模式请通过 Claude 的插件更新流程完成升级。
|
|
131
131
|
|
|
132
|
-
> **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI
|
|
132
|
+
> **插件安装本身即完整** —— hooks、MCP 工具、以及捆绑的 slash 命令(`/mem`、`/lesson`、`/bug`、`/adopt`)全部从插件内运行,无需第二步。slash 命令以从插件目录解析出的绝对路径调用捆绑 CLI(`node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" <cmd>`),因此不依赖 `PATH` 上的任何东西。全局 `claude-mem-lite` **shell** 命令(用于你自己在终端里跑查询)是**可选**的 —— `npm i -g claude-mem-lite` —— 且是**独立**的 npm 安装:插件的自动更新**不会**刷新它,想保持同步就重新跑 `npm i -g claude-mem-lite@latest`。插件要完整工作**并不需要**它。
|
|
133
133
|
|
|
134
134
|
### 方式二:npx(一行命令)
|
|
135
135
|
|
package/cli/common.mjs
CHANGED
|
@@ -417,6 +417,21 @@ export const KNOWN_CLI_FLAGS = new Set([
|
|
|
417
417
|
// --has?" suggestion. Locked by tests/cli-flag-allowlist.test.mjs.
|
|
418
418
|
]);
|
|
419
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Flags exactly ONE subcommand reads, mapped to that command. KNOWN_CLI_FLAGS is a union, so
|
|
422
|
+
* without this `recent --apply` was accepted in silence while `--apply` did nothing there.
|
|
423
|
+
* Every other command mem-cli dispatches now reports such a flag, and the typo suggester stops
|
|
424
|
+
* offering it there. Install-family commands (doctor, status, …) route to install.mjs and are
|
|
425
|
+
* not covered. Only flags with a single reader
|
|
426
|
+
* belong here; tests/cli-flag-allowlist.test.mjs derives that from the sources.
|
|
427
|
+
*/
|
|
428
|
+
export const COMMAND_SCOPED_FLAGS = new Map([
|
|
429
|
+
['apply', 'verify-apply'],
|
|
430
|
+
['digest', 'verify-apply'],
|
|
431
|
+
['print-project', 'verify-apply'],
|
|
432
|
+
['undo', 'verify-apply'],
|
|
433
|
+
]);
|
|
434
|
+
|
|
420
435
|
/** Levenshtein distance, early-exit past `max` (cheap enough for a handful of flags). */
|
|
421
436
|
function editDistance(a, b, max = 2) {
|
|
422
437
|
const m = a.length,
|
|
@@ -444,16 +459,25 @@ function editDistance(a, b, max = 2) {
|
|
|
444
459
|
* project — a typo produced a wrong result with zero signal. Returns [{flag, suggestion}].
|
|
445
460
|
* Unknown flags with NO close match are omitted: they may be a valid flag we didn't
|
|
446
461
|
* catalog, so silence beats a false alarm. Warning-only by contract — never fails.
|
|
462
|
+
* A flag in COMMAND_SCOPED_FLAGS given to any other `cmd` is reported too, with `owner` set.
|
|
447
463
|
* @param {object} flags Parsed flags from parseArgs.
|
|
448
|
-
* @
|
|
464
|
+
* @param {string} [cmd] The subcommand being run; without it scoped flags are not checked.
|
|
465
|
+
* @returns {Array<{flag: string, suggestion: string|null, owner?: string}>}
|
|
449
466
|
*/
|
|
450
|
-
export function suggestUnknownFlags(flags) {
|
|
467
|
+
export function suggestUnknownFlags(flags, cmd) {
|
|
451
468
|
const result = [];
|
|
452
469
|
for (const key of Object.keys(flags)) {
|
|
470
|
+
const owner = COMMAND_SCOPED_FLAGS.get(key);
|
|
471
|
+
if (cmd && owner && owner !== cmd) {
|
|
472
|
+
result.push({ flag: key, suggestion: null, owner });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
453
475
|
if (!key || KNOWN_CLI_FLAGS.has(key)) continue;
|
|
454
476
|
let best = null,
|
|
455
477
|
bestDist = 3;
|
|
456
478
|
for (const known of KNOWN_CLI_FLAGS) {
|
|
479
|
+
const knownOwner = COMMAND_SCOPED_FLAGS.get(known);
|
|
480
|
+
if (cmd && knownOwner && knownOwner !== cmd) continue;
|
|
457
481
|
const d = editDistance(key, known);
|
|
458
482
|
if (d < bestDist) {
|
|
459
483
|
bestDist = d;
|
package/cli/verify-apply.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
markUndone,
|
|
24
24
|
} from '../lib/verify-apply-core.mjs';
|
|
25
25
|
import { parseArgs, out, fail, rejectBareStringFlags } from './common.mjs';
|
|
26
|
+
import { shellWord } from '../cli-path.mjs';
|
|
26
27
|
|
|
27
28
|
const USAGE =
|
|
28
29
|
'[mem] Usage: claude-mem-lite verify-apply <proposals.json> [--project P] (dry run)\n' +
|
|
@@ -34,8 +35,6 @@ const BACKUP_DIR = join(DB_DIR, 'backups');
|
|
|
34
35
|
// The commands this prints must run as printed. `claude-mem-lite` is on PATH only after an
|
|
35
36
|
// optional global npm install (which may also be a different, stale code home), so name the
|
|
36
37
|
// node binary and THIS cli.mjs — the one that produced the plan.
|
|
37
|
-
/** A path as one shell word: as-is when it is plain, single-quoted otherwise. */
|
|
38
|
-
const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
39
38
|
const SELF = process.argv[1] ? `node ${shellWord(process.argv[1])}` : 'claude-mem-lite';
|
|
40
39
|
|
|
41
40
|
function readJson(path, what) {
|
package/cli-path.mjs
CHANGED
|
@@ -22,4 +22,14 @@ import { join, dirname } from 'node:path';
|
|
|
22
22
|
// module from its unused-export report entirely. Enforced by
|
|
23
23
|
// tests/no-url-module-paths.test.mjs.
|
|
24
24
|
export const CLI_PATH = join(dirname(fileURLToPath(import.meta.url)), 'cli.mjs');
|
|
25
|
-
|
|
25
|
+
/**
|
|
26
|
+
* A path as one shell word: as-is when it is plain, single-quoted otherwise. Conditional on
|
|
27
|
+
* purpose — CLI_INVOKE is LLM-visible (MCP instructions, every "Equivalent CLI" hint), so an
|
|
28
|
+
* ordinary install path must stay byte-identical; only a path with a space or a shell
|
|
29
|
+
* metacharacter gets quotes, which it needs to survive as ONE argument.
|
|
30
|
+
* @param {string} s
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
export const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
34
|
+
|
|
35
|
+
export const CLI_INVOKE = `node ${shellWord(CLI_PATH)}`;
|
package/commands/adopt.md
CHANGED
|
@@ -64,4 +64,4 @@ server builds its instructions once at boot, so the MCP-instructions trim
|
|
|
64
64
|
(or re-attaches the mem-lite MCP server). A `/exit` + fresh session is enough.
|
|
65
65
|
Same caveat applies in reverse for `/unadopt`.
|
|
66
66
|
|
|
67
|
-
!node ${CLAUDE_PLUGIN_ROOT}/cli.mjs adopt $ARGUMENTS
|
|
67
|
+
!node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" adopt $ARGUMENTS
|
package/commands/bug.md
CHANGED
|
@@ -49,7 +49,7 @@ goes in `--lesson` so it lands in the high-weight `lesson_learned` field.
|
|
|
49
49
|
body in the positional content and trim `--lesson` to the core description if
|
|
50
50
|
longer:
|
|
51
51
|
|
|
52
|
-
node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<body>" \
|
|
52
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" save "<body>" \
|
|
53
53
|
--type bugfix \
|
|
54
54
|
--title "<first 60 chars of description>" \
|
|
55
55
|
--lesson "<description, ≤500 chars>" \
|
package/commands/lesson.md
CHANGED
|
@@ -42,7 +42,7 @@ field. `--lesson` is capped at 500 chars (longer values are rejected), so for a
|
|
|
42
42
|
long lesson keep the full text in the positional content and trim `--lesson` to
|
|
43
43
|
the core insight:
|
|
44
44
|
|
|
45
|
-
node ${CLAUDE_PLUGIN_ROOT}/cli.mjs save "<full text>" \
|
|
45
|
+
node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" save "<full text>" \
|
|
46
46
|
--type discovery \
|
|
47
47
|
--title "<first 60 chars of text>" \
|
|
48
48
|
--lesson "<core insight, ≤500 chars>" \
|
package/commands/mem.md
CHANGED
|
@@ -23,16 +23,16 @@ Search and browse your project memory efficiently.
|
|
|
23
23
|
|
|
24
24
|
When the user invokes `/mem`, parse their intent:
|
|
25
25
|
|
|
26
|
-
- `/mem search <query>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
|
|
27
|
-
- `/mem recent` or `/mem recent 20` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recent [N]` via Bash
|
|
28
|
-
- `/mem recall <file>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs recall <file>` via Bash
|
|
29
|
-
- `/mem timeline <id>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs timeline --anchor <id>` via Bash
|
|
26
|
+
- `/mem search <query>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" search <query>` via Bash
|
|
27
|
+
- `/mem recent` or `/mem recent 20` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" recent [N]` via Bash
|
|
28
|
+
- `/mem recall <file>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" recall <file>` via Bash
|
|
29
|
+
- `/mem timeline <id>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" timeline --anchor <id>` via Bash
|
|
30
30
|
- `/mem save <text>` → call `mem_save` MCP tool with the text as content
|
|
31
|
-
- `/mem stats` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs stats` via Bash
|
|
32
|
-
- `/mem get <ids>` → run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <ids>` via Bash
|
|
31
|
+
- `/mem stats` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" stats` via Bash
|
|
32
|
+
- `/mem get <ids>` → run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <ids>` via Bash
|
|
33
33
|
- `/mem cleanup` → run `mem_maintain(action="scan")`, report pending purge count and stale items to user, ask for confirmation, then run `mem_maintain(action="execute", operations=["purge_stale"], confirm=true)` if confirmed. **`confirm=true` is required and is not optional politeness:** without it the call returns a dry-run PREVIEW and deletes nothing, while still succeeding — so you would report a cleanup that never happened.
|
|
34
34
|
- `/mem cleanup Nd` (e.g. `60d`) → same as above but add `retain_days=N` to only purge items older than N days. **`retain_days` must be between 7 and 365**; anything outside that range is rejected by the schema, so `/mem cleanup 3d` cannot be honoured — say so rather than silently substituting the default.
|
|
35
35
|
- `/mem cleanup keep Nd` (e.g. `keep 14d`) → same as above with `retain_days=N`, same 7–365 range.
|
|
36
|
-
- `/mem <query>` (no subcommand) → treat as search, run `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs search <query>` via Bash
|
|
36
|
+
- `/mem <query>` (no subcommand) → treat as search, run `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" search <query>` via Bash
|
|
37
37
|
|
|
38
|
-
Use Bash commands first. For detailed data, use `node ${CLAUDE_PLUGIN_ROOT}/cli.mjs get <id>` via Bash.
|
|
38
|
+
Use Bash commands first. For detailed data, use `node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" get <id>` via Bash.
|
package/commands/unadopt.md
CHANGED
|
@@ -43,4 +43,4 @@ Once unadopted, the conservative hook layer (SessionStart `File Lessons` /
|
|
|
43
43
|
`Key Context`, MCP instructions `WHEN TO USE`) returns to verbose mode on the
|
|
44
44
|
next session start.
|
|
45
45
|
|
|
46
|
-
!node ${CLAUDE_PLUGIN_ROOT}/cli.mjs unadopt $ARGUMENTS
|
|
46
|
+
!node "${CLAUDE_PLUGIN_ROOT}/cli.mjs" unadopt $ARGUMENTS
|
package/hook-handoff.mjs
CHANGED
|
@@ -386,18 +386,28 @@ export function buildAndSaveHandoff(db, sessionId, project, type, episodeSnapsho
|
|
|
386
386
|
// (session_summaries.key_decisions is a different column, a JSON array from Haiku), and
|
|
387
387
|
// every existing assertion on it is a substring/regex match on the title, so the added
|
|
388
388
|
// prefix is not a contract change for them.
|
|
389
|
-
|
|
389
|
+
//
|
|
390
|
+
// The cap applies AFTER the low-signal filter (D#40). This read was `LIMIT 10` → filter →
|
|
391
|
+
// slice(0, 5), which made the SQL limit a reachability bound: six low-signal rows among the
|
|
392
|
+
// newest ten evicted older real decisions the section had room for, and two writers raise
|
|
393
|
+
// importance without reading the title, so such rows do reach this pool. Streaming and
|
|
394
|
+
// stopping at five keeps the JS filter exactly as it was (the SQL NOT LIKE twin is
|
|
395
|
+
// case-insensitive, so swapping it in would also change WHICH titles count as noise).
|
|
396
|
+
const decisions = [];
|
|
397
|
+
for (const d of db
|
|
390
398
|
.prepare(
|
|
391
399
|
`
|
|
392
400
|
SELECT title, type FROM observations
|
|
393
401
|
WHERE (memory_session_id = ? OR project = ?) AND COALESCE(importance, 1) >= 2
|
|
394
402
|
AND ${liveObsFilterSql('')} ${obsWindowClause}
|
|
395
|
-
ORDER BY created_at_epoch DESC
|
|
403
|
+
ORDER BY created_at_epoch DESC, id DESC
|
|
396
404
|
`,
|
|
397
405
|
)
|
|
398
|
-
.
|
|
399
|
-
|
|
400
|
-
.
|
|
406
|
+
.iterate(sessionId, project, ...obsWindowParams)) {
|
|
407
|
+
if (!d.title || LOW_SIGNAL_TITLE.test(d.title)) continue;
|
|
408
|
+
decisions.push(d);
|
|
409
|
+
if (decisions.length === 5) break;
|
|
410
|
+
}
|
|
401
411
|
|
|
402
412
|
// 5b. Next steps — the remaining work a paused note spells out, which is the only
|
|
403
413
|
// next-step source in this system that a human wrote down on purpose. Deliberately not
|
package/install.mjs
CHANGED
|
@@ -97,6 +97,7 @@ import { clearNativeBindingBreakage, readNativeBindingBreakage } from './lib/nat
|
|
|
97
97
|
import { sweepStaleTestFixtures } from './lib/tmp-fixture-sweep.mjs';
|
|
98
98
|
import { acquireLock } from './lib/proc-lock.mjs';
|
|
99
99
|
import { atomicWriteFileSync } from './lib/atomic-write.mjs';
|
|
100
|
+
import { shellWord } from './cli-path.mjs';
|
|
100
101
|
import { isMemHook, launcherEntryPath } from './lib/hook-prune.mjs';
|
|
101
102
|
|
|
102
103
|
// Re-export for backward compatibility — tests/install-hook-scripts.test.mjs
|
|
@@ -272,7 +273,7 @@ export function hookManifestRepairHint(cacheRoot, marketplaceRoot) {
|
|
|
272
273
|
const src = join(marketplaceRoot, 'hooks', 'hooks.json');
|
|
273
274
|
const dst = join(cacheRoot, 'hooks', 'hooks.json');
|
|
274
275
|
return pluginCacheHookEvents(marketplaceRoot).ok
|
|
275
|
-
? `cp
|
|
276
|
+
? `cp ${shellWord(src)} ${shellWord(dst)} && restart Claude Code`
|
|
276
277
|
: `no usable marketplace copy to restore from — reinstall the plugin (/plugin uninstall then /plugin install), then restart Claude Code`;
|
|
277
278
|
}
|
|
278
279
|
|
|
@@ -723,7 +724,7 @@ function registerMcpServer() {
|
|
|
723
724
|
ok('MCP server registered: mem-lite');
|
|
724
725
|
} catch (e) {
|
|
725
726
|
fail('MCP registration failed: ' + e.message);
|
|
726
|
-
warn(
|
|
727
|
+
warn(`Try manually: claude mcp add -s user -t stdio mem-lite -- node ${shellWord(SERVER_PATH)}`);
|
|
727
728
|
}
|
|
728
729
|
}
|
|
729
730
|
}
|
|
@@ -1674,7 +1675,7 @@ async function status() {
|
|
|
1674
1675
|
push(
|
|
1675
1676
|
'warn',
|
|
1676
1677
|
'cli',
|
|
1677
|
-
`CLI: installed at ${join(binDir, 'claude-mem-lite')} but ${binDir} is not on PATH — add it: export PATH
|
|
1678
|
+
`CLI: installed at ${join(binDir, 'claude-mem-lite')} but ${binDir} is not on PATH — add it: export PATH=${shellWord(binDir)}:"$PATH"`,
|
|
1678
1679
|
{ available: false, linked: join(binDir, 'claude-mem-lite') },
|
|
1679
1680
|
);
|
|
1680
1681
|
} else {
|
|
@@ -1754,8 +1755,9 @@ async function doctor() {
|
|
|
1754
1755
|
return;
|
|
1755
1756
|
}
|
|
1756
1757
|
const last = checks[checks.length - 1];
|
|
1757
|
-
//
|
|
1758
|
-
//
|
|
1758
|
+
// Unreachable today: the Node-version check at the top of doctor() always records a
|
|
1759
|
+
// check before the first log(). Kept as a guard — a detail with no check to attach to is
|
|
1760
|
+
// dropped under --json, where the human face would have printed it.
|
|
1759
1761
|
if (!last) return;
|
|
1760
1762
|
(last.details ??= []).push(msg.trim());
|
|
1761
1763
|
};
|
|
@@ -2004,7 +2006,7 @@ async function doctor() {
|
|
|
2004
2006
|
// misbehaving, which is the worst moment to hand out the one entry that cannot
|
|
2005
2007
|
// survive a missing module. Pre-ship review of v6.7.0 caught this one left behind.
|
|
2006
2008
|
dwarn(
|
|
2007
|
-
`Hook self-heal: a recent hook fire degraded to exit-0${detail} — run \`node ${join(PROJECT_DIR, 'cli.mjs')} repair\``,
|
|
2009
|
+
`Hook self-heal: a recent hook fire degraded to exit-0${detail} — run \`node ${shellWord(join(PROJECT_DIR, 'cli.mjs'))} repair\``,
|
|
2008
2010
|
);
|
|
2009
2011
|
} else {
|
|
2010
2012
|
ok('Hook self-heal: no recent silent hook breakage');
|
|
@@ -2020,7 +2022,7 @@ async function doctor() {
|
|
|
2020
2022
|
// should not pay for another round of child spawns to ask it twice.
|
|
2021
2023
|
if (brokenRoots.length > 0) {
|
|
2022
2024
|
fail(
|
|
2023
|
-
`Native DB binding: unusable in ${brokenRoots.map((b) => b.label).join(', ')} — run \`node ${join(PROJECT_DIR, 'cli.mjs')} rebuild-binding\` (repairs every broken install, not just this one)`,
|
|
2025
|
+
`Native DB binding: unusable in ${brokenRoots.map((b) => b.label).join(', ')} — run \`node ${shellWord(join(PROJECT_DIR, 'cli.mjs'))} rebuild-binding\` (repairs every broken install, not just this one)`,
|
|
2024
2026
|
);
|
|
2025
2027
|
issues++;
|
|
2026
2028
|
} else if (breakage) {
|
|
@@ -2136,7 +2138,9 @@ async function doctor() {
|
|
|
2136
2138
|
);
|
|
2137
2139
|
for (const p of orphanPaths.slice(0, 5)) log(` missing: ${p}`);
|
|
2138
2140
|
if (orphanPaths.length > 5) log(` ... +${orphanPaths.length - 5} more`);
|
|
2139
|
-
log(
|
|
2141
|
+
log(
|
|
2142
|
+
` Repair: node ${shellWord(join(PROJECT_DIR, 'install.mjs'))} uninstall # removes the dead hook entries`,
|
|
2143
|
+
);
|
|
2140
2144
|
issues++;
|
|
2141
2145
|
} else if (hasHooks) {
|
|
2142
2146
|
ok('Orphan hooks: none (all hook targets present)');
|
|
@@ -2406,12 +2410,12 @@ async function doctor() {
|
|
|
2406
2410
|
// already there.
|
|
2407
2411
|
const noCodeInstall =
|
|
2408
2412
|
!shape.managed && !shape.activePluginVersion && !hasAnyManagedCode(INSTALL_DIR, SOURCE_FILES);
|
|
2409
|
-
const installRemedy = `node ${join(PROJECT_DIR, 'install.mjs')} install`;
|
|
2413
|
+
const installRemedy = `node ${shellWord(join(PROJECT_DIR, 'install.mjs'))} install`;
|
|
2410
2414
|
try {
|
|
2411
2415
|
const skipDrift = !shape.managed && !!shape.activePluginVersion;
|
|
2412
2416
|
const { checkDevDrift } = await import('./lib/doctor-drift.mjs');
|
|
2413
2417
|
const r = skipDrift ? null : checkDevDrift(INSTALL_DIR, SOURCE_FILES);
|
|
2414
|
-
const devRemedy = `re-run: node ${join(PROJECT_DIR, 'install.mjs')} install --dev`;
|
|
2418
|
+
const devRemedy = `re-run: node ${shellWord(join(PROJECT_DIR, 'install.mjs'))} install --dev`;
|
|
2415
2419
|
const nameList = (files, count) => {
|
|
2416
2420
|
const suffix = count > files.length ? ` +${count - files.length} more` : '';
|
|
2417
2421
|
return `${files.join(', ')}${suffix}`;
|
|
@@ -2483,7 +2487,7 @@ async function doctor() {
|
|
|
2483
2487
|
`a damaged one. Fix: ${installRemedy}`
|
|
2484
2488
|
: `Managed files: ${r.missingCount} missing (${parts.join('; ')}) — a copy install resolves ` +
|
|
2485
2489
|
`imports against the install dir, so these throw at hook time. Fix: claude-mem-lite self-update ` +
|
|
2486
|
-
`(or: node ${join(INSTALL_DIR, 'cli.mjs')} repair)`,
|
|
2490
|
+
`(or: node ${shellWord(join(INSTALL_DIR, 'cli.mjs'))} repair)`,
|
|
2487
2491
|
);
|
|
2488
2492
|
}
|
|
2489
2493
|
// Complete copy install: no message — drift is a dev-install concern.
|
|
@@ -2511,7 +2515,7 @@ async function doctor() {
|
|
|
2511
2515
|
// `noCodeInstall` above: the `repair` route runs from an entry point that is itself absent.
|
|
2512
2516
|
const scriptRemedy = noCodeInstall
|
|
2513
2517
|
? installRemedy
|
|
2514
|
-
: `claude-mem-lite self-update (or: node ${join(INSTALL_DIR, 'cli.mjs')} repair)`;
|
|
2518
|
+
: `claude-mem-lite self-update (or: node ${shellWord(join(INSTALL_DIR, 'cli.mjs'))} repair)`;
|
|
2515
2519
|
if (skipScripts) {
|
|
2516
2520
|
ok('Hook scripts: n/a (plugin-only install — hooks run from the plugin cache)');
|
|
2517
2521
|
} else if (!h.present) {
|
package/lib/binding-probe.mjs
CHANGED
|
@@ -10,6 +10,7 @@ import { execSync, spawnSync } from 'node:child_process';
|
|
|
10
10
|
import { existsSync, renameSync } from 'node:fs';
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { join } from 'node:path';
|
|
13
|
+
import { shellWord } from '../cli-path.mjs';
|
|
13
14
|
|
|
14
15
|
// npm >= 12 blocks lifecycle scripts by default, so a plain `npm rebuild` exits 0
|
|
15
16
|
// WITHOUT compiling — see the rebuild() comment below. Step 1 of the heal chain, and
|
|
@@ -57,9 +58,10 @@ export const NATIVE_BINDING_SOURCE_BUILD_CMD = 'npm run --prefix node_modules/be
|
|
|
57
58
|
* @returns {string}
|
|
58
59
|
*/
|
|
59
60
|
export function nativeBindingRepairHint(root) {
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
|
|
61
|
+
// shellWord, not "…": INSTALL_DIR / a plugin-cache root can contain spaces (and, under
|
|
62
|
+
// double quotes, a `$` or backtick still expands), and a `cd` that splits hands the user a
|
|
63
|
+
// command that fails on exactly the machines least able to debug it.
|
|
64
|
+
const npmPair = `cd ${shellWord(root)} && ${NATIVE_BINDING_REBUILD_CMD} && ${NATIVE_BINDING_SOURCE_BUILD_CMD}`;
|
|
63
65
|
// The pair above heals a platform better-sqlite3 ships NO prebuild for. It cannot heal a
|
|
64
66
|
// prebuild that is present and will not load — 13 selects `prebuilds/<target>.node` on
|
|
65
67
|
// existence alone and prefers it over `build/`, so the addon those two commands compile
|
|
@@ -68,7 +70,7 @@ export function nativeBindingRepairHint(root) {
|
|
|
68
70
|
// runs the first command they are given and stops looking when it reports success.
|
|
69
71
|
const cli = join(root, 'cli.mjs');
|
|
70
72
|
if (!existsSync(cli)) return npmPair;
|
|
71
|
-
return `node
|
|
73
|
+
return `node ${shellWord(cli)} rebuild-binding (or, without the CLI: ${npmPair})`;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
// Set on a re-exec'd child so one failed heal cannot fork-bomb the CLI.
|
package/lib/db-unusable.mjs
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import { basename, dirname } from 'node:path';
|
|
34
34
|
import { readSnapshots } from './db-backup.mjs';
|
|
35
|
+
import { shellWord } from '../cli-path.mjs';
|
|
35
36
|
|
|
36
37
|
/** Marker prefix for lib/record-once.mjs. Per project, like the skew one. */
|
|
37
38
|
export const DB_UNUSABLE_MARKER_PREFIX = '.db-unusable-logged-';
|
|
@@ -140,7 +141,7 @@ export function isDbUnusableError(err) {
|
|
|
140
141
|
* @returns {{kind: 'restore'|'set-aside'|'unknown', command: string, note: string, snapshotCount: number}}
|
|
141
142
|
*/
|
|
142
143
|
export function dbUnusableRemedy(dbPath) {
|
|
143
|
-
const clear = `rm -f
|
|
144
|
+
const clear = `rm -f ${shellWord(`${dbPath}-wal`)} ${shellWord(`${dbPath}-shm`)}`;
|
|
144
145
|
const snap = readSnapshots(dbPath);
|
|
145
146
|
if (!snap.ok) {
|
|
146
147
|
return {
|
|
@@ -155,7 +156,7 @@ export function dbUnusableRemedy(dbPath) {
|
|
|
155
156
|
if (snap.snapshots.length === 0) {
|
|
156
157
|
return {
|
|
157
158
|
kind: 'set-aside',
|
|
158
|
-
command: `${clear} && mv
|
|
159
|
+
command: `${clear} && mv ${shellWord(dbPath)} ${shellWord(`${dbPath}.corrupt`)}`,
|
|
159
160
|
snapshotCount: 0,
|
|
160
161
|
note:
|
|
161
162
|
'No backup snapshot exists beside the database. That command sets the broken file ' +
|
|
@@ -170,7 +171,7 @@ export function dbUnusableRemedy(dbPath) {
|
|
|
170
171
|
.sort((a, b) => b.mtimeMs - a.mtimeMs || (a.path < b.path ? 1 : -1))[0];
|
|
171
172
|
return {
|
|
172
173
|
kind: 'restore',
|
|
173
|
-
command: `${clear} && cp
|
|
174
|
+
command: `${clear} && cp ${shellWord(newest.path)} ${shellWord(dbPath)}`,
|
|
174
175
|
snapshotCount: snap.snapshots.length,
|
|
175
176
|
note:
|
|
176
177
|
`Restores the newest of ${snap.snapshots.length} backup snapshot(s). Move the broken ` +
|
package/lib/import-jsonl.mjs
CHANGED
|
@@ -45,18 +45,25 @@ const TOOL_TO_TYPE = {
|
|
|
45
45
|
* something the scrubber rewrites (a bearer token in a `Bash` command, a path segment shaped
|
|
46
46
|
* like a key) never matched itself and re-imported on every run (R12 pre-ship review P3-4).
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* idempotent
|
|
52
|
-
* three runs
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
48
|
+
* One scrub per side, and it happens HERE, so both sites get the same string by
|
|
49
|
+
* construction and neither may wrap this call in another scrub. An earlier cut scrubbed
|
|
50
|
+
* here AND left the storage side's `scrubRecord` scrubbing `title` again — equal only while
|
|
51
|
+
* `scrubSecrets` is idempotent, which it was not until D#52 (`Bash: deploy --token ghp_…
|
|
52
|
+
* secret: v` imported three rows for three runs). The fix then returned the RAW title for
|
|
53
|
+
* both sites to scrub (D#46).
|
|
54
|
+
*
|
|
55
|
+
* Scrub FIRST, cut second (D#47 P3-8). Cutting first handed the scrubber a token the cut
|
|
56
|
+
* had split, too short for its own pattern (`ghp_` needs 30+ characters), and the prefix
|
|
57
|
+
* was stored in plaintext — up to 29 of a GitHub token's 36 secret characters, at 29 of 41
|
|
58
|
+
* cut positions. The whole title goes through the scrubber; the cap counts from the end of
|
|
59
|
+
* the `<tool>: ` prefix.
|
|
56
60
|
*/
|
|
61
|
+
const IMPORTED_TITLE_DETAIL_MAX = 80;
|
|
57
62
|
function importedObsTitle(toolUse) {
|
|
58
63
|
const detail = toolUse?.input?.command || toolEditPath(toolUse?.input) || '';
|
|
59
|
-
|
|
64
|
+
const prefix = `${toolUse?.name || 'unknown'}: `;
|
|
65
|
+
const scrubbed = scrubRecord('observations', { title: `${prefix}${detail}` }).title;
|
|
66
|
+
return scrubbed.slice(0, prefix.length + IMPORTED_TITLE_DETAIL_MAX);
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
function dedupKey(parts) {
|
|
@@ -113,7 +120,9 @@ function importPrompt(db, ev, project, seenPrompts) {
|
|
|
113
120
|
if (text.startsWith('<task-notification>')) return false;
|
|
114
121
|
const sessionId = ev.sessionId || 'imported';
|
|
115
122
|
const ts = ev.timestamp || new Date().toISOString();
|
|
116
|
-
|
|
123
|
+
// Scrub, THEN cap: cutting first handed the scrubber a token the cap had split, too short
|
|
124
|
+
// for its own pattern, and stored the prefix (same order bug as importedObsTitle).
|
|
125
|
+
const safe = scrubSecrets(text).slice(0, 10000);
|
|
117
126
|
// Dedup key uses the scrubbed text so a re-run computes the same key as the
|
|
118
127
|
// first run (which persisted the scrubbed text). Keying on raw input would
|
|
119
128
|
// make idempotency fragile if the scrub policy changes.
|
|
@@ -146,14 +155,16 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
146
155
|
|
|
147
156
|
const toolName = toolUse.name || 'unknown';
|
|
148
157
|
const type = TOOL_TO_TYPE[toolName] || 'change';
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
// Each part is scrubbed BEFORE its 4000-character cap, for the reason importedObsTitle
|
|
159
|
+
// gives: a cap that splits a token leaves a prefix too short for the scrubber's pattern.
|
|
160
|
+
// The body is therefore kept out of the scrubRecord call below, so it is scrubbed once.
|
|
161
|
+
const inputJson = scrubSecrets(
|
|
162
|
+
typeof toolUse.input === 'object' ? JSON.stringify(toolUse.input) : String(toolUse.input ?? ''),
|
|
163
|
+
).slice(0, 4000);
|
|
153
164
|
const resultText =
|
|
154
165
|
typeof toolResult?.content === 'string'
|
|
155
|
-
? toolResult.content
|
|
156
|
-
: JSON.stringify(toolResult?.content ?? '').slice(0, 4000);
|
|
166
|
+
? scrubSecrets(toolResult.content)
|
|
167
|
+
: scrubSecrets(JSON.stringify(toolResult?.content ?? '')).slice(0, 4000);
|
|
157
168
|
|
|
158
169
|
// D#35: this gated on `input.file_path` alone, and `NotebookEdit` carries
|
|
159
170
|
// `notebook_path` and never `file_path` — so the branch named NotebookEdit
|
|
@@ -184,8 +195,18 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
184
195
|
// both: `narrative` as the durable home, `text` as the index copy the ingest paths
|
|
185
196
|
// write directly.
|
|
186
197
|
const body = `${inputJson}\n---\n${resultText}`;
|
|
187
|
-
const safe =
|
|
188
|
-
|
|
198
|
+
const safe = {
|
|
199
|
+
...scrubRecord('observations', {
|
|
200
|
+
subtitle: '',
|
|
201
|
+
concepts: '',
|
|
202
|
+
facts: '',
|
|
203
|
+
lesson_learned: null,
|
|
204
|
+
search_aliases: null,
|
|
205
|
+
}),
|
|
206
|
+
// Outside scrubRecord on purpose: body and title were each already scrubbed, once.
|
|
207
|
+
text: body,
|
|
208
|
+
narrative: body,
|
|
209
|
+
// The title: this string IS the cross-run dedup key — `importedObsTitle` is the single home for
|
|
189
210
|
// it, because the two sites that must agree byte-for-byte did not. An earlier cut of
|
|
190
211
|
// this round kept the title on `file_path` alone to avoid the migration — and shipped
|
|
191
212
|
// `NotebookEdit: ` with an empty label for exactly the rows D#35 had just made
|
|
@@ -196,14 +217,7 @@ function importToolPair(db, toolUse, toolResult, project) {
|
|
|
196
217
|
// carry the old key, so the next import re-adds each of them ONCE and then
|
|
197
218
|
// matches forever. Stated in the CHANGELOG rather than absorbed silently.
|
|
198
219
|
title: importedObsTitle(toolUse),
|
|
199
|
-
|
|
200
|
-
text: body,
|
|
201
|
-
narrative: body,
|
|
202
|
-
concepts: '',
|
|
203
|
-
facts: '',
|
|
204
|
-
lesson_learned: null,
|
|
205
|
-
search_aliases: null,
|
|
206
|
-
});
|
|
220
|
+
};
|
|
207
221
|
|
|
208
222
|
const inserted = db
|
|
209
223
|
.prepare(
|
|
@@ -326,9 +340,9 @@ export async function importJsonl(db, path, { project }) {
|
|
|
326
340
|
seenToolUseIds.add(callKey);
|
|
327
341
|
|
|
328
342
|
// Cross-call dedup: synthesize the title the previous run would have written and check
|
|
329
|
-
// the seenObs set seeded from the DB.
|
|
330
|
-
//
|
|
331
|
-
const titlePreview =
|
|
343
|
+
// the seenObs set seeded from the DB. The same function as the storage site, scrub and
|
|
344
|
+
// cut included, so the scrubber cannot rewrite one and not the other.
|
|
345
|
+
const titlePreview = importedObsTitle(useEv);
|
|
332
346
|
const ts = useEv.timestamp || new Date().toISOString();
|
|
333
347
|
// Match the storage convention from importToolPair (memId-prefixed) so
|
|
334
348
|
// the seenObs entries seeded from the DB can be matched on a re-run.
|
package/lib/install-shape.mjs
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { existsSync, readdirSync, realpathSync, readFileSync } from 'node:fs';
|
|
34
34
|
import { join } from 'node:path';
|
|
35
35
|
import { homedir } from 'node:os';
|
|
36
|
+
import { shellWord } from '../cli-path.mjs';
|
|
36
37
|
|
|
37
38
|
import {
|
|
38
39
|
probeBindingInFreshProcess,
|
|
@@ -324,7 +325,7 @@ export function probeRuntimeRoots(roots, deps = {}) {
|
|
|
324
325
|
// "none resolvable from a parent directory" and was caught printing that
|
|
325
326
|
// next to a probe error naming the parent tree it had just loaded from.
|
|
326
327
|
error: `${error} — and this install owns no node_modules/better-sqlite3, so there is nothing here to rebuild`,
|
|
327
|
-
repair: `cd ${root} && npm install --omit=dev`,
|
|
328
|
+
repair: `cd ${shellWord(root)} && npm install --omit=dev`,
|
|
328
329
|
};
|
|
329
330
|
});
|
|
330
331
|
}
|
|
@@ -25,6 +25,7 @@ import { join, dirname } from 'node:path';
|
|
|
25
25
|
import { readFileSync, writeFileSync, mkdirSync, renameSync, unlinkSync } from 'node:fs';
|
|
26
26
|
import { fileURLToPath } from 'node:url';
|
|
27
27
|
import { isNativeBindingError, flattenBindingError } from './binding-probe.mjs';
|
|
28
|
+
import { shellWord } from '../cli-path.mjs';
|
|
28
29
|
|
|
29
30
|
export const NATIVE_BINDING_HINT_COOLDOWN_MS = 6 * 60 * 60 * 1000; // 6h
|
|
30
31
|
const MARKER_NAME = 'native-binding-hint-last';
|
|
@@ -45,7 +46,7 @@ export const NATIVE_BINDING_BROKEN_MARKER = 'native-binding-broken';
|
|
|
45
46
|
// D#207: join(), not `new URL('../cli.mjs', …)` — that form makes knip drop the named
|
|
46
47
|
// module out of its unused-export report. cli.mjs is a knip entry point so nothing was
|
|
47
48
|
// lost here, but the rule is enforced for the class rather than per-file.
|
|
48
|
-
const CLI_REBUILD_BINDING = `node ${join(dirname(fileURLToPath(import.meta.url)), '..', 'cli.mjs')} rebuild-binding`;
|
|
49
|
+
const CLI_REBUILD_BINDING = `node ${shellWord(join(dirname(fileURLToPath(import.meta.url)), '..', 'cli.mjs'))} rebuild-binding`;
|
|
49
50
|
|
|
50
51
|
// Stable-ish identity of a fault so DISTINCT failures get DISTINCT cooldown
|
|
51
52
|
// windows: the same fault → same key (suppressed within the window), a different
|
package/mem-cli.mjs
CHANGED
|
@@ -3692,11 +3692,13 @@ async function runDispatch(argv) {
|
|
|
3692
3692
|
// project — a misspelled flag changed results with zero signal. Warn (stderr, non-fatal)
|
|
3693
3693
|
// when a flag looks like a misspelling of a real one; stdout + exit code stay untouched,
|
|
3694
3694
|
// so JSON/text consumers are unaffected. Mirrors the unknown-COMMAND suggester in cli.mjs.
|
|
3695
|
-
for (const { flag, suggestion } of suggestUnknownFlags(parseArgs(cmdArgs).flags)) {
|
|
3695
|
+
for (const { flag, suggestion, owner } of suggestUnknownFlags(parseArgs(cmdArgs).flags, cmd)) {
|
|
3696
3696
|
process.stderr.write(
|
|
3697
|
-
|
|
3698
|
-
? `[mem]
|
|
3699
|
-
:
|
|
3697
|
+
owner
|
|
3698
|
+
? `[mem] --${flag} is read only by ${owner}; ${cmd} does not read it.\n`
|
|
3699
|
+
: suggestion
|
|
3700
|
+
? `[mem] Unknown flag --${flag}; did you mean --${suggestion}?\n`
|
|
3701
|
+
: `[mem] Unknown flag --${flag} — ignored, it had no effect. Run "claude-mem-lite help" for this command's flags.\n`,
|
|
3700
3702
|
);
|
|
3701
3703
|
}
|
|
3702
3704
|
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.12.
|
|
3
|
+
"version": "6.12.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "claude-mem-lite",
|
|
9
|
-
"version": "6.12.
|
|
9
|
+
"version": "6.12.2",
|
|
10
10
|
"os": [
|
|
11
11
|
"darwin",
|
|
12
12
|
"linux",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-mem-lite",
|
|
3
|
-
"version": "6.12.
|
|
3
|
+
"version": "6.12.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",
|
|
@@ -125,7 +125,10 @@ const NB_MANUAL_CMD =
|
|
|
125
125
|
// INSTALL_DIR (import.meta.url) so it works on a plugin-only install, where
|
|
126
126
|
// bare `claude-mem-lite` is not on PATH and ~/.claude-mem-lite/ holds no source.
|
|
127
127
|
// cli.mjs routes `repair` → install.mjs. (review #3)
|
|
128
|
-
|
|
128
|
+
// Quote a path as ONE shell word for a printed command. A copy of cli-path.mjs's, not an
|
|
129
|
+
// import (node: builtins only here); tests/cli-path-invocation.test.mjs keeps them equal.
|
|
130
|
+
const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
131
|
+
const CLI_REPAIR = `node ${shellWord(join(INSTALL_DIR, 'cli.mjs'))} repair`;
|
|
129
132
|
|
|
130
133
|
// Last-resort recovery string for users whose `cli.mjs repair` path
|
|
131
134
|
// itself failed (install.mjs missing / repair errored / retry still drifting).
|
|
@@ -490,7 +493,7 @@ function healNativeBindingIfBroken() {
|
|
|
490
493
|
const installer = join(INSTALL_DIR, 'install.mjs');
|
|
491
494
|
if (!existsSync(installer)) {
|
|
492
495
|
process.stderr.write(
|
|
493
|
-
`[claude-mem-lite] native DB binding unusable and install.mjs is missing — run: cd
|
|
496
|
+
`[claude-mem-lite] native DB binding unusable and install.mjs is missing — run: cd ${shellWord(INSTALL_DIR)} && ${NB_MANUAL_CMD}\n`,
|
|
494
497
|
);
|
|
495
498
|
return;
|
|
496
499
|
}
|
package/scripts/launch.mjs
CHANGED
|
@@ -9,6 +9,9 @@ import { homedir } from 'node:os';
|
|
|
9
9
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
const ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(__dirname, '..');
|
|
12
|
+
// Quote a path as ONE shell word for a printed command. A copy of cli-path.mjs's, not an
|
|
13
|
+
// import (node: builtins only here); tests/cli-path-invocation.test.mjs keeps them equal.
|
|
14
|
+
const shellWord = (s) => (/^[\w@%+=:,./-]+$/.test(s) ? s : `'${s.replace(/'/g, `'\\''`)}'`);
|
|
12
15
|
|
|
13
16
|
if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
14
17
|
// Platform gate BEFORE npm, not after it. package.json's `os` field is an npm install
|
|
@@ -34,7 +37,7 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
|
34
37
|
`[claude-mem-lite] Nothing was installed, so the MCP server cannot start. See "Platform Support" in the README.\n`,
|
|
35
38
|
);
|
|
36
39
|
process.stderr.write(
|
|
37
|
-
`[claude-mem-lite] To install anyway: cd
|
|
40
|
+
`[claude-mem-lite] To install anyway: cd ${shellWord(ROOT)} && npm install --omit=dev --force\n`,
|
|
38
41
|
);
|
|
39
42
|
process.exit(1);
|
|
40
43
|
}
|
|
@@ -93,7 +96,7 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
|
|
|
93
96
|
process.stderr.write(
|
|
94
97
|
`[claude-mem-lite] npm printed its own error above — read its "npm error code" line first. Common causes: read-only directory, disk full, network blocked.\n`,
|
|
95
98
|
);
|
|
96
|
-
process.stderr.write(`[claude-mem-lite] Repair: cd
|
|
99
|
+
process.stderr.write(`[claude-mem-lite] Repair: cd ${shellWord(ROOT)} && npm install --omit=dev\n`);
|
|
97
100
|
process.exit(1);
|
|
98
101
|
}
|
|
99
102
|
}
|
|
@@ -648,7 +648,8 @@ try {
|
|
|
648
648
|
ORDER BY
|
|
649
649
|
CASE WHEN o.lesson_learned IS NOT NULL AND o.lesson_learned != '' THEN 0 ELSE 1 END,
|
|
650
650
|
${citeFactorClause('o')} DESC,
|
|
651
|
-
o.created_at_epoch DESC
|
|
651
|
+
o.created_at_epoch DESC,
|
|
652
|
+
o.id DESC
|
|
652
653
|
LIMIT ${obsLimit}
|
|
653
654
|
`,
|
|
654
655
|
)
|
|
@@ -691,7 +692,8 @@ try {
|
|
|
691
692
|
${eventsBodyFilter}
|
|
692
693
|
ORDER BY
|
|
693
694
|
CASE WHEN body IS NOT NULL AND body != '' THEN 0 ELSE 1 END,
|
|
694
|
-
created_at_epoch DESC
|
|
695
|
+
created_at_epoch DESC,
|
|
696
|
+
id DESC
|
|
695
697
|
LIMIT ${eventsLimit}
|
|
696
698
|
`,
|
|
697
699
|
)
|
package/server.mjs
CHANGED
|
@@ -75,7 +75,7 @@ import { buildLessonNudge } from './lib/save-nudge.mjs';
|
|
|
75
75
|
import { formatObsFieldValue, obsFieldLabel, formatPendingPurgeLine } from './cli/common.mjs';
|
|
76
76
|
// The partial-export warning points the caller at the CLI twin, which exports the complete
|
|
77
77
|
// set by default — the invocation has to be the one that actually works on this install.
|
|
78
|
-
import { CLI_INVOKE } from './cli-path.mjs';
|
|
78
|
+
import { CLI_INVOKE, shellWord } from './cli-path.mjs';
|
|
79
79
|
import { neutralizeContextDelimiters, neutralizeSkillDelimiters, queryLabel } from './format-utils.mjs';
|
|
80
80
|
import {
|
|
81
81
|
memSearchSchema,
|
|
@@ -214,7 +214,7 @@ try {
|
|
|
214
214
|
console.error(`[claude-mem-lite] FATAL: Database cannot be opened: ${err.message}`);
|
|
215
215
|
if (err.walRecoveryAttempted) {
|
|
216
216
|
console.error(
|
|
217
|
-
`[claude-mem-lite] Try: rm
|
|
217
|
+
`[claude-mem-lite] Try: rm ${shellWord(`${DB_PATH}-wal`)} ${shellWord(`${DB_PATH}-shm`)} or reinstall with: node install.mjs install`,
|
|
218
218
|
);
|
|
219
219
|
} else {
|
|
220
220
|
console.error(
|