memhouse 0.18.10 → 0.18.12

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.
Files changed (44) hide show
  1. package/AGENTS.md +10 -11
  2. package/CHANGELOG.md +153 -13
  3. package/README.md +2 -2
  4. package/SECURITY.md +4 -2
  5. package/bin/memhouse.js +105 -49
  6. package/docs/guides/admin-password.md +1 -1
  7. package/docs/guides/configuration.md +2 -2
  8. package/docs/guides/install.md +1 -1
  9. package/docs/guides/invite.md +1 -1
  10. package/docs/guides/storage.md +10 -7
  11. package/docs/guides/uninstall.md +4 -2
  12. package/docs/releases/README.md +2 -0
  13. package/docs/releases/v0.18.10.md +7 -7
  14. package/docs/releases/v0.18.11.md +187 -0
  15. package/docs/releases/v0.18.12.md +59 -0
  16. package/docs/tutorials/01-first-house.md +2 -2
  17. package/editors/claude.js +70 -12
  18. package/editors/cursor-agent.js +1 -1
  19. package/examples/01-local-one-machine/README.md +4 -4
  20. package/examples/02-existing-clickhouse/README.md +7 -7
  21. package/memhouse/DESIGN.md +9 -2
  22. package/memhouse/RESUME-AND-UPDATE.md +1 -1
  23. package/memhouse/capabilities.js +1 -1
  24. package/memhouse/convert.js +3 -3
  25. package/memhouse/delivery/PROMPT.md +1 -1
  26. package/memhouse/delivery/plugin/.claude-plugin/plugin.json +1 -1
  27. package/memhouse/delivery/plugin/reference/HOUSE.md +21 -9
  28. package/memhouse/delivery/plugin/skills/access/SKILL.md +2 -2
  29. package/memhouse/delivery/plugin/skills/admin/SKILL.md +3 -3
  30. package/memhouse/delivery/plugin/skills/house/SKILL.md +2 -2
  31. package/memhouse/delivery/plugin/skills/recall/SKILL.md +36 -16
  32. package/memhouse/delivery/plugin/skills/sql/SKILL.md +2 -2
  33. package/memhouse/flags.js +28 -1
  34. package/memhouse/host.js +11 -6
  35. package/memhouse/house/HOUSE.md +6 -6
  36. package/memhouse/house/house.js +31 -5
  37. package/memhouse/invite-target.js +37 -0
  38. package/memhouse/self-update.js +1 -1
  39. package/memhouse/server/queries.js +11 -6
  40. package/memhouse/shipper/ship.js +148 -31
  41. package/package.json +3 -3
  42. package/public/THIRD_PARTY_NOTICES.txt +2114 -0
  43. package/ui/README.md +16 -0
  44. package/ui/vite.config.js +4 -1
package/bin/memhouse.js CHANGED
@@ -42,17 +42,7 @@ const LOG_DIR = path.join(HOME_DIR, 'logs');
42
42
  const argv = process.argv.slice(2);
43
43
  const cmd = argv[0] && !argv[0].startsWith('-') ? argv[0] : null;
44
44
  const rest = cmd ? argv.slice(1) : argv;
45
- const flags = {};
46
- const positional = [];
47
- for (let i = 0; i < rest.length; i++) {
48
- const a = rest[i];
49
- if (a.startsWith('--')) {
50
- const key = a.slice(2);
51
- const next = rest[i + 1];
52
- if (next !== undefined && !next.startsWith('--')) { flags[key] = next; i++; }
53
- else flags[key] = true;
54
- } else positional.push(a);
55
- }
45
+ const { flags, positional } = require(path.join(REPO_ROOT, 'memhouse', 'flags')).parseArgv(rest);
56
46
  const JSON_OUT = flags.json === true;
57
47
 
58
48
  // Unknown flags stop the run — see memhouse/flags.js for why a shrug is not enough.
