memhouse 0.18.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -83,6 +83,7 @@ memhouse deploy --local | --down stand up (or remove) a local house
83
83
  memhouse invite <name> mint a member + house, hand them one env file
84
84
  memhouse members [--db X] who is in a house, and what each can reach
85
85
  memhouse whoami [--admin] which credential is in play, and what it may do
86
+ memhouse instance which memhouse this is: home, binary, house, rooms, scope, bindings
86
87
  memhouse rooms what your rooms are actually called
87
88
  memhouse share <user> [--only …|--revoke|--list]
88
89
  let a housemate read yours, in whole or in part
@@ -162,6 +163,18 @@ build sits on no tag and is never auto-updated; `update --check` says so and nam
162
163
  way forward. An invite from a house on a channel carries it, so the invitee follows it
163
164
  too.
164
165
 
166
+ ## Instances
167
+
168
+ An instance is a home: an env file naming a house, the daemons shipping to it, and the
169
+ playbooks whose sessions it ships (`MEMHOUSE_CLAUDE_ROOTS`). A machine can run several,
170
+ each under its own `MEMHOUSE_HOME`, on different channels. `plugins install claude` from an
171
+ instance installs into exactly the playbooks that instance ships and **binds** them: it
172
+ writes `MEMHOUSE_HOME` and `MEMHOUSE_BIN` into each playbook's `settings.json` `env`, which
173
+ Claude Code applies to every session there. So the `/mem:*` skills in a bound playbook read
174
+ that instance's house and run that instance's binary, whatever the shell they were started
175
+ from had. `plugins list` shows the binding; `plugins remove` takes it out. Everything else
176
+ in `settings.json` is left as it was.
177
+
165
178
  ## Going back into a session
166
179
 
167
180
  `search` finds the conversation; `resume` hands you back into it.
package/bin/memhouse.js CHANGED
@@ -273,14 +273,24 @@ function portOf(u) {
273
273
  // "not running" for every correctly service-managed install, and `doctor` fails on a
274
274
  // healthy machine. Returns { running, via }.
275
275
  function shipperHealth() {
276
+ // "running" is the pid; whether it is DOING anything is the log. Both, always.
277
+ const lastPass = (() => {
278
+ try {
279
+ const passlog = require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'passlog'));
280
+ const f = path.join(LOG_DIR, 'shipper.log'); const st = fs.statSync(f);
281
+ const fd = fs.openSync(f, 'r'); const len = Math.min(st.size, 65536); const buf = Buffer.alloc(len);
282
+ fs.readSync(fd, buf, 0, len, st.size - len); fs.closeSync(fd);
283
+ return passlog.lastPass(buf.toString('utf-8'));
284
+ } catch { return { outcome: null }; }
285
+ })();
276
286
  const pid = pidOf('shipper');
277
- if (pid) return { running: true, via: `daemon (pid ${pid})` };
287
+ if (pid) return { running: true, via: `daemon (pid ${pid})`, lastPass };
278
288
  try {
279
289
  const svc = require(path.join(REPO_ROOT, 'memhouse', 'service.js'));
280
290
  const st = svc.status();
281
- if (st.installed) return { running: st.running, via: `service (${st.kind})` };
291
+ if (st.installed) return { running: st.running, via: `service (${st.kind})`, lastPass };
282
292
  } catch { /* no service integration on this platform */ }
283
- return { running: false, via: null };
293
+ return { running: false, via: null, lastPass };
284
294
  }
285
295
 
286
296
  // Room routing for the CLI's own queries. The shipper and dashboard resolve rooms through
@@ -405,7 +415,9 @@ Setup onboard interactive wizard: discover → configure →
405
415
  --print-sql print the SQL, run it yourself
406
416
  with admin: --admin-user --admin-password [--member NAME]
407
417
  builds house + user + rooms + grants, then verifies as
408
- the member. The admin credential is never stored.
418
+ the member. The admin credential IS written to the env
419
+ file (mode 600) so invite/members need no flags; delete
420
+ those two lines if you would rather pass it each time.
409
421
  --env FILE installs from an invite file (see: invite)
410
422
  invite <name> mint a member and write the env file their install needs
