memhouse 0.7.1 → 0.9.0
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 +79 -43
- package/bin/memhouse.js +153 -147
- package/editors/antigravity.js +7 -15
- package/editors/cursor.js +18 -24
- package/editors/goose.js +9 -21
- package/editors/index.js +19 -70
- package/editors/opencode.js +6 -15
- package/editors/sqlite.js +74 -0
- package/editors/windsurf.js +3 -3
- package/editors/zed.js +10 -15
- package/memhouse/COMPETITION.md +9 -5
- package/memhouse/DESIGN.md +29 -30
- package/memhouse/RESUME-AND-UPDATE.md +7 -10
- package/memhouse/delivery/AGENT-INSTALL.md +15 -13
- package/memhouse/delivery/PROMPT.md +7 -9
- package/memhouse/delivery/kernel-install.md +18 -33
- package/memhouse/delivery/plugin/skills/search/SKILL.md +25 -38
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +15 -28
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +23 -36
- package/memhouse/host.js +1 -1
- package/memhouse/house/HOUSE.md +74 -0
- package/memhouse/house/house.js +170 -0
- package/memhouse/{per-member/schema-member.sql.tpl → house/schema.sql.tpl} +8 -11
- package/memhouse/resume.js +1 -1
- package/memhouse/self-update.js +4 -4
- package/memhouse/server/queries.js +8 -2
- package/memhouse/shipper/ship.js +30 -37
- package/package.json +2 -6
- package/memhouse/per-member/INSTALL.md +0 -228
- package/memhouse/per-member/PLAN.md +0 -174
- package/memhouse/per-member/PROVISIONING.md +0 -177
- package/memhouse/per-member/README.md +0 -48
- package/memhouse/per-member/SCHEMA.md +0 -120
- package/memhouse/per-member/SHARING.md +0 -149
- package/memhouse/per-member/provision.js +0 -297
- package/memhouse/per-member/rooms.js +0 -252
- package/memhouse/per-member/schema-merge.sql.tpl +0 -24
package/bin/memhouse.js
CHANGED
|
@@ -23,7 +23,7 @@ const SHIP_JS = path.join(REPO_ROOT, 'memhouse', 'shipper', 'ship.js');
|
|
|
23
23
|
const SERVER_JS = path.join(REPO_ROOT, 'memhouse', 'server', 'server.js');
|
|
24
24
|
const DELIVERY = path.join(REPO_ROOT, 'memhouse', 'delivery');
|
|
25
25
|
const PKG = require(path.join(REPO_ROOT, 'package.json'));
|
|
26
|
-
const { roomNames, ROOM_TYPES,
|
|
26
|
+
const { roomNames, ROOM_TYPES, MEMBER_PIN, installCommand } = require(path.join(REPO_ROOT, 'memhouse', 'house', 'house'));
|
|
27
27
|
const envfile = require(path.join(REPO_ROOT, 'memhouse', 'envfile'));
|
|
28
28
|
|
|
29
29
|
const HOME_DIR = process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
|
|
@@ -65,7 +65,10 @@ function resolveConfig() {
|
|
|
65
65
|
url: pick('url', 'MEMHOUSE_URL', 'http://localhost:8123'),
|
|
66
66
|
user: pick('user', 'MEMHOUSE_USER', 'memhouse_root'),
|
|
67
67
|
password: pick('password', 'MEMHOUSE_PASSWORD', ''),
|
|
68
|
-
|
|
68
|
+
// The house defaults to the USER'S OWN NAME (`polat` ships into `polat.messages`),
|
|
69
|
+
// because a house is a database and the natural first house is your own. Any name
|
|
70
|
+
// works — user 'alice' with house 'default' is a supported pairing.
|
|
71
|
+
db: pick('db', 'MEMHOUSE_DB', pick('user', 'MEMHOUSE_USER', 'memhouse_root')),
|
|
69
72
|
port: pick('port', 'MEMHOUSE_PORT', '4640'),
|
|
70
73
|
// Did anything actually SAY which house this is, or are the values above just the
|
|
71
74
|
// defaults? The defaults are not neutral — localhost:8123 as memhouse_root is a real
|
|
@@ -353,18 +356,15 @@ Engine: MEMHOUSE_ENGINE pins docker or podman when both are installed and one ca
|
|
|
353
356
|
`;
|
|
354
357
|
|
|
355
358
|
// A skipped adapter and an editor the user does not have look identical — both
|
|
356
|
-
// contribute zero sessions. Say which happened
|
|
359
|
+
// contribute zero sessions. Say which happened.
|
|
360
|
+
//
|
|
361
|
+
// There was a second branch here for the one cause that hit five adapters at once: a
|
|
362
|
+
// missing better-sqlite3 native binding, fixable only by reinstalling with a flag. With
|
|
363
|
+
// SQLite now inside Node there is nothing to install and nothing to advise — every
|
|
364
|
+
// remaining failure is one adapter's own store, and its own message is the whole story.
|
|
357
365
|
function printAdapterErrors(errors) {
|
|
358
366
|
if (!errors || errors.length === 0) return;
|
|
359
|
-
const
|
|
360
|
-
const other = errors.filter((e) => !e.missingBinding);
|
|
361
|
-
if (blocked.length) {
|
|
362
|
-
console.log(warn(`${blocked.length} adapter${blocked.length > 1 ? 's' : ''} skipped — better-sqlite3 has no native binding: ${blocked.map((e) => e.source).join(', ')}`));
|
|
363
|
-
console.log(' their sessions are NOT being shipped. npm >= 12 blocks install scripts by default; rebuild with:');
|
|
364
|
-
console.log(' ${installCommand()}');
|
|
365
|
-
console.log(' (from a checkout: npm install --no-audit --no-fund, which package.json already allows)');
|
|
366
|
-
}
|
|
367
|
-
for (const e of other) console.log(warn(`${e.source.padEnd(16)} skipped: ${e.message}`));
|
|
367
|
+
for (const e of errors) console.log(warn(`${e.source.padEnd(16)} skipped: ${e.message}`));
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
async function cmdDiscover() {
|
|
@@ -600,38 +600,20 @@ function generatePassword() {
|
|
|
600
600
|
* same templates provision.js applies, so the two cannot drift into different houses.
|
|
601
601
|
*/
|
|
602
602
|
function memberSql(db, member, password) {
|
|
603
|
-
|
|
604
|
-
//
|
|
605
|
-
//
|
|
606
|
-
// the play UI, curl — so every name is qualified here and there is no `USE`.
|
|
607
|
-
//
|
|
608
|
-
// `currentDatabase()` in the Merge engine is the trap, and it is silent:
|
|
609
|
-
// it is evaluated at CREATE time, not at read time, so a block run without the house
|
|
610
|
-
// selected builds Merge rooms pointing at `default` that return zero rows forever. The
|
|
611
|
-
// literal name goes in instead.
|
|
603
|
+
// The template is written unqualified because the shipper applies it with the house
|
|
604
|
+
// already selected. A human pastes this somewhere unknown — clickhouse-client, the
|
|
605
|
+
// play UI, curl — so every name is qualified here and there is no `USE`.
|
|
612
606
|
const qualify = (sql) => sql
|
|
613
|
-
.replace(/CREATE TABLE IF NOT EXISTS (\w+)/g, `CREATE TABLE IF NOT EXISTS ${db}.$1`)
|
|
614
|
-
|
|
615
|
-
.replace(/Merge\(currentDatabase\(\)/g, `Merge('${db}'`);
|
|
616
|
-
const rooms = qualify(fs.readFileSync(path.join(here, 'schema-member.sql.tpl'), 'utf-8')
|
|
617
|
-
.replaceAll('{{MEMBER}}', member));
|
|
618
|
-
const merge = qualify(fs.readFileSync(path.join(here, 'schema-merge.sql.tpl'), 'utf-8')
|
|
619
|
-
.replaceAll('{{TEMPLATE_MEMBER}}', member));
|
|
620
|
-
// The SAME grant set provision.js issues. These two paths build the same house, and when
|
|
621
|
-
// only one of them was narrowed, `install --print-sql` — the path INSTALL.md calls the
|
|
622
|
-
// common case — still emitted GRANT ALL, which includes CREATE TABLE on the member's own
|
|
623
|
-
// room name. A member provisioned that way could drop their room, recreate it as
|
|
624
|
-
// Merge('<db>','^messages_'), and double every other member's rows in the team room:
|
|
625
|
-
// exactly the attack the narrowing was for, fully reachable, one release later.
|
|
626
|
-
const grants = ROOM_TYPES.flatMap((t) => [
|
|
627
|
-
`GRANT ${MEMBER_PRIVS} ON ${db}.${t}_${member} TO ${member};`,
|
|
628
|
-
`GRANT SELECT ON ${db}.${t}_${member} TO ${member} WITH GRANT OPTION;`,
|
|
629
|
-
]).join('\n');
|
|
630
|
-
const mergeGrants = Object.values(mergeRooms())
|
|
631
|
-
.map((n) => `GRANT SELECT ON ${db}.${n} TO ${member};`).join('\n');
|
|
607
|
+
.replace(/CREATE TABLE IF NOT EXISTS (\w+)/g, `CREATE TABLE IF NOT EXISTS ${db}.$1`);
|
|
608
|
+
const rooms = qualify(fs.readFileSync(path.join(REPO_ROOT, 'memhouse', 'house', 'schema.sql.tpl'), 'utf-8'));
|
|
632
609
|
return `-- memhouse: everything '${member}' needs in house '${db}'. Run as a user with
|
|
633
610
|
-- ACCESS MANAGEMENT (a stock 'default' with access_management=1 will do).
|
|
634
611
|
--
|
|
612
|
+
-- A house is a database; its rooms are three shared tables. Everyone granted on the
|
|
613
|
+
-- database writes into the same tables, and every row carries who (user_id, stamped by
|
|
614
|
+
-- the server) and where from (host, the install's fingerprint). Adding a housemate later
|
|
615
|
+
-- is the same two statements: CREATE USER, GRANT ALL ON the house.
|
|
616
|
+
--
|
|
635
617
|
-- Every name is qualified and there is no USE, so this runs anywhere: clickhouse-client,
|
|
636
618
|
-- the play UI, curl, a GUI. Order matters only in that the database comes first.
|
|
637
619
|
--
|
|
@@ -655,31 +637,16 @@ CREATE USER IF NOT EXISTS ${member} IDENTIFIED BY '${password.replace(/'/g, "\\'
|
|
|
655
637
|
|
|
656
638
|
${rooms.trim()}
|
|
657
639
|
|
|
658
|
-
--
|
|
659
|
-
--
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
--
|
|
665
|
-
--
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
${mergeGrants}
|
|
669
|
-
|
|
670
|
-
-- Resource ceilings. Confidentiality isolation is a grant that is absent; this is the
|
|
671
|
-
-- availability half, and it did not exist until 0.4.7 — a member could raise
|
|
672
|
-
-- max_memory_usage, zero max_execution_time and take a shared server down for everyone.
|
|
673
|
-
-- CONST, not MAX: a plain default is advisory, and MAX is not enough either because 0 means
|
|
674
|
-
-- UNLIMITED in ClickHouse and 0 satisfies any MAX. Measured — with a MAX ceiling in force,
|
|
675
|
-
-- SETTINGS max_memory_usage = 0 was accepted and the ceiling was gone.
|
|
676
|
-
-- CREATE ... OR REPLACE, which this used, DETACHES every user already on the profile, so
|
|
677
|
-
-- provisioning a second member silently stripped the first member's ceilings. IF NOT
|
|
678
|
-
-- EXISTS + ALTER edits the same object in place and keeps every assignment.
|
|
679
|
-
CREATE SETTINGS PROFILE IF NOT EXISTS ${MEMBER_PROFILE};
|
|
680
|
-
ALTER SETTINGS PROFILE ${MEMBER_PROFILE} SETTINGS NONE;
|
|
681
|
-
ALTER SETTINGS PROFILE ${MEMBER_PROFILE} SETTINGS ${MEMBER_PROFILE_SETTINGS};
|
|
682
|
-
ALTER USER ${member} SETTINGS PROFILE '${MEMBER_PROFILE}';
|
|
640
|
+
-- The whole database. ALL on your own house reaches nothing outside it — the database is
|
|
641
|
+
-- the boundary — and it is what lets the shipper create and evolve its own tables.
|
|
642
|
+
GRANT ALL ON ${db}.* TO ${member};
|
|
643
|
+
|
|
644
|
+
-- Pin async_insert on the user (ADD SETTING merges; a bare SETTINGS clause would replace
|
|
645
|
+
-- the user's whole list). The pin keeps the user_id stamp honest: a MATERIALIZED
|
|
646
|
+
-- currentUser() is computed during the INSERT, and an async flush stores it as the empty
|
|
647
|
+
-- string. CONST refuses the override; a client that never mentions the setting is held
|
|
648
|
+
-- at 0 and its insert lands stamped.
|
|
649
|
+
ALTER USER ${member} ADD SETTING ${MEMBER_PIN};
|
|
683
650
|
`;
|
|
684
651
|
}
|
|
685
652
|
|
|
@@ -707,22 +674,19 @@ async function adminBootstrap(cfg, admin) {
|
|
|
707
674
|
catch { console.log(bad(`house '${cfg.db}' does not exist and '${admin.user}' cannot create it: ${e.message}`)); return null; }
|
|
708
675
|
}
|
|
709
676
|
|
|
710
|
-
// 2. the user.
|
|
711
|
-
//
|
|
712
|
-
//
|
|
713
|
-
//
|
|
714
|
-
// SELECT ON system.users. That is the isolation working — but the raw 497 names neither
|
|
715
|
-
// side, while INSTALL.md promises install "refuses, names both sides, and creates
|
|
716
|
-
// nothing". Say who you are not, and what the owner has to run instead.
|
|
677
|
+
// 2. the user. A shared house makes an existing user unremarkable — rows are told apart
|
|
678
|
+
// by the server-stamped user_id, so "the handle is taken" hands nobody anything. What
|
|
679
|
+
// still matters is proof: connecting AS the member below is what validates whichever
|
|
680
|
+
// password is in play, existing user or fresh one.
|
|
717
681
|
let exists;
|
|
718
682
|
try {
|
|
719
683
|
exists = (await chRows(adminCfg, `SELECT name FROM system.users WHERE name = '${admin.member}'`, { database: '' })).length > 0;
|
|
720
684
|
} catch (e) {
|
|
721
685
|
if (/Not enough privileges|ACCESS_DENIED|system\.users/i.test(e.message || '')) {
|
|
722
686
|
console.log(bad(`'${admin.user}' is not an admin of this house — it cannot read system.users, so it cannot provision a member`));
|
|
723
|
-
console.log('
|
|
724
|
-
console.log(`
|
|
725
|
-
console.log(' If you already HAVE a
|
|
687
|
+
console.log(' Joining a house is two statements for whoever owns it:');
|
|
688
|
+
console.log(` CREATE USER ${admin.member} IDENTIFIED BY '…'; GRANT ALL ON ${cfg.db}.* TO ${admin.member};`);
|
|
689
|
+
console.log(' If you already HAVE a credential on this house, you do not need admin:');
|
|
726
690
|
console.log(` memhouse install --url ${adminCfg.url} --user ${admin.member} --password …`);
|
|
727
691
|
return null;
|
|
728
692
|
}
|
|
@@ -730,19 +694,14 @@ async function adminBootstrap(cfg, admin) {
|
|
|
730
694
|
}
|
|
731
695
|
let password = flags['member-password'] || null;
|
|
732
696
|
if (exists) {
|
|
733
|
-
if (
|
|
734
|
-
console.log(bad(`ClickHouse user '${admin.member}' already exists
|
|
735
|
-
console.log(
|
|
736
|
-
console.log(' currentUser(), so their rows and yours would be indistinguishable.');
|
|
737
|
-
console.log(' If this is you on a new machine, prove it:');
|
|
738
|
-
console.log(` memhouse install --adopt-user --member ${admin.member} --member-password '…' …`);
|
|
739
|
-
console.log(` If it is someone else, pick another handle: --member <name>`);
|
|
697
|
+
if (!password) {
|
|
698
|
+
console.log(bad(`ClickHouse user '${admin.member}' already exists — pass --member-password to install as them.`));
|
|
699
|
+
console.log(` Or pick another handle: --member <name>`);
|
|
740
700
|
return null;
|
|
741
701
|
}
|
|
742
|
-
if (!password) { console.log(bad('--adopt-user requires --member-password — the password is the proof')); return null; }
|
|
743
702
|
try { await ch({ ...cfg, user: admin.member, password }, 'SELECT 1', { database: '' }); }
|
|
744
|
-
catch { console.log(bad(
|
|
745
|
-
console.log(ok(`
|
|
703
|
+
catch { console.log(bad(`'${admin.member}' did not authenticate with that password`)); return null; }
|
|
704
|
+
console.log(ok(`using the existing ClickHouse user '${admin.member}' (password verified)`));
|
|
746
705
|
} else {
|
|
747
706
|
if (!password) {
|
|
748
707
|
password = generatePassword();
|
|
@@ -758,33 +717,18 @@ async function adminBootstrap(cfg, admin) {
|
|
|
758
717
|
createdUser = admin.member;
|
|
759
718
|
}
|
|
760
719
|
|
|
761
|
-
// 3.
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
// REFUSAL, so a first-time installer reads a success as a warning about someone else.
|
|
771
|
-
const provisionArgs = [provision, '--member', admin.member, '--merge'];
|
|
772
|
-
if (createdUser) provisionArgs.push('--user-just-created');
|
|
773
|
-
const rc = spawnSync(process.execPath, provisionArgs, {
|
|
774
|
-
stdio: 'inherit',
|
|
775
|
-
env: { ...process.env, MEM_URL: cfg.url, MEM_USER: admin.user, MEM_PASSWORD: admin.password, MEM_DB: cfg.db },
|
|
776
|
-
});
|
|
777
|
-
if (rc.status !== 0) {
|
|
778
|
-
// "nothing was written" was a lie whenever the CREATE USER above had already run —
|
|
779
|
-
// and it runs before this. Observed: provision.js died on a missing dependency, this
|
|
780
|
-
// printed the all-clear, and the next install refused with the identity-takeover
|
|
781
|
-
// message because the user it said it had not created was there. Say what exists, and
|
|
782
|
-
// name the two ways forward.
|
|
783
|
-
console.log(bad('provisioning failed'));
|
|
720
|
+
// 3. the grant and the pin. GRANT ALL on the house is the whole tenancy model: it
|
|
721
|
+
// reaches nothing outside the database, and it is what lets the member's own shipper
|
|
722
|
+
// create and evolve the rooms (--ensure-schema below).
|
|
723
|
+
try {
|
|
724
|
+
await q(`GRANT ALL ON ${cfg.db}.* TO ${admin.member}`, { database: '' });
|
|
725
|
+
await q(`ALTER USER ${admin.member} ADD SETTING ${MEMBER_PIN}`, { database: '' });
|
|
726
|
+
console.log(ok(`granted the house: ALL ON ${cfg.db}.* to '${admin.member}', async_insert pinned`));
|
|
727
|
+
} catch (e) {
|
|
728
|
+
console.log(bad(`could not grant the house to '${admin.member}': ${e.message}`));
|
|
784
729
|
if (createdUser) {
|
|
785
|
-
console.log(` ClickHouse user '${createdUser}' WAS created before this failed
|
|
786
|
-
console.log(
|
|
787
|
-
console.log(` memhouse install --adopt-user --member ${createdUser} --member-password '<the password above>' …`);
|
|
730
|
+
console.log(` ClickHouse user '${createdUser}' WAS created before this failed.`);
|
|
731
|
+
console.log(` Continue once fixed: memhouse install --member ${createdUser} --member-password '<the password above>' …`);
|
|
788
732
|
console.log(` Or undo it as the admin: DROP USER ${createdUser}`);
|
|
789
733
|
} else {
|
|
790
734
|
console.log(' nothing was written');
|
|
@@ -793,11 +737,15 @@ async function adminBootstrap(cfg, admin) {
|
|
|
793
737
|
}
|
|
794
738
|
|
|
795
739
|
// The step that makes this trustworthy: stop being admin, and prove the credential we
|
|
796
|
-
// are about to persist
|
|
740
|
+
// are about to persist can build and reach the rooms itself.
|
|
797
741
|
const memberCfg = { ...cfg, user: admin.member, password };
|
|
742
|
+
if (run(SHIP_JS, ['--ensure-schema'], memberCfg) !== 0) {
|
|
743
|
+
console.log(bad(`'${admin.member}' could not create the rooms in '${cfg.db}' — nothing written to disk`));
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
798
746
|
try {
|
|
799
|
-
const seen = await chRows(memberCfg, `SELECT count() AS n FROM system.tables WHERE database = '${cfg.db}' AND name IN ('
|
|
800
|
-
if (Number(seen[0]?.n) !== 3) { console.log(bad(`'${admin.member}' cannot see
|
|
747
|
+
const seen = await chRows(memberCfg, `SELECT count() AS n FROM system.tables WHERE database = '${cfg.db}' AND name IN ('sessions','messages','tool_calls')`, { database: '' });
|
|
748
|
+
if (Number(seen[0]?.n) !== 3) { console.log(bad(`'${admin.member}' cannot see the three rooms — nothing written`)); return null; }
|
|
801
749
|
} catch (e) { console.log(bad(`'${admin.member}' could not connect after provisioning: ${e.message}`)); return null; }
|
|
802
750
|
console.log(ok(`verified as '${admin.member}' — admin credential discarded, not stored`));
|
|
803
751
|
return memberCfg;
|
|
@@ -855,6 +803,16 @@ async function cmdInstall({ interactive }) {
|
|
|
855
803
|
let cfg = resolveConfig();
|
|
856
804
|
const adminUser = flags['admin-user'];
|
|
857
805
|
|
|
806
|
+
// The house name is spliced unquoted into CREATE DATABASE and GRANT ALL ON <db>.* —
|
|
807
|
+
// validate it HERE, on every install mode, not only where deploy happens to choose it.
|
|
808
|
+
// Found by the acceptance run, driven end to end: `install --db system` sailed through,
|
|
809
|
+
// granted a member CHECK/SHOW/SELECT/INSERT/ALTER/DROP/TRUNCATE/… ON system.* and
|
|
810
|
+
// created sessions/messages/tool_calls INSIDE the server's system database, exit 0.
|
|
811
|
+
// On a shared or kernel ClickHouse that is a granted-everything-on-system user minted
|
|
812
|
+
// by a memhouse one-liner.
|
|
813
|
+
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(cfg.db, 'house'); }
|
|
814
|
+
catch (e) { console.log(bad(e.message)); return 1; }
|
|
815
|
+
|
|
858
816
|
// Option 1 — print the SQL and stop. For the common case: you have admin on this
|
|
859
817
|
// ClickHouse and would rather run four statements yourself than hand a credential to an
|
|
860
818
|
// installer. Nothing is written and nothing is contacted.
|
|
@@ -893,7 +851,7 @@ async function cmdInstall({ interactive }) {
|
|
|
893
851
|
if (keyProblem) {
|
|
894
852
|
console.log(bad(`the rooms are here, but their sorting keys are wrong: ${keyProblem}`));
|
|
895
853
|
console.log(' a ship pass would corrupt them, so `memhouse ship` will refuse.');
|
|
896
|
-
console.log(` rebuild them from ${path.join(REPO_ROOT, 'memhouse', '
|
|
854
|
+
console.log(` rebuild them from ${path.join(REPO_ROOT, 'memhouse', 'house', 'schema.sql.tpl')}, then: memhouse ship --full`);
|
|
897
855
|
console.log(' no config was written — nothing here reads as installed.');
|
|
898
856
|
return 1;
|
|
899
857
|
}
|
|
@@ -907,8 +865,8 @@ async function cmdInstall({ interactive }) {
|
|
|
907
865
|
}
|
|
908
866
|
if (flags['no-ship'] !== true && run(SHIP_JS, [], cfg) !== 0) return 1;
|
|
909
867
|
console.log(ok('installed — next: memhouse start (dashboard + shipper loop)'));
|
|
910
|
-
console.log('
|
|
911
|
-
console.log(`
|
|
868
|
+
console.log(' adding a housemate later is two statements for the admin:');
|
|
869
|
+
console.log(` CREATE USER <name> IDENTIFIED BY '…'; GRANT ALL ON ${cfg.db}.* TO <name>;`);
|
|
912
870
|
return 0;
|
|
913
871
|
}
|
|
914
872
|
|
|
@@ -917,7 +875,7 @@ async function cmdInstall({ interactive }) {
|
|
|
917
875
|
// authenticating as memhouse_root instead: the three flags did nothing, --yes satisfied
|
|
918
876
|
// haveAll so no prompt asked who you were, and resolveConfig's default was used AS A
|
|
919
877
|
// CREDENTIAL. Refuse rather than do something else silently.
|
|
920
|
-
const adminOnly = ['
|
|
878
|
+
const adminOnly = ['member', 'member-password'].filter((k) => flags[k] !== undefined);
|
|
921
879
|
if (!adminUser && adminOnly.length) {
|
|
922
880
|
console.log(bad(`--${adminOnly.join(', --')} ${adminOnly.length > 1 ? 'are' : 'is'} only read with --admin-user`));
|
|
923
881
|
console.log(' Those flags provision a member, which needs house admin. Without them this');
|
|
@@ -1030,7 +988,7 @@ async function cmdInstall({ interactive }) {
|
|
|
1030
988
|
if (keyProblem) {
|
|
1031
989
|
console.log(bad(`the rooms are here, but their sorting keys are wrong: ${keyProblem}`));
|
|
1032
990
|
console.log(' a ship pass would corrupt them, so `memhouse ship` will refuse.');
|
|
1033
|
-
console.log(` rebuild them from ${path.join(REPO_ROOT, 'memhouse', '
|
|
991
|
+
console.log(` rebuild them from ${path.join(REPO_ROOT, 'memhouse', 'house', 'schema.sql.tpl')}, then: memhouse ship --full`);
|
|
1034
992
|
console.log(' no config was written — nothing here reads as installed.');
|
|
1035
993
|
return 1;
|
|
1036
994
|
}
|
|
@@ -1268,8 +1226,11 @@ async function cmdDoctor() {
|
|
|
1268
1226
|
const checks = [];
|
|
1269
1227
|
const add = (okFlag, label, hint) => checks.push({ ok: okFlag, label, hint });
|
|
1270
1228
|
|
|
1271
|
-
|
|
1272
|
-
|
|
1229
|
+
// 24 because SQLite comes from `node:sqlite`, which is stable there. It works on 22.13+
|
|
1230
|
+
// but prints an ExperimentalWarning on every command, and 20 is past EOL. Below the
|
|
1231
|
+
// floor, the five SQLite-backed adapters are the part that actually breaks.
|
|
1232
|
+
const [major] = process.versions.node.split('.').map(Number);
|
|
1233
|
+
add(major >= 24, `node ${process.versions.node}`, 'need >= 24 — node:sqlite is stable there; the SQLite-backed adapters need it');
|
|
1273
1234
|
add(fs.existsSync(ENV_FILE), `config ${ENV_FILE}`, 'run: memhouse install');
|
|
1274
1235
|
// doctor is exempt from requireConfig — diagnosing an unconfigured machine is its job —
|
|
1275
1236
|
// but "exempt from refusing" is not "licensed to log in somewhere". With no config it
|
|
@@ -1320,7 +1281,7 @@ async function cmdDoctor() {
|
|
|
1320
1281
|
add(t === objects.length, t === objects.length
|
|
1321
1282
|
? `schema: 3/3 rooms in '${cfg.db}' (${objects.join(', ')})`
|
|
1322
1283
|
: `schema: ${t}/${objects.length} rooms in '${cfg.db}' — missing ${absent.join(', ')}`,
|
|
1323
|
-
`run: memhouse install
|
|
1284
|
+
`run: memhouse install — with ALL on the house, ship --ensure-schema builds them`);
|
|
1324
1285
|
} catch (e) { add(false, 'schema check', `${e.message} — run: memhouse install`); }
|
|
1325
1286
|
// Columns, not just rooms. A room with a column missing accepts every insert and
|
|
1326
1287
|
// discards that field — measured, 85 rows shipped with the value thrown away while
|
|
@@ -1328,7 +1289,7 @@ async function cmdDoctor() {
|
|
|
1328
1289
|
// counted rooms.
|
|
1329
1290
|
try {
|
|
1330
1291
|
const { templateColumns } = require(path.join(REPO_ROOT, 'memhouse', 'shipper', 'ship.js'));
|
|
1331
|
-
const tpl = fs.readFileSync(path.join(REPO_ROOT, 'memhouse', '
|
|
1292
|
+
const tpl = fs.readFileSync(path.join(REPO_ROOT, 'memhouse', 'house', 'schema.sql.tpl'), 'utf-8');
|
|
1332
1293
|
const want = templateColumns(tpl);
|
|
1333
1294
|
// Names AND types AND the MATERIALIZED kind. Comparing names alone left every other
|
|
1334
1295
|
// kind of drift invisible, with measured consequences: a `UInt64` column narrowed to
|
|
@@ -1393,7 +1354,7 @@ async function cmdDoctor() {
|
|
|
1393
1354
|
add(false, `sorting keys: only ${checked}/${ROOM_TYPES.length} rooms are MergeTree — the rest have no sorting key at all`,
|
|
1394
1355
|
'a room was replaced by a Merge/View/Log engine; rebuild it from the schema template as the house owner');
|
|
1395
1356
|
} else add(wrongKeys.length === 0, `sorting keys${wrongKeys.length ? `: wrong on ${wrongKeys.join(', ')}` : ` carry origin correctly (${checked}/${ROOM_TYPES.length} rooms)`}`,
|
|
1396
|
-
`rebuild those rooms from the schema template, then: memhouse ship --full\n template: ${path.join(REPO_ROOT, 'memhouse', '
|
|
1357
|
+
`rebuild those rooms from the schema template, then: memhouse ship --full\n template: ${path.join(REPO_ROOT, 'memhouse', 'house', 'schema.sql.tpl')}`);
|
|
1397
1358
|
} catch (e) { add(false, 'sorting keys', e.message); }
|
|
1398
1359
|
try {
|
|
1399
1360
|
// countIf, not any(). `any()` returns an arbitrary row's value, so on a house with
|
|
@@ -1484,11 +1445,11 @@ async function cmdDoctor() {
|
|
|
1484
1445
|
}
|
|
1485
1446
|
adapterErrors = getAdapterErrors();
|
|
1486
1447
|
for (const t of thrown) {
|
|
1487
|
-
if (!adapterErrors.some((e) => e.source === t.source)) adapterErrors.push(
|
|
1448
|
+
if (!adapterErrors.some((e) => e.source === t.source)) adapterErrors.push(t);
|
|
1488
1449
|
}
|
|
1489
|
-
// Any adapter that could not be read is a failure, whatever the cause — a
|
|
1490
|
-
//
|
|
1491
|
-
|
|
1450
|
+
// Any adapter that could not be read is a failure, whatever the cause — a locked or
|
|
1451
|
+
// corrupt store loses sessions just as surely as the missing native binding this
|
|
1452
|
+
// used to have a separate remedy for.
|
|
1492
1453
|
const failed = adapterErrors.map((e) => e.source);
|
|
1493
1454
|
add(adapterErrors.length === 0,
|
|
1494
1455
|
// "visible locally" is what the ADAPTERS see; `stats` reports what the house HOLDS,
|
|
@@ -1497,9 +1458,7 @@ async function cmdDoctor() {
|
|
|
1497
1458
|
// find the other number elsewhere (161 here, 156 there, on a real machine) gives
|
|
1498
1459
|
// them no way to tell "correctly skipped" from "silently dropped".
|
|
1499
1460
|
`adapters: ${seen} sessions visible locally${failed.length ? ` (${failed.length} skipped: ${failed.join(', ')})` : ''} — compare with what the house holds: memhouse stats`,
|
|
1500
|
-
|
|
1501
|
-
: adapterErrors.length ? adapterErrors.map((e) => `${e.source}: ${e.message}`).join('; ')
|
|
1502
|
-
: undefined);
|
|
1461
|
+
adapterErrors.length ? adapterErrors.map((e) => `${e.source}: ${e.message}`).join('; ') : undefined);
|
|
1503
1462
|
} catch (e) { add(false, 'adapters', e.message); }
|
|
1504
1463
|
// Which models in this house have no price. An unpriced model is not an error — a new
|
|
1505
1464
|
// release always lands before pricing.json catches up — but it IS silent: calculateCost
|
|
@@ -1696,12 +1655,12 @@ async function cmdUpdate() {
|
|
|
1696
1655
|
|
|
1697
1656
|
if (kind === 'npx') {
|
|
1698
1657
|
console.log(warn('nothing to update — npx resolves the registry on every run'));
|
|
1699
|
-
console.log(' to keep a version around: npm install -g memhouse
|
|
1658
|
+
console.log(' to keep a version around: npm install -g memhouse');
|
|
1700
1659
|
return 1;
|
|
1701
1660
|
}
|
|
1702
1661
|
if (kind === 'local-dep') {
|
|
1703
1662
|
console.log(warn('this is a project dependency, not a global install'));
|
|
1704
|
-
console.log(` upgrade it where it lives: npm install memhouse@latest
|
|
1663
|
+
console.log(` upgrade it where it lives: npm install memhouse@latest`);
|
|
1705
1664
|
return 1;
|
|
1706
1665
|
}
|
|
1707
1666
|
|
|
@@ -1729,10 +1688,11 @@ async function cmdUpdate() {
|
|
|
1729
1688
|
}
|
|
1730
1689
|
}
|
|
1731
1690
|
} else {
|
|
1732
|
-
//
|
|
1733
|
-
//
|
|
1734
|
-
// five SQLite-backed adapters
|
|
1735
|
-
|
|
1691
|
+
// Plain, with no flag to forget. This carried `--allow-scripts=better-sqlite3` for as
|
|
1692
|
+
// long as SQLite was a native module: npm never remembered it, and an upgrade without
|
|
1693
|
+
// it left the binding unbuilt and the five SQLite-backed adapters reading zero
|
|
1694
|
+
// sessions on every pass afterwards. There is no binding to build now.
|
|
1695
|
+
const r = spawnSync('npm', ['install', '-g', 'memhouse@latest'], { stdio: 'inherit' });
|
|
1736
1696
|
if (r.status !== 0) {
|
|
1737
1697
|
console.log(bad('npm install failed — nothing was restarted, the running version is unchanged'));
|
|
1738
1698
|
console.log(' if it was EACCES: ls -ld "$(npm prefix -g)" — root-owned needs sudo, yours needs a chown');
|
|
@@ -1931,8 +1891,8 @@ async function cmdReset() {
|
|
|
1931
1891
|
} catch { /* pre-origin house: nothing to protect, ensureSchema will add the column */ }
|
|
1932
1892
|
|
|
1933
1893
|
if (flags.yes !== true) {
|
|
1934
|
-
// Name the rooms
|
|
1935
|
-
//
|
|
1894
|
+
// Name the rooms, and the scope honestly: the tables are shared, but reset only ever
|
|
1895
|
+
// deletes rows whose user_id is the CALLER's — a housemate's rows are untouched.
|
|
1936
1896
|
const scope = allOrigins
|
|
1937
1897
|
? `EVERY row, including ${imported} imported one(s) that re-shipping CANNOT restore,`
|
|
1938
1898
|
: 'the rows the shipper wrote';
|
|
@@ -2271,10 +2231,43 @@ async function cmdUninstall() {
|
|
|
2271
2231
|
: process.env.MEMHOUSE_PASSWORD ? 'MEMHOUSE_PASSWORD' : null;
|
|
2272
2232
|
if (initialised && rotateAsk) {
|
|
2273
2233
|
console.log(bad(`${rotateAsk} cannot change the credential of an existing house — the image only applies it when it initialises the data directory.`));
|
|
2274
|
-
console.log(
|
|
2234
|
+
console.log(` to rotate: ALTER USER ${priorCfg.user || 'memhouse_root'} IDENTIFIED BY '…' inside the house, then: memhouse setup --password …`);
|
|
2275
2235
|
console.log(' to start over (DESTROYS the memory): memhouse deploy --down');
|
|
2276
2236
|
process.exitCode = 2; break;
|
|
2277
2237
|
}
|
|
2238
|
+
// Who owns this house. A local deploy is the pilot's own machine and their own
|
|
2239
|
+
// memory, so the house user is THEM — suggested from the OS username (`polat`, not
|
|
2240
|
+
// `memhouse_root`) — created by the container itself at first init with full
|
|
2241
|
+
// rights. No admin/member split, no provisioning dance: a URI and a credential is
|
|
2242
|
+
// the whole install. The name is fixed at volume init exactly like the password,
|
|
2243
|
+
// so an existing house keeps the user it was born with.
|
|
2244
|
+
const osName = (os.userInfo().username || '').replace(/[^A-Za-z0-9_]/g, '_').replace(/^[0-9_]+/, '');
|
|
2245
|
+
let houseUser;
|
|
2246
|
+
if (initialised) {
|
|
2247
|
+
// The env FILE, not resolveConfig(): flags outrank the file there, so comparing
|
|
2248
|
+
// `--user other` against priorCfg.user compares the flag against itself and the
|
|
2249
|
+
// refusal below can never fire — while the volume stays initialised with the
|
|
2250
|
+
// original name and every later connection fails auth.
|
|
2251
|
+
const storedUser = readEnvFile().MEMHOUSE_USER;
|
|
2252
|
+
// No stored user means the env file is gone or emptied — the stored PASSWORD is
|
|
2253
|
+
// gone with it, so the `initialised && !reusable` refusal below already owns this
|
|
2254
|
+
// case. Refusing `--user` here on a guessed 'memhouse_root' would tell the pilot
|
|
2255
|
+
// their house "was initialised with memhouse_root" on no evidence at all.
|
|
2256
|
+
if (storedUser && flags.user && flags.user !== true && flags.user !== storedUser) {
|
|
2257
|
+
console.log(bad(`--user cannot rename the user of an existing house — the image creates it only when it initialises the data directory.`));
|
|
2258
|
+
console.log(` this house was initialised with '${storedUser}'. To start over (DESTROYS the memory): memhouse deploy --down`);
|
|
2259
|
+
process.exitCode = 2; break;
|
|
2260
|
+
}
|
|
2261
|
+
houseUser = storedUser || 'memhouse_root';
|
|
2262
|
+
} else if (flags.user && flags.user !== true) {
|
|
2263
|
+
houseUser = String(flags.user);
|
|
2264
|
+
} else if (flags.yes !== true && process.stdin.isTTY) {
|
|
2265
|
+
houseUser = (await ask(`House user (this becomes your member name)`, osName || 'memhouse_root')).trim() || osName || 'memhouse_root';
|
|
2266
|
+
} else {
|
|
2267
|
+
houseUser = osName || 'memhouse_root';
|
|
2268
|
+
}
|
|
2269
|
+
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(houseUser, 'house user'); }
|
|
2270
|
+
catch (e) { console.log(bad(`'${houseUser}' cannot be the house user — ${e.message}`)); process.exitCode = 2; break; }
|
|
2278
2271
|
// An initialised volume with no credential to reuse — env file deleted, emptied, or
|
|
2279
2272
|
// never written — is the same lockout by another route: a generated password would
|
|
2280
2273
|
// be ignored by the server and then written over the config as if it worked.
|
|
@@ -2301,7 +2294,17 @@ async function cmdUninstall() {
|
|
|
2301
2294
|
const tag = flags.tag || process.env.MEMHOUSE_CH_TAG || dep.managedTag() || dep.DEFAULT_TAG;
|
|
2302
2295
|
// The database is part of the destination, not a detail of it — see the service
|
|
2303
2296
|
// check below, which must compare it.
|
|
2304
|
-
|
|
2297
|
+
// STATED sources only, then the house user. priorCfg.db is unusable here: resolveConfig
|
|
2298
|
+
// computes a DEFAULT for it (the user's name, itself defaulted), so going through it
|
|
2299
|
+
// handed a fresh deploy `memhouse_root` as the house name while the superuser was
|
|
2300
|
+
// 'polat' — the memory would live in a database named after a role account nobody
|
|
2301
|
+
// chose.
|
|
2302
|
+
const targetDb = flags.db || process.env.MEMHOUSE_DB || readEnvFile().MEMHOUSE_DB || houseUser;
|
|
2303
|
+
// Same check install runs, but EARLY — deploy initialises the container volume and
|
|
2304
|
+
// writes the env file before its install step, so a refusal that waits for install
|
|
2305
|
+
// arrives after the damage.
|
|
2306
|
+
try { require(path.join(REPO_ROOT, 'memhouse', 'house', 'house')).assertUsableName(targetDb, 'house'); }
|
|
2307
|
+
catch (e) { console.log(bad(e.message)); process.exitCode = 2; break; }
|
|
2305
2308
|
{
|
|
2306
2309
|
// A service-managed shipper keeps the environment it was installed with, so ANY
|
|
2307
2310
|
// switch that repoints the config leaves it shipping somewhere else — not only an
|
|
@@ -2355,7 +2358,7 @@ async function cmdUninstall() {
|
|
|
2355
2358
|
}
|
|
2356
2359
|
const pw = reusable || crypto.randomBytes(16).toString('hex');
|
|
2357
2360
|
if (reusable) console.log(ok('reusing the existing house credential (its data volume is already initialised)'));
|
|
2358
|
-
const r = dep.up({ password: pw, port, tag });
|
|
2361
|
+
const r = dep.up({ password: pw, port, tag, user: houseUser });
|
|
2359
2362
|
if (!r.ok) { console.log(bad(r.msg)); process.exitCode = 1; break; }
|
|
2360
2363
|
console.log(ok(`ClickHouse starting via ${r.engine} on ${r.url} (loopback only)`));
|
|
2361
2364
|
|
|
@@ -2367,7 +2370,10 @@ async function cmdUninstall() {
|
|
|
2367
2370
|
// house that eventually came up is unreachable forever. The image applies the
|
|
2368
2371
|
// password only at first init, so there is no way back from that.
|
|
2369
2372
|
if (!reusable) {
|
|
2370
|
-
|
|
2373
|
+
// db INCLUDED: this file is read back by the install below, and writing
|
|
2374
|
+
// priorCfg's computed default here made that install refuse over a mismatch
|
|
2375
|
+
// with a file this same command had just written.
|
|
2376
|
+
writeEnvFile({ ...priorCfg, url: r.url, user: houseUser, password: pw, db: targetDb });
|
|
2371
2377
|
console.log(ok(`credential saved to ${ENV_FILE} before waiting — the volume is initialised with it`));
|
|
2372
2378
|
}
|
|
2373
2379
|
|
|
@@ -2417,10 +2423,10 @@ async function cmdUninstall() {
|
|
|
2417
2423
|
// anticipates it) and it is NOT the narrow set INSTALL.md's grant-set argument
|
|
2418
2424
|
// describes. A user who later adds a second member should know which house they
|
|
2419
2425
|
// have.
|
|
2420
|
-
console.log(warn(`this house is yours alone: '${
|
|
2421
|
-
console.log(' Adding a
|
|
2422
|
-
console.log(`
|
|
2423
|
-
flags.url = r.url; flags.user =
|
|
2426
|
+
console.log(warn(`this house is yours alone: '${houseUser}' is its superuser, and that is the credential being saved`));
|
|
2427
|
+
console.log(' Adding a housemate later is two statements:');
|
|
2428
|
+
console.log(` CREATE USER <name> IDENTIFIED BY '…'; GRANT ALL ON ${targetDb}.* TO <name>;`);
|
|
2429
|
+
flags.url = r.url; flags.user = houseUser; flags.password = pw;
|
|
2424
2430
|
flags.db = targetDb;
|
|
2425
2431
|
flags.yes = true;
|
|
2426
2432
|
process.exitCode = await cmdInstall({ interactive: false });
|
package/editors/antigravity.js
CHANGED
|
@@ -2,16 +2,7 @@ const { execSync, execFileSync } = require('child_process');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
const os = require('os');
|
|
5
|
-
|
|
6
|
-
// of the whole adapter set: editors/index.js imports every adapter, so one missing
|
|
7
|
-
// binding threw before any adapter ran and `discover` reported ZERO editors — losing the
|
|
8
|
-
// eleven that need no native code at all. The adapterErrors path exists to degrade one
|
|
9
|
-
// adapter at a time, and a top-level throw walks straight past it.
|
|
10
|
-
let Database = null;
|
|
11
|
-
function openDb(...a) {
|
|
12
|
-
if (!Database) Database = require('better-sqlite3');
|
|
13
|
-
return new Database(...a);
|
|
14
|
-
}
|
|
5
|
+
const { openReadOnly, textOf } = require('./sqlite');
|
|
15
6
|
const { getAppDataPath } = require('./base');
|
|
16
7
|
const adapterErrors = require('./adapter-errors');
|
|
17
8
|
|
|
@@ -244,13 +235,14 @@ function readGlobalStateValue(key) {
|
|
|
244
235
|
|
|
245
236
|
let db = null;
|
|
246
237
|
try {
|
|
247
|
-
|
|
238
|
+
// fileMustExist is gone with better-sqlite3, not lost: node:sqlite refuses to
|
|
239
|
+
// create a database it is opening read-only, and the existsSync above still stands.
|
|
240
|
+
db = openReadOnly(ANTIGRAVITY_GLOBAL_STORAGE_DB);
|
|
248
241
|
const row = db.prepare('SELECT value FROM ItemTable WHERE key = ?').get(key);
|
|
249
242
|
if (!row) return null;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
return v == null ? null : String(v);
|
|
243
|
+
// ItemTable.value is declared BLOB and usually holds TEXT — textOf covers both,
|
|
244
|
+
// including the Uint8Array node:sqlite returns for the byte case.
|
|
245
|
+
return row.value == null ? null : textOf(row.value);
|
|
254
246
|
} catch (e) {
|
|
255
247
|
// Only the offline half of getChats() reads this store; live cascades come over
|
|
256
248
|
// RPC. So a failure here costs some sessions, not all of them.
|