memhouse 0.8.0 → 0.9.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/README.md CHANGED
@@ -1,19 +1,31 @@
1
1
  # memhouse
2
2
 
3
- Long-term memory for coding agents. memhouse reads the session transcripts your
4
- editors already write to disk — **17 of them**, Claude Code, Codex, Cursor, Zed,
5
- Copilot, Gemini CLI and the rest — parses them locally, and ships typed rows into
6
- ClickHouse. Nothing is proxied or intercepted. Then you can search every past
7
- session, see what it cost, and let an agent query its own history.
3
+ **Claude Code deletes your transcripts after 30 days.** (`cleanupPeriodDays`,
4
+ default 30 — session files are removed at startup.) Measured on the machine this
5
+ was built on: months of daily use, and the oldest surviving local transcript was
6
+ nine days old. Every problem you solved before that exists nowhere — unless
7
+ something durable was keeping it.
8
+
9
+ memhouse is that something. It reads the session transcripts your editors already
10
+ write to disk — **17 of them**: Claude Code, Codex, Cursor, Zed, Copilot, Gemini
11
+ CLI and the rest — parses them locally, and ships typed rows into a ClickHouse
12
+ **you own**. Nothing is proxied, intercepted, or sent to anyone's cloud. Then you
13
+ can search every past session, see what it cost, resume the conversation that
14
+ solved this before, and let an agent query its own history.
15
+
16
+ And every row says where it came from, in a way no client can fake: `user_id` is
17
+ stamped **by the server** (`MATERIALIZED currentUser()`, async inserts pinned off
18
+ so the stamp cannot be skipped) and `host` is a fingerprint minted once per
19
+ install. A team pointed at one house is a team memory with real attribution — not
20
+ a label somebody's client claimed.
8
21
 
9
22
  ```bash
10
- npm install -g memhouse --allow-scripts=better-sqlite3
23
+ npm install -g memhouse
11
24
  memhouse onboard
12
25
  ```
13
26
 