@@ -90,7 +80,7 @@ function resolveConfig() {
90
80
  url: pick('url', 'MEMHOUSE_URL', 'http://localhost:8123'),
91
81
  user: pick('user', 'MEMHOUSE_USER', 'memhouse_root'),
92
82
  password: pick('password', 'MEMHOUSE_PASSWORD', ''),
93
- // The house defaults to the USER'S OWN NAME (`polat` ships into `polat.messages`),
83
+ // The house defaults to the USER'S OWN NAME (`sam` ships into `sam.messages`),
94
84
  // because a house is a database and the natural first house is your own. Any name
95
85
  // works — user 'alice' with house 'default' is a supported pairing.
96
86
  // ONE house name unless somebody has a reason. The member's rooms are named for them
@@ -341,6 +331,22 @@ async function ask(question, dflt) {
341
331
  // reason in .cause. `✗ not connected: fetch failed` names neither the host nor the problem,
342
332
  // while `ship` prints ECONNREFUSED for the identical condition — the three commands
343
333
  // disagreed about the same event.
334
+ // Is `url` an HTTP endpoint that answers at all? ClickHouse's /ping needs no credential, so
335
+ // this checks an address without sending one. Any HTTP response counts — a proxy that
336
+ // wants auth on every path still proves the address is reachable.
337
+ async function pingUrl(url) {
338
+ let target;
339
+ try { target = new URL('ping', String(url).endsWith('/') ? String(url) : `${url}/`); }
340
+ catch { return { ok: false, reason: 'not a URL' }; }
341
+ try {
342
+ await fetch(target, { signal: AbortSignal.timeout(8000), redirect: 'manual' });
343
+ return { ok: true };
344
+ } catch (e) {
345
+ const c = e && e.cause && (e.cause.code || e.cause.message);
346
+ return { ok: false, reason: c || (e && e.name === 'TimeoutError' ? 'timed out' : String(e && e.message || e)) };
347
+ }
348
+ }
349
+
344
350
  function netReason(e) {
345
351
  const cause = e && e.cause;
346
352
  const inner = cause && (cause.message || cause.code);
@@ -358,7 +364,7 @@ function netReason(e) {
358
364
  // --admin-password (warned), then MEMHOUSE_ADMIN_PASSWORD, then the stored credential
359
365
  // of that SAME admin, then a TTY prompt. null = none, and the caller has been told how
360
366
  // to give one without putting it on the command line. See memhouse/admin-secret.js.
361
- async function adminPasswordFor(adminUser) {
367
+ async function adminPasswordFor(adminUser, out = {}) {
362
368
  const as = require(path.join(REPO_ROOT, 'memhouse', 'admin-secret'));
363
369
  const file = readEnvFile();
364
370
  let r;
@@ -369,9 +375,11 @@ async function adminPasswordFor(adminUser) {
369
375
  } catch (e) { console.log(bad(e.message)); return null; }
370
376
  // stderr: `members --json` and friends must emit nothing but JSON on stdout.
371
377
  if (r.source === 'flag') console.error(warn(as.ARGV_WARNING));
378
+ out.source = r.source;
372
379
  if (r.password !== undefined) return r.password;
373
380
  if (process.stdin.isTTY) {
374
381
  const p = await askSecret(` password for '${adminUser}'`);
382
+ out.source = 'prompt';
375
383
  if (p) return p;
376
384
  console.log(bad('no password given'));
377
385
  return null;
@@ -762,7 +770,7 @@ function printLocalHouseOffer(engines) {
762
770
  // The ClickHouse user IS the identity; rooms are named for whatever the server answers
763
771
  // to currentUser(). The home directory only produces a SUGGESTION, and a suggestion that
764
772
  // had to be changed is printed rather than applied quietly — sanitizing collides
765
- // (`ramazan.polat` and `ramazan-polat` both land on `ramazan_polat`), so the human has to
773
+ // (`jane.doe` and `jane-doe` both land on `jane_doe`), so the human has to
766
774
  // see it.
767
775
  function suggestMember() {
768
776
  const base = path.basename(os.homedir() || '');
@@ -1079,7 +1087,7 @@ async function cmdInstall({ interactive }) {
1079
1087
  for (const k of wanted) process.env[k] = parsed[k];
1080
1088
  // The invite also carries what the inviter's house is ON: its channel (a `team` house
1081
1089
  // is 0.18.x, published under `team`). Dropping it wrote an env with no pin, and the
1082
- // member's first `memhouse update` followed `latest` — zeo's 0.17 line — and
1090
+ // member's first `memhouse update` followed `latest` — then the 0.17 line — and
1083
1091
  // downgraded a working team install onto the wrong layout. A flag still wins.
1084
1092
  for (const k of ['MEMHOUSE_CHANNEL', 'MEMHOUSE_EDITORS', 'MEMHOUSE_CLAUDE_ROOTS']) if (parsed[k]) process.env[k] = parsed[k];
1085
1093
  console.log(ok(`using the invite file ${String(flags.env)} (nothing persisted until the install proves out)`));
@@ -2248,15 +2256,31 @@ function installKind() {
2248
2256
  async function cmdUpdate() {
2249
2257
  const { kind, root } = installKind();
2250
2258
  const cfg0 = resolveConfig();
2251
- // --channel pins for this run AND every later one: the point of pinning is that the next
2252
- // person to type `memhouse update` does not have to remember it.
2253
- if (flags.channel && flags.channel !== true && String(flags.channel) !== cfg0.channel) {
2254
- writeEnvFile({ ...cfg0, channel: String(flags.channel) });
2255
- console.log(ok(`channel pinned in ${short(ENV_FILE)}: ${flags.channel}`));
2256
- }
2257
- const pinned = flags.channel && flags.channel !== true ? String(flags.channel) : (cfg0.channel || null);
2259
+ const asked = flags.channel && flags.channel !== true ? String(flags.channel) : null;
2260
+ // What is PINNED is what the env file says. resolveConfig folds --channel into cfg0, so
2261
+ // comparing the flag with cfg0.channel compared it with itself, and the pin was never
2262
+ // written at all.
2263
+ const stored = readEnvFile().MEMHOUSE_CHANNEL || null;
2264
+ const pinned = asked || cfg0.channel || null;
2258
2265
  const tags = await chan.fetchTags();
2259
2266
  const pick = chan.pickChannel({ version: PKG.version, pinned, tags });
2267
+ // --channel pins for this run AND every later one: the point of pinning is that the next
2268
+ // person to type `memhouse update` does not have to remember it. Only a channel the
2269
+ // registry actually has is pinned. The pin used to be written before anything was
2270
+ // checked, so `--channel tema` (or any typo) left every later `update` following a tag
2271
+ // that does not exist, --no-install included, which checks no channel at all (A5).
2272
+ if (asked && asked !== stored) {
2273
+ if (!tags) {
2274
+ console.log(warn(`the registry did not answer, so channel '${asked}' cannot be checked — not pinned`));
2275
+ } else if (!tags[asked]) {
2276
+ console.log(bad(`no channel '${asked}' on the registry — nothing pinned, nothing changed`));
2277
+ console.log(` channels: ${Object.keys(tags).join(', ')}`);
2278
+ return 1;
2279
+ } else {
2280
+ writeEnvFile({ ...cfg0, channel: asked });
2281
+ console.log(ok(`channel pinned in ${short(ENV_FILE)}: ${asked}`));
2282
+ }
2283
+ }
2260
2284
  const latest = pick.target;
2261
2285
  if (JSON_OUT && flags.check) return console.log(JSON.stringify({ kind, root, current: PKG.version, channel: pick.channel, target: pick.target, reason: pick.reason, tags }, null, 2));
2262
2286
  console.log(` installed ${PKG.version} (${kind}: ${root})`);
@@ -2264,12 +2288,18 @@ async function cmdUpdate() {
2264
2288
  console.log(` target ${pick.target || (tags ? 'nothing to install' : 'unknown — the registry did not answer')}`);
2265
2289
  if (latest && latest === PKG.version && kind !== 'checkout') console.log(ok('already current'));
2266
2290
  if (flags.check) return 0;
2267
- if (!pick.channel && kind !== 'checkout') {
2291
+ // --no-install asks for the restart and the house checks only; the files are already in
2292
+ // place, so which channel they came from does not matter. Refusing here for want of one
2293
+ // made the command every nightly prints for itself ("memhouse update --no-install")
2294
+ // return 1 on exactly the builds that print it — a tarball is on no registry tag — and
2295
+ // leave the old shipper running the old code.
2296
+ const noInstall = flags['no-install'] === true;
2297
+ if (!pick.channel && kind !== 'checkout' && !noInstall) {
2268
2298
  console.log(warn('no automatic update for this install'));
2269
2299
  console.log(` ${pick.reason}`);
2270
2300
  return 1;
2271
2301
  }
2272
- if (pick.channel && !pick.target && kind !== 'checkout') {
2302
+ if (pick.channel && !pick.target && kind !== 'checkout' && !noInstall) {
2273
2303
  console.log(bad(`channel '${pick.channel}' has no version on the registry — nothing to install, nothing changed`));
2274
2304
  return 1;
2275
2305
  }
@@ -2279,7 +2309,9 @@ async function cmdUpdate() {
2279
2309
  console.log(' to keep a version around: npm install -g memhouse');
2280
2310
  return 1;
2281
2311
  }
2282
- if (kind === 'local-dep') {
2312
+ // --no-install installs nothing, so where the files came from does not matter: restart and
2313
+ // check the house. The refusal used to come first and turned it away (A6).
2314
+ if (kind === 'local-dep' && !noInstall) {
2283
2315
  console.log(warn('this is a project dependency, not a global install'));
2284
2316
  console.log(` upgrade it where it lives: npm install memhouse@${pick.channel || 'latest'}`);
2285
2317
  return 1;
@@ -2292,7 +2324,7 @@ async function cmdUpdate() {
2292
2324
  try { svc = require(path.join(REPO_ROOT, 'memhouse', 'service.js')).status(); } catch { /* unsupported platform */ }
2293
2325
  const wasRunning = { shipper: !!pidOf('shipper'), dashboard: !!pidOf('dashboard') };
2294
2326
 
2295
- if (flags['no-install'] === true) {
2327
+ if (noInstall) {
2296
2328
  // The files were already replaced by other means — a hand-typed `npm i -g`, a tarball,
2297
2329
  // a configuration manager. The npm/git half is exactly what such a pilot has already
2298
2330
  // done, and the half a bare install leaves undone (restart, migrations, schema heal)
@@ -2326,7 +2358,9 @@ async function cmdUpdate() {
2326
2358
  return 1;
2327
2359
  }
2328
2360
  }
2329
- console.log(ok('files updated'));
2361
+ // Said only when something was installed. Under --no-install it claimed "files updated"
2362
+ // for files nobody touched (A6).
2363
+ if (!noInstall) console.log(ok('files updated'));
2330
2364
  // The house may have been converted to the one layout since this machine last ran:
2331
2365
  // the member's rooms moved from <name>.* to mem.<name>_*. Fix the env here, before the
2332
2366
  // shipper restarts, or its first pass refuses the old address forever.
@@ -2466,7 +2500,7 @@ async function cmdUpdate() {
2466
2500
  // Every Claude Code instance on this machine, not just the default one.
2467
2501
  //
2468
2502
  // A pilot rarely has one. `~/.claude` is the stock install, `CLAUDE_CONFIG_DIR` points at
2469
- // whichever they are running right now, and Kommander-style playbooks live under
2503
+ // whichever they are running right now, and playbook installs live under
2470
2504
  // `~/.claude-playbooks/<name>[/playbook]`, each a complete config directory with its own
2471
2505
  // skills/. Installing into one and calling it done leaves /mem:recall missing from
2472
2506
  // every other instance the pilot uses — silently, because a missing skill does not announce
@@ -3709,14 +3743,36 @@ async function cmdInvite() {
3709
3743
  // itself is tried below — which works for whoever set the house up as its superuser.
3710
3744
  if (flags['admin-user'] === true) { console.log(bad('--admin-user needs a name, e.g. --admin-user default')); return 1; }
3711
3745
  let adminUser = (flags['admin-user'] ? String(flags['admin-user']) : '') || cfg.adminUser || undefined;
3746
+ // Who named this admin decides where its credential may go (G4, memhouse/invite-target.js):
3747
+ // one given for this command provisions at --url; one this install keeps provisions only
3748
+ // at the house it was saved for.
3749
+ const adminUserSource = flags['admin-user'] ? 'flag'
3750
+ : (process.env.MEMHOUSE_ADMIN_USER ? 'env' : (adminUser ? 'file' : 'member'));
3712
3751
  let adminPass;
3752
+ const pwSource = {};
3713
3753
  if (adminUser) {
3714
3754
  // --admin-password-file / --admin-password / MEMHOUSE_ADMIN_PASSWORD / the stored one
3715
3755
  // for this same admin / a prompt. `--admin-user` as a flag used to switch the
3716
3756
  // environment off, so the only scriptable path put the password on argv.
3717
- adminPass = await adminPasswordFor(adminUser);
3757
+ adminPass = await adminPasswordFor(adminUser, pwSource);
3718
3758
  if (adminPass === null) return 1;
3719
3759
  }
3760
+ const target = require(path.join(REPO_ROOT, 'memhouse', 'invite-target'))
3761
+ .provisionTarget({ url, storedUrl: configuredUrl, adminUserSource, passwordSource: pwSource.source || null });
3762
+ if (target.error) { console.log(bad(target.error)); return 1; }
3763
+ // A stored credential is never sent to --url, so check that the INVITEE can reach it
3764
+ // without one: ClickHouse answers /ping unauthenticated.
3765
+ if (target.stored) {
3766
+ const reach = await pingUrl(url);
3767
+ if (!reach.ok) {
3768
+ console.log(bad(`cannot reach ${url}: ${reach.reason}`));
3769
+ console.log(' --url is the address the INVITEE will use, and invite checks it from here first.');
3770
+ console.log(' A `deploy --local` house is bound to loopback on purpose, so no LAN address');
3771
+ console.log(' reaches it and no invitee could either — publish it (tunnel, reverse proxy)');
3772
+ console.log(' before inviting, or use --print-sql and let the invitee be told the address.');
3773
+ return 1;
3774
+ }
3775
+ }
3720
3776
  if (!adminUser) {
3721
3777
  const c = requireConfig(cfg, 'invite');
3722
3778
  if (!c.user) return 1;
@@ -3735,20 +3791,18 @@ async function cmdInvite() {
3735
3791
  // unreachable host — a typo, a loopback-bound house, a tunnel that is down — come back
3736
3792
  // as "you are only a member", which sends the reader after a privilege they already
3737
3793
  // have. Caught on the testbed, where a wrong --url produced exactly that.
3794
+ // The member credential is stored, so it is checked against the house it was saved for
3795
+ // (target.url), never against --url; --url's reachability was checked above, unauthenticated.
3738
3796
  try {
3739
- await ch({ ...c, url }, 'SELECT 1', { database: '' });
3797
+ await ch({ ...c, url: target.url }, 'SELECT 1', { database: '' });
3740
3798
  } catch (e) {
3741
- console.log(bad(`cannot reach ${url} as '${c.user}': ${netReason(e)}`));
3742
- console.log(' --url is the address the INVITEE will use, and invite checks it from here first.');
3743
- console.log(' A `deploy --local` house is bound to loopback on purpose, so no LAN address');
3744
- console.log(' reaches it and no invitee could either — publish it (tunnel, reverse proxy)');
3745
- console.log(' before inviting, or use --print-sql and let the invitee be told the address.');
3799
+ console.log(bad(`cannot reach your house ${target.url} as '${c.user}': ${netReason(e)}`));
3746
3800
  return 1;
3747
3801
  }
3748
3802
  // What this credential may do, judged from its own grants and SCOPE-AWARE — a member
3749
3803
  // holds CREATE DATABASE inside `ON <their-db>.*`, which mints no new house at all.
3750
3804
  // Shared with `whoami` so the two can never disagree about who is an administrator.
3751
- const caps = capabilitiesFrom(await readGrants({ ...c, url }, c.user));
3805
+ const caps = capabilitiesFrom(await readGrants({ ...c, url: target.url }, c.user));
3752
3806
  if (caps.canProvision) {
3753
3807
  adminUser = c.user; adminPass = c.password;
3754
3808
  console.log(ok(`inviting as your own credential '${c.user}' (it can create users and houses here)`));
@@ -3771,13 +3825,12 @@ async function cmdInvite() {
3771
3825
  }
3772
3826
  }
3773
3827
 
3774
- // WHERE to connect to provision. --url is the address the INVITEE will use — it may be
3775
- // a LAN IP or tunnel that is not this admin's own endpoint, and sending the stored
3776
- // credential there would hand it to whatever answers a typo'd or hostile --url. So the
3777
- // stored-credential path provisions at the admin's OWN configured cfg.url; only an
3778
- // EXPLICIT --admin-* (where the admin typed the target themselves) provisions at --url.
3779
- // Either way the invite FILE carries --url, the invitee's path.
3780
- const provisionUrl = flags['admin-user'] ? url : cfg.url;
3828
+ // WHERE to connect to provision: decided above by provisionTarget(). A stored credential
3829
+ // provisions at the house it was saved for; one given for this command, at --url. Either
3830
+ // way the invite FILE carries --url, the invitee's path. (This used to read
3831
+ // `flags['admin-user'] ? url : cfg.url` — but cfg.url IS --url once the flag is given, so
3832
+ // the stored credential went to whatever --url named.)
3833
+ const provisionUrl = target.url;
3781
3834
 
3782
3835
  // Is there already a house here, with somebody's memory in it? The house is created
3783
3836
  // with CREATE DATABASE IF NOT EXISTS, so inviting a name whose database already exists
@@ -4037,8 +4090,10 @@ async function cmdUninstall() {
4037
4090
  //
4038
4091
  // (default) daemons, service, runtime state. Config and identity KEPT.
4039
4092
  // --credentials also forget the house: the env file, with its password.
4040
- // --full-removal all of MEMHOUSE_HOME, fingerprint included. The next install is a
4041
- // NEW host whose rows do not join this machine's history.
4093
+ // --full-removal all of MEMHOUSE_HOME, host.json included. The next install
4094
+ // derives the same fingerprint from the OS machine id (0.18.6+), so it
4095
+ // is the same host unless the machine was renamed meanwhile or has no
4096
+ // readable machine id (then it falls back to random: a new host).
4042
4097
  //
4043
4098
  // No tier touches the house: transcripts live in ClickHouse and re-ship from the local
4044
4099
  // session stores regardless.
@@ -4059,8 +4114,9 @@ async function cmdUninstall() {
4059
4114
  if (flags.yes !== true && process.stdin.isTTY) {
4060
4115
  const id = hostjs.read(HOME_DIR);
4061
4116
  console.log(warn(`--full-removal also discards this machine's host identity${id ? ` (${id.id})` : ''}.`));
4062
- console.log(' Rows already shipped keep that name, a future install gets a new one, and');
4063
- console.log(" this machine then reads as two. Your transcripts themselves are safe.");
4117
+ console.log(' A reinstall derives it again from this machine\'s own id, so it normally comes');
4118
+ console.log(' back the same — unless the machine is renamed first, or has no readable machine');
4119
+ console.log(" id, and then it reads as two machines. Your transcripts themselves are safe.");
4064
4120
  const a = (await ask('Remove everything, including the host identity? (yes/no)', 'no')).toLowerCase();
4065
4121
  if (a !== 'yes' && a !== 'y') { console.log('aborted — daemons are stopped, nothing was removed'); return; }
4066
4122
  }
@@ -4556,7 +4612,7 @@ async function cmdUninstall() {
4556
4612
  process.exitCode = 2; break;
4557
4613
  }
4558
4614
  // Who owns this house. A local deploy is the pilot's own machine and their own
4559
- // memory, so the house user is THEM — suggested from the OS username (`polat`, not
4615
+ // memory, so the house user is THEM — suggested from the OS username (`sam`, not
4560
4616
  // `memhouse_root`) — created by the container itself at first init with full
4561
4617
  // rights. No admin/member split, no provisioning dance: a URI and a credential is
4562
4618
  // the whole install. The name is fixed at volume init exactly like the password,
@@ -4617,7 +4673,7 @@ async function cmdUninstall() {
4617
4673
  // STATED sources only, then the house user. priorCfg.db is unusable here: resolveConfig
4618
4674
  // computes a DEFAULT for it (the user's name, itself defaulted), so going through it
4619
4675
  // handed a fresh deploy `memhouse_root` as the house name while the superuser was
4620
- // 'polat' — the memory would live in a database named after a role account nobody
4676
+ // 'sam' — the memory would live in a database named after a role account nobody
4621
4677
  // chose.
4622
4678
  const targetDb = flags.db || process.env.MEMHOUSE_DB || readEnvFile().MEMHOUSE_DB || 'mem';
4623
4679
  // Same check install runs, but EARLY — deploy initialises the container volume and
@@ -40,7 +40,7 @@ options. It never hangs, and it never guesses.
40
40
  The best form: the value exists only in the one command's environment.
41
41
 
42
42
  ```bash
43
- with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:pilot/house-admin -- \
43
+ with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:house-admin -- \
44
44
  memhouse invite alice --url https://house.example.com --admin-user default
45
45
 
46
46
  # 1Password
@@ -33,7 +33,7 @@ it:
33
33
 
34
34
  ```
35
35
  Watched directories (override with the variable shown, in the env file):
36
- claude ~/.claude, ~/.claude-playbooks/kommander, … [MEMHOUSE_CLAUDE_ROOTS]
36
+ claude ~/.claude, ~/.claude-playbooks/work, … [MEMHOUSE_CLAUDE_ROOTS]
37
37
  codex ~/.codex [MEMHOUSE_CODEX_ROOTS]
38
38
  gemini-cli ~/.gemini [MEMHOUSE_GEMINI_CLI_ROOTS]
39
39
  cursor ~/Library/Application Support/Cursor/User, … [built-in]
@@ -43,7 +43,7 @@ Two kinds of line in the env file narrow it, or `--editors` / `--claude-roots` a
43
43
 
44
44
  ```
45
45
  MEMHOUSE_EDITORS='claude'
46
- MEMHOUSE_CLAUDE_ROOTS='~/.claude-playbooks/kommander-chaos'
46
+ MEMHOUSE_CLAUDE_ROOTS='~/.claude-playbooks/work-team'
47
47
  ```
48
48
 
49
49
  - `MEMHOUSE_EDITORS` names the adapters to run.
@@ -64,7 +64,7 @@ MEMHOUSE_PASSWORD=… memhouse install --url https://house.example.com --user al
64
64
  memhouse install --env invite-alice.env
65
65
 
66
66
  # you hold the server's admin: memhouse creates the member, then ships as the member
67
- with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:pilot/house-admin -- \
67
+ with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:house-admin -- \
68
68
  memhouse install --url https://house.example.com --admin-user default --member alice
69
69
 
70
70
  # you hold nothing: print the SQL for whoever administers the server
@@ -33,7 +33,7 @@ memhouse invite alice --url https://house.example.com
33
33
  `--admin-password-file`, or from a prompt.
34
34
 
35
35
  ```bash
36
- with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:pilot/house-admin -- \
36
+ with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:house-admin -- \
37
37
  memhouse invite alice --url https://house.example.com --admin-user default
38
38
  ```
39
39
 
@@ -23,8 +23,8 @@ database once leaked into each other. A member can read their isolation back wit
23
23
 
24
24
  | | rooms | each member holds |
25
25
  |---|---|---|
26
- | alone on a laptop | `mem.polat_*` | one grant on `mem.polat_*` |
27
- | a team on one server | `mem.polat_*`, `mem.alice_*`, … | one grant each, on their own |
26
+ | alone on a laptop | `mem.sam_*` | one grant on `mem.sam_*` |
27
+ | a team on one server | `mem.sam_*`, `mem.alice_*`, … | one grant each, on their own |
28
28
 
29
29
  Reading across members is a `UNION ALL` over the rooms you hold: your own, and any shared
30
30
  with you. Naming a room you were not granted is an error, never a silent omission.
@@ -34,11 +34,14 @@ with you. Naming a room you were not granted is an error, never a silent omissio
34
34
  - **`user_id`** is stamped by the server: `MATERIALIZED currentUser()`. Async inserts are
35
35
  pinned off for members, so the stamp cannot be skipped. `WHERE user_id = 'alice'` is one
36
36
  person.
37
- - **`host`** is the machine. Each install mints a random fingerprint once and keeps it in
38
- `$MEMHOUSE_HOME/host.json`, and every row carries `<hostname>-<8 hex>`. It is random,
39
- not derived from the machine:
40
- - a derived id would merge two laptops that both answer to `MacBook-Pro`;
41
- - a derived id would split one machine in two when it is renamed.
37
+ - **`host`** is the machine. Every row carries `<hostname>-<8 hex>`, where the 8 hex come
38
+ from a fingerprint of the OS's own machine id (IOPlatformUUID on macOS,
39
+ `/etc/machine-id` on Linux), hashed so the raw id never leaves the machine. It is kept in
40
+ `$MEMHOUSE_HOME/host.json`. It is not taken from the hostname:
41
+ - two laptops that both answer to `MacBook-Pro` still get different ids;
42
+ - a renamed machine keeps its id, and `status` reports the new name beside it;
43
+ - reinstalling, even after `uninstall --full-removal`, brings the same machine back
44
+ under the same id. Where no machine id can be read, it falls back to a random one.
42
45
 
43
46
  `memhouse status` shows this machine's id. `memhouse doctor` names every host in your
44
47
  rooms.
@@ -12,8 +12,10 @@ npm uninstall -g memhouse # then remove the package
12
12
  - **The default is the conservative one.** Stopping the shipper is routine: before an
13
13
  upgrade, while debugging, or when a laptop should go quiet for a week. It should not
14
14
  also mean "forget which house I use and who this machine is".
15
- - **`--full-removal` asks first.** A later reinstall mints a **new** machine identity, and
16
- this machine then reads as two machines in your history.
15
+ - **`--full-removal` asks first.** It deletes `host.json`. A later reinstall derives the
16
+ same identity again from the machine's own id, so your history stays one machine, unless
17
+ the machine was renamed in between or has no readable machine id. Then it comes back as
18
+ a new host.
17
19
  - **Your memory stays.** The rooms in ClickHouse are untouched by every tier. What the
18
20
  shipper wrote can be rebuilt from the local session stores with `memhouse ship --full`,
19
21
  for as long as the editors keep them.
@@ -5,6 +5,8 @@ do, how to upgrade, what is deprecated and when it goes, and what was deferred.
5
5
 
6
6
  | version | date | headline |
7
7
  |---|---|---|
8
+ | [0.18.12](v0.18.12.md) | 2026-10-29 | skills: allowed tools restricted to read-only commands |
9
+ | [0.18.11](v0.18.11.md) | 2026-10-10 | sessions with subagents stop re-shipping; security: no stored credential sent to `invite --url` |
8
10
  | [0.18.10](v0.18.10.md) | 2026-10-08 | security: the admin password never on argv; no member password printed; a scoped share reaches only its scope |
9
11
 
10
12
  Earlier versions: [`CHANGELOG.md`](../../CHANGELOG.md).
@@ -8,7 +8,7 @@ A security release:
8
8
 
9
9
  Everyone on 0.18.x should upgrade.
10
10
 
11
- **No drill was run for this release; waived by the pilot (2026-10-08). The drill runs before 0.19.0.**
11
+ **No drill was run for this release; waived by the maintainer (2026-10-08). The drill runs before 0.19.0.**
12
12
 
13
13
  ```bash
14
14
  memhouse update # or: npm install -g memhouse@latest, then memhouse update --no-install
@@ -34,14 +34,14 @@ The admin-assisted commands (`install --member`, `invite`, `passwd`, `convert`,
34
34
  5. a prompt, when there is a terminal to prompt on.
35
35
 
36
36
  ```bash
37
- with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:pilot/house-admin -- \
37
+ with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:house-admin -- \
38
38
  memhouse invite alice --url https://house.example.com --admin-user default
39
39
  printf '%s' "$ADMIN_PASSWORD" | memhouse invite alice --url … --admin-user default --admin-password-file -
40
40
  ```
41
41
 
42
42
  Before this release, `--admin-user` as a flag switched every other source off. The only
43
43
  scriptable admin path therefore put the password on argv, where `ps` shows it for as long
44
- as the command runs and shell history keeps it. Found by O's sandbox rehearsal (finding 6).
44
+ as the command runs and shell history keeps it. Found in a sandbox rehearsal (finding 6).
45
45
  With no source and no terminal, the command stops and prints the three ways above instead
46
46
  of hanging or guessing.
47
47
 
@@ -179,7 +179,7 @@ before release by four models. Three said "ship with fixes"; the fourth said "do
179
179
  mostly for pre-existing issues. Its pre-tag fixes are not all done, and this release was
180
180
  not held for them.
181
181
 
182
- Status of the 0.18.10 pre-tag list (`review-0.18.10-consolidated`):
182
+ Status of the 0.18.10 pre-tag list (the consolidated pre-tag review):
183
183
 
184
184
  | item | what | status |
185
185
  |---|---|---|
@@ -197,7 +197,7 @@ Status of the 0.18.10 pre-tag list (`review-0.18.10-consolidated`):
197
197
  | G4 | `invite --url` connects with the stored credential to the invitee-facing URL | deferred to 0.18.11 (security) |
198
198
  | G26 | skills point at `../reference/HOUSE.md`; the file is `../../reference/HOUSE.md` | deferred to 0.18.11 |
199
199
  | G11 | an idle house's dashboard goes empty after the stats TTL | deferred to 0.18.11 |
200
- | GLM-F1 | a subagent file read before its first line is written re-sorts its siblings | deferred to 0.18.11 (with the RC) |
200
+ | SUB-1 | a subagent file read before its first line is written re-sorts its siblings | deferred to 0.18.11 (with the RC) |
201
201
 
202
202
  ## Documentation
203
203
 
@@ -219,8 +219,8 @@ every tag and what it changed.
219
219
  - CI is green on `main` at the release commit: the syntax gate, unit tests, the
220
220
  one-layout and invite matrices, and a headless e2e on ClickHouse 25.11 and 26.7.
221
221
  - No live house was touched to prepare this release.
222
- - **No drill was run for this release; waived by the pilot (2026-10-08). The drill runs before 0.19.0.**
222
+ - **No drill was run for this release; waived by the maintainer (2026-10-08). The drill runs before 0.19.0.**
223
223
  `AGENTS.md` asks for a drill before a release that changes a surface a person touches,
224
224
  and this one changes install, invite and share. A drill was prepared on the testbed, but
225
- its setup was refused by this session's permission layer, and the pilot chose to release
225
+ its setup could not run in the release environment, and the maintainer chose to release
226
226
  without it.