claude-mem-lite 6.1.0 → 6.2.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.
@@ -10,7 +10,7 @@
10
10
  "plugins": [
11
11
  {
12
12
  "name": "claude-mem-lite",
13
- "version": "6.1.0",
13
+ "version": "6.2.0",
14
14
  "source": "./",
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)."
16
16
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
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
@@ -134,16 +134,23 @@ How claude-mem-lite differs from the major neighbors in the LLM-memory space (ve
134
134
 
135
135
  | Platform | Status | Notes |
136
136
  |----------|--------|-------|
137
- | **Linux** | Supported | Primary development and testing platform |
137
+ | **Linux** | Supported | Primary development and testing platform; the whole CI matrix runs here |
138
138
  | **macOS** | Supported | Fully compatible (Intel and Apple Silicon) |
139
- | **Windows** | Not supported | Uses POSIX shell scripts (`post-tool-use.sh`, `setup.sh`) and Unix file locking; WSL2 may work but is untested |
139
+ | **Windows** | Installs, not CI-covered | The MCP server, the CLI and the `node` hooks work (`better-sqlite3` ships `win32-x64` and `win32-arm64` prebuilds, so nothing is compiled). **Three hook commands run under `bash`** — `setup.sh`, `post-tool-use.sh`, `pre-agent-inject.sh` and need Git for Windows or WSL on `PATH`; `claude-mem-lite doctor` reports it when `bash` cannot be found. No GitHub Actions runner exercises Windows, so this rests on user reports ([#28](https://github.com/sdsrss/claude-mem-lite/issues/28)), not on a green pipeline |
140
+ | **WSL2** | Untested | Linux under the hood, so it should behave as the Linux row; nobody has reported either way |
141
+
142
+ From v5.1.0 through v6.1.0, `package.json` declared `os: ["darwin", "linux"]`. That is an npm *install*
143
+ gate, not a runtime check: on Windows it made `npm install` exit `EBADPLATFORM`, which the
144
+ plugin launcher runs on the first MCP start after every plugin update — so the server never
145
+ came up and `/mcp` reported `CONNECTION_CLOSED`. `win32` is now in the list. A platform that
146
+ is still outside it gets a message naming both sides of the mismatch instead of a guess.
140
147
 
141
148
  ## Requirements
142
149
 
143
150
  - **Node.js** >= 22
144
151
  - **Claude Code** CLI installed and configured (`claude` command available)
145
- - **SQLite3** support (provided by `better-sqlite3`, compiled on install)
146
- - **Platform**: Linux or macOS (see [Platform Support](#platform-support))
152
+ - **SQLite3** support (provided by `better-sqlite3` 13, which ships prebuilt binaries for 8 platforms — no compiler needed on any of them; a platform it has no prebuild for falls back to building from source)
153
+ - **Platform**: Linux or macOS; Windows installs and runs but is not CI-covered and needs Git Bash or WSL for three hooks (see [Platform Support](#platform-support))
147
154
 
148
155
  ## Installation
149
156
 
package/README.zh-CN.md CHANGED
@@ -100,16 +100,22 @@
100
100
 
101
101
  | 平台 | 状态 | 说明 |
102
102
  |------|------|------|
103
- | **Linux** | 支持 | 主要开发和测试平台 |
103
+ | **Linux** | 支持 | 主要开发和测试平台;整个 CI 矩阵都跑在这里 |
104
104
  | **macOS** | 支持 | 完全兼容(Intel 和 Apple Silicon) |
105
- | **Windows** | 暂不支持 | 使用 POSIX shell 脚本(`post-tool-use.sh`、`setup.sh`)和 Unix 文件锁;WSL2 可能可用但未经测试 |
105
+ | **Windows** | 可安装,但无 CI 覆盖 | MCP server、CLI `node` 类 hook 均可用(`better-sqlite3` 自带 `win32-x64` / `win32-arm64` 预编译产物,无需编译)。**有三个 hook 命令走 `bash`** —— `setup.sh`、`post-tool-use.sh`、`pre-agent-inject.sh` —— 需要 PATH 上有 Git for Windows 或 WSL;`bash` 找不到时 `claude-mem-lite doctor` 会报出来。GitHub Actions 没有 Windows runner,所以这一行依据的是用户报告([#28](https://github.com/sdsrss/claude-mem-lite/issues/28))而不是绿色流水线 |
106
+ | **WSL2** | 未测试 | 底层就是 Linux,预期与 Linux 行一致;但无人报告过实际结果 |
107
+
108
+ v5.1.0 到 v6.1.0 之间,`package.json` 声明的是 `os: ["darwin", "linux"]`。那是 npm 的**安装**门禁,不是运行时检查:
109
+ 在 Windows 上它让 `npm install` 以 `EBADPLATFORM` 退出,而插件启动器每次插件更新后的首次 MCP 启动都要跑这条
110
+ 安装 —— 于是 server 起不来,`/mcp` 报 `CONNECTION_CLOSED`。现在 `win32` 已加入该列表。仍不在列表内的平台会拿到
111
+ 一条同时点明“声明了什么”和“当前是什么”的消息,而不是一句猜测。
106
112
 
107
113
  ## 环境要求
108
114
 
109
115
  - **Node.js** >= 22(v4.0.0 起:better-sqlite3 13 要求 >=22,Node 20 已于 2026-04 EOL;`package.json` 的 `engines` 是唯一事实来源)
110
116
  - **Claude Code** CLI 已安装并配置(`claude` 命令可用)
111
- - **SQLite3** 支持(由 `better-sqlite3` 提供,安装时编译)
112
- - **平台**:Linux 或 macOS(参见[平台支持](#平台支持))
117
+ - **SQLite3** 支持(由 `better-sqlite3` 13 提供,它自带 8 个平台的预编译产物,这些平台上都不需要编译器;没有对应预编译产物的平台才会回退到源码编译)
118
+ - **平台**:Linux 或 macOS;Windows 可安装运行,但无 CI 覆盖,且三个 hook 需要 Git Bash 或 WSL(参见[平台支持](#平台支持))
113
119
 
114
120
  ## 安装
115
121
 
package/install.mjs CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  scanPluginCacheHookPollution,
62
62
  hasInstallManagedHooks,
63
63
  pluginCacheHookEvents,
64
+ settingsHookCommands,
64
65
  } from './plugin-cache-guard.mjs';
65
66
  import { SOURCE_FILES, HOOK_SCRIPT_FILES } from './source-files.mjs';
66
67
  import {
@@ -266,6 +267,59 @@ export function buildDoctorSummary(issues, warnings) {
266
267
  return `${issues} issue(s) found.${warnSuffix}`;
267
268
  }
268
269
 
270
+ /**
271
+ * How many LIVE hook commands invoke `bash`, and which scripts they are.
272
+ *
273
+ * There are two hook registrations and only one is live per install shape, which is what
274
+ * the first cut of doctor's interpreter check got wrong (pre-ship review P1-1). The plugin
275
+ * shape reads `hooks/hooks.json` out of the plugin cache. The npm / npx / `git clone` shape
276
+ * has no such file — `hooks/hooks.json` is in RELEASE_SIGNED_FILES but NOT in SOURCE_FILES,
277
+ * so nothing deploys it to ~/.claude-mem-lite/ — and registers its hooks in settings.json
278
+ * instead. Reading only the manifest therefore answered "zero bash hooks" on the one shape
279
+ * where two of them are live.
280
+ *
281
+ * Returns THREE outcomes, never two. `count: null` means no registration could be read, and
282
+ * that is deliberately distinct from a count of zero: zero is an answer, null is the absence
283
+ * of one, and a diagnostic that reports them identically tells the reader to stop looking.
284
+ *
285
+ * @param {{manifestPath: string, settingsCommands?: string[], installDir: string}} opts
286
+ * @returns {{count: number|null, source: 'manifest'|'settings'|null, scripts: string[]}}
287
+ */
288
+ export function resolveBashHookCount({ manifestPath, settingsCommands = [], installDir }) {
289
+ const basenames = (commands) =>
290
+ commands
291
+ .map((c) => {
292
+ const m = c.match(/([^/"\s]+\.sh)/);
293
+ return m ? m[1] : c;
294
+ })
295
+ .sort();
296
+
297
+ if (existsSync(manifestPath)) {
298
+ try {
299
+ const parsed = JSON.parse(readFileSync(manifestPath, 'utf8'));
300
+ const commands = [];
301
+ for (const matchers of Object.values(parsed?.hooks || {})) {
302
+ for (const m of matchers || []) {
303
+ for (const h of m?.hooks || []) commands.push(String(h?.command || ''));
304
+ }
305
+ }
306
+ const bash = commands.filter((c) => c.startsWith('bash '));
307
+ return { count: bash.length, source: 'manifest', scripts: basenames(bash) };
308
+ } catch {
309
+ // A torn manifest is not evidence of zero bash hooks. Fall through to settings.json,
310
+ // and if that says nothing about us either, the caller gets null.
311
+ }
312
+ }
313
+ // Only OUR entries: settings.json is shared with every other tool the user installs, so a
314
+ // foreign `bash "…"` line is not ours to report on, and — the discriminating half — a
315
+ // settings.json that names nothing of ours is not evidence that no hook needs bash. It is
316
+ // evidence we are reading the wrong registration.
317
+ const ours = settingsCommands.filter((c) => c.includes(installDir));
318
+ if (ours.length === 0) return { count: null, source: null, scripts: [] };
319
+ const bash = ours.filter((c) => c.startsWith('bash '));
320
+ return { count: bash.length, source: 'settings', scripts: basenames(bash) };
321
+ }
322
+
269
323
  // Dev installs symlink server.mjs → the project's source file. Used to suppress
270
324
  // misleading "first run" messages since hook-update.mjs skips state-writes in
271
325
  // this mode (see hook-update.mjs isDevMode).
@@ -1865,6 +1919,68 @@ async function doctor() {
1865
1919
  dwarn('Hook scripts: check failed — ' + e.message);
1866
1920
  }
1867
1921
 
1922
+ // Hook interpreter. Some hook commands are `bash "<script>"` (the PostToolUse and
1923
+ // Agent prefilters, plus setup.sh in the plugin manifest) — the rest are `node`. If bash
1924
+ // cannot run, those commands fail and nothing says so; the check above grades whether the
1925
+ // FILES are present, which they are.
1926
+ //
1927
+ // Keyed on whether bash runs, not on process.platform === 'win32'. A Windows user with
1928
+ // Git for Windows on PATH — the normal case, since Claude Code shells out to bash for its
1929
+ // own Bash tool — has a working configuration and must not be warned; a stripped
1930
+ // container with no bash has a broken one and must be, whatever its platform. This is
1931
+ // also what issue #28's P3-19 intent asked for: `os: [darwin, linux]` was added so a
1932
+ // Windows user "should be told rather than handed a string of silent catch blocks", and
1933
+ // blocking the install told them nothing. This is the telling.
1934
+ try {
1935
+ const {
1936
+ count: bashCommands,
1937
+ source: countSource,
1938
+ scripts: bashScripts,
1939
+ } = resolveBashHookCount({
1940
+ manifestPath: join(PROJECT_DIR, 'hooks', 'hooks.json'),
1941
+ settingsCommands: settingsHookCommands(homedir()),
1942
+ installDir: INSTALL_DIR,
1943
+ });
1944
+ if (bashCommands === null) {
1945
+ // NOT `ok`. Pre-ship review (P1-1) found the first cut printing "no hook command needs
1946
+ // bash" here, on a shape where two of them are registered — a green line that ends the
1947
+ // reader's search is worse than the silence this check exists to remove.
1948
+ dwarn(
1949
+ 'Hook interpreter: could not read either hook registration — neither ' +
1950
+ `${join(PROJECT_DIR, 'hooks', 'hooks.json')} nor a claude-mem-lite entry in ` +
1951
+ `${join(homedir(), '.claude', 'settings.json')} — so whether any hook needs bash is unknown.`,
1952
+ );
1953
+ } else if (bashCommands === 0) {
1954
+ ok(`Hook interpreter: no hook command needs bash (per the ${countSource})`);
1955
+ } else {
1956
+ let bashOk = false;
1957
+ try {
1958
+ execFileSync('bash', ['-c', 'exit 0'], { stdio: 'ignore', timeout: 5000 });
1959
+ bashOk = true;
1960
+ } catch {
1961
+ /* not resolvable, or not runnable — either way the hooks that need it cannot fire */
1962
+ }
1963
+ if (bashOk) {
1964
+ ok(`Hook interpreter: bash present (${bashCommands} hook command(s) need it)`);
1965
+ } else {
1966
+ // dwarn, not an issue: everything else works. Saying "broken" about an install
1967
+ // whose MCP server and node hooks are fine would be the mirror of the defect that
1968
+ // sent this round's reporter looking at their disk and their network.
1969
+ // The scripts are NAMED from the live registration rather than described from
1970
+ // memory — the first cut wrote "(episode Read-tracking and the subagent prefilter)",
1971
+ // a two-item gloss on a count of three (P3-1).
1972
+ dwarn(
1973
+ `Hook interpreter: bash not found on PATH — the ${bashCommands} hook command(s) that ` +
1974
+ `invoke it cannot fire (${bashScripts.join(', ')}). The MCP server and the node ` +
1975
+ 'hooks are unaffected. On Windows, install Git for Windows or use WSL; elsewhere ' +
1976
+ 'this means a stripped PATH.',
1977
+ );
1978
+ }
1979
+ }
1980
+ } catch (e) {
1981
+ dwarn('Hook interpreter: check failed — ' + e.message);
1982
+ }
1983
+
1868
1984
  // Stale temp files
1869
1985
  try {
1870
1986
  // hook-update + the episode workers write runtime/ + staging under DB_DIR
@@ -0,0 +1,87 @@
1
+ // lib/platform-gate.mjs — is THIS platform inside the `os` list package.json declares?
2
+ //
3
+ // Why this is a module and not four lines inside scripts/launch.mjs, which is its only
4
+ // consumer: launch.mjs cannot be imported. Its top level installs dependencies, probes the
5
+ // native binding and then imports the MCP server, so a test that imported it would start a
6
+ // server. The alternative to a real import is a source-text scan of launch.mjs, which this
7
+ // repo has repeatedly found to be walkable — a guard that greps for a string passes for a
8
+ // change that keeps the string and deletes the behaviour. So the predicate lives here where
9
+ // tests can drive it, and launch.mjs keeps the wording and the exit.
10
+ //
11
+ // `os` is an npm INSTALL GATE, evaluated against the ROOT package being installed. npm
12
+ // rejects with EBADPLATFORM before it resolves anything, which is why a platform left off
13
+ // the list does not degrade — it fails the install outright, and in plugin mode that install
14
+ // is on the path of every first MCP launch after an update (issue #28).
15
+ //
16
+ // Node built-ins only: this runs BEFORE `npm install`, so node_modules may not exist yet.
17
+
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { join } from 'node:path';
20
+
21
+ /**
22
+ * npm's own `checkList` from npm-install-checks, reproduced rather than approximated.
23
+ *
24
+ * The rule is not "is it in the list": an entry may be NEGATED with `!`, and a list of
25
+ * nothing but negations allows every platform it does not name. `list.includes(platform)`
26
+ * agrees with npm on the simple case and disagrees on `["!win32"]` — where it would tell a
27
+ * linux user their platform is unsupported while npm installs happily. A diagnostic that is
28
+ * wrong in that direction is worse than none.
29
+ *
30
+ * @param {string[]|string} list The `os` value as declared.
31
+ * @param {string} platform A `process.platform` value.
32
+ * @returns {boolean} True when npm would allow the install.
33
+ */
34
+ export function platformAllowed(list, platform) {
35
+ const entries = typeof list === 'string' ? [list] : list;
36
+ if (!Array.isArray(entries)) return true;
37
+ if (entries.length === 1 && entries[0] === 'any') return true;
38
+ let negated = 0;
39
+ let match = false;
40
+ for (const entry of entries) {
41
+ const negate = typeof entry === 'string' && entry.startsWith('!');
42
+ const test = negate ? entry.slice(1) : entry;
43
+ if (negate) {
44
+ negated++;
45
+ if (platform === test) return false;
46
+ } else if (platform === test) {
47
+ match = true;
48
+ }
49
+ }
50
+ // An all-negation list (and, by the same arithmetic, an empty one) allows anything it
51
+ // has not vetoed above.
52
+ return match || negated === entries.length;
53
+ }
54
+
55
+ /**
56
+ * The `os` list `<root>/package.json` declares, or null when there is nothing to enforce.
57
+ *
58
+ * Fails OPEN on every error. A missing or torn package.json is the incomplete-install case
59
+ * that launch-preflight.mjs already diagnoses with a repair command; turning it into a
60
+ * platform complaint here would replace a good message with a wrong one.
61
+ *
62
+ * @param {string} root Directory holding package.json.
63
+ * @returns {string[]|null}
64
+ */
65
+ export function readDeclaredPlatforms(root) {
66
+ const p = join(root, 'package.json');
67
+ if (!existsSync(p)) return null;
68
+ try {
69
+ const os = JSON.parse(readFileSync(p, 'utf8'))?.os;
70
+ if (typeof os === 'string') return [os];
71
+ return Array.isArray(os) ? os : null;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Would npm refuse to install this package on this platform?
79
+ *
80
+ * @param {{root: string, platform?: string}} opts
81
+ * @returns {{blocked: boolean, declared: string[]|null, platform: string}}
82
+ */
83
+ export function platformGate({ root, platform = process.platform }) {
84
+ const declared = readDeclaredPlatforms(root);
85
+ if (declared === null) return { blocked: false, declared: null, platform };
86
+ return { blocked: !platformAllowed(declared, platform), declared, platform };
87
+ }
@@ -1,15 +1,16 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "claude-mem-lite",
9
- "version": "6.1.0",
9
+ "version": "6.2.0",
10
10
  "os": [
11
11
  "darwin",
12
- "linux"
12
+ "linux",
13
+ "win32"
13
14
  ],
14
15
  "dependencies": {
15
16
  "@modelcontextprotocol/sdk": "^1.30.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-mem-lite",
3
- "version": "6.1.0",
3
+ "version": "6.2.0",
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",
@@ -9,7 +9,8 @@
9
9
  },
10
10
  "os": [
11
11
  "darwin",
12
- "linux"
12
+ "linux",
13
+ "win32"
13
14
  ],
14
15
  "bin": {
15
16
  "claude-mem-lite": "./cli.mjs"
@@ -106,6 +107,7 @@
106
107
  "lib/hook-stdout.mjs",
107
108
  "lib/proc-lock.mjs",
108
109
  "lib/atomic-write.mjs",
110
+ "lib/platform-gate.mjs",
109
111
  "lib/hook-prune.mjs",
110
112
  "lib/proxy-fetch.mjs",
111
113
  "lib/llm-provider-probe.mjs",
@@ -117,8 +117,16 @@ export function hasInstallManagedHooks(opts) {
117
117
  }
118
118
  }
119
119
 
120
- /** Every `command` string under settings.json `hooks`, in registration order. */
121
- function settingsHookCommands(home) {
120
+ /**
121
+ * Every `command` string under settings.json `hooks`, in registration order.
122
+ *
123
+ * Exported since the issue-#28 round: doctor's hook-interpreter check needs the LIVE
124
+ * registration, and on the npm / npx / `git clone` shape that is settings.json rather than
125
+ * `hooks/hooks.json` (which SOURCE_FILES does not deploy). Reading it here rather than
126
+ * hand-rolling a second walk keeps the two consumers on one parser — the twin-drift class
127
+ * this repo keeps paying for.
128
+ */
129
+ export function settingsHookCommands(home) {
122
130
  const settingsPath = join(home, '.claude', 'settings.json');
123
131
  if (!existsSync(settingsPath)) return [];
124
132
  let s;
@@ -11,6 +11,36 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
11
11
  const ROOT = process.env.CLAUDE_PLUGIN_ROOT || join(__dirname, '..');
12
12
 
13
13
  if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
14
+ // Platform gate BEFORE npm, not after it. package.json's `os` field is an npm install
15
+ // gate: npm exits EBADPLATFORM without resolving anything, so the catch below sees only
16
+ // "Command failed" and answers with a fixed cause list that cannot contain this cause.
17
+ // That is issue #28 — a Windows user got CONNECTION_CLOSED in /mcp plus a wrong reason,
18
+ // where the field was added (b6a2579, R10 P3-19) precisely so they would be TOLD.
19
+ // Letting npm fail and then guessing is the shape that failed; asking the manifest first
20
+ // is the shape that names both sides of the mismatch. Guarded import: lib/ can be absent
21
+ // in an incomplete install, which launch-preflight.mjs below diagnoses properly, and a
22
+ // missing diagnostic must never become a new failure mode.
23
+ try {
24
+ const { platformGate } = await import('../lib/platform-gate.mjs');
25
+ const gate = platformGate({ root: ROOT });
26
+ if (gate.blocked) {
27
+ process.stderr.write(
28
+ `[claude-mem-lite] npm install is blocked by this package's own platform list (npm EBADPLATFORM).\n`,
29
+ );
30
+ process.stderr.write(
31
+ `[claude-mem-lite] package.json declares os: ${gate.declared.join(', ')} — this machine is ${gate.platform}\n`,
32
+ );
33
+ process.stderr.write(
34
+ `[claude-mem-lite] Nothing was installed, so the MCP server cannot start. See "Platform Support" in the README.\n`,
35
+ );
36
+ process.stderr.write(
37
+ `[claude-mem-lite] To install anyway: cd "${ROOT}" && npm install --omit=dev --force\n`,
38
+ );
39
+ process.exit(1);
40
+ }
41
+ } catch (e) {
42
+ process.stderr.write(`[claude-mem-lite] platform check skipped: ${e.message}\n`);
43
+ }
14
44
  process.stderr.write('[claude-mem-lite] Installing dependencies...\n');
15
45
  try {
16
46
  execSync('npm install --omit=dev', {
@@ -55,8 +85,13 @@ if (!existsSync(join(ROOT, 'node_modules', 'better-sqlite3'))) {
55
85
  (e?.signal ? `npm killed by ${e.signal}` : '') ||
56
86
  'unknown error';
57
87
  process.stderr.write(`[claude-mem-lite] npm install failed in ${ROOT} — ${detail}\n`);
88
+ // "Likely cause: …" until issue #28: it asserted three causes, and the one that was
89
+ // actually firing (EBADPLATFORM, gated above) was not among them. stderr is inherited,
90
+ // so npm's own `npm error code <CODE>` line is already on this stream a few lines up —
91
+ // point at that instead of competing with it. A guess presented as a diagnosis costs
92
+ // more than no diagnosis: it sends the reader looking at their disk and their network.
58
93
  process.stderr.write(
59
- `[claude-mem-lite] Likely cause: read-only directory, disk full, or network blocked.\n`,
94
+ `[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`,
60
95
  );
61
96
  process.stderr.write(`[claude-mem-lite] Repair: cd "${ROOT}" && npm install --omit=dev\n`);
62
97
  process.exit(1);
package/source-files.mjs CHANGED
@@ -142,6 +142,11 @@ export const SOURCE_FILES = [
142
142
  // + auto-update lock). Must ship or a partial install/update skips them.
143
143
  'lib/proc-lock.mjs',
144
144
  'lib/atomic-write.mjs',
145
+ // Dynamically imported by scripts/launch.mjs BEFORE `npm install` runs, to answer
146
+ // EBADPLATFORM with both sides of the mismatch instead of a guessed cause (issue #28).
147
+ // The import is guarded, so omitting this file would not crash the launcher — it would
148
+ // silently restore the wrong diagnosis, which is the failure this shipped to fix.
149
+ 'lib/platform-gate.mjs',
145
150
  // Shared settings.json hook classification + dangling-entry reconciliation.
146
151
  // Statically imported by install.mjs AND dynamically by hook-update.mjs's
147
152
  // post-swap step — missing it from the manifest would break auto-update's