memhouse 0.20.0 → 0.20.1
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/CHANGELOG.md +60 -0
- package/bin/memhouse.js +206 -26
- package/docs/guides/install.md +3 -0
- package/docs/guides/invite.md +7 -0
- package/docs/guides/search-and-resume.md +30 -0
- package/docs/guides/share.md +4 -0
- package/docs/releases/README.md +1 -0
- package/docs/releases/v0.20.1.md +114 -0
- package/memhouse/codex-plugin.js +194 -0
- package/memhouse/delivery/INVITATION.md.tpl +1 -1
- package/memhouse/delivery/codex/AGENTS-BLOCK.md +11 -0
- package/memhouse/delivery/codex/SKILL.md +50 -0
- package/memhouse/delivery/plugin/reference/HOUSE.md +7 -9
- package/memhouse/delivery/plugin/skills/access/SKILL.md +1 -1
- package/memhouse/flags.js +1 -1
- package/memhouse/help.js +37 -0
- package/memhouse/house/schema-choice.js +15 -6
- package/memhouse/invitation.js +8 -2
- package/memhouse/plugin-copies.js +13 -1
- package/memhouse/release-hint.js +24 -0
- package/memhouse/share.js +31 -1
- package/memhouse/version-notice.js +66 -0
- package/package.json +2 -2
- package/public/assets/{index-CQSJ3nhV.js → index-CcNnhfM3.js} +1 -1
- package/public/index.html +1 -1
- package/ui/src/lib/dashboard-token.js +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,66 @@ Versions before 0.8.0 were beta-only. Beta installs of 0.7.x and earlier should
|
|
|
4
4
|
uninstall and reinstall — the 0.8.0 layout is new, and an in-place `update` +
|
|
5
5
|
migration path is planned work, not a promise the old versions can cash.
|
|
6
6
|
|
|
7
|
+
## 0.20.1 — 2026-11-02
|
|
8
|
+
|
|
9
|
+
Release notes: [`docs/releases/v0.20.1.md`](docs/releases/v0.20.1.md).
|
|
10
|
+
|
|
11
|
+
- **Dashboard: a browser that refuses sessionStorage still opens it.** The page keeps the token
|
|
12
|
+
in memory for that load when sessionStorage is refused; it used to show the "open it with
|
|
13
|
+
`memhouse dashboard`" page instead.
|
|
14
|
+
- **doctor names the channel when it says a release is the latest.** An unpriced model's hint now
|
|
15
|
+
says "the latest release on '<channel>'", so an install pinned to another line is not told it
|
|
16
|
+
is on the newest release overall. Offline, the hint no longer claims the install is current.
|
|
17
|
+
- **/mem skills: the raw-SQL recipe runs `memhouse room-sql` as written.** The reference's
|
|
18
|
+
schema-2 recipe wrapped it in a `q()` shell function, so every call asked for approval despite
|
|
19
|
+
the skills' pre-approval. It now runs `memhouse room-sql <<'SQL'` directly.
|
|
20
|
+
- **install, invite and `deploy --local` name a refused credential up front.** When the server
|
|
21
|
+
refuses the credential (ClickHouse code 516, or 194 for a missing password), they now stop
|
|
22
|
+
before writing anything with "could not authenticate as '<user>' at <url>", instead of reading
|
|
23
|
+
the house as new and failing at the next step. Code 194 counts as an authentication failure
|
|
24
|
+
everywhere 516 does.
|
|
25
|
+
- **`memhouse <command> --help` prints that command's help.** It used to print the whole page
|
|
26
|
+
for every command. A command with no entry of its own still gets the whole page, and
|
|
27
|
+
`memhouse q --help` lists the named queries (the same as `memhouse q list`).
|
|
28
|
+
- **`memhouse share --list` also lists the shares you received.** Beside who reads your
|
|
29
|
+
sessions, it names each member whose sessions this credential can read, with the command
|
|
30
|
+
that reads them (`memhouse q recent --member <them>`); `--json` adds `received`. An
|
|
31
|
+
administrator is told it reads every member's sessions instead.
|
|
32
|
+
- **`memhouse discover` flags an unreachable house only when it is this install's.** The
|
|
33
|
+
default local address (`http://localhost:8123`) is probed as a discovery; when nothing
|
|
34
|
+
answers there and nothing is configured to use it, it is a plain line, not a ✗. `--json`
|
|
35
|
+
marks each endpoint `configured`.
|
|
36
|
+
- **`memhouse update` on a machine with no member install checks no house.** An
|
|
37
|
+
administrator's machine (only `MEMHOUSE_URL` and the admin variables) used to run the schema
|
|
38
|
+
heal and the migration check as the default member and print its authentication failures as
|
|
39
|
+
the house's state. It now says there is no member install here and skips those steps.
|
|
40
|
+
- **`memhouse members` tells a share from a member's own tables.** Each account's line names its
|
|
41
|
+
own tables, then each member whose sessions it reads ("reads alice's sessions (a share,
|
|
42
|
+
scoped)" or "full", where the row policies can be listed), then any other grant, which
|
|
43
|
+
memhouse does not make itself. A scoped share used to read as a plain list of another
|
|
44
|
+
member's tables. `--json` adds `own`, `shares` and `other`.
|
|
45
|
+
- **A new minor version's first run says so.** The first CLI call of a newer major.minor on a
|
|
46
|
+
machine prints one line to stderr, naming the version it replaces and the shipped release
|
|
47
|
+
notes to read. Patch releases, downgrades and fresh installs say nothing; a stamp in the
|
|
48
|
+
memhouse home (`version-seen`) records the last version that ran. An install from before the
|
|
49
|
+
stamp is read from its home: a 0.18 install moving to 0.20.1 is told "it ran 0.18 or
|
|
50
|
+
earlier".
|
|
51
|
+
- **`memhouse plugins install codex`.** Codex gets a short, marker-delimited block in its
|
|
52
|
+
`AGENTS.md` (never read or print the env file or a `MEMHOUSE_*` value; `MEMHOUSE_URL` is the
|
|
53
|
+
house, `MEMHOUSE_PORT` the dashboard; past sessions go to the skill) and a `memhouse` skill with
|
|
54
|
+
the read recipes, under `CODEX_HOME` or `~/.codex`. `plugins remove codex` takes out exactly
|
|
55
|
+
those: the rest of `AGENTS.md` stays byte-identical, and a `skills/memhouse` that memhouse did
|
|
56
|
+
not install is left alone. Opt-in: nothing is written there unless it ran; the first run of a
|
|
57
|
+
new version refreshes a copy it finds installed. The `/mem` skills stay Claude Code's.
|
|
58
|
+
- **An invite's password expires.** `memhouse invite` sets the new member's password to stop
|
|
59
|
+
working after 7 days (`--expires <days>`, 1 to 365), by the server's clock. The invite's
|
|
60
|
+
output, the invite file and `MEMHOUSE-INVITATION.md` say when. `install --env` lifts the
|
|
61
|
+
expiry when it changes the password, and when `--keep-password` keeps it. An expired invite
|
|
62
|
+
is refused at install with "this invite expired at … ask whoever invited you for a new one",
|
|
63
|
+
before anything is contacted or written. Inviting the same name again says the unused
|
|
64
|
+
account must be removed first (`DROP USER <name>`); its tables are kept and reused.
|
|
65
|
+
`invite --print-sql` prints the same expiry, with a placeholder for the date.
|
|
66
|
+
|
|
7
67
|
## 0.20.0 — 2026-10-29
|
|
8
68
|
|
|
9
69
|
Release notes: [`docs/releases/v0.20.0.md`](docs/releases/v0.20.0.md). Read its "Read this
|
package/bin/memhouse.js
CHANGED
|
@@ -39,6 +39,8 @@ const dashPort = require(path.join(REPO_ROOT, 'memhouse', 'server', 'dashboard-p
|
|
|
39
39
|
const HOME_DIR = process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
|
|
40
40
|
const ENV_FILE = path.join(HOME_DIR, 'env');
|
|
41
41
|
const RUN_DIR = path.join(HOME_DIR, 'run');
|
|
42
|
+
// How long an invite's password works unless --expires says otherwise.
|
|
43
|
+
const INVITE_DAYS = 7;
|
|
42
44
|
const LOG_DIR = path.join(HOME_DIR, 'logs');
|
|
43
45
|
|
|
44
46
|
// ── args ────────────────────────────────────────────────────────────────────────
|
|
@@ -91,6 +93,14 @@ try {
|
|
|
91
93
|
process.exit(2);
|
|
92
94
|
}
|
|
93
95
|
|
|
96
|
+
// A new minor version's first run here says so, once, with its release notes
|
|
97
|
+
// (memhouse/version-notice.js). stderr only, like the skills refresh below. It runs first: it reads
|
|
98
|
+
// the version that refresh records (skills-checked) before the refresh moves it.
|
|
99
|
+
require(path.join(REPO_ROOT, 'memhouse', 'version-notice')).checkVersionNotice({
|
|
100
|
+
home: process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse'),
|
|
101
|
+
version: require(path.join(REPO_ROOT, 'package.json')).version, repoRoot: REPO_ROOT,
|
|
102
|
+
log: (l) => console.error(`memhouse: ${l.replace(os.homedir(), '~')}`),
|
|
103
|
+
});
|
|
94
104
|
// A new version's first run refreshes the /mem skills bound to this home (an npm upgrade does
|
|
95
105
|
// not run `update`, which used to be the only refresh). After that, one stamp read per call.
|
|
96
106
|
// stderr only, so --json output stays clean. memhouse/plugin-copies.js.
|
|
@@ -622,7 +632,9 @@ Setup onboard interactive wizard: discover → configure →
|
|
|
622
632
|
and one grant on exactly those. Refuses a name whose rooms
|
|
623
633
|
already hold messages; --adopt takes them over (same
|
|
624
634
|
person, new credential). Not an admin? --print-sql prints
|
|
625
|
-
the plan to hand to whoever is.
|
|
635
|
+
the plan to hand to whoever is. The invited password
|
|
636
|
+
expires after --expires DAYS (default 7); install's
|
|
637
|
+
password change lifts it
|
|
626
638
|
passwd rotate this member's password + rewrite the env file
|
|
627
639
|
(admin-assisted: --admin-user, password as for install)
|
|
628
640
|
setup (re)write the connection config only (--yes = no prompts)
|
|
@@ -686,6 +698,9 @@ Agents plugins list | install claude [--target DIR] | remove
|
|
|
686
698
|
acts on EVERY Claude config dir found (~/.claude,
|
|
687
699
|
CLAUDE_CONFIG_DIR, ~/.claude-playbooks/*), all
|
|
688
700
|
selected by default; --yes takes them all unasked
|
|
701
|
+
plugins install codex Codex: a block in AGENTS.md (the rules) and a
|
|
702
|
+
memhouse skill (the read recipes), under CODEX_HOME or
|
|
703
|
+
~/.codex; plugins remove codex takes out exactly those
|
|
689
704
|
prompt print the memory system-prompt snippet
|
|
690
705
|
prompt --install print an install prompt for an agent, with this
|
|
691
706
|
machine's state and the one route that applies
|
|
@@ -757,8 +772,13 @@ async function cmdDiscover() {
|
|
|
757
772
|
out.adapterErrors = getAdapterErrors();
|
|
758
773
|
} catch (e) { out.editorsError = e.message; }
|
|
759
774
|
|
|
760
|
-
|
|
761
|
-
|
|
775
|
+
// The configured house, and the default local address as a discovery. An unreachable local
|
|
776
|
+
// address that nothing is configured to use is not a failure: it was flagged ✗ on machines
|
|
777
|
+
// whose house is elsewhere, and read as something to fix.
|
|
778
|
+
const stated = resolveConfig();
|
|
779
|
+
const configuredUrl = stated.stated ? stated.url : null;
|
|
780
|
+
for (const url of [...new Set([...(configuredUrl ? [configuredUrl] : []), 'http://localhost:8123'])]) {
|
|
781
|
+
const probe = { url, reachable: false, configured: url === configuredUrl };
|
|
762
782
|
try {
|
|
763
783
|
const res = await fetch(url.replace(/\/$/, '') + '/ping', { signal: AbortSignal.timeout(3000) });
|
|
764
784
|
probe.reachable = res.ok;
|
|
@@ -791,7 +811,7 @@ async function cmdDiscover() {
|
|
|
791
811
|
printAdapterErrors(out.adapterErrors);
|
|
792
812
|
console.log('\nClickHouse endpoints:');
|
|
793
813
|
for (const p of out.clickhouse) {
|
|
794
|
-
if (!p.reachable) console.log(bad(`${p.url} — unreachable`));
|
|
814
|
+
if (!p.reachable) console.log(p.configured ? bad(`${p.url} — unreachable (this install's house)`) : ` · ${p.url} — no ClickHouse answers here; nothing is configured to use it`);
|
|
795
815
|
else if (p.auth) console.log(warn(`${p.url} — reachable, credentials needed`));
|
|
796
816
|
else console.log(ok(`${p.url} — v${p.version}${p.hasHouse ? ', house present' : ''}${p.kernel ? ', KERNEL detected' : ''}`));
|
|
797
817
|
}
|
|
@@ -1024,7 +1044,7 @@ function handoffLines({ url, db, member, invite }) {
|
|
|
1024
1044
|
`-- MEMHOUSE_DB='${db}'`,
|
|
1025
1045
|
];
|
|
1026
1046
|
if (isV3(resolveConfig())) lines.push('-- MEMHOUSE_SCHEMA=3');
|
|
1027
|
-
if (invite) lines.push('-- MEMHOUSE_INVITE=1');
|
|
1047
|
+
if (invite) lines.push('-- MEMHOUSE_INVITE=1', "-- MEMHOUSE_INVITE_EXPIRES='<the same moment in UTC, e.g. 2026-11-06T09:00:00Z>'");
|
|
1028
1048
|
lines.push('-- They install with: memhouse install --env <that file> (it deletes the file afterwards)');
|
|
1029
1049
|
if (flags['member-password'] !== undefined) {
|
|
1030
1050
|
lines.push('-- The --member-password you gave is not printed: use it where the placeholder stands.');
|
|
@@ -1090,6 +1110,19 @@ async function adminBootstrap(cfg, admin) {
|
|
|
1090
1110
|
let password = flags['member-password'] || null;
|
|
1091
1111
|
if (exists) {
|
|
1092
1112
|
if (!password) {
|
|
1113
|
+
// An account still carrying VALID UNTIL holds an invite nobody installed: install lifts it.
|
|
1114
|
+
let pending = null;
|
|
1115
|
+
try {
|
|
1116
|
+
const r = await chRows(adminCfg, `SHOW CREATE USER \`${admin.member}\``, { database: '' });
|
|
1117
|
+
const m = /VALID UNTIL '([^']+)'/.exec(String(r[0] ? Object.values(r[0])[0] : ''));
|
|
1118
|
+
if (m && m[1] !== 'infinity') pending = m[1];
|
|
1119
|
+
} catch { /* not readable: the plain refusal below */ }
|
|
1120
|
+
if (pending) {
|
|
1121
|
+
console.log(bad(`ClickHouse user '${admin.member}' already exists: invited, never installed (its invite password expires at ${pending}, server time)`));
|
|
1122
|
+
console.log(` To invite them again, remove that account first, as the administrator: DROP USER \`${admin.member}\``);
|
|
1123
|
+
console.log(' then run the invite again; its tables are kept and reused');
|
|
1124
|
+
return null;
|
|
1125
|
+
}
|
|
1093
1126
|
console.log(bad(`ClickHouse user '${admin.member}' already exists — give their password (MEMHOUSE_MEMBER_PASSWORD, or --member-password-file) to install as them.`));
|
|
1094
1127
|
console.log(` Or pick another handle: --member <name>`);
|
|
1095
1128
|
return null;
|
|
@@ -1344,6 +1377,14 @@ async function cmdInstall({ interactive }) {
|
|
|
1344
1377
|
console.log(ok(`using the invite file ${String(flags.env)} (nothing persisted until the install proves out)`));
|
|
1345
1378
|
_inviteFileToShred = path.resolve(String(flags.env));
|
|
1346
1379
|
_inviteWantsRotate = parsed.MEMHOUSE_INVITE === '1';
|
|
1380
|
+
// An expired invite: its password no longer works (VALID UNTIL), so say that, before the
|
|
1381
|
+
// server answers with a bare "authentication failed".
|
|
1382
|
+
const until = Date.parse(parsed.MEMHOUSE_INVITE_EXPIRES || '');
|
|
1383
|
+
if (Number.isFinite(until) && Date.now() > until) {
|
|
1384
|
+
console.log(bad(`this invite expired at ${parsed.MEMHOUSE_INVITE_EXPIRES} (UTC) — ask whoever invited you for a new one; nothing was changed`));
|
|
1385
|
+
_inviteFileToShred = null;
|
|
1386
|
+
return 1;
|
|
1387
|
+
}
|
|
1347
1388
|
}
|
|
1348
1389
|
let cfg = resolveConfig();
|
|
1349
1390
|
const adminUser = flags['admin-user'];
|
|
@@ -1394,7 +1435,7 @@ async function cmdInstall({ interactive }) {
|
|
|
1394
1435
|
const adminPassword = await adminPasswordFor(String(adminUser));
|
|
1395
1436
|
if (adminPassword === null) return 1;
|
|
1396
1437
|
{
|
|
1397
|
-
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows({ ...cfg, user: String(adminUser), password: adminPassword }, sql, { database: '' }), cfg.db, member), member);
|
|
1438
|
+
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows({ ...cfg, user: String(adminUser), password: adminPassword }, sql, { database: '' }), cfg.db, member), member, { user: String(adminUser), url: cfg.url });
|
|
1398
1439
|
if (pick.refuse) { console.log(bad(pick.refuse)); console.log(' nothing was changed'); return 1; }
|
|
1399
1440
|
cfg.schema = pick.schema === '3' ? '3' : '';
|
|
1400
1441
|
}
|
|
@@ -1514,7 +1555,7 @@ async function cmdInstall({ interactive }) {
|
|
|
1514
1555
|
// Joining a house: its schema, detected, never the default (a 0.20 client on an unconverted
|
|
1515
1556
|
// 0.18 house stays on its rooms until the admin converts it and this machine switches).
|
|
1516
1557
|
{
|
|
1517
|
-
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows(cfg, sql, { database: '' }), cfg.db, cfg.user), cfg.user);
|
|
1558
|
+
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows(cfg, sql, { database: '' }), cfg.db, cfg.user), cfg.user, { user: cfg.user, url: cfg.url });
|
|
1518
1559
|
if (pick.refuse) { console.log(bad(pick.refuse)); console.log(' nothing was changed'); return 1; }
|
|
1519
1560
|
cfg.schema = pick.schema === '3' ? '3' : '';
|
|
1520
1561
|
}
|
|
@@ -1695,6 +1736,12 @@ async function cmdInstall({ interactive }) {
|
|
|
1695
1736
|
// password must never be what happens by default. The password is kept only on an explicit
|
|
1696
1737
|
// no (the question, or --keep-password), and then the output says who still knows it. A failed
|
|
1697
1738
|
// rotation never fails the install: it keeps the invite file and says how to retry.
|
|
1739
|
+
// A member holds ALTER USER on itself, so it can lift the invite's VALID UNTIL.
|
|
1740
|
+
async function clearInviteExpiry(cfg) {
|
|
1741
|
+
try { const c = cfg || resolveConfig(); await ch(c, `ALTER USER \`${c.user}\` VALID UNTIL 'infinity'`, { database: '' }); return true; }
|
|
1742
|
+
catch { return false; }
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1698
1745
|
async function finishInvite(cfg) {
|
|
1699
1746
|
if (_inviteWantsRotate) {
|
|
1700
1747
|
let go = flags['keep-password'] !== true;
|
|
@@ -1715,6 +1762,9 @@ async function finishInvite(cfg) {
|
|
|
1715
1762
|
for (const k of drift.changed) process.env[k] = drift.values[k];
|
|
1716
1763
|
if (drift.values.MEMHOUSE_PASSWORD !== undefined && flags.password !== undefined) flags.password = drift.values.MEMHOUSE_PASSWORD;
|
|
1717
1764
|
if (cfg) cfg.password = process.env.MEMHOUSE_PASSWORD;
|
|
1765
|
+
// The new password carries no expiry (a member's own ALTER USER … IDENTIFIED BY drops
|
|
1766
|
+
// VALID UNTIL); say so explicitly, in case a server keeps it.
|
|
1767
|
+
await clearInviteExpiry(cfg);
|
|
1718
1768
|
}
|
|
1719
1769
|
if (code !== 0) {
|
|
1720
1770
|
// Rotation failed and its own state may be uncertain — KEEP the invite file (it
|
|
@@ -1725,6 +1775,8 @@ async function finishInvite(cfg) {
|
|
|
1725
1775
|
}
|
|
1726
1776
|
} else {
|
|
1727
1777
|
console.log(warn('kept the invited password, as asked: whoever invited you still knows it. Change it with: memhouse passwd'));
|
|
1778
|
+
// Kept, so its expiry must go: the account would otherwise stop working on the invite's date.
|
|
1779
|
+
if (!(await clearInviteExpiry(cfg))) console.log(warn('could not clear the invite\'s expiry: the password stops working on its date unless you change it (memhouse passwd)'));
|
|
1728
1780
|
}
|
|
1729
1781
|
}
|
|
1730
1782
|
if (_inviteFileToShred) {
|
|
@@ -2297,18 +2349,17 @@ function confirmEmptyStore(value) {
|
|
|
2297
2349
|
// Whether `memhouse update` helps depends on whether a newer release exists: on the latest one,
|
|
2298
2350
|
// sending the reader to update loops (update says current, the gap stays).
|
|
2299
2351
|
let _behind;
|
|
2352
|
+
// Whether a newer release is out on this install's channel, read once per doctor
|
|
2353
|
+
// (memhouse/release-hint.js). fetchTags returns null offline; that is "unknown", not "current".
|
|
2300
2354
|
async function releaseBehind(cfg) {
|
|
2301
2355
|
if (_behind === undefined) {
|
|
2302
|
-
|
|
2303
|
-
|
|
2356
|
+
let tags = null;
|
|
2357
|
+
try { tags = await chan.fetchTags(); } catch { /* offline: unknown, not current */ }
|
|
2358
|
+
_behind = require(path.join(REPO_ROOT, 'memhouse', 'release-hint')).releaseState({ version: PKG.version, pinned: cfg.channel || null, tags });
|
|
2304
2359
|
}
|
|
2305
2360
|
return _behind;
|
|
2306
2361
|
}
|
|
2307
|
-
function pricingHint(b) {
|
|
2308
|
-
if (b.up) return `memhouse update installs ${b.up.target}, which may price it`;
|
|
2309
|
-
if (b.known) return `this is the latest release (${PKG.version}) and it has no price for it: its cost stays unknown until a release adds one`;
|
|
2310
|
-
return 'prices ship with each release: a newer one may add them (memhouse update), and on the latest one their cost stays unknown until a release does';
|
|
2311
|
-
}
|
|
2362
|
+
function pricingHint(b) { return require(path.join(REPO_ROOT, 'memhouse', 'release-hint')).pricingHint(b, PKG.version); }
|
|
2312
2363
|
|
|
2313
2364
|
async function cmdDoctor() {
|
|
2314
2365
|
if (flags['confirm-empty-store'] !== undefined) return confirmEmptyStore(flags['confirm-empty-store']);
|
|
@@ -3310,7 +3361,12 @@ async function cmdUpdate() {
|
|
|
3310
3361
|
// the member's rooms moved from <name>.* to mem.<name>_*. Fix the env here, before the
|
|
3311
3362
|
// shipper restarts, or its first pass refuses the old address forever.
|
|
3312
3363
|
// (Schema 3 has no such move: its tables were created in place, under the configured house.)
|
|
3313
|
-
|
|
3364
|
+
// A machine with no member install (an administrator's, with only MEMHOUSE_URL and the
|
|
3365
|
+
// MEMHOUSE_ADMIN_* variables) has no house of its own to bring up to date: the steps below would
|
|
3366
|
+
// connect as the default member and print its authentication failures as the house's state.
|
|
3367
|
+
const memberHere = !!resolveConfig().userStated;
|
|
3368
|
+
if (!memberHere) console.log(ok('no member install on this machine: no house schema or migration to check here (members and the admin do that on theirs)'));
|
|
3369
|
+
if (memberHere && !isV3(resolveConfig())) { try { await adoptMovedHouse(resolveConfig()); } catch { /* doctor will say */ } }
|
|
3314
3370
|
|
|
3315
3371
|
// Bring the house to the schema the NEW code expects, before any daemon restarts.
|
|
3316
3372
|
// A release that adds rooms (0.18.3's stat tables) used to land after the restart, so
|
|
@@ -3319,7 +3375,7 @@ async function cmdUpdate() {
|
|
|
3319
3375
|
// 500'd — all of it self-healing eventually, all of it alarming meanwhile. Two real
|
|
3320
3376
|
// members hit this within an hour of each other. The new binary owns the new schema, so
|
|
3321
3377
|
// it runs it here, in the one moment the daemons are already down.
|
|
3322
|
-
{
|
|
3378
|
+
if (memberHere) {
|
|
3323
3379
|
const r = spawnSync(process.execPath, [path.join(REPO_ROOT, 'memhouse', 'shipper', 'ship.js'), '--ensure-schema'],
|
|
3324
3380
|
{ env: childEnv(resolveConfig()), encoding: 'utf-8', stdio: 'pipe', timeout: 120000 });
|
|
3325
3381
|
const said = `${r.stdout || ''}${r.stderr || ''}`.trim().split('\n').filter((l) => l.includes('[memhouse]')).pop();
|
|
@@ -3406,6 +3462,7 @@ async function cmdUpdate() {
|
|
|
3406
3462
|
// non-interactive, no flag name it and print the command; NEVER auto-run — a cron
|
|
3407
3463
|
// or CI invocation must not start a house-wide copy
|
|
3408
3464
|
const cfg = resolveConfig();
|
|
3465
|
+
if (!memberHere) return 0;
|
|
3409
3466
|
if (cfg.url && cfg.user && isV3(cfg)) {
|
|
3410
3467
|
// Schema 3: the 0.18 migrations are about the 0.18 rooms, which this install neither reads
|
|
3411
3468
|
// nor writes. What it needs is its five tables, verified — ship.js --ensure-schema does that
|
|
@@ -3529,8 +3586,23 @@ async function cmdPlugins() {
|
|
|
3529
3586
|
// somewhere else than the directory just inspected.
|
|
3530
3587
|
const self = `memhouse plugins install claude${flags.target ? ` --target ${flags.target}` : ''}`;
|
|
3531
3588
|
|
|
3589
|
+
// Codex: one block in <codex home>/AGENTS.md and one skill, opt-in (memhouse/codex-plugin.js).
|
|
3590
|
+
const cxp = require(path.join(REPO_ROOT, 'memhouse', 'codex-plugin'));
|
|
3591
|
+
const cxHome = cxp.codexHome();
|
|
3592
|
+
const codexLine = () => {
|
|
3593
|
+
const st = cxp.status({ codexHome: cxHome });
|
|
3594
|
+
if (st.installed) {
|
|
3595
|
+
const o = st.owner;
|
|
3596
|
+
const runs = cxp.invocation({ home: o.home, bin: o.bin });
|
|
3597
|
+
console.log(ok(`codex: installed in ${short(cxHome)} (${o.version || 'unknown version'}) — runs \`${runs}\`${st.block ? '' : '; the AGENTS.md block is missing'}`));
|
|
3598
|
+
} else if (st.foreignSkill) console.log(warn(`codex: ${short(st.foreignSkill)} is not memhouse's — left alone`));
|
|
3599
|
+
else console.log(` codex: not installed in ${short(cxHome)} — memhouse plugins install codex`);
|
|
3600
|
+
if (st.damaged) console.log(warn(`codex: ${st.damaged}`));
|
|
3601
|
+
};
|
|
3602
|
+
|
|
3532
3603
|
if (sub === 'list') {
|
|
3533
3604
|
console.log(`skills available: ${invocations}`);
|
|
3605
|
+
codexLine();
|
|
3534
3606
|
if (!targets.length) { console.log(warn('no Claude Code config directory found')); return 0; }
|
|
3535
3607
|
for (const t of targets) {
|
|
3536
3608
|
console.log(isPluginInstalled(t.dir)
|
|
@@ -3577,7 +3649,33 @@ async function cmdPlugins() {
|
|
|
3577
3649
|
return 0;
|
|
3578
3650
|
}
|
|
3579
3651
|
|
|
3580
|
-
|
|
3652
|
+
if (sub === 'install' && positional[1] === 'codex') {
|
|
3653
|
+
try {
|
|
3654
|
+
const r = cxp.install({ codexHome: cxHome, home: HOME_DIR, bin: thisBin, version: PKG.version, deliveryDir: DELIVERY });
|
|
3655
|
+
console.log(ok(`codex: ${r.created ? 'created' : 'updated'} the memhouse block in ${short(r.file)}`));
|
|
3656
|
+
console.log(ok(`codex: skill ${short(r.dir)} — Codex lists it as 'memhouse'`));
|
|
3657
|
+
console.log(` the block and the skill run \`${r.mh}\`; remove both with: memhouse plugins remove codex`);
|
|
3658
|
+
console.log(' loads in the next Codex session');
|
|
3659
|
+
return 0;
|
|
3660
|
+
} catch (e) { console.log(bad(`codex: ${e.message}`)); return 1; }
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
if (sub === 'remove' && positional[1] === 'codex') {
|
|
3664
|
+
let r;
|
|
3665
|
+
try { r = cxp.remove({ codexHome: cxHome }); } catch (e) { console.log(bad(`codex: ${e.message}`)); return 1; }
|
|
3666
|
+
if (r.block) {
|
|
3667
|
+
const f = short(cxp.agentsFile(cxHome));
|
|
3668
|
+
console.log(ok(r.fileDeleted ? `codex: removed ${f} (it held only the memhouse block)`
|
|
3669
|
+
: r.emptiedLink ? `codex: removed the memhouse block from ${f}; it held only the block, so its link target is now empty and the link stays`
|
|
3670
|
+
: `codex: removed the memhouse block from ${f}; the rest of the file is as it was`));
|
|
3671
|
+
}
|
|
3672
|
+
if (r.skill) console.log(ok(`codex: removed the skill ${short(cxp.skillDir(cxHome))}`));
|
|
3673
|
+
if (r.foreignSkill) console.log(warn(`codex: ${short(r.foreignSkill)} is not memhouse's (no ${cxp.OWNER_FILE}) — left alone`));
|
|
3674
|
+
if (!r.block && !r.skill) console.log(warn(`codex: nothing of memhouse's in ${short(cxHome)}`));
|
|
3675
|
+
return 0;
|
|
3676
|
+
}
|
|
3677
|
+
|
|
3678
|
+
console.log('usage: memhouse plugins [list | install claude|codex | remove claude|codex] [--target DIR] [--yes]');
|
|
3581
3679
|
return 2;
|
|
3582
3680
|
}
|
|
3583
3681
|
|
|
@@ -5017,9 +5115,41 @@ async function probePermissive(cfg, db) {
|
|
|
5017
5115
|
}
|
|
5018
5116
|
}
|
|
5019
5117
|
|
|
5118
|
+
/**
|
|
5119
|
+
* Whose sessions this credential can read in the house: a share received is the housemate's
|
|
5120
|
+
* tables in this same database, and system.tables lists only what a credential may read. Every
|
|
5121
|
+
* member has a <name>_sessions table on either layout. Returns { all } for a credential that
|
|
5122
|
+
* reads every house (an administrator: everyone would be listed), else { members }, or null when
|
|
5123
|
+
* the tables cannot be listed.
|
|
5124
|
+
*/
|
|
5125
|
+
async function sharesReceived(cfg) {
|
|
5126
|
+
try {
|
|
5127
|
+
const caps = capabilitiesFrom(await readGrants(cfg, cfg.user));
|
|
5128
|
+
if (caps.canReadEveryHouse || caps.isSuperuser) return { all: true, members: [] };
|
|
5129
|
+
const rows = await chRows(cfg, `SELECT name FROM system.tables WHERE database = ${sqlStr(cfg.db)} AND endsWith(name, '_sessions')`, { database: '' });
|
|
5130
|
+
const members = rows.map((r) => String(r.name).slice(0, -'_sessions'.length))
|
|
5131
|
+
.filter((m) => m && m !== cfg.user && /^[A-Za-z][A-Za-z0-9_]*$/.test(m)).sort();
|
|
5132
|
+
return { all: false, members };
|
|
5133
|
+
} catch { return null; }
|
|
5134
|
+
}
|
|
5135
|
+
|
|
5136
|
+
/** The received half of `share --list`: whose sessions this credential reads, and how. */
|
|
5137
|
+
function printReceived(cfg, received) {
|
|
5138
|
+
console.log('');
|
|
5139
|
+
if (!received) { console.log(` whose sessions you can read: could not list the tables in '${cfg.db}'`); return; }
|
|
5140
|
+
if (received.all) { console.log(` this credential reads every member's sessions in '${cfg.db}' (it administers the server)`); return; }
|
|
5141
|
+
if (!received.members.length) { console.log(` nobody has shared their sessions with you in '${cfg.db}'`); return; }
|
|
5142
|
+
console.log(` whose sessions you can read in '${cfg.db}' (shared with you; a scoped share shows only the rows its owner chose):`);
|
|
5143
|
+
for (const m of received.members) {
|
|
5144
|
+
console.log(` ${m.padEnd(16)} ${isV3(cfg) ? `memhouse q recent --member ${m}` : `memhouse room-sql "SELECT … FROM ${m}_messages …"`}`);
|
|
5145
|
+
}
|
|
5146
|
+
}
|
|
5147
|
+
|
|
5020
5148
|
/** Who can read this house, what they are scoped to, and any policy left behind. */
|
|
5021
5149
|
async function shareList(cfg, share) {
|
|
5022
5150
|
const db = cfg.db;
|
|
5151
|
+
const received = await sharesReceived(cfg);
|
|
5152
|
+
const receivedJson = received ? (received.all ? { all: true } : received.members.map((m) => ({ member: m }))) : null;
|
|
5023
5153
|
let recorded = [];
|
|
5024
5154
|
try {
|
|
5025
5155
|
recorded = await chRows(cfg, `SELECT substring(key, 7) AS user, value AS state FROM ${physicalRoom('meta', cfg.user)} FINAL WHERE key LIKE 'share:%' ORDER BY key`, { database: db });
|
|
@@ -5039,9 +5169,10 @@ async function shareList(cfg, share) {
|
|
|
5039
5169
|
const covers = isV3(cfg) ? { tables: share.V3_TABLES.map((t) => `${cfg.user}_${t}`) } : { rooms: `${cfg.user}_*` };
|
|
5040
5170
|
|
|
5041
5171
|
if (!recorded.length && !policies.length) {
|
|
5042
|
-
if (JSON_OUT) { console.log(JSON.stringify({ database: db, ...covers, grantees: [] }, null, 2)); return 0; }
|
|
5172
|
+
if (JSON_OUT) { console.log(JSON.stringify({ database: db, ...covers, grantees: [], received: receivedJson }, null, 2)); return 0; }
|
|
5043
5173
|
console.log(ok(`nobody has been granted a read of your rooms in '${db}'`));
|
|
5044
5174
|
console.log(` Share it with: memhouse share <user> [--only project=<name>]`);
|
|
5175
|
+
printReceived(cfg, received);
|
|
5045
5176
|
return 0;
|
|
5046
5177
|
}
|
|
5047
5178
|
// In a shared house a grantee reads YOUR ROOMS, never the database — every other member
|
|
@@ -5060,6 +5191,7 @@ async function shareList(cfg, share) {
|
|
|
5060
5191
|
})),
|
|
5061
5192
|
policies_readable: policiesReadable,
|
|
5062
5193
|
note: "memhouse's own record, not ClickHouse's grant table — a grant made by hand does not appear here",
|
|
5194
|
+
received: receivedJson,
|
|
5063
5195
|
}, null, 2));
|
|
5064
5196
|
return 0;
|
|
5065
5197
|
}
|
|
@@ -5073,6 +5205,7 @@ async function shareList(cfg, share) {
|
|
|
5073
5205
|
for (const p of scoped) console.log(` ${String(p.table).padEnd(11)} ${p.select_filter}`);
|
|
5074
5206
|
}
|
|
5075
5207
|
console.log(' A grant made by hand does not appear here — this list is what memhouse recorded.');
|
|
5208
|
+
printReceived(cfg, received);
|
|
5076
5209
|
|
|
5077
5210
|
// Policies whose room is gone: dropping a house does NOT drop its policies, and a house
|
|
5078
5211
|
// later recreated under the same name silently inherits them. Listing them takes a credential
|
|
@@ -5237,9 +5370,15 @@ async function offerInvite() {
|
|
|
5237
5370
|
|
|
5238
5371
|
async function cmdInvite() {
|
|
5239
5372
|
const name = positional[0];
|
|
5240
|
-
if (!name) { console.log('usage: memhouse invite <name> --url <house-url> [--admin-user … [--admin-password-file FILE|-]] [--db <house>] [--out <file>] [--adopt] [--schema 2|3] [--print-sql]'); return 2; }
|
|
5373
|
+
if (!name) { console.log('usage: memhouse invite <name> --url <house-url> [--admin-user … [--admin-password-file FILE|-]] [--db <house>] [--out <file>] [--adopt] [--schema 2|3] [--expires DAYS] [--print-sql]'); return 2; }
|
|
5241
5374
|
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(name, 'member'); }
|
|
5242
5375
|
catch (e) { console.log(bad(e.message)); return 1; }
|
|
5376
|
+
// How long the invite's password works (VALID UNTIL): checked before anything is contacted.
|
|
5377
|
+
const expiresDays = flags.expires === undefined ? INVITE_DAYS : flags.expires === true ? NaN : Number(flags.expires);
|
|
5378
|
+
if (!Number.isInteger(expiresDays) || expiresDays < 1 || expiresDays > 365) {
|
|
5379
|
+
console.log(bad(`--expires takes a whole number of days, 1 to 365 (default ${INVITE_DAYS}); got '${flags.expires === true ? '' : flags.expires}'`));
|
|
5380
|
+
return 2;
|
|
5381
|
+
}
|
|
5243
5382
|
const cfg = resolveConfig();
|
|
5244
5383
|
// The URL must be STATED and must work from the INVITEE's machine. cfg.url falls back
|
|
5245
5384
|
// to localhost:8123 — dead, or worse, someone else's house, on every other machine.
|
|
@@ -5291,6 +5430,9 @@ async function cmdInvite() {
|
|
|
5291
5430
|
console.log(memberSql(db, name));
|
|
5292
5431
|
// Nothing is contacted, so nothing is detected: schema 3 unless stated, as install --print-sql.
|
|
5293
5432
|
if (statedSchema !== '2') console.log(memberSqlV3(db, name));
|
|
5433
|
+
// The same expiry a run of invite sets. A placeholder, like the password's: the statement
|
|
5434
|
+
// does not parse until a quoted moment in the server's time zone stands in its place.
|
|
5435
|
+
console.log(`-- The invited password expires, as \`memhouse invite\` sets it: put the moment ${expiresDays} day${expiresDays === 1 ? '' : 's'}\n-- from now, quoted, in the server's time zone, where <valid-until> stands. install lifts it.\nALTER USER \`${name}\` VALID UNTIL <valid-until>;`);
|
|
5294
5436
|
console.log(handoffLines({ url, db, member: name, invite: true }));
|
|
5295
5437
|
return 0;
|
|
5296
5438
|
}
|
|
@@ -5410,7 +5552,7 @@ async function cmdInvite() {
|
|
|
5410
5552
|
// The house decides, as for an install: a new house is schema 3, one on the 0.18 rooms
|
|
5411
5553
|
// keeps them, and an ambiguous one is refused.
|
|
5412
5554
|
{
|
|
5413
|
-
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows(adminCfg, sql, { database: '' }), db, name), name);
|
|
5555
|
+
const pick = schemaChoice.chooseSchema(statedSchema, await schemaChoice.detectHouseSchema((sql) => chRows(adminCfg, sql, { database: '' }), db, name), name, { user: adminCfg.user, url: adminCfg.url });
|
|
5414
5556
|
if (pick.refuse) { console.log(bad(pick.refuse)); console.log(' nothing was changed'); return 1; }
|
|
5415
5557
|
cfg.schema = pick.schema === '3' ? '3' : '';
|
|
5416
5558
|
}
|
|
@@ -5447,6 +5589,19 @@ async function cmdInvite() {
|
|
|
5447
5589
|
if (built) built.url = url; // the invitee reaches the house at --url, not the admin's endpoint
|
|
5448
5590
|
if (!built) return 1;
|
|
5449
5591
|
|
|
5592
|
+
// The invite's password expires: a file that sits unused, or was read where it should not
|
|
5593
|
+
// have been, stops working on its own. The server's own clock sets the date (VALID UNTIL is
|
|
5594
|
+
// read in its time zone); install's rotation clears it, as does --keep-password.
|
|
5595
|
+
let expires = null;
|
|
5596
|
+
try {
|
|
5597
|
+
const adminCfg = { ...cfg, url: provisionUrl, db, user: adminUser, password: adminPass || '' };
|
|
5598
|
+
const r = (await chRows(adminCfg, `SELECT toString(now() + toIntervalDay(${expiresDays})) AS lit, toUnixTimestamp(now() + toIntervalDay(${expiresDays})) AS ts`, { database: '' }))[0];
|
|
5599
|
+
await ch(adminCfg, `ALTER USER \`${name}\` VALID UNTIL ${sqlStr(r.lit)}`, { database: '' });
|
|
5600
|
+
expires = new Date(Number(r.ts) * 1000).toISOString().replace(/\.\d{3}Z$/, 'Z');
|
|
5601
|
+
} catch (e) {
|
|
5602
|
+
console.log(warn(`could not set the invite's expiry (${String(e.message).split('\n')[0]}): its password works until install changes it`));
|
|
5603
|
+
}
|
|
5604
|
+
|
|
5450
5605
|
const out = path.resolve(flags.out && flags.out !== true ? String(flags.out) : `invite-${name}.env`);
|
|
5451
5606
|
const sq = envfile.quoteShell;
|
|
5452
5607
|
const body = [
|
|
@@ -5465,6 +5620,8 @@ async function cmdInvite() {
|
|
|
5465
5620
|
'# Issued by an invitation (no admin access here): install changes it so the inviter',
|
|
5466
5621
|
'# no longer knows it.',
|
|
5467
5622
|
'MEMHOUSE_INVITE=1',
|
|
5623
|
+
...(expires ? [`# The password above stops working at ${expires} (UTC): install before then, or ask for a new invite.`,
|
|
5624
|
+
`MEMHOUSE_INVITE_EXPIRES=${sq(expires)}`] : []),
|
|
5468
5625
|
'',
|
|
5469
5626
|
].join('\n');
|
|
5470
5627
|
// Atomic + private + no symlink follow: write a fresh temp with O_EXCL at 0600, then
|
|
@@ -5506,11 +5663,12 @@ async function cmdInvite() {
|
|
|
5506
5663
|
const page = require(path.join(REPO_ROOT, 'memhouse', 'invitation')).invitationPage({
|
|
5507
5664
|
out, name, url, db: built.db, schema: isV3(cfg) ? '3' : '', install,
|
|
5508
5665
|
version: prerelease ? `${base}-…` : PKG.version,
|
|
5509
|
-
inviter: cfg.user || require('os').userInfo().username,
|
|
5666
|
+
inviter: cfg.user || require('os').userInfo().username, expires,
|
|
5510
5667
|
});
|
|
5511
5668
|
fs.writeFileSync(page.path, page.text);
|
|
5512
5669
|
console.log(ok(`guide written: ${page.path}`));
|
|
5513
5670
|
} catch (e) { console.log(warn(`could not write the invitation guide: ${e.message}`)); }
|
|
5671
|
+
if (expires) console.log(ok(`the invite expires at ${expires} (UTC), in ${expiresDays} day${expiresDays === 1 ? '' : 's'} (--expires <days> sets it); after that, invite again`));
|
|
5514
5672
|
console.log(` send ${name} BOTH files over a channel you trust (croc, a password manager — not chat):`);
|
|
5515
5673
|
console.log(` ${path.basename(out)} their credential — one-time, install deletes it`);
|
|
5516
5674
|
console.log(` MEMHOUSE-INVITATION.md the steps, nothing secret`);
|
|
@@ -5737,8 +5895,14 @@ async function cmdUninstall() {
|
|
|
5737
5895
|
// invite, uninstall and friends act on first call with no separate confirm step, so a
|
|
5738
5896
|
// --help that fell through to the default branch would DO the thing instead of
|
|
5739
5897
|
// describing it. One check ahead of the switch, for every command, is the whole fix.
|
|
5898
|
+
// The command's own entries, not the whole page (memhouse/help.js); `q` lists its named queries.
|
|
5740
5899
|
if (cmd && (flags.help === true || process.argv.slice(2).some((a) => a === '-h' || a === '--help'))) {
|
|
5741
|
-
|
|
5900
|
+
if (cmd === 'q') {
|
|
5901
|
+
const schema = isV3(resolveConfig()) ? 3 : 2;
|
|
5902
|
+
for (const l of qc().renderQ('list', await qc().runQ(null, null, qc().parseQ([], {}), { schema }))) console.log(l);
|
|
5903
|
+
return;
|
|
5904
|
+
}
|
|
5905
|
+
console.log(require(path.join(REPO_ROOT, 'memhouse', 'help')).helpFor(HELP, cmd, PKG.version) || HELP); return;
|
|
5742
5906
|
}
|
|
5743
5907
|
const cfg = resolveConfig();
|
|
5744
5908
|
|
|
@@ -6027,15 +6191,21 @@ async function cmdUninstall() {
|
|
|
6027
6191
|
console.error(warn(`could not read who holds which role (${String(e.message).split('\n')[0]}): access held through a role is not shown`));
|
|
6028
6192
|
}
|
|
6029
6193
|
rows = share.membersAccess({ grants, roleGrants, db });
|
|
6194
|
+
// Whether a share is scoped: its row policies, where this credential can list them.
|
|
6195
|
+
try { rows.policies = await chRows(who, `SELECT short_name, table FROM system.row_policies WHERE database = ${sqlStr(db)}`, { database: '' }); }
|
|
6196
|
+
catch { rows.policies = null; }
|
|
6030
6197
|
} catch (e) {
|
|
6031
6198
|
console.log(bad(`could not read the grant table: ${e.message}`));
|
|
6032
6199
|
console.log(' Pass an admin credential: memhouse members --admin-user <a> (its password from MEMHOUSE_ADMIN_PASSWORD, or --admin-password-file -)');
|
|
6033
6200
|
process.exitCode = 1; break;
|
|
6034
6201
|
}
|
|
6202
|
+
const share = require(path.join(REPO_ROOT, 'memhouse', 'share'));
|
|
6203
|
+
const described = (r) => share.describeAccess(r, { policies: rows.policies });
|
|
6035
6204
|
if (JSON_OUT) {
|
|
6036
|
-
console.log(JSON.stringify({ database: db, roles_read: !rolesUnread, members: rows.map((r) => ({
|
|
6205
|
+
console.log(JSON.stringify({ database: db, roles_read: !rolesUnread, policies_read: rows.policies !== null, members: rows.map((r) => ({
|
|
6037
6206
|
name: r.u, scope: Number(r.db_wide) === 1 ? 'database' : 'rooms',
|
|
6038
6207
|
rooms: Number(r.db_wide) === 1 ? null : r.list,
|
|
6208
|
+
...(Number(r.db_wide) === 1 ? {} : described(r)),
|
|
6039
6209
|
})) }, null, 2));
|
|
6040
6210
|
break;
|
|
6041
6211
|
}
|
|
@@ -6047,9 +6217,19 @@ async function cmdUninstall() {
|
|
|
6047
6217
|
}
|
|
6048
6218
|
const owners = rows.filter((r) => Number(r.db_wide) === 1);
|
|
6049
6219
|
console.log(` '${db}' — ${rows.length} member(s):`);
|
|
6220
|
+
// Each account's own tables, the members it reads (a share: scoped or full where the row
|
|
6221
|
+
// policies can be listed), and anything else, which memhouse does not grant itself.
|
|
6050
6222
|
for (const r of rows) {
|
|
6051
|
-
if (Number(r.db_wide) === 1) console.log(` ${String(r.u).padEnd(16)} the whole database`);
|
|
6052
|
-
|
|
6223
|
+
if (Number(r.db_wide) === 1) { console.log(` ${String(r.u).padEnd(16)} the whole database`); continue; }
|
|
6224
|
+
const d = described(r);
|
|
6225
|
+
const pad = ' '.repeat(21);
|
|
6226
|
+
const lines = [];
|
|
6227
|
+
if (d.own.length) lines.push(`own: ${d.own.join(', ')}`);
|
|
6228
|
+
for (const s of d.shares) lines.push(`reads ${s.from}'s sessions (a share${s.scoped === null ? '' : s.scoped ? ', scoped' : ', full'}): ${s.tables.join(', ')}`);
|
|
6229
|
+
if (d.other.length) lines.push(`other grants (not memhouse's own shape — check them): ${d.other.join(', ')}`);
|
|
6230
|
+
if (!lines.length) lines.push('(none)');
|
|
6231
|
+
console.log(` ${String(r.u).padEnd(16)} ${lines[0]}`);
|
|
6232
|
+
for (const l of lines.slice(1)) console.log(`${pad}${l}`);
|
|
6053
6233
|
}
|
|
6054
6234
|
if (owners.length && rows.length > 1) {
|
|
6055
6235
|
console.log('');
|
|
@@ -6506,7 +6686,7 @@ async function cmdUninstall() {
|
|
|
6506
6686
|
// grants. The superuser is at hand here, so they are built now, and the member install
|
|
6507
6687
|
// below finds them ready. Without this, deploy --local on 0.20 stopped at the install.
|
|
6508
6688
|
const pick = schemaChoice.chooseSchema(flags.schema && flags.schema !== true ? String(flags.schema) : '',
|
|
6509
|
-
await schemaChoice.detectHouseSchema((sql) => chRows(adminCfg, sql, { database: '' }), targetDb, houseUser), houseUser);
|
|
6689
|
+
await schemaChoice.detectHouseSchema((sql) => chRows(adminCfg, sql, { database: '' }), targetDb, houseUser), houseUser, { user: adminCfg.user, url: adminCfg.url });
|
|
6510
6690
|
if (pick.refuse) { console.log(bad(pick.refuse)); process.exitCode = 1; break; }
|
|
6511
6691
|
if (pick.schema === '3') {
|
|
6512
6692
|
const v3 = require(path.join(REPO_ROOT, 'memhouse', 'house', 'schema-profile'));
|
package/docs/guides/install.md
CHANGED
|
@@ -128,3 +128,6 @@ The path proves nothing on its own: Homebrew's prefix is `/usr/local` on Intel a
|
|
|
128
128
|
- `memhouse plugins install claude`: the `/mem:*` skills, for Claude Code only. Codex and the
|
|
129
129
|
other editors' sessions ship all the same; an agent there reads the house with the CLI
|
|
130
130
|
(`memhouse q`, `memhouse room-sql`). See [Search, resume and skills](search-and-resume.md).
|
|
131
|
+
- `memhouse plugins install codex`: for Codex, a short block in its `AGENTS.md` (the rules) and
|
|
132
|
+
a `memhouse` skill (the read recipes). Nothing is written under the Codex home unless you run
|
|
133
|
+
it. See [Codex](search-and-resume.md#codex).
|
package/docs/guides/invite.md
CHANGED
|
@@ -27,6 +27,13 @@ memhouse invite alice --url https://house.example.com
|
|
|
27
27
|
- **Alice installs with `memhouse install --env invite-alice.env`.** It offers to rotate
|
|
28
28
|
the password to one only she knows, which works because she is granted `ALTER USER` on
|
|
29
29
|
herself. Then it deletes the file. Once she rotates, the password you saw stops working.
|
|
30
|
+
- **The invite expires.** The password in the file stops working 7 days after the invite
|
|
31
|
+
(`--expires <days>` sets 1 to 365), counted by the server's clock. The output, the file and
|
|
32
|
+
the invitation page say when. Installing lifts the expiry, whether she changes the password
|
|
33
|
+
or keeps it with `--keep-password`. After the date, `install` refuses the file and tells her
|
|
34
|
+
to ask for a new invite. Her account is still there, so remove it first (`DROP USER alice`, as
|
|
35
|
+
the administrator; `invite` says so when it meets it), then invite her again. Her tables are
|
|
36
|
+
kept and reused.
|
|
30
37
|
|
|
31
38
|
## Which admin credential
|
|
32
39
|
|
|
@@ -99,6 +99,36 @@ missing skill never announces itself.
|
|
|
99
99
|
For an agent that does not use Claude Code skills, `memhouse prompt` prints a snippet for
|
|
100
100
|
its system prompt: where memory lives and how to query it.
|
|
101
101
|
|
|
102
|
+
### Codex
|
|
103
|
+
|
|
104
|
+
The `/mem` skills are for Claude Code. For Codex:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
memhouse plugins install codex # under CODEX_HOME, or ~/.codex
|
|
108
|
+
memhouse plugins list # shows the codex copy and the command it runs
|
|
109
|
+
memhouse plugins remove codex
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
It writes two things, and only when you run it:
|
|
113
|
+
|
|
114
|
+
- **A block at the top of `AGENTS.md`**, between `<!-- memhouse:begin … -->` and
|
|
115
|
+
`<!-- memhouse:end -->`. Codex puts this file in every prompt, so the block holds only what must
|
|
116
|
+
always hold: never read or print `~/.memhouse/env` or a `MEMHOUSE_*` value; `MEMHOUSE_URL` is the
|
|
117
|
+
house and `MEMHOUSE_PORT` the dashboard; questions about past sessions go to the `memhouse`
|
|
118
|
+
skill. The file is created if it is missing. Installing again replaces the block where it
|
|
119
|
+
stands. Removing takes out the block and the blank line after it, so the rest of the file is
|
|
120
|
+
what it was, byte for byte; a file that held only the block is deleted, or, when `AGENTS.md`
|
|
121
|
+
is a symlink, its target is emptied and the link kept. Leave the markers
|
|
122
|
+
alone: with one of them missing, install and remove refuse and change nothing.
|
|
123
|
+
- **A skill, `skills/memhouse/`**, with the read recipes: `memhouse q`, `--member` for a
|
|
124
|
+
housemate's sessions, `memhouse room-sql`. Codex lists it by name and description and reads
|
|
125
|
+
the rest when it uses it. The directory carries `.memhouse.json`; a `skills/memhouse` without
|
|
126
|
+
it is someone else's, and install refuses to replace it and remove leaves it.
|
|
127
|
+
|
|
128
|
+
For an instance with its own home or binary, both name it: `MEMHOUSE_HOME=<home> <binary> q …`.
|
|
129
|
+
After an upgrade, the first `memhouse` command refreshes both, as it does the `/mem` copies; a
|
|
130
|
+
block you took out by hand stays out.
|
|
131
|
+
|
|
102
132
|
## Writing your own queries
|
|
103
133
|
|
|
104
134
|
On schema 3 (0.20), the tables are `mem.<you>_session_parts` (one row per part of a message:
|
package/docs/guides/share.md
CHANGED
|
@@ -106,6 +106,10 @@ included. A grant someone made by hand in SQL does not appear there. A member ca
|
|
|
106
106
|
`system.grants` or `system.row_policies`; an administrator can, with `SHOW GRANTS FOR <user>`
|
|
107
107
|
and the queries in [Operate](operate.md#who-is-here).
|
|
108
108
|
|
|
109
|
+
It also lists the other direction: each member whose sessions your credential can read, with
|
|
110
|
+
the command that reads them. A scoped share shows only the rows its owner chose, and its filter
|
|
111
|
+
is the owner's to see.
|
|
112
|
+
|
|
109
113
|
## Reading what was shared with you
|
|
110
114
|
|
|
111
115
|
Shared tables sit in the same database as yours, under the other member's name, for
|
package/docs/releases/README.md
CHANGED
|
@@ -5,6 +5,7 @@ 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.20.1](v0.20.1.md) | 2026-11-02 | invites expire; `memhouse plugins install codex`; per-command help and clearer CLI answers |
|
|
8
9
|
| [0.20.0](v0.20.0.md) | 2026-10-29 | schema 3: a session is its store, nothing cut or lost, forks billed once, ten editors through normalisers; Apache-2.0 |
|
|
9
10
|
| [0.18.11](v0.18.11.md) | 2026-10-10 | sessions with subagents stop re-shipping; security: no stored credential sent to `invite --url` |
|
|
10
11
|
| [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 |
|