memhouse 0.18.0 → 0.18.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/README.md +13 -0
- package/bin/memhouse.js +137 -6
- package/memhouse/binding.js +81 -0
- package/memhouse/delivery/INVITATION.md.tpl +20 -3
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/memhouse/delivery/plugin/reference/HOUSE.md +7 -0
- package/memhouse/delivery/plugin/skills/access/SKILL.md +1 -1
- package/memhouse/delivery/plugin/skills/house/SKILL.md +16 -0
- package/memhouse/flags.js +1 -1
- package/memhouse/invitefile.js +38 -0
- package/package.json +1 -1
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
|
@@ -438,6 +438,7 @@ Data ship one incremental pass (--full | --loop [sec])
|
|
|
438
438
|
search <terms…> full-text search across all sessions
|
|
439
439
|
resume <session-id> print the command that reopens that session in its editor
|
|
440
440
|
sessions-query print the session rollup SQL for this credential
|
|
441
|
+
instance which memhouse this is: home, binary, house, rooms, scope, daemons, bound playbooks
|
|
441
442
|
rooms what your rooms are actually called (--json)
|
|
442
443
|
members who is in a house and what each can reach (--db, admin)
|
|
443
444
|
start | stop | shipper loop + dashboard as background daemons
|
|
@@ -1235,6 +1236,10 @@ async function cmdInstall({ interactive }) {
|
|
|
1235
1236
|
// deliberately for exactly this reason; install used to do nothing at all.
|
|
1236
1237
|
if (displaces && shipperHealth().running) {
|
|
1237
1238
|
const st = (() => { try { return require(path.join(REPO_ROOT, 'memhouse', 'service.js')).status(); } catch { return null; } })();
|
|
1239
|
+
// Same server and database, different user, is a credential swap, not a house move —
|
|
1240
|
+
// "old house" reads as data loss when nothing moved. A drill flagged the conflation.
|
|
1241
|
+
const sameHouse = prior.MEMHOUSE_URL && sameEndpoint(prior.MEMHOUSE_URL, cfg.url) && (!prior.MEMHOUSE_DB || prior.MEMHOUSE_DB === cfg.db);
|
|
1242
|
+
const whatMoved = sameHouse ? 'old credential' : 'old house';
|
|
1238
1243
|
const viaService = String(shipperHealth().via || '').startsWith('service');
|
|
1239
1244
|
if (viaService && st) {
|
|
1240
1245
|
const cmd = st.kind === 'systemd'
|
|
@@ -1242,10 +1247,10 @@ async function cmdInstall({ interactive }) {
|
|
|
1242
1247
|
: ['launchctl', ['kickstart', '-k', `gui/${process.getuid()}/com.memhouse.shipper`]];
|
|
1243
1248
|
const r = spawnSync(cmd[0], cmd[1], { stdio: 'pipe', encoding: 'utf-8' });
|
|
1244
1249
|
console.log(r.status === 0 ? ok('shipper restarted against the new house')
|
|
1245
|
-
: warn(
|
|
1250
|
+
: warn(`restart the shipper yourself, or it keeps writing under the ${whatMoved}: memhouse service restart`));
|
|
1246
1251
|
} else {
|
|
1247
1252
|
const pid = pidOf('shipper');
|
|
1248
|
-
if (pid) { try { process.kill(pid, 'SIGTERM'); console.log(ok(`stopped the shipper (pid ${pid}) — it was
|
|
1253
|
+
if (pid) { try { process.kill(pid, 'SIGTERM'); console.log(ok(`stopped the shipper (pid ${pid}) — it was running under the ${whatMoved}`)); } catch { /* raced */ } }
|
|
1249
1254
|
console.log(' start it against the new one: memhouse start');
|
|
1250
1255
|
}
|
|
1251
1256
|
}
|
|
@@ -2417,7 +2422,19 @@ async function cmdPlugins() {
|
|
|
2417
2422
|
const invocations = names.map((n) => `/mem:${n}`).join(', ');
|
|
2418
2423
|
// --target overrides the discovery rather than joining it: given one, that is the only
|
|
2419
2424
|
// directory touched.
|
|
2420
|
-
|
|
2425
|
+
// Targets: --target names one; otherwise an instance that ships specific playbooks
|
|
2426
|
+
// (MEMHOUSE_CLAUDE_ROOTS) installs into exactly those — the plugin belongs where the
|
|
2427
|
+
// sessions come from; failing both, every Claude config dir on the machine.
|
|
2428
|
+
const binding = require(path.join(REPO_ROOT, 'memhouse', 'binding'));
|
|
2429
|
+
const scope = require(path.join(REPO_ROOT, 'editors', 'scope'));
|
|
2430
|
+
const icfg = resolveConfig();
|
|
2431
|
+
const scoped = !flags.target && icfg.claudeRoots
|
|
2432
|
+
? scope.parseList(icfg.claudeRoots).map(scope.expandHome).map((d) => ({ dir: d, why: 'this instance ships it' }))
|
|
2433
|
+
: null;
|
|
2434
|
+
const targets = flags.target ? [{ dir: flags.target, why: '--target' }] : (scoped || claudeTargets());
|
|
2435
|
+
// What the skills in a bound playbook run and read: this binary, this home.
|
|
2436
|
+
const thisBin = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
2437
|
+
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
2438
|
// Whatever --target was given has to reappear in the advice, or pasting it installs
|
|
2422
2439
|
// somewhere else than the directory just inspected.
|
|
2423
2440
|
const self = `memhouse plugins install claude${flags.target ? ` --target ${flags.target}` : ''}`;
|
|
@@ -2427,7 +2444,7 @@ async function cmdPlugins() {
|
|
|
2427
2444
|
if (!targets.length) { console.log(warn('no Claude Code config directory found')); return 0; }
|
|
2428
2445
|
for (const t of targets) {
|
|
2429
2446
|
console.log(isPluginInstalled(t.dir)
|
|
2430
|
-
? ok(`installed in ${short(t.dir)} (${t.why})`)
|
|
2447
|
+
? ok(`installed in ${short(t.dir)} (${t.why}) — ${boundLabel(t.dir)}`)
|
|
2431
2448
|
: warn(`not installed in ${short(t.dir)} (${t.why})`));
|
|
2432
2449
|
}
|
|
2433
2450
|
if (!targets.some((t) => isPluginInstalled(t.dir))) console.log(` install with: ${self}`);
|
|
@@ -2442,7 +2459,13 @@ async function cmdPlugins() {
|
|
|
2442
2459
|
}
|
|
2443
2460
|
const chosen = await chooseTargets(targets, 'Install');
|
|
2444
2461
|
if (!chosen.length) { console.log(warn('nothing installed')); return 0; }
|
|
2445
|
-
for (const t of chosen)
|
|
2462
|
+
for (const t of chosen) {
|
|
2463
|
+
console.log(ok(`installed ${names.length} skills into ${short(installPluginInto(t.dir))}`));
|
|
2464
|
+
try {
|
|
2465
|
+
binding.bind(t.dir, { home: HOME_DIR, bin: thisBin });
|
|
2466
|
+
console.log(ok(` bound: sessions under ${short(t.dir)} read ${short(HOME_DIR)} and run ${short(thisBin)}`));
|
|
2467
|
+
} catch (e) { console.log(warn(` not bound: ${e.message}`)); }
|
|
2468
|
+
}
|
|
2446
2469
|
console.log(` loads as mem@skills-dir next session — invoke ${invocations}`);
|
|
2447
2470
|
return 0;
|
|
2448
2471
|
}
|
|
@@ -2458,6 +2481,8 @@ async function cmdPlugins() {
|
|
|
2458
2481
|
// else's work.
|
|
2459
2482
|
try { fs.rmdirSync(path.join(t.dir, 'skills')); } catch { /* not empty: leave it */ }
|
|
2460
2483
|
console.log(ok(`removed ${short(path.join(t.dir, 'skills', 'mem'))}`));
|
|
2484
|
+
try { if (binding.unbind(t.dir)) console.log(ok(` unbound: MEMHOUSE_HOME/MEMHOUSE_BIN removed from ${short(path.join(t.dir, 'settings.json'))}`)); }
|
|
2485
|
+
catch (e) { console.log(warn(` could not unbind: ${e.message}`)); }
|
|
2461
2486
|
}
|
|
2462
2487
|
return 0;
|
|
2463
2488
|
}
|
|
@@ -3335,6 +3360,53 @@ async function cmdWhoami() {
|
|
|
3335
3360
|
return 0;
|
|
3336
3361
|
}
|
|
3337
3362
|
|
|
3363
|
+
/**
|
|
3364
|
+
* Bare `memhouse` with an invite file nearby: offer to process it. Returns null when there
|
|
3365
|
+
* is nothing to offer (caller prints help), else the exit code of the install it ran or
|
|
3366
|
+
* declined. Never touches the file's password: what it shows is the user, the house and
|
|
3367
|
+
* the server, which the guide beside the file shows too.
|
|
3368
|
+
*/
|
|
3369
|
+
async function offerInvite() {
|
|
3370
|
+
const inv = require(path.join(REPO_ROOT, 'memhouse', 'invitefile'));
|
|
3371
|
+
// The current directory only. The guide says "from the directory holding the file", and
|
|
3372
|
+
// reaching into ~/Downloads surfaced unrelated invitations from other houses — and could
|
|
3373
|
+
// auto-join the wrong one under --yes. If they are somewhere else, they name the file.
|
|
3374
|
+
const found = inv.findInvites([process.cwd()]);
|
|
3375
|
+
if (!found.length) return null;
|
|
3376
|
+
const described = found.map((f) => ({ ...f, ...inv.describeInvite((() => { try { return fs.readFileSync(f.file, 'utf-8'); } catch { return ''; } })()) }));
|
|
3377
|
+
const usable = described.filter((d) => d.complete);
|
|
3378
|
+
if (!usable.length) {
|
|
3379
|
+
console.log(warn(`found ${found.length === 1 ? short(found[0].file) : `${found.length} invite files`} — not a complete invite (missing ${described[0].missing.join(', ')})`));
|
|
3380
|
+
return null;
|
|
3381
|
+
}
|
|
3382
|
+
const cfg = resolveConfig();
|
|
3383
|
+
const interactive = process.stdin.isTTY && flags.yes !== true;
|
|
3384
|
+
console.log(ok(`found an invitation: ${short(usable[0].file)}${usable.length > 1 ? ` (and ${usable.length - 1} more)` : ''}`));
|
|
3385
|
+
let pick = usable[0];
|
|
3386
|
+
if (usable.length > 1) {
|
|
3387
|
+
usable.forEach((d, i) => console.log(` ${i + 1}) ${short(d.file)} join ${d.url} as '${d.user}'`));
|
|
3388
|
+
if (!interactive) { console.log(' several invitations here — name one: memhouse install --env <file>'); return 1; }
|
|
3389
|
+
const a = (await ask('Which one? (number, or n)', '1')).trim().toLowerCase();
|
|
3390
|
+
const n = Number(a);
|
|
3391
|
+
if (!Number.isInteger(n) || n < 1 || n > usable.length) { console.log(' nothing done.'); return 1; }
|
|
3392
|
+
pick = usable[n - 1];
|
|
3393
|
+
}
|
|
3394
|
+
console.log(` it joins ${pick.url}, house '${pick.db}', as member '${pick.user}'${pick.channel ? `, channel ${pick.channel}` : ''}.`);
|
|
3395
|
+
if (cfg.stated && cfg.user) {
|
|
3396
|
+
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).`));
|
|
3397
|
+
}
|
|
3398
|
+
console.log(' Joining rotates the password to one only this machine knows, deletes the file, and starts shipping.');
|
|
3399
|
+
if (!interactive) {
|
|
3400
|
+
if (flags.yes === true) { flags.env = pick.file; flags.force = cfg.stated && cfg.user ? true : flags.force; return cmdInstall({ interactive: false }); }
|
|
3401
|
+
console.log(` Join with: memhouse install --env ${path.basename(pick.file)} (or: memhouse --yes)`);
|
|
3402
|
+
return 0;
|
|
3403
|
+
}
|
|
3404
|
+
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();
|
|
3405
|
+
if (!(a === 'y' || a === 'yes' || (a === '' && !(cfg.stated && cfg.user)))) { console.log(' nothing done. Later: memhouse install --env ' + path.basename(pick.file)); return 0; }
|
|
3406
|
+
flags.env = pick.file; if (cfg.stated && cfg.user) flags.force = true;
|
|
3407
|
+
return cmdInstall({ interactive: false });
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3338
3410
|
async function cmdInvite() {
|
|
3339
3411
|
const name = positional[0];
|
|
3340
3412
|
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,7 +3878,15 @@ async function cmdUninstall() {
|
|
|
3806
3878
|
const cfg = resolveConfig();
|
|
3807
3879
|
|
|
3808
3880
|
switch (cmd) {
|
|
3809
|
-
case null:
|
|
3881
|
+
case null: {
|
|
3882
|
+
// Bare `memhouse`. If an invite file is where the user is standing — or in their
|
|
3883
|
+
// downloads — the likeliest reason they typed this is that they were handed it. Ask.
|
|
3884
|
+
// Otherwise, the help screen as before. Nothing happens without a yes.
|
|
3885
|
+
const code = await offerInvite();
|
|
3886
|
+
if (code === null) console.log(HELP); else process.exitCode = code;
|
|
3887
|
+
break;
|
|
3888
|
+
}
|
|
3889
|
+
case 'help': console.log(HELP); break;
|
|
3810
3890
|
// `--version` and `-v` land here as flags, not as the `version` verb, so they used to
|
|
3811
3891
|
// fall through to HELP: forty lines of help and exit 0 is not what --version means.
|
|
3812
3892
|
case 'version': console.log(PKG.version); break;
|
|
@@ -3943,6 +4023,57 @@ async function cmdUninstall() {
|
|
|
3943
4023
|
if (!au) console.log(' (run with --admin-user to be sure you are seeing every account)');
|
|
3944
4024
|
break;
|
|
3945
4025
|
}
|
|
4026
|
+
case 'instance': {
|
|
4027
|
+
// Which memhouse am I talking to? One screen: the instance, its binary, its house,
|
|
4028
|
+
// its rooms, what it ships, its host, its daemons, and which playbooks are bound to
|
|
4029
|
+
// it. A machine can run several instances; this is how they are told apart.
|
|
4030
|
+
const cfg = resolveConfig();
|
|
4031
|
+
const binding = require(path.join(REPO_ROOT, 'memhouse', 'binding'));
|
|
4032
|
+
const scope = require(path.join(REPO_ROOT, 'editors', 'scope'));
|
|
4033
|
+
const hostmod = require(path.join(REPO_ROOT, 'memhouse', 'host.js'));
|
|
4034
|
+
const thisBin = (() => { try { return fs.realpathSync(process.argv[1]); } catch { return process.argv[1]; } })();
|
|
4035
|
+
const name = binding.instanceName(HOME_DIR, readEnvFile().MEMHOUSE_NAME);
|
|
4036
|
+
const host = (() => { try { return hostmod.read(HOME_DIR); } catch { return null; } })();
|
|
4037
|
+
const daemons = { shipper: pidOf('shipper') || null, dashboard: pidOf('dashboard') || null };
|
|
4038
|
+
const realHome = (() => { try { return fs.realpathSync(HOME_DIR); } catch { return HOME_DIR; } })();
|
|
4039
|
+
const cands = new Map();
|
|
4040
|
+
for (const d of scope.parseList(cfg.claudeRoots).map(scope.expandHome)) cands.set(d, d);
|
|
4041
|
+
for (const t of claudeTargets()) cands.set(t.dir, t.dir);
|
|
4042
|
+
const bound = [...cands.keys()].filter((d) => {
|
|
4043
|
+
const b = binding.boundTo(binding.readSettings(d)); if (!b) return false;
|
|
4044
|
+
try { return fs.realpathSync(b.home) === realHome; } catch { return b.home === HOME_DIR; }
|
|
4045
|
+
});
|
|
4046
|
+
let rooms = null; let counts = {};
|
|
4047
|
+
if (cfg.stated && cfg.user) {
|
|
4048
|
+
rooms = roomNames(cfg.user);
|
|
4049
|
+
try {
|
|
4050
|
+
// system.tables, not system.parts: a member holds no grant on the latter, and the
|
|
4051
|
+
// former is filtered to what they may see. total_rows is approximate under merges
|
|
4052
|
+
// and exact enough for "is anything in there".
|
|
4053
|
+
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);
|
|
4054
|
+
} catch { /* unreachable house: rooms still listed, counts absent */ }
|
|
4055
|
+
}
|
|
4056
|
+
const out = {
|
|
4057
|
+
instance: name, home: HOME_DIR, envFile: ENV_FILE, binary: thisBin, version: PKG.version, channel: cfg.channel || null,
|
|
4058
|
+
house: cfg.stated ? { url: cfg.url, db: cfg.db, member: cfg.user, adminCredential: !!cfg.adminUser, adminUser: cfg.adminUser || null } : null,
|
|
4059
|
+
rooms: rooms ? [...ROOM_TYPES, ...META_TYPES].map((t) => ({ table: `${cfg.db}.${rooms.physical[t]}`, rows: counts[rooms.physical[t]] ?? null })) : [],
|
|
4060
|
+
ships: scope.describe({ MEMHOUSE_EDITORS: cfg.editors, MEMHOUSE_CLAUDE_ROOTS: cfg.claudeRoots }),
|
|
4061
|
+
host: host ? host.id : null, daemons: { ...daemons, dashboardUrl: daemons.dashboard ? `http://localhost:${cfg.port}` : null },
|
|
4062
|
+
boundPlaybooks: bound,
|
|
4063
|
+
};
|
|
4064
|
+
if (JSON_OUT) { console.log(JSON.stringify(out, null, 2)); break; }
|
|
4065
|
+
const line = (k, v) => console.log(` ${k.padEnd(9)} ${v}`);
|
|
4066
|
+
line('instance', `${name} (${short(HOME_DIR)})`);
|
|
4067
|
+
line('binary', `${short(thisBin)} ${PKG.version}${cfg.channel ? ` channel ${cfg.channel}` : ''}`);
|
|
4068
|
+
if (!out.house) { line('house', 'none configured — memhouse install'); break; }
|
|
4069
|
+
line('house', `${cfg.url} db ${cfg.db} member ${cfg.user} admin credential: ${cfg.adminUser ? `yes (${cfg.adminUser})` : 'no'}`);
|
|
4070
|
+
line('rooms', out.rooms.map((r) => `${r.table}${r.rows === null ? '' : ` ${r.rows}`}`).join(' · '));
|
|
4071
|
+
line('ships', out.ships);
|
|
4072
|
+
line('host', host ? host.id : 'not minted yet (first ship mints it)');
|
|
4073
|
+
line('daemons', `shipper ${daemons.shipper ? `running (pid ${daemons.shipper})` : 'stopped'} dashboard ${daemons.dashboard ? `running (pid ${daemons.dashboard}) → http://localhost:${cfg.port}` : 'stopped'}`);
|
|
4074
|
+
line('bound', bound.length ? bound.map(short).join(', ') : 'no playbook is bound to this instance — memhouse plugins install claude');
|
|
4075
|
+
break;
|
|
4076
|
+
}
|
|
3946
4077
|
case 'rooms': {
|
|
3947
4078
|
// What are my rooms called? One answer, in one place: named for the member, inside the
|
|
3948
4079
|
// 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
|
|
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,6 +21,18 @@ 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}}
|
|
@@ -30,12 +42,14 @@ On most Linux distros the global install needs `sudo`; if `npm` says `EACCES`, t
|
|
|
30
42
|
|
|
31
43
|
## 2. Join the house
|
|
32
44
|
|
|
33
|
-
From the directory holding `{{FILE}}
|
|
45
|
+
From the directory holding `{{FILE}}`, just run memhouse — it finds the invitation and asks:
|
|
34
46
|
|
|
35
47
|
```
|
|
36
|
-
memhouse
|
|
48
|
+
memhouse
|
|
37
49
|
```
|
|
38
50
|
|
|
51
|
+
(or, without the question: `memhouse install --env {{FILE}}`)
|
|
52
|
+
|
|
39
53
|
What that does, in order: connects to `{{URL}}` as `{{NAME}}`, **rotates your password to
|
|
40
54
|
one only your machine knows**, creates your rooms, writes your config to `~/.memhouse/env`,
|
|
41
55
|
deletes `{{FILE}}`, and starts shipping in the background. The password in the file stops
|
|
@@ -115,6 +129,9 @@ narrower than the one memhouse issues — ask {{INVITER}} to run `memhouse insta
|
|
|
115
129
|
You are installing memhouse for the person who handed you this file. Five rules; three of
|
|
116
130
|
them exist because an agent broke them:
|
|
117
131
|
|
|
132
|
+
0. **Your credential is `{{FILE}}` in the directory this page is in.** If `Read
|
|
133
|
+
MEMHOUSE-INVITATION.md` is all you were given, `ls invite-*.env` beside it is the file
|
|
134
|
+
the steps refer to; confirm it is there before step 1.
|
|
118
135
|
1. **Never print `{{FILE}}` or any `MEMHOUSE_*` value.** No `cat`, no `echo $MEMHOUSE_PASSWORD`,
|
|
119
136
|
no `set -x`. This very session will be shipped into the house; a password printed here is
|
|
120
137
|
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.
|
|
4
|
+
"version": "0.18.1",
|
|
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
|
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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memhouse",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.1",
|
|
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": {
|