411
423
  (--url [--db NAME] [--out FILE]; --admin-user and
@@ -438,6 +450,7 @@ Data ship one incremental pass (--full | --loop [sec])
438
450
  search <terms…> full-text search across all sessions
439
451
  resume <session-id> print the command that reopens that session in its editor
440
452
  sessions-query print the session rollup SQL for this credential
453
+ instance which memhouse this is: home, binary, house, rooms, scope, daemons, bound playbooks
441
454
  rooms what your rooms are actually called (--json)
442
455
  members who is in a house and what each can reach (--db, admin)
443
456
  start | stop | shipper loop + dashboard as background daemons
@@ -968,6 +981,16 @@ function startShipperBackground(cfg) {
968
981
  console.log(' dashboard (browse / search / analyze): memhouse start');
969
982
  }
970
983
 
984
+ // The store is node:sqlite, stable from Node 24. On 22 everything green-lights and the
985
+ // SQLite-backed adapters silently ship nothing — 36 messages went out before anyone noticed.
986
+ // doctor said so; install and onboard did not. Now they refuse, with the same sentence.
987
+ function engineOk() {
988
+ const major = Number(process.versions.node.split('.')[0]);
989
+ if (major >= 24) return true;
990
+ console.log(bad(`node ${process.versions.node} — need >= 24 — node:sqlite is stable there; the SQLite-backed adapters need it`));
991
+ process.exitCode = 1; return false;
992
+ }
993
+
971
994
  async function cmdInstall({ interactive }) {
972
995
  // --env <file>: an INVITE intake. The file carries MEMHOUSE_URL/USER/PASSWORD/DB from
973
996
  // `memhouse invite` on the admin's machine; loading it into the environment BEFORE
@@ -984,6 +1007,10 @@ async function cmdInstall({ interactive }) {
984
1007
  // this machine with a file that cannot authenticate on the invitee's.
985
1008
  const missing = wanted.filter((k) => !parsed[k]);
986
1009
  if (missing.length) { console.log(bad(`--env ${flags.env} is missing ${missing.join(', ')} — not a complete invite file?`)); return 1; }
1010
+ try {
1011
+ const h = new URL(String(parsed.MEMHOUSE_URL || '')).hostname;
1012
+ if (/^(localhost|127\.0\.0\.1|\[?::1\]?)$/.test(h)) console.log(warn(`${parsed.MEMHOUSE_URL} is loopback — it points at THIS machine; only right if a tunnel or port-forward puts the house here`));
1013
+ } catch { /* not a URL; the missing-fields check above already spoke */ }
987
1014
  // Clear ambient MEMHOUSE_* so ONLY the file speaks (exported vars normally win over the
988
1015
  // file; an invite intake is the one place they must not).
989
1016
  for (const k of ['MEMHOUSE_URL', 'MEMHOUSE_USER', 'MEMHOUSE_PASSWORD', 'MEMHOUSE_DB', 'MEMHOUSE_PORT']) delete process.env[k];
@@ -1235,6 +1262,10 @@ async function cmdInstall({ interactive }) {
1235
1262
  // deliberately for exactly this reason; install used to do nothing at all.
1236
1263
  if (displaces && shipperHealth().running) {
1237
1264
  const st = (() => { try { return require(path.join(REPO_ROOT, 'memhouse', 'service.js')).status(); } catch { return null; } })();
1265
+ // Same server and database, different user, is a credential swap, not a house move —
1266
+ // "old house" reads as data loss when nothing moved. A drill flagged the conflation.
1267
+ const sameHouse = prior.MEMHOUSE_URL && sameEndpoint(prior.MEMHOUSE_URL, cfg.url) && (!prior.MEMHOUSE_DB || prior.MEMHOUSE_DB === cfg.db);
1268
+ const whatMoved = sameHouse ? 'old credential' : 'old house';
1238
1269
  const viaService = String(shipperHealth().via || '').startsWith('service');
1239
1270
  if (viaService && st) {
1240
1271
  const cmd = st.kind === 'systemd'
@@ -1242,10 +1273,10 @@ async function cmdInstall({ interactive }) {
1242
1273
  : ['launchctl', ['kickstart', '-k', `gui/${process.getuid()}/com.memhouse.shipper`]];
1243
1274
  const r = spawnSync(cmd[0], cmd[1], { stdio: 'pipe', encoding: 'utf-8' });
1244
1275
  console.log(r.status === 0 ? ok('shipper restarted against the new house')
1245
- : warn('restart the shipper yourself, or it keeps writing to the old house: memhouse service restart'));
1276
+ : warn(`restart the shipper yourself, or it keeps writing under the ${whatMoved}: memhouse service restart`));
1246
1277
  } else {
1247
1278
  const pid = pidOf('shipper');
1248
- if (pid) { try { process.kill(pid, 'SIGTERM'); console.log(ok(`stopped the shipper (pid ${pid}) — it was pointed at the old house`)); } catch { /* raced */ } }
1279
+ if (pid) { try { process.kill(pid, 'SIGTERM'); console.log(ok(`stopped the shipper (pid ${pid}) — it was running under the ${whatMoved}`)); } catch { /* raced */ } }
1249
1280
  console.log(' start it against the new one: memhouse start');
1250
1281
  }
1251
1282
  }
@@ -1280,6 +1311,18 @@ async function finishInvite(cfg) {
1280
1311
  }
1281
1312
  if (go) {
1282
1313
  const code = await cmdPasswd({ quiet: true });
1314
+ if (code === 0) {
1315
+ // The rotation rewrote the env FILE. But --env hoisted the invite's values into
1316
+ // process.env, and resolveConfig prefers process.env over the file — so the
1317
+ // shipper spawned a few lines later got the INVITE password, which the rotation
1318
+ // had just killed, and failed every pass with "Authentication failed" while every
1319
+ // fresh CLI call (reading the file) worked. A drill member watched an empty house
1320
+ // for twelve minutes. Adopt what the file now says before anything else is spawned.
1321
+ const drift = envfile.credentialDrift(fs.readFileSync(ENV_FILE, 'utf-8'), process.env);
1322
+ for (const k of drift.changed) process.env[k] = drift.values[k];
1323
+ if (drift.values.MEMHOUSE_PASSWORD !== undefined && flags.password !== undefined) flags.password = drift.values.MEMHOUSE_PASSWORD;
1324
+ if (cfg) cfg.password = process.env.MEMHOUSE_PASSWORD;
1325
+ }
1283
1326
  if (code !== 0) {
1284
1327
  // Rotation failed and its own state may be uncertain — KEEP the invite file (it
1285
1328
  // still carries the password the config was just written from) so nothing is
@@ -1318,7 +1361,8 @@ function printGettingStarted(cfg) {
1318
1361
  console.log(' /mem:sql, /mem:access, /mem:admin');
1319
1362
  console.log(` ${mh} search <terms> find a past conversation right now`);
1320
1363
  console.log(` ${mh} doctor every line a check mark = healthy`);
1321
- console.log(' The house keeps shipping as you work; nothing else to do.');
1364
+ if (flags['no-ship'] === true) console.log(' Shipping is OFF (--no-ship). Start it when ready: memhouse start');
1365
+ else console.log(' The house keeps shipping as you work; nothing else to do.');
1322
1366
  }
1323
1367
 
1324
1368
  async function cmdOnboard() {
@@ -1600,7 +1644,12 @@ async function cmdStatus() {
1600
1644
  console.log(` ${f.writer.padEnd(28)} ${String(f.version || '?').padEnd(8)} last ship ${fleetAge(f.ageMs)}${mark}`);
1601
1645
  }
1602
1646
  }
1603
- console.log(out.shipper.running ? ok(`shipper: running — ${out.shipper.via}`) : warn('shipper: not running'));
1647
+ {
1648
+ const fail = require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'passlog')).describeFailure(out.shipper.lastPass);
1649
+ console.log(!out.shipper.running ? warn('shipper: not running')
1650
+ : fail ? warn(`shipper: running — ${out.shipper.via} — but ${fail}`)
1651
+ : ok(`shipper: running — ${out.shipper.via}`));
1652
+ }
1604
1653
  console.log(out.daemons.dashboard ? ok(`dashboard: running (pid ${out.daemons.dashboard})`) : warn('dashboard: not running'));
1605
1654
  if (out.daemons.dashboard) console.log(` dashboard → http://localhost:${runningPort(cfg)}`);
1606
1655
  // Exit non-zero when the house is unreachable. `status` is what a health check, a
@@ -1947,7 +1996,9 @@ async function cmdDoctor() {
1947
1996
  'that adapter does not extract usage; the messages are stored, the numbers are not');
1948
1997
  } catch { /* a house that cannot be read is already reported above */ }
1949
1998
  const sh = shipperHealth();
1950
- add(sh.running, `shipper${sh.via ? ` — ${sh.via}` : ''}`, 'memhouse start (or: memhouse service install)');
1999
+ const shFail = require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'passlog')).describeFailure(sh.lastPass);
2000
+ add(sh.running && !shFail, `shipper${sh.via ? ` — ${sh.via}` : ''}${shFail ? ` — ${shFail}` : ''}`,
2001
+ shFail ? (sh.lastPass.auth ? 'memhouse stop && memhouse start' : 'read: ~/.memhouse/logs/shipper.log') : 'memhouse start (or: memhouse service install)');
1951
2002
  add(!!pidOf('dashboard'), 'dashboard daemon', 'memhouse start');
1952
2003
  add(fs.existsSync(path.join(REPO_ROOT, 'public', 'index.html')), 'dashboard UI built', 'built automatically by memhouse start');
1953
2004
 
@@ -2417,7 +2468,19 @@ async function cmdPlugins() {
2417
2468
  const invocations = names.map((n) => `/mem:${n}`).join(', ');
2418
2469
  // --target overrides the discovery rather than joining it: given one, that is the only
2419
2470
  // directory touched.
2420
- const targets = flags.target ? [{ dir: flags.target, why: '--target' }] : claudeTargets();
2471
+ // Targets: --target names one; otherwise an instance that ships specific playbooks
2472
+ // (MEMHOUSE_CLAUDE_ROOTS) installs into exactly those — the plugin belongs where the
2473
+ // sessions come from; failing both, every Claude config dir on the machine.
2474
+ const binding = require(path.join(REPO_ROOT, 'memhouse', 'binding'));
2475
+ const scope = require(path.join(REPO_ROOT, 'editors', 'scope'));
2476
+ const icfg = resolveConfig();
2477
+ const scoped = !flags.target && icfg.claudeRoots
2478
+ ? scope.parseList(icfg.claudeRoots).map(scope.expandHome).map((d) => ({ dir: d, why: 'this instance ships it' }))
2479
+ : null;
2480
+ const targets = flags.target ? [{ dir: flags.target, why: '--target' }] : (scoped || claudeTargets());
2481
+ // What the skills in a bound playbook run and read: this binary, this home.
2482
+ const thisBin = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
2483
+ const boundLabel = (dir) => { const b = binding.boundTo(binding.readSettings(dir)); return b ? `bound to ${short(b.home)}` : 'unbound — sessions use whatever MEMHOUSE_HOME the shell has'; };
2421
2484
  // Whatever --target was given has to reappear in the advice, or pasting it installs
2422
2485
  // somewhere else than the directory just inspected.
2423
2486
  const self = `memhouse plugins install claude${flags.target ? ` --target ${flags.target}` : ''}`;
@@ -2427,7 +2490,7 @@ async function cmdPlugins() {
2427
2490
  if (!targets.length) { console.log(warn('no Claude Code config directory found')); return 0; }
2428
2491
  for (const t of targets) {
2429
2492
  console.log(isPluginInstalled(t.dir)
2430
- ? ok(`installed in ${short(t.dir)} (${t.why})`)
2493
+ ? ok(`installed in ${short(t.dir)} (${t.why}) — ${boundLabel(t.dir)}`)
2431
2494
  : warn(`not installed in ${short(t.dir)} (${t.why})`));
2432
2495
  }
2433
2496
  if (!targets.some((t) => isPluginInstalled(t.dir))) console.log(` install with: ${self}`);
@@ -2442,7 +2505,13 @@ async function cmdPlugins() {
2442
2505
  }
2443
2506
  const chosen = await chooseTargets(targets, 'Install');
2444
2507
  if (!chosen.length) { console.log(warn('nothing installed')); return 0; }
2445
- for (const t of chosen) console.log(ok(`installed ${names.length} skills into ${short(installPluginInto(t.dir))}`));
2508
+ for (const t of chosen) {
2509
+ console.log(ok(`installed ${names.length} skills into ${short(installPluginInto(t.dir))}`));
2510
+ try {
2511
+ binding.bind(t.dir, { home: HOME_DIR, bin: thisBin });
2512
+ console.log(ok(` bound: sessions under ${short(t.dir)} read ${short(HOME_DIR)} and run ${short(thisBin)}`));
2513
+ } catch (e) { console.log(warn(` not bound: ${e.message}`)); }
2514
+ }
2446
2515
  console.log(` loads as mem@skills-dir next session — invoke ${invocations}`);
2447
2516
  return 0;
2448
2517
  }
@@ -2458,6 +2527,8 @@ async function cmdPlugins() {
2458
2527
  // else's work.
2459
2528
  try { fs.rmdirSync(path.join(t.dir, 'skills')); } catch { /* not empty: leave it */ }
2460
2529
  console.log(ok(`removed ${short(path.join(t.dir, 'skills', 'mem'))}`));
2530
+ try { if (binding.unbind(t.dir)) console.log(ok(` unbound: MEMHOUSE_HOME/MEMHOUSE_BIN removed from ${short(path.join(t.dir, 'settings.json'))}`)); }
2531
+ catch (e) { console.log(warn(` could not unbind: ${e.message}`)); }
2461
2532
  }
2462
2533
  return 0;
2463
2534
  }
@@ -3335,6 +3406,53 @@ async function cmdWhoami() {
3335
3406
  return 0;
3336
3407
  }
3337
3408
 
3409
+ /**
3410
+ * Bare `memhouse` with an invite file nearby: offer to process it. Returns null when there
3411
+ * is nothing to offer (caller prints help), else the exit code of the install it ran or
3412
+ * declined. Never touches the file's password: what it shows is the user, the house and
3413
+ * the server, which the guide beside the file shows too.
3414
+ */
3415
+ async function offerInvite() {
3416
+ const inv = require(path.join(REPO_ROOT, 'memhouse', 'invitefile'));
3417
+ // The current directory only. The guide says "from the directory holding the file", and
3418
+ // reaching into ~/Downloads surfaced unrelated invitations from other houses — and could
3419
+ // auto-join the wrong one under --yes. If they are somewhere else, they name the file.
3420
+ const found = inv.findInvites([process.cwd()]);
3421
+ if (!found.length) return null;
3422
+ const described = found.map((f) => ({ ...f, ...inv.describeInvite((() => { try { return fs.readFileSync(f.file, 'utf-8'); } catch { return ''; } })()) }));
3423
+ const usable = described.filter((d) => d.complete);
3424
+ if (!usable.length) {
3425
+ console.log(warn(`found ${found.length === 1 ? short(found[0].file) : `${found.length} invite files`} — not a complete invite (missing ${described[0].missing.join(', ')})`));
3426
+ return null;
3427
+ }
3428
+ const cfg = resolveConfig();
3429
+ const interactive = process.stdin.isTTY && flags.yes !== true;
3430
+ console.log(ok(`found an invitation: ${short(usable[0].file)}${usable.length > 1 ? ` (and ${usable.length - 1} more)` : ''}`));
3431
+ let pick = usable[0];
3432
+ if (usable.length > 1) {
3433
+ usable.forEach((d, i) => console.log(` ${i + 1}) ${short(d.file)} join ${d.url} as '${d.user}'`));
3434
+ if (!interactive) { console.log(' several invitations here — name one: memhouse install --env <file>'); return 1; }
3435
+ const a = (await ask('Which one? (number, or n)', '1')).trim().toLowerCase();
3436
+ const n = Number(a);
3437
+ if (!Number.isInteger(n) || n < 1 || n > usable.length) { console.log(' nothing done.'); return 1; }
3438
+ pick = usable[n - 1];
3439
+ }
3440
+ console.log(` it joins ${pick.url}, house '${pick.db}', as member '${pick.user}'${pick.channel ? `, channel ${pick.channel}` : ''}.`);
3441
+ if (cfg.stated && cfg.user) {
3442
+ console.log(warn(`this machine already ships to ${cfg.url} as '${cfg.user}' — joining REPLACES that (the old config is kept as env.pre-install).`));
3443
+ }
3444
+ console.log(' Joining rotates the password to one only this machine knows, deletes the file, and starts shipping.');
3445
+ if (!interactive) {
3446
+ if (flags.yes === true) { flags.env = pick.file; flags.force = cfg.stated && cfg.user ? true : flags.force; return cmdInstall({ interactive: false }); }
3447
+ console.log(` Join with: memhouse install --env ${path.basename(pick.file)} (or: memhouse --yes)`);
3448
+ return 0;
3449
+ }
3450
+ const a = (await ask(cfg.stated && cfg.user ? 'Replace the current house and join? (yes/no)' : 'Join now? (Y/n)', cfg.stated && cfg.user ? 'no' : 'Y')).trim().toLowerCase();
3451
+ if (!(a === 'y' || a === 'yes' || (a === '' && !(cfg.stated && cfg.user)))) { console.log(' nothing done. Later: memhouse install --env ' + path.basename(pick.file)); return 0; }
3452
+ flags.env = pick.file; if (cfg.stated && cfg.user) flags.force = true;
3453
+ return cmdInstall({ interactive: false });
3454
+ }
3455
+
3338
3456
  async function cmdInvite() {
3339
3457
  const name = positional[0];
3340
3458
  if (!name) { console.log('usage: memhouse invite <name> --url <house-url> [--admin-user … [--admin-password …]] [--db <house>] [--out <file>] [--adopt] [--print-sql]'); return 2; }
@@ -3806,13 +3924,21 @@ async function cmdUninstall() {
3806
3924
  const cfg = resolveConfig();
3807
3925
 
3808
3926
  switch (cmd) {
3809
- case null: case 'help': console.log(HELP); break;
3927
+ case null: {
3928
+ // Bare `memhouse`. If an invite file is where the user is standing — or in their
3929
+ // downloads — the likeliest reason they typed this is that they were handed it. Ask.
3930
+ // Otherwise, the help screen as before. Nothing happens without a yes.
3931
+ const code = await offerInvite();
3932
+ if (code === null) console.log(HELP); else process.exitCode = code;
3933
+ break;
3934
+ }
3935
+ case 'help': console.log(HELP); break;
3810
3936
  // `--version` and `-v` land here as flags, not as the `version` verb, so they used to
3811
3937
  // fall through to HELP: forty lines of help and exit 0 is not what --version means.
3812
3938
  case 'version': console.log(PKG.version); break;
3813
3939
  case 'discover': await cmdDiscover(); break;
3814
- case 'onboard': process.exitCode = await cmdOnboard(); break;
3815
- case 'install': process.exitCode = await cmdInstall({ interactive: false }); break;
3940
+ case 'onboard': if (!engineOk()) break; process.exitCode = await cmdOnboard(); break;
3941
+ case 'install': if (!engineOk()) break; process.exitCode = await cmdInstall({ interactive: false }); break;
3816
3942
  case 'setup': {
3817
3943
  // Dual-mode like install: --yes (with any --url/--user/--password/--db/
3818
3944
  // --port overrides, already resolved into cfg) skips every prompt so
@@ -3943,6 +4069,57 @@ async function cmdUninstall() {
3943
4069
  if (!au) console.log(' (run with --admin-user to be sure you are seeing every account)');
3944
4070
  break;
3945
4071
  }
4072
+ case 'instance': {
4073
+ // Which memhouse am I talking to? One screen: the instance, its binary, its house,
4074
+ // its rooms, what it ships, its host, its daemons, and which playbooks are bound to
4075
+ // it. A machine can run several instances; this is how they are told apart.
4076
+ const cfg = resolveConfig();
4077
+ const binding = require(path.join(REPO_ROOT, 'memhouse', 'binding'));
4078
+ const scope = require(path.join(REPO_ROOT, 'editors', 'scope'));
4079
+ const hostmod = require(path.join(REPO_ROOT, 'memhouse', 'host.js'));
4080
+ const thisBin = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
4081
+ const name = binding.instanceName(HOME_DIR, readEnvFile().MEMHOUSE_NAME);
4082
+ const host = (() => { try { return hostmod.read(HOME_DIR); } catch { return null; } })();
4083
+ const daemons = { shipper: pidOf('shipper') || null, dashboard: pidOf('dashboard') || null };
4084
+ const realHome = (() => { try { return fs.realpathSync(HOME_DIR); } catch { return HOME_DIR; } })();
4085
+ const cands = new Map();
4086
+ for (const d of scope.parseList(cfg.claudeRoots).map(scope.expandHome)) cands.set(d, d);
4087
+ for (const t of claudeTargets()) cands.set(t.dir, t.dir);
4088
+ const bound = [...cands.keys()].filter((d) => {
4089
+ const b = binding.boundTo(binding.readSettings(d)); if (!b) return false;
4090
+ try { return fs.realpathSync(b.home) === realHome; } catch { return b.home === HOME_DIR; }
4091
+ });
4092
+ let rooms = null; let counts = {};
4093
+ if (cfg.stated && cfg.user) {
4094
+ rooms = roomNames(cfg.user);
4095
+ try {
4096
+ // system.tables, not system.parts: a member holds no grant on the latter, and the
4097
+ // former is filtered to what they may see. total_rows is approximate under merges
4098
+ // and exact enough for "is anything in there".
4099
+ for (const r of await chRows(cfg, `SELECT name AS table, total_rows AS n FROM system.tables WHERE database = ${sqlStr(cfg.db)} AND startsWith(name, ${sqlStr(`${cfg.user}_`)})`, { database: '' })) counts[r.table] = Number(r.n);
4100
+ } catch { /* unreachable house: rooms still listed, counts absent */ }
4101
+ }
4102
+ const out = {
4103
+ instance: name, home: HOME_DIR, envFile: ENV_FILE, binary: thisBin, version: PKG.version, channel: cfg.channel || null,
4104
+ house: cfg.stated ? { url: cfg.url, db: cfg.db, member: cfg.user, adminCredential: !!cfg.adminUser, adminUser: cfg.adminUser || null } : null,
4105
+ rooms: rooms ? [...ROOM_TYPES, ...META_TYPES].map((t) => ({ table: `${cfg.db}.${rooms.physical[t]}`, rows: counts[rooms.physical[t]] ?? null })) : [],
4106
+ ships: scope.describe({ MEMHOUSE_EDITORS: cfg.editors, MEMHOUSE_CLAUDE_ROOTS: cfg.claudeRoots }),
4107
+ host: host ? host.id : null, daemons: { ...daemons, dashboardUrl: daemons.dashboard ? `http://localhost:${cfg.port}` : null },
4108
+ boundPlaybooks: bound,
4109
+ };
4110
+ if (JSON_OUT) { console.log(JSON.stringify(out, null, 2)); break; }
4111
+ const line = (k, v) => console.log(` ${k.padEnd(9)} ${v}`);
4112
+ line('instance', `${name} (${short(HOME_DIR)})`);
4113
+ line('binary', `${short(thisBin)} ${PKG.version}${cfg.channel ? ` channel ${cfg.channel}` : ''}`);
4114
+ if (!out.house) { line('house', 'none configured — memhouse install'); break; }
4115
+ line('house', `${cfg.url} db ${cfg.db} member ${cfg.user} admin credential: ${cfg.adminUser ? `yes (${cfg.adminUser})` : 'no'}`);
4116
+ line('rooms', out.rooms.map((r) => `${r.table}${r.rows === null ? '' : ` ${r.rows}`}`).join(' · '));
4117
+ line('ships', out.ships);
4118
+ line('host', host ? host.id : 'not minted yet (first ship mints it)');
4119
+ line('daemons', `shipper ${daemons.shipper ? `running (pid ${daemons.shipper})` : 'stopped'} dashboard ${daemons.dashboard ? `running (pid ${daemons.dashboard}) → http://localhost:${cfg.port}` : 'stopped'}`);
4120
+ line('bound', bound.length ? bound.map(short).join(', ') : 'no playbook is bound to this instance — memhouse plugins install claude');
4121
+ break;
4122
+ }
3946
4123
  case 'rooms': {
3947
4124
  // What are my rooms called? One answer, in one place: named for the member, inside the
3948
4125
  // configured house. It resolves through roomNames(), so it cannot drift from what the
@@ -0,0 +1,81 @@
1
+ // An instance's plugin is bound to the instance that installed it.
2
+ //
3
+ // A memhouse INSTANCE is a home: an env file naming a house, the daemons that ship to it,
4
+ // and the playbooks (Claude Code config dirs) whose sessions it ships. Its /mem:* skills
5
+ // belong in exactly those playbooks, and inside them they must read THAT house and run
6
+ // THAT binary — a machine can carry two instances on two channels, and a bare `memhouse`
7
+ // with a session-inherited MEMHOUSE_HOME would pick whichever the shell happened to have.
8
+ //
9
+ // Claude Code applies a settings.json `env` block to every session under a config dir.
10
+ // So `plugins install` stamps two variables there, beside the skills it copies:
11
+ //
12
+ // MEMHOUSE_HOME the instance's home — which house the skills read
13
+ // MEMHOUSE_BIN the instance's binary — which memhouse the skills run
14
+ //
15
+ // Nothing else in the file is touched; other env keys survive; `plugins remove` takes the
16
+ // two back out. Pure functions here, file I/O below them, both unit-tested.
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+
21
+ const KEYS = ['MEMHOUSE_HOME', 'MEMHOUSE_BIN'];
22
+
23
+ function bindSettings(json, { home, bin }) {
24
+ const out = { ...(json || {}) };
25
+ out.env = { ...(out.env || {}), MEMHOUSE_HOME: home, MEMHOUSE_BIN: bin };
26
+ return out;
27
+ }
28
+
29
+ function unbindSettings(json) {
30
+ const out = { ...(json || {}) };
31
+ if (out.env) {
32
+ out.env = { ...out.env };
33
+ for (const k of KEYS) delete out.env[k];
34
+ if (!Object.keys(out.env).length) delete out.env;
35
+ }
36
+ return out;
37
+ }
38
+
39
+ function boundTo(json) {
40
+ const env = (json && json.env) || {};
41
+ return env.MEMHOUSE_HOME ? { home: env.MEMHOUSE_HOME, bin: env.MEMHOUSE_BIN || null } : null;
42
+ }
43
+
44
+ function settingsPath(dir) { return path.join(dir, 'settings.json'); }
45
+
46
+ function readSettings(dir) {
47
+ try { return JSON.parse(fs.readFileSync(settingsPath(dir), 'utf-8')); } catch { return null; }
48
+ }
49
+
50
+ function writeSettings(dir, json) {
51
+ fs.writeFileSync(settingsPath(dir), `${JSON.stringify(json, null, 2)}\n`);
52
+ }
53
+
54
+ /** Stamp the binding into a config dir's settings.json; returns what was written. */
55
+ function bind(dir, { home, bin }) {
56
+ const cur = readSettings(dir);
57
+ if (cur === null && fs.existsSync(settingsPath(dir))) throw new Error(`${settingsPath(dir)} is not valid JSON — not touching it`);
58
+ writeSettings(dir, bindSettings(cur || {}, { home, bin }));
59
+ return { home, bin };
60
+ }
61
+
62
+ function unbind(dir) {
63
+ const cur = readSettings(dir);
64
+ if (!cur || !boundTo(cur)) return false;
65
+ writeSettings(dir, unbindSettings(cur));
66
+ return true;
67
+ }
68
+
69
+ /**
70
+ * What an instance is called: MEMHOUSE_NAME if the env file sets one, else its home
71
+ * directory with the conventional `.memhouse-` prefix removed — `~/.memhouse-stage` is
72
+ * "stage", `~/.memhouse` is "default", `~/alice-sandbox/memhouse` is "memhouse".
73
+ */
74
+ function instanceName(home, envName = '') {
75
+ if (envName) return envName;
76
+ const base = path.basename(String(home || '').replace(/[\/]+$/, ''));
77
+ if (base === '.memhouse') return 'default';
78
+ return base.replace(/^\.memhouse-/, '') || 'default';
79
+ }
80
+
81
+ module.exports = { bindSettings, unbindSettings, boundTo, bind, unbind, readSettings, KEYS, instanceName };
@@ -8,7 +8,7 @@ You received two things:
8
8
 
9
9
  | file | what it is |
10
10
  |---|---|
11
- | `{{FILE}}` | **your credential.** One-time: install consumes it and deletes it. Treat it like a password until then. |
11
+ | `{{FILE}}` | **your credential**, in this same folder. One-time: install consumes it and deletes it. Treat it like a password until then. |
12
12
  | this page | the steps. Nothing secret in here. |
13
13
 
14
14
  Do it yourself, or hand this page to your coding agent. From the directory holding both
@@ -21,21 +21,39 @@ claude "Read MEMHOUSE-INVITATION.md and follow it. Ask me before each step that
21
21
  Any agent that can run shell commands works the same way; the rules it must keep are at
22
22
  the end of this page.
23
23
 
24
+ ## 0. Find your credential
25
+
26
+ The install needs `{{FILE}}`, which was sent alongside this page. Confirm it is here first —
27
+ run this from the same directory, and if it prints the filename you are ready:
28
+
29
+ ```
30
+ ls invite-*.env
31
+ ```
32
+
33
+ If it is missing, it did not travel with this page; get it from whoever invited you before
34
+ going on. Do not print its contents — the last line of it is a password.
35
+
24
36
  ## 1. Install memhouse
25
37
 
26
38
  {{INSTALL}}
27
39
 
28
40
  On most Linux distros the global install needs `sudo`; if `npm` says `EACCES`, that is why.
41
+ If you handed this page to an agent and it reports that a permission classifier blocked the
42
+ install (a global `npm install` is exactly what such classifiers stop), run this one line
43
+ yourself in a terminal and tell the agent to continue from step 2 — that is the intended
44
+ split: the person installs software, the agent does the rest.
29
45
  `memhouse --version` should print `{{VERSION}}`.
30
46
 
31
47
  ## 2. Join the house
32
48
 
33
- From the directory holding `{{FILE}}`:
49
+ From the directory holding `{{FILE}}`, just run memhouse — it finds the invitation and asks:
34
50
 
35
51
  ```
36
- memhouse install --env {{FILE}}
52
+ memhouse
37
53
  ```
38
54
 
55
+ (or, without the question: `memhouse install --env {{FILE}}`)
56
+
39
57
  What that does, in order: connects to `{{URL}}` as `{{NAME}}`, **rotates your password to
40
58
  one only your machine knows**, creates your rooms, writes your config to `~/.memhouse/env`,
41
59
  deletes `{{FILE}}`, and starts shipping in the background. The password in the file stops
@@ -115,6 +133,9 @@ narrower than the one memhouse issues — ask {{INVITER}} to run `memhouse insta
115
133
  You are installing memhouse for the person who handed you this file. Five rules; three of
116
134
  them exist because an agent broke them:
117
135
 
136
+ 0. **Your credential is `{{FILE}}` in the directory this page is in.** If `Read
137
+ MEMHOUSE-INVITATION.md` is all you were given, `ls invite-*.env` beside it is the file
138
+ the steps refer to; confirm it is there before step 1.
118
139
  1. **Never print `{{FILE}}` or any `MEMHOUSE_*` value.** No `cat`, no `echo $MEMHOUSE_PASSWORD`,
119
140
  no `set -x`. This very session will be shipped into the house; a password printed here is
120
141
  a password archived.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "mem",
3
3
  "description": "Conversation memory for AI agents over the memhouse typed ClickHouse store: recall past sessions and answer from them, run analytics, check the house, bring people in, and administer the server. Skills install as /mem:recall, /mem:sql, /mem:house, /mem:access, and /mem:admin.",
4
- "version": "0.18.0",
4
+ "version": "0.18.2",
5
5
  "author": {
6
6
  "name": "Ramazan Polat"
7
7
  },
@@ -36,6 +36,13 @@ rows, and *you* read them.
36
36
  memhouse ships back into the house — the archive is insert-only, so a leaked credential
37
37
  cannot be withdrawn.
38
38
 
39
+ **Which memhouse.** `plugins install` stamps two variables into this config directory's
40
+ `settings.json`, so every session here carries them: `MEMHOUSE_HOME` is the instance whose
41
+ house these skills read, and `MEMHOUSE_BIN` is that instance's binary. **Every `memhouse …`
42
+ command in these skills means `"${MEMHOUSE_BIN:-memhouse}"`.** A machine can run two
43
+ instances on two channels; the bare name would be whichever the shell found first. Do not
44
+ override either variable.
45
+
39
46
  Credentials resolve **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`** (default
40
47
  `~/.memhouse/env`) — the same order the CLI uses. The dance below snapshots the
41
48
  environment first, because sourcing the file would otherwise clobber an already-exported
@@ -26,7 +26,7 @@ This runs the `memhouse invite` CLI and nothing else. **Do not hand-run `CREATE
26
26
  `GRANT` with an admin password substituted into a command** — that burns the credential
27
27
  into a transcript on disk. The CLI passes it to one process and never echoes it.
28
28
 
29
- 1. **Check the binary:** `command -v memhouse`. Absent → tell the user to
29
+ 1. **Check the binary:** `command -v "${MEMHOUSE_BIN:-memhouse}"`. Absent → tell the user to
30
30
  `npm install -g memhouse`. There is no in-skill fallback, on purpose.
31
31
 
32
32
  2. **Gather only two things:** the invitee's **name** (validate `[A-Za-z][A-Za-z0-9_]*`;
@@ -32,6 +32,22 @@ stop:
32
32
  > `memhouse deploy --local` stands one up bound to loopback. Someone invited you?
33
33
  > `memhouse install --env invite-<you>.env`.
34
34
 
35
+ ## First: which memhouse this is
36
+
37
+ Before anything else, run and show verbatim:
38
+
39
+ ```
40
+ "${MEMHOUSE_BIN:-memhouse}" instance
41
+ ```
42
+
43
+ One screen: the instance's name and home, the binary and channel, the house (server,
44
+ database, member, whether an admin credential is present), the five rooms with row counts,
45
+ what this machine ships, the host identity, the daemons, and which playbooks are bound to
46
+ this instance. A machine can run several instances on different channels; this is how the
47
+ user tells them apart, and how you know which house every answer below comes from. If the
48
+ instance shown is not the one the user meant, stop and say so — the fix is launching
49
+ under the right playbook, not overriding variables.
50
+
35
51
  ## The report
36
52
 
37
53
  Lead with the one-line verdict — *working and current*, *working but stale*, or *not
@@ -46,4 +46,19 @@ function assertSingleLine(env) {
46
46
  }
47
47
  }
48
48
 
49
- module.exports = { parse, quoteShell, quoteSystemd, assertSingleLine };
49
+ /**
50
+ * Which connection values in an env file's text differ from what a process currently holds.
51
+ * A running shipper was spawned with one credential; `install --env` rotates the password
52
+ * and rewrites the file, and a member can `memhouse passwd` under a running daemon. In
53
+ * both cases the FILE is the truth and the process copy is stale. Pure; used by the
54
+ * shipper (adopt on auth failure) and by install (adopt before spawning).
55
+ */
56
+ function credentialDrift(text, current = {}) {
57
+ const file = parse(String(text || ''));
58
+ const keys = ['MEMHOUSE_URL', 'MEMHOUSE_USER', 'MEMHOUSE_PASSWORD', 'MEMHOUSE_DB'];
59
+ const changed = keys.filter((k) => file[k] !== undefined && file[k] !== current[k]);
60
+ const values = Object.fromEntries(changed.map((k) => [k, file[k]]));
61
+ return { changed, values };
62
+ }
63
+
64
+ module.exports = { credentialDrift, parse, quoteShell, quoteSystemd, assertSingleLine };
package/memhouse/flags.js CHANGED
@@ -16,7 +16,7 @@ const COMMAND_FLAGS = {
16
16
  null: [], help: [], version: [],
17
17
  whoami: ['admin'],
18
18
  share: ['only', 'revoke', 'list'],
19
- discover: [], stats: [], stop: [], doctor: [], 'sessions-query': [], rooms: [], members: ['db','admin-user','admin-password'],
19
+ discover: [], stats: [], stop: [], doctor: [], 'sessions-query': [], rooms: [], instance: [], members: ['db','admin-user','admin-password'],
20
20
  status: [], start: [],
21
21
  onboard: ['url', 'user', 'password', 'db', 'port', 'no-ship', 'target', 'house-port', 'tag', 'local'],
22
22
  install: ['url', 'user', 'password', 'db', 'port', 'no-ship', 'env', 'print-sql', 'member',
@@ -0,0 +1,38 @@
1
+ // An invite file found where the invitee is standing.
2
+ //
3
+ // The guide says "from the directory holding the file, run memhouse install --env <file>".
4
+ // A person who just downloaded `invite-alice.env` and typed `memhouse` should not be shown
5
+ // the help screen; they should be asked whether to join. This module finds candidate files
6
+ // and reads what they promise; the CLI decides whether to ask, and install does the work.
7
+ // Nothing here prints a value from the file — the URL, user and database are safe to show,
8
+ // the password never is.
9
+
10
+ const fs = require('fs');
11
+ const path = require('path');
12
+ const envfile = require('./envfile');
13
+
14
+ const REQUIRED = ['MEMHOUSE_URL', 'MEMHOUSE_USER', 'MEMHOUSE_PASSWORD', 'MEMHOUSE_DB'];
15
+
16
+ /** `invite-<name>.env` files in the given directories, first directory first. */
17
+ function findInvites(dirs) {
18
+ const out = [];
19
+ for (const dir of dirs) {
20
+ let names = [];
21
+ try { names = fs.readdirSync(dir); } catch { continue; }
22
+ for (const n of names.sort()) if (/^invite-[A-Za-z0-9_]+\.env$/.test(n)) out.push({ file: path.join(dir, n), dir });
23
+ }
24
+ return out;
25
+ }
26
+
27
+ /** What the file promises, without the secret. */
28
+ function describeInvite(text) {
29
+ const parsed = envfile.parse(String(text || ''));
30
+ const missing = REQUIRED.filter((k) => !parsed[k]);
31
+ return {
32
+ user: parsed.MEMHOUSE_USER || null, url: parsed.MEMHOUSE_URL || null, db: parsed.MEMHOUSE_DB || null,
33
+ channel: parsed.MEMHOUSE_CHANNEL || null, isInvite: parsed.MEMHOUSE_INVITE === '1',
34
+ complete: missing.length === 0, missing,
35
+ };
36
+ }
37
+
38
+ module.exports = { findInvites, describeInvite, REQUIRED };
@@ -61,7 +61,10 @@ function plan({ db, member, password = null }) {
61
61
  { sql: `CREATE DATABASE IF NOT EXISTS ${db}`, why: 'the house', optional: false },
62
62
  ];
63
63
  if (password !== null) {
64
- steps.push({ sql: `CREATE USER IF NOT EXISTS ${member} IDENTIFIED BY ${sqlString(password)}`, why: 'the member', optional: false });
64
+ // No IF NOT EXISTS. The live path checks first and refuses an existing user; the printed
65
+ // plan used to no-op here, apply the grants, and hand out a password the server never saw —
66
+ // found on a realm that mints its own users. Now it fails loudly on line one instead.
67
+ steps.push({ sql: `CREATE USER ${member} IDENTIFIED BY ${sqlString(password)}`, why: 'the member — fails if it already exists; then this password is NOT theirs', optional: false });
65
68
  }
66
69
  steps.push(
67
70
  { sql: roomGrant(db, member), why: `their rooms — ${db}.${roomPattern(member)} — and nothing else in the house`, optional: false },
@@ -0,0 +1,37 @@
1
+ // What the shipper's log says about its LAST pass.
2
+ //
3
+ // A daemon whose every pass fails is "running" by pid. A drill member's shipper failed
4
+ // auth for twelve minutes while `status` said "shipper: running" and `doctor` was green,
5
+ // and she — and her agent — read an empty house as "still catching up". The log beside
6
+ // it had the answer on every pass. This reads it. Pure; the CLI reads the file.
7
+ //
8
+ // Lines have no timestamps, so "last" is textual order: the later of the last success
9
+ // line and the last failure line wins.
10
+
11
+ const OK = /\[memhouse\] shipped (\d+) sessions[^\n]*?→ (\d+) msg rows/;
12
+ const FAIL = /\[memhouse\] pass failed: ([^\n]*)/;
13
+
14
+ /** @returns {{outcome:'ok'|'failed'|null, sessions?:number, rows?:number, why?:string, auth?:boolean}} */
15
+ function lastPass(logText) {
16
+ const text = String(logText || '');
17
+ let lastOk = -1; let lastFail = -1; let okM = null; let failM = null;
18
+ for (const m of text.matchAll(new RegExp(OK.source, 'g'))) { lastOk = m.index; okM = m; }
19
+ for (const m of text.matchAll(new RegExp(FAIL.source, 'g'))) { lastFail = m.index; failM = m; }
20
+ if (lastOk < 0 && lastFail < 0) return { outcome: null };
21
+ if (lastFail > lastOk) {
22
+ const why = failM[1].trim();
23
+ return { outcome: 'failed', why, auth: /Authentication failed|ACCESS_DENIED|Not enough privileges/i.test(why) };
24
+ }
25
+ return { outcome: 'ok', sessions: Number(okM[1]), rows: Number(okM[2]) };
26
+ }
27
+
28
+ /** One line for a person: null when there is nothing to say (no passes yet, or the last one was fine). */
29
+ function describeFailure(lp) {
30
+ if (!lp || lp.outcome !== 'failed') return null;
31
+ const why = lp.why.length > 90 ? `${lp.why.slice(0, 87)}…` : lp.why;
32
+ return lp.auth
33
+ ? `the last pass FAILED — ${why} — restart it: memhouse stop && memhouse start`
34
+ : `the last pass FAILED — ${why} — see: memhouse doctor`;
35
+ }
36
+
37
+ module.exports = { lastPass, describeFailure };
@@ -1251,7 +1251,7 @@ async function printStats(client) {
1251
1251
 
1252
1252
  async function main() {
1253
1253
  const argv = process.argv.slice(2);
1254
- const client = makeClient();
1254
+ let client = makeClient();
1255
1255
  try {
1256
1256
  if (argv.includes('--ensure-schema')) {
1257
1257
  const n = await ensureSchema(client);
@@ -1289,6 +1289,7 @@ async function main() {
1289
1289
  `${r.msgRows} msg rows, ${r.toolRows} tool rows in ${((Date.now() - t0) / 1000).toFixed(1)}s`);
1290
1290
  } catch (e) {
1291
1291
  failed = true;
1292
+ var adoptedCredential = false;
1292
1293
  // A connection failure arrives as an AggregateError whose own `message` is empty,
1293
1294
  // so this printed "[memhouse] pass failed:" and nothing else, after 35 lines of
1294
1295
  // driver internals. Dig out a cause and name the fix; `status` and `doctor`
@@ -1299,7 +1300,22 @@ async function main() {
1299
1300
  if (/ECONNREFUSED|fetch failed|ENOTFOUND|EAI_AGAIN|socket hang up/i.test(why + causes.join(' '))) {
1300
1301
  console.error(`[memhouse] ${process.env.MEMHOUSE_URL} did not answer — is the house running?`);
1301
1302
  } else if (/Authentication failed|ACCESS_DENIED|Not enough privileges/i.test(why)) {
1302
- console.error(`[memhouse] the credential in ${process.env.MEMHOUSE_HOME || '~/.memhouse'}/env was rejected by ${process.env.MEMHOUSE_URL}`);
1303
+ // This process was spawned with one credential; the env FILE may hold a newer one
1304
+ // — `install --env` rotates the invite password and rewrites the file, and
1305
+ // `memhouse passwd` can run under a live daemon. Retrying the dead copy for
1306
+ // 300s a pass while the file beside it is right is what a drill member watched
1307
+ // for twelve minutes. Adopt the file, rebuild the client, retry now.
1308
+ const envPath = require('path').join(process.env.MEMHOUSE_HOME || require('path').join(require('os').homedir(), '.memhouse'), 'env');
1309
+ let drift = { changed: [] };
1310
+ try { drift = require('../envfile').credentialDrift(require('fs').readFileSync(envPath, 'utf-8'), process.env); } catch { /* no file: nothing to adopt */ }
1311
+ if (drift.changed.length) {
1312
+ for (const k of drift.changed) process.env[k] = drift.values[k];
1313
+ client = makeClient();
1314
+ adoptedCredential = true;
1315
+ console.error(`[memhouse] ${envPath} holds a newer ${drift.changed.map((k) => k.replace('MEMHOUSE_', '').toLowerCase()).join('/')} — adopting it and retrying now`);
1316
+ } else {
1317
+ console.error(`[memhouse] the credential this shipper holds was rejected by ${process.env.MEMHOUSE_URL} (and ${envPath} says the same)`);
1318
+ }
1303
1319
  } else if (/does not exist|UNKNOWN_TABLE|UNKNOWN_DATABASE/i.test(why)) {
1304
1320
  console.error('[memhouse] the house is missing a room this pass needed');
1305
1321
  }
@@ -1314,7 +1330,7 @@ async function main() {
1314
1330
  }
1315
1331
  full = false; // --full applies to the first pass only; loop passes stay incremental
1316
1332
  if (loop) {
1317
- const waitMs = failed ? Math.min(retryMs, intervalSec * 1000) : intervalSec * 1000;
1333
+ const waitMs = failed ? (adoptedCredential ? 1000 : Math.min(retryMs, intervalSec * 1000)) : intervalSec * 1000;
1318
1334
  if (failed) {
1319
1335
  console.error(`[memhouse] retrying in ${Math.round(waitMs / 1000)}s`);
1320
1336
  retryMs = Math.min(retryMs * 2, intervalSec * 1000);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.18.0",
3
+ "version": "0.18.2",
4
4
  "description": "memhouse \u2014 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": {