14
- That is the whole install. `onboard` finds your editors, sets up a house, ships,
15
- and starts the dashboard. **Don't drop `--allow-scripts`** — see
16
- [below](#the-allow-scripts-flag).
27
+ That is the whole install — no flags, nothing to compile. `onboard` finds your
28
+ editors, sets up a house, ships, and starts the dashboard.
17
29
 
18
30
  **On most Linux distros that first line needs `sudo`.** A distro-packaged Node puts
19
31
  its global prefix under `/usr`, which you do not own, so npm stops with `EACCES`.
@@ -22,7 +34,7 @@ Ubuntu machine:
22
34
 
23
35
  ```bash
24
36
  ls -ld "$(npm prefix -g)" # root-owned? use sudo. Owned by you? do NOT — see EACCES below
25
- sudo npm install -g memhouse --allow-scripts=better-sqlite3
37
+ sudo npm install -g memhouse
26
38
  ```
27
39
 
28
40
  If the prefix is yours (Homebrew, fnm, nvm, volta), `sudo` is the wrong fix and
@@ -149,13 +161,11 @@ memhouse update # upgrade, restart the daemons, check the house schem
149
161
  memhouse update --check # compare versions, change nothing
150
162
  ```
151
163
 
152
- A bare `npm i -g memhouse@latest` does one third of the job. It does not re-pass
153
- `--allow-scripts=better-sqlite3`, so the five SQLite adapters go quiet; it does not
154
- restart the daemons, which keep parsing with the code they booted with (a shipper here
155
- once ran 1d16h out of a directory that had been *moved*); and it does not check whether
156
- your house predates a room the new shipper needs. `update` does all three, and from a
157
- checkout it rebuilds `public/` — otherwise `git pull` serves you the previous release's
158
- dashboard forever.
164
+ A bare `npm i -g memhouse@latest` does half the job. It does not restart the daemons,
165
+ which keep parsing with the code they booted with (a shipper here once ran 1d16h out of
166
+ a directory that had been *moved*); and it does not check whether your house predates a
167
+ room the new shipper needs. `update` does both, and from a checkout it rebuilds
168
+ `public/` — otherwise `git pull` serves you the previous release's dashboard forever.
159
169
 
160
170
  The daemons also notice on their own: each one compares the installed version against
161
171
  what it booted with and hands over — exiting under systemd/launchd so the supervisor
@@ -210,24 +220,22 @@ has it wrong, so an old house cannot be corrupted by a new shipper.
210
220
 
211
221
  ## Troubleshooting
212
222
 
213
- ### The `--allow-scripts` flag
223
+ ### An editor reads zero sessions
214
224
 
215
- Five adapters — **cursor, zed, opencode, goose, antigravity** — read sessions out
216
- of SQLite files, and `better-sqlite3` builds its native binding from an install
217
- script. npm 12 blocks install scripts by default, so without the flag those five
218
- read nothing and you ship a partial history.
225
+ Five adapters — **cursor, zed, opencode, goose, antigravity** — read sessions out of
226
+ SQLite files. That used to mean `better-sqlite3`, whose native binding is built by
227
+ an npm install script; npm 12 blocks install scripts by default, so a plain install left
228
+ all five reading nothing and the fix was a flag
229
+ (`--allow-scripts=better-sqlite3`) you had to re-type on every upgrade. SQLite is
230
+ `node:sqlite` now — part of Node — so that failure class is gone along with the flag.
219
231
 
220
- (This said six and named windsurf, which is wrong: windsurf lists its sessions over
221
- language-server RPC and touches SQLite only to read a usage key, so a dead binding
222
- costs it numbers, not sessions. `editors/index.js` has the real list.)
223
-
224
- It is not silent: `memhouse discover` and `memhouse doctor` both name the skipped
225
- adapters. The other twelve adapters keep working.
226
-
227
- `npx` takes the same flag, before the package name:
232
+ What can still go wrong is one store at a time: a `state.vscdb` locked by a running
233
+ editor, a corrupt file, a schema a new editor release changed. None of it is silent —
234
+ `memhouse discover` and `memhouse doctor` both name the skipped adapter and say why,
235
+ and the other sixteen keep working.
228
236
 
229
237
  ```bash
230
- npx --allow-scripts=better-sqlite3 -y memhouse discover
238
+ npx -y memhouse discover # no flags here either
231
239
  ```
232
240
 
233
241
  ### `EACCES` on install
@@ -262,7 +270,7 @@ A rootless podman house has the same problem for the same reason, and
262
270
  ## Working from a checkout
263
271
 
264
272
  ```bash
265
- npm install # the repo's allowScripts field covers the binding
273
+ npm install # two runtime deps, no build step
266
274
  node bin/memhouse.js …
267
275
  npm test # syntax gate + unit checks
268
276
  ```
@@ -293,6 +301,16 @@ In constellation terms (`TERMINOLOGY.md`) memhouse is an **agency**: a **house**
293
301
  alone would hold; the shipper is what makes it act. It competes with
294
302
  memory-house; if it wins, it becomes memory-house v4.
295
303
 
304
+ ## What's supported
305
+
306
+ | Surface | Status |
307
+ |---|---|
308
+ | macOS | primary — developed and driven here daily |
309
+ | Linux | real — installs, ships, `service install` (systemd `--user`), rootless podman `deploy --local`; exercised on Debian and Ubuntu machines |
310
+ | Windows | **untested** — the adapters declare Windows paths but no one has run a single install; treat it as unknown until this row changes |
311
+ | ClickHouse | 25.11 and 26.x, both in CI; 26.2+ needs no flags, 25.x needs the text-index flag the shipper already passes |
312
+ | Node | **≥ 24** — SQLite comes from `node:sqlite`, which is stable in 24. It works flagless on 22.13+ but prints `ExperimentalWarning` on every command (measured on v22.23.2), and Node 20 is past EOL. There is no native dependency and no build step at any version. |
313
+
296
314
  ## Heritage & license
297
315
 
298
316
  Built on [agentlytics](https://github.com/f/agentlytics) by Fatih Kadir Akın (MIT)
package/bin/memhouse.js CHANGED
@@ -356,18 +356,15 @@ Engine: MEMHOUSE_ENGINE pins docker or podman when both are installed and one ca
356
356
  `;
357
357
 
358
358
  // A skipped adapter and an editor the user does not have look identical — both
359
- // contribute zero sessions. Say which happened, and how to fix the one that is fixable.
359
+ // contribute zero sessions. Say which happened.
360
+ //
361
+ // There was a second branch here for the one cause that hit five adapters at once: a
362
+ // missing better-sqlite3 native binding, fixable only by reinstalling with a flag. With
363
+ // SQLite now inside Node there is nothing to install and nothing to advise — every
364
+ // remaining failure is one adapter's own store, and its own message is the whole story.
360
365
  function printAdapterErrors(errors) {
361
366
  if (!errors || errors.length === 0) return;
362
- const blocked = errors.filter((e) => e.missingBinding);
363
- const other = errors.filter((e) => !e.missingBinding);
364
- if (blocked.length) {
365
- console.log(warn(`${blocked.length} adapter${blocked.length > 1 ? 's' : ''} skipped — better-sqlite3 has no native binding: ${blocked.map((e) => e.source).join(', ')}`));
366
- console.log(' their sessions are NOT being shipped. npm >= 12 blocks install scripts by default; rebuild with:');
367
- console.log(' ${installCommand()}');
368
- console.log(' (from a checkout: npm install --no-audit --no-fund, which package.json already allows)');
369
- }
370
- for (const e of other) console.log(warn(`${e.source.padEnd(16)} skipped: ${e.message}`));
367
+ for (const e of errors) console.log(warn(`${e.source.padEnd(16)} skipped: ${e.message}`));
371
368
  }
372
369
 
373
370
  async function cmdDiscover() {
@@ -1229,8 +1226,11 @@ async function cmdDoctor() {
1229
1226
  const checks = [];
1230
1227
  const add = (okFlag, label, hint) => checks.push({ ok: okFlag, label, hint });
1231
1228
 
1232
- const [major, minor] = process.versions.node.split('.').map(Number);
1233
- add(major > 20 || (major === 20 && minor >= 19), `node ${process.versions.node}`, 'need >= 20.19');
1229
+ // 24 because SQLite comes from `node:sqlite`, which is stable there. It works on 22.13+
1230
+ // but prints an ExperimentalWarning on every command, and 20 is past EOL. Below the
1231
+ // floor, the five SQLite-backed adapters are the part that actually breaks.
1232
+ const [major] = process.versions.node.split('.').map(Number);
1233
+ add(major >= 24, `node ${process.versions.node}`, 'need >= 24 — node:sqlite is stable there; the SQLite-backed adapters need it');
1234
1234
  add(fs.existsSync(ENV_FILE), `config ${ENV_FILE}`, 'run: memhouse install');
1235
1235
  // doctor is exempt from requireConfig — diagnosing an unconfigured machine is its job —
1236
1236
  // but "exempt from refusing" is not "licensed to log in somewhere". With no config it
@@ -1445,11 +1445,11 @@ async function cmdDoctor() {
1445
1445
  }
1446
1446
  adapterErrors = getAdapterErrors();
1447
1447
  for (const t of thrown) {
1448
- if (!adapterErrors.some((e) => e.source === t.source)) adapterErrors.push({ ...t, missingBinding: false });
1448
+ if (!adapterErrors.some((e) => e.source === t.source)) adapterErrors.push(t);
1449
1449
  }
1450
- // Any adapter that could not be read is a failure, whatever the cause — a
1451
- // locked or corrupt store loses just as many sessions as a missing binding.
1452
- const blocked = adapterErrors.filter((e) => e.missingBinding).map((e) => e.source);
1450
+ // Any adapter that could not be read is a failure, whatever the cause — a locked or
1451
+ // corrupt store loses sessions just as surely as the missing native binding this
1452
+ // used to have a separate remedy for.
1453
1453
  const failed = adapterErrors.map((e) => e.source);
1454
1454
  add(adapterErrors.length === 0,
1455
1455
  // "visible locally" is what the ADAPTERS see; `stats` reports what the house HOLDS,
@@ -1458,9 +1458,7 @@ async function cmdDoctor() {
1458
1458
  // find the other number elsewhere (161 here, 156 there, on a real machine) gives
1459
1459
  // them no way to tell "correctly skipped" from "silently dropped".
1460
1460
  `adapters: ${seen} sessions visible locally${failed.length ? ` (${failed.length} skipped: ${failed.join(', ')})` : ''} — compare with what the house holds: memhouse stats`,
1461
- blocked.length ? installCommand()
1462
- : adapterErrors.length ? adapterErrors.map((e) => `${e.source}: ${e.message}`).join('; ')
1463
- : undefined);
1461
+ adapterErrors.length ? adapterErrors.map((e) => `${e.source}: ${e.message}`).join('; ') : undefined);
1464
1462
  } catch (e) { add(false, 'adapters', e.message); }
1465
1463
  // Which models in this house have no price. An unpriced model is not an error — a new
1466
1464
  // release always lands before pricing.json catches up — but it IS silent: calculateCost
@@ -1657,12 +1655,12 @@ async function cmdUpdate() {
1657
1655
 
1658
1656
  if (kind === 'npx') {
1659
1657
  console.log(warn('nothing to update — npx resolves the registry on every run'));
1660
- console.log(' to keep a version around: npm install -g memhouse --allow-scripts=better-sqlite3');
1658
+ console.log(' to keep a version around: npm install -g memhouse');
1661
1659
  return 1;
1662
1660
  }
1663
1661
  if (kind === 'local-dep') {
1664
1662
  console.log(warn('this is a project dependency, not a global install'));
1665
- console.log(` upgrade it where it lives: npm install memhouse@latest --allow-scripts=better-sqlite3`);
1663
+ console.log(` upgrade it where it lives: npm install memhouse@latest`);
1666
1664
  return 1;
1667
1665
  }
1668
1666
 
@@ -1690,10 +1688,11 @@ async function cmdUpdate() {
1690
1688
  }
1691
1689
  }
1692
1690
  } else {
1693
- // --allow-scripts is not optional and not remembered: npm >= 12 blocks install scripts
1694
- // by default, so an upgrade without it leaves better-sqlite3 with no binding and the
1695
- // five SQLite-backed adapters read zero sessions on every pass afterwards.
1696
- const r = spawnSync('npm', ['install', '-g', 'memhouse@latest', '--allow-scripts=better-sqlite3'], { stdio: 'inherit' });
1691
+ // Plain, with no flag to forget. This carried `--allow-scripts=better-sqlite3` for as
1692
+ // long as SQLite was a native module: npm never remembered it, and an upgrade without
1693
+ // it left the binding unbuilt and the five SQLite-backed adapters reading zero
1694
+ // sessions on every pass afterwards. There is no binding to build now.
1695
+ const r = spawnSync('npm', ['install', '-g', 'memhouse@latest'], { stdio: 'inherit' });
1697
1696
  if (r.status !== 0) {
1698
1697
  console.log(bad('npm install failed — nothing was restarted, the running version is unchanged'));
1699
1698
  console.log(' if it was EACCES: ls -ld "$(npm prefix -g)" — root-owned needs sudo, yours needs a chown');
@@ -2,16 +2,7 @@ const { execSync, execFileSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
- // Required lazily. A top-level require makes this native module a load-time dependency
6
- // of the whole adapter set: editors/index.js imports every adapter, so one missing
7
- // binding threw before any adapter ran and `discover` reported ZERO editors — losing the
8
- // eleven that need no native code at all. The adapterErrors path exists to degrade one
9
- // adapter at a time, and a top-level throw walks straight past it.
10
- let Database = null;
11
- function openDb(...a) {
12
- if (!Database) Database = require('better-sqlite3');
13
- return new Database(...a);
14
- }
5
+ const { openReadOnly, textOf } = require('./sqlite');
15
6
  const { getAppDataPath } = require('./base');
16
7
  const adapterErrors = require('./adapter-errors');
17
8
 
@@ -244,13 +235,14 @@ function readGlobalStateValue(key) {
244
235
 
245
236
  let db = null;
246
237
  try {
247
- db = openDb(ANTIGRAVITY_GLOBAL_STORAGE_DB, { readonly: true, fileMustExist: true });
238
+ // fileMustExist is gone with better-sqlite3, not lost: node:sqlite refuses to
239
+ // create a database it is opening read-only, and the existsSync above still stands.
240
+ db = openReadOnly(ANTIGRAVITY_GLOBAL_STORAGE_DB);
248
241
  const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
249
242
  if (!row) return null;
250
- const v = row.value;
251
- if (typeof v === 'string') return v;
252
- if (Buffer.isBuffer(v) || v instanceof Uint8Array) return Buffer.from(v).toString('utf-8');
253
- return v == null ? null : String(v);
243
+ // ItemTable.value is declared BLOB and usually holds TEXT — textOf covers both,
244
+ // including the Uint8Array node:sqlite returns for the byte case.
245
+ return row.value == null ? null : textOf(row.value);
254
246
  } catch (e) {
255
247
  // Only the offline half of getChats() reads this store; live cascades come over
256
248
  // RPC. So a failure here costs some sessions, not all of them.
package/editors/cursor.js CHANGED
@@ -1,13 +1,4 @@
1
- // Required lazily. A top-level require makes this native module a load-time dependency
2
- // of the whole adapter set: editors/index.js imports every adapter, so one missing
3
- // binding threw before any adapter ran and `discover` reported ZERO editors — losing the
4
- // eleven that need no native code at all. The adapterErrors path exists to degrade one
5
- // adapter at a time, and a top-level throw walks straight past it.
6
- let Database = null;
7
- function openDb(...a) {
8
- if (!Database) Database = require('better-sqlite3');
9
- return new Database(...a);
10
- }
1
+ const { openReadOnly, textOf, bytesOf } = require('./sqlite');
11
2
  const path = require('path');
12
3
  const fs = require('fs');
13
4
  const os = require('os');
@@ -52,16 +43,16 @@ function hexToString(hex) {
52
43
  function readStoreMeta(db) {
53
44
  const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('0');
54
45
  if (!row) return null;
55
- const hex = typeof row.value === 'string' ? row.value : Buffer.from(row.value).toString('hex');
46
+ const hex = typeof row.value === 'string' ? row.value : bytesOf(row.value).toString('hex');
56
47
  try {
57
48
  return JSON.parse(hexToString(hex));
58
49
  } catch {
59
- try { return JSON.parse(row.value); } catch { return null; }
50
+ try { return JSON.parse(textOf(row.value)); } catch { return null; }
60
51
  }
61
52
  }
62
53
 
63
54
  function parseTreeBlob(data) {
64
- const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
55
+ const buf = bytesOf(data);
65
56
  const messageRefs = [];
66
57
  const childRefs = [];
67
58
  let offset = 0;
@@ -114,7 +105,10 @@ function collectStoreMessages(db, rootBlobId) {
114
105
  if (!row) return;
115
106
  const data = row.data;
116
107
  try {
117
- const json = typeof data === 'string' ? JSON.parse(data) : JSON.parse(data.toString('utf-8'));
108
+ // textOf, not data.toString(): a message blob arrives as a Uint8Array, whose own
109
+ // toString() renders the bytes as "123,45,…" and would send every message in the
110
+ // tree down the tree-blob path below.
111
+ const json = JSON.parse(textOf(data));
118
112
  if (json && json.role) { allMessages.push(normalizeStoreMessage(json)); return; }
119
113
  } catch { /* tree blob */ }
120
114
  const { messageRefs, childRefs } = parseTreeBlob(data);
@@ -156,11 +150,11 @@ function getWorkspaceMap() {
156
150
 
157
151
  function getComposerHeaders(stateDbPath) {
158
152
  try {
159
- const db = openDb(stateDbPath, { readonly: true });
153
+ const db = openReadOnly(stateDbPath);
160
154
  const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
161
155
  db.close();
162
156
  if (!row) return [];
163
- const data = JSON.parse(row.value);
157
+ const data = JSON.parse(textOf(row.value));
164
158
  return (data.allComposers || []).map((c) => ({
165
159
  composerId: c.composerId,
166
160
  name: c.name || null,
@@ -183,7 +177,7 @@ function getModelPreference(globalDb) {
183
177
  try {
184
178
  const row = globalDb.prepare("SELECT value FROM ItemTable WHERE key = 'cursor/lastSingleModelPreference'").get();
185
179
  if (!row) return null;
186
- const pref = JSON.parse(row.value);
180
+ const pref = JSON.parse(textOf(row.value));
187
181
  return pref.composer || pref.agent || null;
188
182
  } catch { return null; }
189
183
  }
@@ -197,7 +191,7 @@ function getComposerBubbles(globalDb, composerId) {
197
191
  const bubbles = [];
198
192
  for (const row of rows) {
199
193
  try {
200
- const obj = JSON.parse(row.value);
194
+ const obj = JSON.parse(textOf(row.value));
201
195
  bubbles.push(obj);
202
196
  } catch (e) {
203
197
  // Historically treated as "binary blob, skip", but a row that will not decode
@@ -286,7 +280,7 @@ function getChats() {
286
280
  // Source 1: ~/.cursor/chats store.db
287
281
  for (const { workspace, chatId, dbPath } of getAgentStoreChats()) {
288
282
  try {
289
- const db = openDb(dbPath, { readonly: true });
283
+ const db = openReadOnly(dbPath);
290
284
  const meta = readStoreMeta(db);
291
285
  db.close();
292
286
  if (meta) {
@@ -325,7 +319,7 @@ function getChats() {
325
319
 
326
320
  // Source 2: workspaceStorage composers
327
321
  let globalDb = null;
328
- try { globalDb = openDb(GLOBAL_STORAGE_DB, { readonly: true }); }
322
+ try { globalDb = openReadOnly(GLOBAL_STORAGE_DB); }
329
323
  catch (e) {
330
324
  // Absent is normal — Cursor may simply not be installed. Unreadable is not.
331
325
  if (fs.existsSync(GLOBAL_STORAGE_DB)) adapterErrors.record('cursor', e, GLOBAL_STORAGE_DB);
@@ -378,7 +372,7 @@ function getChats() {
378
372
 
379
373
  function getMessages(chat) {
380
374
  if (chat._type === 'agent-store') {
381
- const db = openDb(chat._dbPath, { readonly: true });
375
+ const db = openReadOnly(chat._dbPath);
382
376
  const msgs = collectStoreMessages(db, chat._rootBlobId);
383
377
  db.close();
384
378
  // Use lastUsedModel as fallback for assistant messages without model info
@@ -391,7 +385,7 @@ function getMessages(chat) {
391
385
  }
392
386
 
393
387
  let globalDb;
394
- try { globalDb = openDb(GLOBAL_STORAGE_DB, { readonly: true }); }
388
+ try { globalDb = openReadOnly(GLOBAL_STORAGE_DB); }
395
389
  catch (e) {
396
390
  // Returning [] unreported would let a re-ship overwrite this session's stored
397
391
  // transcript with nothing.
@@ -416,10 +410,10 @@ function getMessages(chat) {
416
410
 
417
411
  function getCursorAccessToken() {
418
412
  try {
419
- const db = openDb(GLOBAL_STORAGE_DB, { readonly: true });
413
+ const db = openReadOnly(GLOBAL_STORAGE_DB);
420
414
  const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
421
415
  db.close();
422
- return row ? row.value : null;
416
+ return row ? textOf(row.value) : null;
423
417
  } catch { return null; }
424
418
  }
425
419
 
package/editors/goose.js CHANGED
@@ -2,6 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
 
5
+ const { openReadOnly, textOf } = require('./sqlite');
5
6
  const adapterErrors = require('./adapter-errors');
6
7
 
7
8
  const GOOSE_DIR = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions');
@@ -9,30 +10,17 @@ const DB_PATH = path.join(GOOSE_DIR, 'sessions.db');
9
10
  const CONFIG_PATH = path.join(os.homedir(), '.config', 'goose', 'config.yaml');
10
11
 
11
12
  // ============================================================
12
- // Query SQLite via better-sqlite3 (cross-platform)
13
+ // Query SQLite via node:sqlite (cross-platform, part of Node itself)
13
14
  // ============================================================
14
15
 
15
- let Database;
16
- function getDatabase() {
17
- if (!Database) {
18
- try {
19
- Database = require('better-sqlite3');
20
- } catch {
21
- // better-sqlite3 not available
22
- }
23
- }
24
- return Database;
25
- }
26
-
16
+ // This used to swallow "SQLite is unavailable" as a case of its own, because
17
+ // better-sqlite3's binding could genuinely be absent. It cannot be now — so an open
18
+ // that fails is an open that failed, and the one catch below reports it like any other
19
+ // unreadable store.
27
20
  function queryDb(sql) {
28
21
  if (!fs.existsSync(DB_PATH)) return [];
29
- const Db = getDatabase();
30
- if (!Db) {
31
- adapterErrors.record('goose', new Error('better-sqlite3 unavailable'), DB_PATH);
32
- return [];
33
- }
34
22
  try {
35
- const db = new Db(DB_PATH, { readonly: true });
23
+ const db = openReadOnly(DB_PATH);
36
24
  const rows = db.prepare(sql).all();
37
25
  db.close();
38
26
  return rows;
@@ -201,7 +189,7 @@ function getMessagesFromDb(chat) {
201
189
  // A row we cannot decode drops a message from the transcript. Silently, that is
202
190
  // a truncation that looks complete, and a re-ship would replace the stored
203
191
  // transcript with it. Report so the shipper withholds and retries instead.
204
- try { parts = JSON.parse(row.content_json); }
192
+ try { parts = JSON.parse(textOf(row.content_json)); }
205
193
  catch (e) { adapterErrors.record('goose', e, `session ${chat.composerId}`); continue; }
206
194
  if (!Array.isArray(parts)) {
207
195
  adapterErrors.record('goose', new Error('message content_json is not an array'), `session ${chat.composerId}`);
@@ -314,7 +302,7 @@ function getMessagesFromJsonl(chat) {
314
302
  function extractSessionModel(row) {
315
303
  if (row.model_config_json) {
316
304
  try {
317
- const cfg = JSON.parse(row.model_config_json);
305
+ const cfg = JSON.parse(textOf(row.model_config_json));
318
306
  if (cfg.model) return cfg.model;
319
307
  if (cfg.model_id) return cfg.model_id;
320
308
  } catch {}
package/editors/index.js CHANGED
@@ -26,58 +26,19 @@ for (const editor of editors) {
26
26
 
27
27
  // Per-adapter failures from the last getAllChats() pass. An adapter that throws is
28
28
  // skipped so one broken editor cannot take down a scan, but the failure is recorded
29
- // here rather than discarded: a missing better-sqlite3 native binding and an editor
30
- // the user simply does not have both produce zero sessions, and only this tells them
31
- // apart. See getAdapterErrors().
32
- let adapterErrors = [];
33
-
34
- // better-sqlite3 ships a prebuilt binary via an install script. npm >= 12 blocks
35
- // install scripts by default, so a plain `npm i -g memhouse` leaves no binding and
36
- // every SQLite-backed adapter reads zero sessions.
37
- const MISSING_BINDING = /Could not locate the bindings file|Cannot find module 'better-sqlite3'/;
38
-
39
- // Adapters that read sessions out of a SQLite store, so a dead binding costs
40
- // sessions. Membership is narrower than `grep -l better-sqlite3 editors/*.js`:
41
- // windsurf/devin also requires better-sqlite3, but only in getDevinApiKey() for
42
- // usage — its getChats() is pure language-server RPC, so its sessions survive a
43
- // broken binding and listing it here would raise a false alarm. antigravity is
44
- // listed because its *offline* chats come from SQLite, though its live cascades,
45
- // like devin's, come over RPC — a failure there is partial.
46
- // Held as module references, not strings, so the reported name always matches the
47
- // adapter's own `name`.
48
- const SQLITE_BACKED = [antigravity, cursor, goose, opencode, zed].map((m) => m.name);
49
-
50
- // Construct an in-memory database. Loading the module is not enough — better-sqlite3
51
- // resolves the native binding lazily, on first open.
29
+ // here rather than discarded: an unreadable store and an editor the user simply does
30
+ // not have both produce zero sessions, and only this tells them apart. See
31
+ // getAdapterErrors().
52
32
  //
53
- // This only answers "can SQLite work at all". It deliberately cannot tell whether a
54
- // given editor's real store is readable: permissions, locking, corruption, and
55
- // SQLCipher all pass this probe and fail later. Those are reported by the adapters
56
- // themselves through adapter-errors.
57
- function probeSqlite() {
58
- try {
59
- const Database = require('better-sqlite3');
60
- const db = new Database(':memory:');
61
- db.close();
62
- return { available: true };
63
- } catch (e) {
64
- const message = ((e && e.message) || String(e)).split('\n')[0];
65
- return { available: false, message, missingBinding: MISSING_BINDING.test(message) };
66
- }
67
- }
68
-
69
- // Cache only success. A failed probe must be retried: a long-running shipper daemon
70
- // that warned about a missing binding should notice once the user installs it,
71
- // rather than repeating the warning for the life of the process.
72
- let sqliteProbe = null;
73
- function sqliteStatus() {
74
- if (sqliteProbe === null) {
75
- const result = probeSqlite();
76
- if (result.available) sqliteProbe = result;
77
- return result;
78
- }
79
- return sqliteProbe;
80
- }
33
+ // There used to be a second, coarser class beside those: better-sqlite3's native
34
+ // binding could be missing outright, taking all five SQLite-backed adapters down at
35
+ // once for a reason none of them could see, so a separate probe ran an in-memory open
36
+ // and reported the verdict against every one of them. SQLite is `node:sqlite` now — it
37
+ // arrives with Node and cannot go missing — so that probe, its MISSING_BINDING regex,
38
+ // and the `missingBinding` flag they put on every error are gone. What remains is the
39
+ // per-store reporting the adapters do themselves: permissions, locking, corruption and
40
+ // schema drift are all still real and all still land here, one line per adapter.
41
+ let adapterErrors = [];
81
42
 
82
43
  /**
83
44
  * Get all chats from all editor adapters, sorted by most recent first.
@@ -92,11 +53,7 @@ function getAllChats() {
92
53
  chats.push(...editorChats);
93
54
  } catch (e) {
94
55
  const message = (e && e.message) || String(e);
95
- adapterErrors.push({
96
- source: editor.name,
97
- message: message.split('\n')[0],
98
- missingBinding: MISSING_BINDING.test(message),
99
- });
56
+ adapterErrors.push({ source: editor.name, message: message.split('\n')[0] });
100
57
  }
101
58
  }
102
59
 
@@ -110,27 +67,19 @@ function getAllChats() {
110
67
  }
111
68
 
112
69
  /**
113
- * Adapters that could not report their sessions: [{ source, message, missingBinding }].
70
+ * Adapters that could not report their sessions: [{ source, message, detail? }].
114
71
  * Empty when every adapter ran clean.
115
72
  *
116
73
  * Two sources, because there are two ways an adapter goes quiet. One throws out of
117
- * getChats() and is recorded during the pass. The other is a dead SQLite binding,
118
- * which throws nowhere at all — the adapters catch it internally — so it is probed
119
- * for directly and reported against every SQLite-backed adapter at once.
74
+ * getChats() and is recorded during the pass. The other catches its own failure — a
75
+ * locked state.vscdb, a thread blob that will not decompress, a schema the parser does
76
+ * not know — and reports it through the adapter-errors sink so the shipper knows not to
77
+ * overwrite a good stored transcript with a partial read.
120
78
  */
121
79
  function getAdapterErrors() {
122
80
  const errors = adapterErrors.slice();
123
81
  for (const e of adapterErrorSink.recorded()) {
124
- errors.push({ source: e.source, message: e.message, detail: e.detail, missingBinding: MISSING_BINDING.test(e.message) });
125
- }
126
- const sqlite = sqliteStatus();
127
- if (!sqlite.available) {
128
- const already = new Set(errors.map((e) => e.source));
129
- for (const source of SQLITE_BACKED) {
130
- if (!already.has(source)) {
131
- errors.push({ source, message: sqlite.message, missingBinding: sqlite.missingBinding });
132
- }
133
- }
82
+ errors.push({ source: e.source, message: e.message, detail: e.detail });
134
83
  }
135
84
  // One line per adapter, not one per failed query — a broken store is usually hit
136
85
  // many times in a single scan.
@@ -1,16 +1,7 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
- // Required lazily. A top-level require makes this native module a load-time dependency
5
- // of the whole adapter set: editors/index.js imports every adapter, so one missing
6
- // binding threw before any adapter ran and `discover` reported ZERO editors — losing the
7
- // eleven that need no native code at all. The adapterErrors path exists to degrade one
8
- // adapter at a time, and a top-level throw walks straight past it.
9
- let Database = null;
10
- function openDb(...a) {
11
- if (!Database) Database = require('better-sqlite3');
12
- return new Database(...a);
13
- }
4
+ const { openReadOnly, textOf } = require('./sqlite');
14
5
  const adapterErrors = require('./adapter-errors');
15
6
 
16
7
  // OpenCode stores data in XDG-style paths across all platforms
@@ -31,13 +22,13 @@ function getOpenCodeDbPath() {
31
22
  const DB_PATH = getOpenCodeDbPath();
32
23
 
33
24
  // ============================================================
34
- // Query SQLite using better-sqlite3
25
+ // Query SQLite using node:sqlite (built into Node — nothing to install)
35
26
  // ============================================================
36
27
 
37
28
  function queryDb(sql) {
38
29
  if (!fs.existsSync(DB_PATH)) return [];
39
30
  try {
40
- const db = openDb(DB_PATH, { readonly: true });
31
+ const db = openReadOnly(DB_PATH);
41
32
  const rows = db.prepare(sql).all();
42
33
  db.close();
43
34
  return rows;
@@ -94,7 +85,7 @@ function getSqliteSessions() {
94
85
  function getSqliteMessages(sessionId) {
95
86
  if (!fs.existsSync(DB_PATH)) return [];
96
87
  try {
97
- const db = openDb(DB_PATH, { readonly: true });
88
+ const db = openReadOnly(DB_PATH);
98
89
  const messages = db.prepare(
99
90
  `SELECT m.id as msg_id, m.data as msg_data, m.time_created
100
91
  FROM message m WHERE m.session_id = ? ORDER BY m.time_created ASC`
@@ -107,7 +98,7 @@ function getSqliteMessages(sessionId) {
107
98
  // yields a TRUNCATED history that looks complete — and a re-ship would replace
108
99
  // the full stored transcript with the shortened one. Report it so the shipper
109
100
  // refuses to overwrite good data with a partial read.
110
- try { msgData = JSON.parse(msg.msg_data); }
101
+ try { msgData = JSON.parse(textOf(msg.msg_data)); }
111
102
  catch (e) { adapterErrors.record('opencode', e, `message ${msg.msg_id}`); continue; }
112
103
 
113
104
  const role = msgData.role;
@@ -120,7 +111,7 @@ function getSqliteMessages(sessionId) {
120
111
  const contentParts = [];
121
112
  for (const part of parts) {
122
113
  let partData;
123
- try { partData = JSON.parse(part.data); }
114
+ try { partData = JSON.parse(textOf(part.data)); }
124
115
  catch (e) { adapterErrors.record('opencode', e, `part of message ${msg.msg_id}`); continue; }
125
116
  const type = partData.type;
126
117
 
@@ -0,0 +1,74 @@
1
+ // SQLite for the five adapters that read an editor's store directly — cursor, zed,
2
+ // opencode, goose, antigravity — plus windsurf's Devin API-key lookup.
3
+ //
4
+ // This was `better-sqlite3`, a native module whose prebuilt binary arrives through an
5
+ // npm install script — and npm >= 12 blocks install scripts by default, so a plain
6
+ // `npm i -g memhouse` left no binding at all. Those five adapters then read ZERO
7
+ // sessions and looked exactly like editors the pilot does not have. The fix was a flag
8
+ // (`--allow-scripts=better-sqlite3`) that had to be typed on every install and every
9
+ // upgrade, was never remembered by npm, and produced no error when omitted.
10
+ //
11
+ // `node:sqlite` is part of Node itself — stable in 24, no build step, no binary to
12
+ // misplace, nothing to allow. The whole "adapter silently skipped for a missing native
13
+ // binding" failure class is gone, and install is one command with no flags.
14
+ //
15
+ // Still required lazily, but for a different reason than before. On a Node older than
16
+ // the engines floor `require('node:sqlite')` throws, and editors/index.js imports every
17
+ // adapter at load time — a top-level require here would take the eleven adapters that
18
+ // need no SQLite at all down with the five that do, and `discover` would report zero
19
+ // editors. Lazy, that throw lands inside each adapter's own try/catch and is reported
20
+ // through adapter-errors like any other unreadable store.
21
+ let DatabaseSync = null;
22
+
23
+ /**
24
+ * Open an editor's store read-only.
25
+ *
26
+ * Every call site in this repo is a read, so there is no write mode to get wrong.
27
+ * better-sqlite3 spelled the option `readonly` and needed `fileMustExist` beside it to
28
+ * stop a typo'd path from quietly creating an empty database; node:sqlite spells it
29
+ * `readOnly` and refuses on its own — measured, a missing file throws ERR_SQLITE_ERROR
30
+ * "unable to open database file" rather than creating one. Callers that check
31
+ * fs.existsSync() first keep working unchanged; callers that do not still cannot
32
+ * conjure a store that is not there.
33
+ */
34
+ function openReadOnly(file) {
35
+ if (!DatabaseSync) ({ DatabaseSync } = require('node:sqlite'));
36
+ return new DatabaseSync(file, { readOnly: true });
37
+ }
38
+
39
+ /**
40
+ * A column's value as text, whether SQLite handed back TEXT or BLOB.
41
+ *
42
+ * SQLite types values, not columns, and these stores are not consistent about it: the
43
+ * VS Code family declares `ItemTable.value` as BLOB but stores strings in it, while
44
+ * Cursor's agent store keeps real bytes in the same shape of table. better-sqlite3
45
+ * returned a Buffer for the byte case and every `JSON.parse(row.value)` in these
46
+ * adapters worked by accident, because Buffer.toString() decodes utf-8.
47
+ *
48
+ * node:sqlite returns a plain Uint8Array, whose toString() renders "40,181,47,253,…" —
49
+ * a comma-joined list of byte values. That is not utf-8, not JSON, and not empty, so it
50
+ * does not fail loudly; it fails four frames deeper as a parse error against a store
51
+ * that is perfectly healthy. Every read that may be either type goes through here.
52
+ */
53
+ function textOf(v) {
54
+ if (v == null) return v;
55
+ if (typeof v === 'string') return v;
56
+ if (v instanceof Uint8Array) return Buffer.from(v.buffer, v.byteOffset, v.byteLength).toString('utf-8');
57
+ return String(v);
58
+ }
59
+
60
+ /**
61
+ * A column's value as a Buffer, for readers that slice it, hex-encode it, or hand it to
62
+ * zlib. Buffer IS a Uint8Array, so this is a no-op on anything better-sqlite3 would have
63
+ * returned — it exists so the byte-walking code below never has to ask which it got.
64
+ */
65
+ function bytesOf(v) {
66
+ if (v == null) return v;
67
+ if (Buffer.isBuffer(v)) return v;
68
+ if (typeof v === 'string') return Buffer.from(v, 'utf-8');
69
+ // A view over the same memory, not a copy — these blobs run to megabytes and every
70
+ // consumer here only reads.
71
+ return Buffer.from(v.buffer, v.byteOffset, v.byteLength);
72
+ }
73
+
74
+ module.exports = { openReadOnly, textOf, bytesOf };
@@ -484,13 +484,13 @@ function getDevinApiKey(appNames) {
484
484
  dbPath = path.join(HOME, '.config', appName, 'User', 'globalStorage', 'state.vscdb');
485
485
  }
486
486
  if (!fs.existsSync(dbPath)) continue;
487
- const Database = require('better-sqlite3');
488
- const db = new Database(dbPath, { readonly: true });
487
+ const { openReadOnly, textOf } = require('./sqlite');
488
+ const db = openReadOnly(dbPath);
489
489
  try {
490
490
  for (const key of keys) {
491
491
  const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
492
492
  if (!row) continue;
493
- const parsed = JSON.parse(row.value);
493
+ const parsed = JSON.parse(textOf(row.value));
494
494
  if (parsed.apiKey) return parsed.apiKey;
495
495
  }
496
496
  } finally {
package/editors/zed.js CHANGED
@@ -2,16 +2,7 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
 
5
- // Required lazily. A top-level require makes this native module a load-time dependency
6
- // of the whole adapter set: editors/index.js imports every adapter, so one missing
7
- // binding threw before any adapter ran and `discover` reported ZERO editors — losing the
8
- // eleven that need no native code at all. The adapterErrors path exists to degrade one
9
- // adapter at a time, and a top-level throw walks straight past it.
10
- let Database = null;
11
- function openDb(...a) {
12
- if (!Database) Database = require('better-sqlite3');
13
- return new Database(...a);
14
- }
5
+ const { openReadOnly, bytesOf } = require('./sqlite');
15
6
  const adapterErrors = require('./adapter-errors');
16
7
 
17
8
  // Zed stores data in different locations depending on the platform
@@ -72,13 +63,13 @@ function decompressZstd(buf) {
72
63
  }
73
64
 
74
65
  // ============================================================
75
- // Query SQLite using better-sqlite3 (cross-platform)
66
+ // Query SQLite using node:sqlite (cross-platform, no native dependency)
76
67
  // ============================================================
77
68
 
78
69
  function queryDb(sql) {
79
70
  if (!fs.existsSync(THREADS_DB)) return [];
80
71
  try {
81
- const db = openDb(THREADS_DB, { readonly: true });
72
+ const db = openReadOnly(THREADS_DB);
82
73
  const rows = db.prepare(sql).all();
83
74
  db.close();
84
75
  return rows;
@@ -100,7 +91,7 @@ function getThreadColumns() {
100
91
  if (threadColumns) return threadColumns;
101
92
  if (!fs.existsSync(THREADS_DB)) return new Set();
102
93
  try {
103
- const db = openDb(THREADS_DB, { readonly: true });
94
+ const db = openReadOnly(THREADS_DB);
104
95
  const cols = db.prepare('PRAGMA table_info(threads)').all().map((r) => r.name);
105
96
  db.close();
106
97
  // PRAGMA on a missing or renamed table SUCCEEDS with zero rows rather than
@@ -131,10 +122,14 @@ function resetCache() {
131
122
  function queryBlob(id) {
132
123
  if (!fs.existsSync(THREADS_DB)) return null;
133
124
  try {
134
- const db = openDb(THREADS_DB, { readonly: true });
125
+ const db = openReadOnly(THREADS_DB);
135
126
  const row = db.prepare('SELECT data FROM threads WHERE id = ?').get(id);
136
127
  db.close();
137
- return row ? row.data : null;
128
+ // As a Buffer, not the raw Uint8Array node:sqlite hands back. Both callers below
129
+ // treat it as one: the uncompressed path calls .toString('utf-8') on it, which on a
130
+ // bare Uint8Array yields "40,181,47,…" — the byte VALUES, comma-joined — and the
131
+ // thread would then fail JSON.parse as if it were corrupt.
132
+ return row ? bytesOf(row.data) : null;
138
133
  } catch (e) {
139
134
  // This is the message-blob read. A silent null here becomes a zero-message
140
135
  // session, which overwrites the stored transcript on re-ship.
@@ -141,11 +141,15 @@ memory. None ingest agent sessions.
141
141
  into *installed skills*. Our skills must be chosen. One independent analysis of 270
142
142
  real sessions found **only 4.8% involved an explicit history query** — a pull-only
143
143
  model gets invoked rarely.
144
- - **Install weight — we are the heaviest in the category.** AgentsView, deja-vu and
145
- ctx are single dependency-free binaries via Homebrew, Docker, `go install` or curl.
146
- We are Node, need `--allow-scripts=better-sqlite3` (five adapters silently read
147
- nothing without it), and additionally need a ClickHouse to point at. Our README
148
- spends paragraphs on `EACCES`/`sudo` triage. That is a real adoption tax.
144
+ - **Install weight — still the heaviest in the category, but half the tax is paid.**
145
+ AgentsView, deja-vu and ctx are single dependency-free binaries via Homebrew, Docker,
146
+ `go install` or curl. We are Node and need a ClickHouse to point at.
147
+ **Resolved 2026-08-16:** the native-dependency half is gone. `better-sqlite3` was
148
+ replaced by `node:sqlite`, so `npm i -g memhouse` with no flags is now a complete,
149
+ fully working install — no `--allow-scripts`, no build step, and no five-adapters-
150
+ silently-read-nothing failure mode. What remains of the tax is the runtime (Node,
151
+ not a static binary), the ClickHouse dependency, and the `EACCES`/`sudo` prefix
152
+ triage, which was never about the flag and is not fixable from our side.
149
153
  - **Semantic search.** AgentsView (opt-in index), claude-mem (Chroma),
150
154
  claude-history (local embedding model). We are FTS-only. Contested rather than
151
155
  settled — deja-vu made the same choice and was criticised for it on HN.
@@ -95,18 +95,15 @@ original to duplicate the copy.
95
95
 
96
96
  ### The gap
97
97
 
98
- `npm i -g memhouse@latest` does one third of the job, and the other two thirds have each
99
- cost a real machine real sessions:
100
-
101
- 1. **`--allow-scripts=better-sqlite3` is not remembered.** Omit it on the upgrade and
102
- `better-sqlite3` has no binding again — cursor, zed, opencode, goose and antigravity
103
- read zero sessions on every pass afterwards.
104
- 2. **The daemons keep running the old code.** See §3.
105
- 3. **The house can be older than the shipper.** A newer shipper may need a column an
98
+ `npm i -g memhouse@latest` does half the job, and the other half has cost a real machine
99
+ real sessions:
100
+
101
+ 1. **The daemons keep running the old code.** See §3.
102
+ 2. **The house can be older than the shipper.** A newer shipper may need a column an
106
103
  existing room lacks; it detects that and prints the rebuild, but only if something runs
107
104
  it.
108
105
 
109
- And from a checkout there is a fourth: `git pull` updates `ui/src` and leaves the built
106
+ And from a checkout there is a third: `git pull` updates `ui/src` and leaves the built
110
107
  bundle in `public/`, so the dashboard serves the previous release however often it is
111
108
  restarted. A published tarball is immune — `prepack` builds `public/` before publish.
112
109
 
@@ -114,7 +111,7 @@ restarted. A published tarball is immune — `prepack` builds `public/` before p
114
111
 
115
112
  | Kind | Detected by | Action |
116
113
  |---|---|---|
117
- | `global` | under `npm prefix -g` | `npm i -g memhouse@latest --allow-scripts=better-sqlite3` |
114
+ | `global` | under `npm prefix -g` | `npm i -g memhouse@latest` |
118
115
  | `checkout` | a `.git` at the root | `git pull --ff-only` → `npm install` → `npm run build` |
119
116
  | `local-dep` | `node_modules/memhouse`, not under the global prefix | refuse; print the right command for where it lives |
120
117
  | `npx` | `_npx` in the resolved path | refuse — the next `npx` resolves the registry anyway |
@@ -7,21 +7,21 @@ in order; every command is copy-pasteable.
7
7
  machine it runs on — which editors have sessions, whether a ClickHouse answers, whether
8
8
  docker or podman exists — and states the one route that applies. This file is the static
9
9
  long-form version and assumes a reachable ClickHouse already exists, which is the case
10
- `deploy --local` was added to remove. Prereqs: Node >= 20.19 and a reachable
10
+ `deploy --local` was added to remove. Prereqs: Node >= 24 and a reachable
11
11
  ClickHouse >= 26.2 (local server, Cloud, or a kernel house — see
12
12
  `kernel-install.md` for the kernel path). No checkout required — memhouse is on npm.
13
13
 
14
14
  ## 1. Install
15
15
 
16
16
  ```bash
17
- npm install -g memhouse --allow-scripts=better-sqlite3
17
+ npm install -g memhouse
18
18
  ```
19
19
 
20
- `--allow-scripts=better-sqlite3` is required, not cosmetic. Five adapters (cursor,
21
- goose, opencode, zed, antigravity) read SQLite session stores, and npm 12 blocks
22
- the install script that builds the native binding. Without it those five silently
23
- return zero sessions and you ship a partial history. Verify with step 4 —
24
- `memhouse discover` names any adapter it had to skip.
20
+ No flags. This line used to require `--allow-scripts=better-sqlite3`, without which
21
+ five adapters (cursor, goose, opencode, zed, antigravity) silently returned zero
22
+ sessions and you shipped a partial history. SQLite now comes from `node:sqlite`, which
23
+ is part of Node, so there is no install script and nothing to allow. Step 4 still
24
+ verifies — `memhouse discover` names any adapter it had to skip, for any reason.
25
25
 
26
26
  On `EACCES`, decide by **who owns the prefix root**. Not the path — Homebrew's
27
27
  prefix is `/usr/local` on Intel and `/opt/homebrew` on Apple Silicon and is
@@ -34,7 +34,7 @@ ls -ld "$(npm prefix -g)" # the prefix ROOT decides
34
34
  ```
35
35
 
36
36
  Prefix root owned by `root` → a system-managed Node; re-run as
37
- `sudo npm install -g memhouse --allow-scripts=better-sqlite3`.
37
+ `sudo npm install -g memhouse`.
38
38
 
39
39
  Prefix root owned by the current user → the prefix is theirs (Homebrew, fnm, nvm,
40
40
  volta), so the root-owned file npm tripped on is a stray from an earlier `sudo npm`
@@ -42,8 +42,8 @@ and another `sudo` adds more. Repair that path instead:
42
42
  `sudo chown -R "$(id -u):$(id -g)" <path npm named>` (same for `~/.npm`).
43
43
 
44
44
  From a checkout instead (contributors): `npm install --no-audit --no-fund` in the
45
- repo root — its `allowScripts` field already covers the binding — then substitute
46
- `node bin/memhouse.js` for `memhouse` below.
45
+ repo root — nothing compiles — then substitute `node bin/memhouse.js` for `memhouse`
46
+ below.
47
47
 
48
48
  ## 2. Configure the connection
49
49
 
@@ -77,9 +77,11 @@ memhouse discover # which adapters were read, and which were SKIPPED
77
77
  memhouse stats # per-source counts now in the house
78
78
  ```
79
79
 
80
- Read `discover` first. It prints one line per editor it found sessions for, and
81
- warns by name about any adapter it had to skip — that warning is the only signal
82
- that step 1's `--allow-scripts` flag was missed and five editors are dark.
80
+ Read `discover` first. It prints one line per editor it found sessions for, and warns
81
+ by name about any adapter it had to skip. On a healthy install that list is empty: the
82
+ one cause that could take five adapters out at once — a missing native SQLite binding —
83
+ no longer exists, so anything named here is a single editor's own store (locked by a
84
+ running editor, corrupt, or on a schema this parser does not know).
83
85
 
84
86
  `stats` should show one row per source (claude-code, codex, gemini-cli, …) with
85
87
  non-zero sessions/messages. Or verify by SQL — rooms are named for your ClickHouse
@@ -35,7 +35,7 @@ rotate on first connect (`issue-credential{user:'memhouse_root'}`).
35
35
  ## 2. Owner sets up the house
36
36
 
37
37
  ```bash
38
- npm install -g memhouse --allow-scripts=better-sqlite3
38
+ npm install -g memhouse
39
39
  memhouse install --yes \
40
40
  --url https://<kernel-host>:8443 \
41
41
  --user memhouse_root \
@@ -46,9 +46,9 @@ memhouse install --yes \
46
46
  That writes `~/.memhouse/env`, applies the schema, and runs the first ship.
47
47
  `memhouse start` then keeps it fresh on a 300s loop.
48
48
 
49
- Keep `--allow-scripts=better-sqlite3`: without it npm 12 leaves `better-sqlite3`
50
- with no native binding and the five SQLite-backed adapters ship nothing, silently.
51
- `memhouse discover` names any adapter it had to skip.
49
+ No flags: SQLite comes from `node:sqlite`, so there is no native binding to build
50
+ and no adapter that can go dark for want of one. `memhouse discover` still names any
51
+ adapter it had to skip — now always a single editor's own store.
52
52
 
53
53
  The kernel never runs or reads any of this — it is content-blind; the shipper is
54
54
  the agency's own resident, a deterministic `worker` on a loop (no LLM, not an
@@ -50,14 +50,19 @@ const MEMBER_PIN = 'async_insert = 0 CONST';
50
50
  /**
51
51
  * The global-install command that will actually work HERE.
52
52
  *
53
- * Every site that printed `npm install -g memhouse --allow-scripts=better-sqlite3` printed
54
- * a line that fails with EACCES on any distro-packaged Node, because the global prefix is
55
- * root-owned — measured on a clean Ubuntu machine, exit 243. `sudo` is right for exactly
56
- * one of the two causes and makes the other worse, and what tells them apart is who owns
57
- * the prefix — so look, rather than guess.
53
+ * The flag half of this is gone: the command carried `--allow-scripts=better-sqlite3`
54
+ * for as long as SQLite was a native module, and omitting it silently cost five
55
+ * adapters. `node:sqlite` needs no install script, so the base command is now the plain
56
+ * one everybody already types.
57
+ *
58
+ * The `sudo` half stays, because it was never about the flag. A bare install fails with
59
+ * EACCES on any distro-packaged Node, because the global prefix is root-owned — measured
60
+ * on a clean Ubuntu machine, exit 243. `sudo` is right for exactly one of the two causes
61
+ * and makes the other worse, and what tells them apart is who owns the prefix — so look,
62
+ * rather than guess.
58
63
  */
59
64
  function installCommand() {
60
- const base = 'npm install -g memhouse --allow-scripts=better-sqlite3';
65
+ const base = 'npm install -g memhouse';
61
66
  try {
62
67
  const { execFileSync } = require('child_process');
63
68
  const prefix = execFileSync('npm', ['prefix', '-g'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
@@ -6,10 +6,10 @@
6
6
  // of the CLI you just typed, not the version the daemon is running. On macminim a shipper
7
7
  // ran 1d16h out of a directory that had been MOVED — the fix was live, the machine was not.
8
8
  //
9
- // npm's own `postinstall` is the obvious hook and the wrong one. Install scripts are
10
- // blocked by default (`--allow-scripts`), which is the very condition this product already
11
- // has to work around, and a package install that restarts a user's daemons is a surprise
12
- // even when it works.
9
+ // npm's own `postinstall` is the obvious hook and the wrong one. npm >= 12 blocks install
10
+ // scripts by default, so it would not run at all on a stock install — the same default
11
+ // that cost this product its SQLite binding until `node:sqlite` removed the need for one —
12
+ // and a package install that restarts a user's daemons is a surprise even when it works.
13
13
  //
14
14
  // So the daemon notices for itself. Once per loop pass it compares what is on disk against
15
15
  // what it booted with, and when they differ it hands over:
@@ -792,18 +792,17 @@ async function runShip(client, opts = {}) {
792
792
  return { sessions, skipped, msgRows, toolRows, unreadable };
793
793
  }
794
794
 
795
- // Warn once per adapter per pass. `warned` carries across the two call sites so a
796
- // dead binding is not reported twice in the same run.
795
+ // Warn once per adapter per pass. `warned` carries across the two call sites so one
796
+ // unreadable store is not reported twice in the same run.
797
+ //
798
+ // The loud case used to be a missing better-sqlite3 binding, which took five adapters
799
+ // out together and came with a reinstall command attached. SQLite ships with Node now;
800
+ // what is left is per-store, and each error already says what happened.
797
801
  function reportAdapterErrors(warned) {
798
802
  const errors = getAdapterErrors().filter((e) => !warned.has(e.source));
799
803
  if (!errors.length) return;
800
804
  for (const e of errors) warned.add(e.source);
801
- const noBinding = errors.filter((e) => e.missingBinding).map((e) => e.source);
802
- if (noBinding.length) {
803
- console.log(`[memhouse] WARNING: ${noBinding.length} adapter(s) skipped, sessions NOT shipped — better-sqlite3 has no native binding: ${noBinding.join(', ')}`);
804
- console.log(`[memhouse] fix: ${require('../house/house').installCommand()}`);
805
- }
806
- for (const e of errors.filter((x) => !x.missingBinding)) {
805
+ for (const e of errors) {
807
806
  console.log(`[memhouse] WARNING: ${e.source} skipped — ${e.message}`);
808
807
  }
809
808
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "memhouse — agent conversation memory across 17 editors: parse-on-client shippers, a typed ClickHouse store with one set of rooms per member, kernel-agency install, and the agentlytics dashboard",
5
5
  "main": "bin/memhouse.js",
6
6
  "bin": {
@@ -43,10 +43,7 @@
43
43
  "author": "Ramazan Polat <ramazanpolat@gmail.com> (built on agentlytics by fkadev)",
44
44
  "license": "MIT",
45
45
  "engines": {
46
- "node": ">=20.19.0"
47
- },
48
- "allowScripts": {
49
- "better-sqlite3": true
46
+ "node": ">=24.0.0"
50
47
  },
51
48
  "repository": {
52
49
  "type": "git",
@@ -54,7 +51,6 @@
54
51
  },
55
52
  "dependencies": {
56
53
  "@clickhouse/client": "^1.23.1",
57
- "better-sqlite3": "^12.6.2",
58
54
  "express": "^4.22.1"
59
55
  }
60
56
  }