memhouse 0.4.2 → 0.4.3

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/bin/memhouse.js CHANGED
@@ -320,6 +320,8 @@ Data ship one incremental pass (--full | --loop [sec])
320
320
 
321
321
  Agents plugins list | install claude [--target DIR] | remove claude
322
322
  prompt print the memory system-prompt snippet
323
+ prompt --install print an install prompt for an agent, with this
324
+ machine's state and the one route that applies
323
325
 
324
326
  House deploy --local run ClickHouse in docker/podman, then install
325
327
  deploy --down remove the local house (container + volume)
@@ -394,6 +396,129 @@ async function cmdDiscover() {
394
396
  return out;
395
397
  }
396
398
 
399
+ /**
400
+ * Render the agent install prompt with this machine's facts.
401
+ *
402
+ * The value is in what it removes: the CLI has already probed for a house, an engine and
403
+ * an existing config, so the prompt can state the ONE route that applies. A static
404
+ * runbook makes the agent derive that from a decision tree, which is where it picks the
405
+ * wrong branch — most expensively by asking a user with no ClickHouse for a ClickHouse
406
+ * URL, which is exactly what onboard itself used to do.
407
+ */
408
+ async function renderInstallPrompt() {
409
+ const tplPath = path.join(DELIVERY, 'AGENT-INSTALL-PROMPT.md');
410
+ const tpl = fs.readFileSync(tplPath, 'utf-8');
411
+ const body = tpl.split('<!-- PROMPT BODY BELOW -->')[1] || tpl;
412
+
413
+ const cfg = resolveConfig();
414
+ const configured = fs.existsSync(ENV_FILE);
415
+ const engines = deployableEngines();
416
+
417
+ // Probe rather than assume. JSON_OUT is forced off so cmdDiscover returns its object
418
+ // instead of printing JSON and returning undefined.
419
+ const saved = JSON_OUT;
420
+ let found = null;
421
+ try {
422
+ found = await withSuppressedOutput(() => cmdDiscover());
423
+ } catch { /* fall through to the unknown-state wording */ } finally { void saved; }
424
+
425
+ const editors = (found && found.editors || []).filter((e) => e.sessions > 0);
426
+ const reachable = (found && found.clickhouse || []).filter((p) => p.reachable);
427
+ const houseUp = reachable.length > 0;
428
+
429
+ // Is an existing install actually working, or configured-but-broken? A broken one is
430
+ // the case a plain "install it" prompt handles worst.
431
+ let state = 'not installed';
432
+ if (configured) {
433
+ let ok0 = false;
434
+ try { ok0 = !!(await ch(cfg, 'SELECT 1')); } catch { ok0 = false; }
435
+ state = ok0 ? 'installed and connected' : 'configured, but the house does not answer';
436
+ }
437
+
438
+ let plan;
439
+ if (state === 'installed and connected') {
440
+ plan = `memhouse is already installed and connected to \`${cfg.url}\` (database \`${cfg.db}\`).
441
+ **Do not reinstall.** Verify and hand back:
442
+
443
+ 1. \`memhouse doctor\` — every line should be a check mark.
444
+ 2. \`memhouse status\` — note the session and message counts.
445
+ 3. If the shipper is not running: \`memhouse start\`.
446
+ 4. If \`doctor\` reports rooms missing, the house may predate 0.4.0; it will say so by
447
+ name and print the recovery. Follow what it prints rather than reinstalling.`;
448
+ } else if (state.startsWith('configured')) {
449
+ plan = `memhouse is configured to use \`${cfg.url}\` (database \`${cfg.db}\`) but that
450
+ endpoint does not answer. Do NOT reconfigure before finding out why.
451
+
452
+ 1. Is the house simply stopped? If it was a local container, \`memhouse deploy --local\`
453
+ brings the same volume back — it does not lose data.
454
+ 2. Is it a remote house the user's network cannot reach right now? Ask them; do not
455
+ repoint their config at something else on your own initiative.
456
+ 3. Only once the user says the old house is gone for good should you set up a new one,
457
+ and then say plainly that past sessions re-ship from local transcripts but anything
458
+ whose transcript was deleted lives only in the old house.`;
459
+ } else if (houseUp) {
460
+ plan = `A ClickHouse is already reachable at \`${reachable[0].url}\`. Use it.
461
+
462
+ 1. \`npm install -g memhouse --allow-scripts=better-sqlite3\`
463
+ The flag is not optional — six adapters (cursor, zed, opencode, goose, windsurf,
464
+ antigravity) read SQLite stores and silently drop out without the native build.
465
+ 2. Ask the user for the ClickHouse username and password for that endpoint.
466
+ 3. \`memhouse install --url ${reachable[0].url} --user <user> --password <pw>\`
467
+ If they hold admin on it and want memhouse to create the user, database, rooms and
468
+ grants for them, use \`--admin-user\`/\`--admin-password\` instead; memhouse then
469
+ reconnects as the new member and never stores the admin credential.
470
+ If they would rather run the SQL themselves, \`memhouse install --print-sql\` prints
471
+ it and touches nothing.
472
+ 4. \`memhouse start\`.`;
473
+ } else if (engines.length) {
474
+ plan = `There is no ClickHouse to point at, but **${engines.join(' and ')}** is available,
475
+ so memhouse can run one:
476
+
477
+ 1. \`npm install -g memhouse --allow-scripts=better-sqlite3\`
478
+ The flag is not optional — six adapters read SQLite stores and silently drop out
479
+ without the native build.
480
+ 2. \`memhouse deploy --local\`
481
+ This starts a loopback-only ClickHouse, generates a credential, installs, creates the
482
+ user's rooms and runs the first ship. One command, no questions.
483
+ 3. \`memhouse start\`.
484
+
485
+ ${process.platform === 'linux' && engines.includes('podman')
486
+ ? 'On Linux with rootless podman the container stops at logout unless lingering is on.\nIf memhouse warns about this, run what it prints (`loginctl enable-linger <user>`)\nand tell the user why it matters.' : ''}`;
487
+ } else {
488
+ plan = `There is no ClickHouse reachable and no container engine to run one with. You
489
+ cannot finish this install alone — say so rather than improvising.
490
+
491
+ Tell the user they need one of:
492
+ - a ClickHouse they already run (Cloud, a server, a kernel house) plus its credentials;
493
+ - docker or podman installed, after which \`memhouse deploy --local\` does everything.
494
+
495
+ You can still do the harmless half now: \`npm install -g memhouse --allow-scripts=better-sqlite3\`,
496
+ then \`memhouse discover\` to show them what would be shipped once a house exists.`;
497
+ }
498
+
499
+ const out = body
500
+ .replaceAll('{{VERSION}}', PKG.version)
501
+ .replaceAll('{{PLATFORM}}', `${process.platform}/${process.arch}`)
502
+ .replaceAll('{{NODE}}', process.versions.node)
503
+ .replaceAll('{{STATE}}', state)
504
+ .replaceAll('{{EDITORS}}', editors.length
505
+ ? editors.map((e) => `${e.source} (${e.sessions})`).join(', ')
506
+ : 'none found — memhouse would ship nothing until an editor is used')
507
+ .replaceAll('{{HOUSE}}', houseUp ? reachable.map((p) => p.url).join(', ') : 'none reachable')
508
+ .replaceAll('{{ENGINES}}', engines.length ? engines.join(', ') : 'none (no docker, no podman)')
509
+ .replaceAll('{{PLAN}}', plan.trim())
510
+ .replaceAll('{{PORT}}', String(cfg.port));
511
+ process.stdout.write(out.trimStart());
512
+ return 0;
513
+ }
514
+
515
+ /** Run a printing function with stdout swallowed, and give back its return value. */
516
+ async function withSuppressedOutput(fn) {
517
+ const write = process.stdout.write.bind(process.stdout);
518
+ process.stdout.write = () => true;
519
+ try { return await fn(); } finally { process.stdout.write = write; }
520
+ }
521
+
397
522
  /**
398
523
  * Can this machine stand up a house of its own?
399
524
  *
@@ -1123,7 +1248,13 @@ function cmdUninstall() {
1123
1248
  break;
1124
1249
  }
1125
1250
  case 'plugins': process.exitCode = cmdPlugins(); break;
1126
- case 'prompt': process.stdout.write(fs.readFileSync(path.join(DELIVERY, 'PROMPT.md'), 'utf-8')); break;
1251
+ case 'prompt':
1252
+ // Two audiences, two prompts. Bare `prompt` is the memory-USAGE snippet that goes
1253
+ // into a running agent's system prompt; `--install` is the one you hand an agent
1254
+ // that has not installed memhouse yet.
1255
+ if (flags.install === true) process.exitCode = await renderInstallPrompt();
1256
+ else process.stdout.write(fs.readFileSync(path.join(DELIVERY, 'PROMPT.md'), 'utf-8'));
1257
+ break;
1127
1258
  case 'reset': process.exitCode = await cmdReset(); break;
1128
1259
  case 'deploy': {
1129
1260
  const dep = require(path.join(REPO_ROOT, 'mem-house', 'deploy.js'));
@@ -2,7 +2,16 @@ 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
- const Database = require('better-sqlite3');
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
+ }
6
15
  const { getAppDataPath } = require('./base');
7
16
  const adapterErrors = require('./adapter-errors');
8
17
 
@@ -235,7 +244,7 @@ function readGlobalStateValue(key) {
235
244
 
236
245
  let db = null;
237
246
  try {
238
- db = new Database(ANTIGRAVITY_GLOBAL_STORAGE_DB, { readonly: true, fileMustExist: true });
247
+ db = openDb(ANTIGRAVITY_GLOBAL_STORAGE_DB, { readonly: true, fileMustExist: true });
239
248
  const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
240
249
  if (!row) return null;
241
250
  const v = row.value;
package/editors/cursor.js CHANGED
@@ -1,4 +1,13 @@
1
- const Database = require('better-sqlite3');
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
+ }
2
11
  const path = require('path');
3
12
  const fs = require('fs');
4
13
  const os = require('os');
@@ -147,7 +156,7 @@ function getWorkspaceMap() {
147
156
 
148
157
  function getComposerHeaders(stateDbPath) {
149
158
  try {
150
- const db = new Database(stateDbPath, { readonly: true });
159
+ const db = openDb(stateDbPath, { readonly: true });
151
160
  const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'composer.composerData'").get();
152
161
  db.close();
153
162
  if (!row) return [];
@@ -277,7 +286,7 @@ function getChats() {
277
286
  // Source 1: ~/.cursor/chats store.db
278
287
  for (const { workspace, chatId, dbPath } of getAgentStoreChats()) {
279
288
  try {
280
- const db = new Database(dbPath, { readonly: true });
289
+ const db = openDb(dbPath, { readonly: true });
281
290
  const meta = readStoreMeta(db);
282
291
  db.close();
283
292
  if (meta) {
@@ -316,7 +325,7 @@ function getChats() {
316
325
 
317
326
  // Source 2: workspaceStorage composers
318
327
  let globalDb = null;
319
- try { globalDb = new Database(GLOBAL_STORAGE_DB, { readonly: true }); }
328
+ try { globalDb = openDb(GLOBAL_STORAGE_DB, { readonly: true }); }
320
329
  catch (e) {
321
330
  // Absent is normal — Cursor may simply not be installed. Unreadable is not.
322
331
  if (fs.existsSync(GLOBAL_STORAGE_DB)) adapterErrors.record('cursor', e, GLOBAL_STORAGE_DB);
@@ -369,7 +378,7 @@ function getChats() {
369
378
 
370
379
  function getMessages(chat) {
371
380
  if (chat._type === 'agent-store') {
372
- const db = new Database(chat._dbPath, { readonly: true });
381
+ const db = openDb(chat._dbPath, { readonly: true });
373
382
  const msgs = collectStoreMessages(db, chat._rootBlobId);
374
383
  db.close();
375
384
  // Use lastUsedModel as fallback for assistant messages without model info
@@ -382,7 +391,7 @@ function getMessages(chat) {
382
391
  }
383
392
 
384
393
  let globalDb;
385
- try { globalDb = new Database(GLOBAL_STORAGE_DB, { readonly: true }); }
394
+ try { globalDb = openDb(GLOBAL_STORAGE_DB, { readonly: true }); }
386
395
  catch (e) {
387
396
  // Returning [] unreported would let a re-ship overwrite this session's stored
388
397
  // transcript with nothing.
@@ -407,7 +416,7 @@ function getMessages(chat) {
407
416
 
408
417
  function getCursorAccessToken() {
409
418
  try {
410
- const db = new Database(GLOBAL_STORAGE_DB, { readonly: true });
419
+ const db = openDb(GLOBAL_STORAGE_DB, { readonly: true });
411
420
  const row = db.prepare("SELECT value FROM ItemTable WHERE key = 'cursorAuth/accessToken'").get();
412
421
  db.close();
413
422
  return row ? row.value : null;
@@ -1,7 +1,16 @@
1
1
  const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
- const Database = require('better-sqlite3');
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
+ }
5
14
  const adapterErrors = require('./adapter-errors');
6
15
 
7
16
  // OpenCode stores data in XDG-style paths across all platforms
@@ -28,7 +37,7 @@ const DB_PATH = getOpenCodeDbPath();
28
37
  function queryDb(sql) {
29
38
  if (!fs.existsSync(DB_PATH)) return [];
30
39
  try {
31
- const db = new Database(DB_PATH, { readonly: true });
40
+ const db = openDb(DB_PATH, { readonly: true });
32
41
  const rows = db.prepare(sql).all();
33
42
  db.close();
34
43
  return rows;
@@ -85,7 +94,7 @@ function getSqliteSessions() {
85
94
  function getSqliteMessages(sessionId) {
86
95
  if (!fs.existsSync(DB_PATH)) return [];
87
96
  try {
88
- const db = new Database(DB_PATH, { readonly: true });
97
+ const db = openDb(DB_PATH, { readonly: true });
89
98
  const messages = db.prepare(
90
99
  `SELECT m.id as msg_id, m.data as msg_data, m.time_created
91
100
  FROM message m WHERE m.session_id = ? ORDER BY m.time_created ASC`
package/editors/zed.js CHANGED
@@ -2,7 +2,16 @@ const path = require('path');
2
2
  const fs = require('fs');
3
3
  const os = require('os');
4
4
 
5
- const Database = require('better-sqlite3');
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
+ }
6
15
  const adapterErrors = require('./adapter-errors');
7
16
 
8
17
  // Zed stores data in different locations depending on the platform
@@ -69,7 +78,7 @@ function decompressZstd(buf) {
69
78
  function queryDb(sql) {
70
79
  if (!fs.existsSync(THREADS_DB)) return [];
71
80
  try {
72
- const db = new Database(THREADS_DB, { readonly: true });
81
+ const db = openDb(THREADS_DB, { readonly: true });
73
82
  const rows = db.prepare(sql).all();
74
83
  db.close();
75
84
  return rows;
@@ -91,7 +100,7 @@ function getThreadColumns() {
91
100
  if (threadColumns) return threadColumns;
92
101
  if (!fs.existsSync(THREADS_DB)) return new Set();
93
102
  try {
94
- const db = new Database(THREADS_DB, { readonly: true });
103
+ const db = openDb(THREADS_DB, { readonly: true });
95
104
  const cols = db.prepare('PRAGMA table_info(threads)').all().map((r) => r.name);
96
105
  db.close();
97
106
  // PRAGMA on a missing or renamed table SUCCEEDS with zero rows rather than
@@ -122,7 +131,7 @@ function resetCache() {
122
131
  function queryBlob(id) {
123
132
  if (!fs.existsSync(THREADS_DB)) return null;
124
133
  try {
125
- const db = new Database(THREADS_DB, { readonly: true });
134
+ const db = openDb(THREADS_DB, { readonly: true });
126
135
  const row = db.prepare('SELECT data FROM threads WHERE id = ?').get(id);
127
136
  db.close();
128
137
  return row ? row.data : null;
@@ -0,0 +1,58 @@
1
+ # memhouse — agent install prompt (template)
2
+
3
+ `memhouse prompt --install` renders this with THIS machine's facts substituted and
4
+ prints it. Feed the output to a coding agent; it has everything needed to install and
5
+ onboard without asking the user to look anything up.
6
+
7
+ The point of rendering rather than shipping a static runbook: the CLI has already
8
+ probed the machine, so the prompt states the route that applies instead of making the
9
+ agent reason through a decision tree and possibly pick wrong. `AGENT-INSTALL.md` is the
10
+ static long-form version, and it predates `deploy --local`.
11
+
12
+ Placeholders are `{{NAME}}`. Everything below the marker is the prompt body.
13
+
14
+ <!-- PROMPT BODY BELOW -->
15
+ You are installing **memhouse** for the user on this machine.
16
+
17
+ memhouse gives a coding agent long-term memory of past sessions. It reads the session
18
+ transcripts your editors already write to disk — 17 of them — parses them locally, and
19
+ ships typed rows into ClickHouse. Nothing is intercepted or proxied; it reads files that
20
+ already exist.
21
+
22
+ ## What this machine looks like right now
23
+
24
+ - memhouse version: **{{VERSION}}**
25
+ - platform: {{PLATFORM}}, node {{NODE}}
26
+ - current state: **{{STATE}}**
27
+ - editors with sessions found: {{EDITORS}}
28
+ - reachable ClickHouse: {{HOUSE}}
29
+ - container engines available: {{ENGINES}}
30
+
31
+ ## Your task
32
+
33
+ {{PLAN}}
34
+
35
+ ## Rules that matter
36
+
37
+ - **Never invent a credential.** If a step needs a password for a house the user already
38
+ runs, ask them for it. Do not guess, do not read it out of a file they did not name.
39
+ - **Do not run `deploy --down`.** It destroys the container *and its volume*. It is never
40
+ part of an install.
41
+ - The install writes `~/.memhouse/env` at mode 0600. Do not print its contents; do not
42
+ echo a password into the transcript you are writing, which memhouse will then ship.
43
+ - If a command fails, read what it printed before retrying. memhouse's failures name the
44
+ cause and usually name the fix — it is not a generic tool that fails generically.
45
+ - Rooms are named for the ClickHouse user: `sessions_<them>`, `messages_<them>`,
46
+ `tool_calls_<them>`. There are no unsuffixed tables. If you write a query, suffix it.
47
+
48
+ ## When you are done
49
+
50
+ Run `memhouse doctor` and report its output to the user. Every line should be a check
51
+ mark. Then tell them:
52
+
53
+ - where the dashboard is (`http://localhost:{{PORT}}`) and that `memhouse start` runs it
54
+ - that `memhouse search <terms>` searches every past session from every editor
55
+ - that you can now search their history yourself, and offer to try one query
56
+
57
+ If `doctor` reports a failure, say which line failed and what memhouse suggested. Do not
58
+ declare success on a partial install.
@@ -1,7 +1,13 @@
1
1
  # mem-house — agent install
2
2
 
3
3
  Imperative steps for an AI coding agent installing mem-house on a machine. Execute
4
- in order; every command is copy-pasteable. Prereqs: Node >= 20.19 and a reachable
4
+ in order; every command is copy-pasteable.
5
+
6
+ **Prefer `memhouse prompt --install`.** It renders `AGENT-INSTALL-PROMPT.md` against the
7
+ machine it runs on — which editors have sessions, whether a ClickHouse answers, whether
8
+ docker or podman exists — and states the one route that applies. This file is the static
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
5
11
  ClickHouse >= 26.2 (local server, Cloud, or a kernel house — see
6
12
  `kernel-install.md` for the kernel path). No checkout required — memhouse is on npm.
7
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
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": {