memhouse 0.16.0 → 0.17.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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * `memhouse share <user> [--only <scope>] [--revoke]` / `memhouse share --list`
3
+ *
4
+ * Full share is one statement: GRANT SELECT on the house. Partial share adds a ROW
5
+ * POLICY per room, so the grantee reads only the rows the scope allows.
6
+ *
7
+ * Why no permissive catch-all policy for everyone else, which is the obvious design:
8
+ * row policies are OR'd, so a `USING 1 TO ALL EXCEPT alice` policy silently overrides
9
+ * the NEXT scoped grantee's filter — measured, bob saw all 22,500 rows instead of his
10
+ * 1,860. It fails OPEN and it fails quietly. ClickHouse already answers this correctly
11
+ * through `users_without_row_policies_can_read_rows`: a reader with no policy on a table
12
+ * still sees every row. Verified against a non-superuser member owning its own house —
13
+ * owner and full-share grantees were both unaffected while a third user was scoped.
14
+ *
15
+ * That setting is server config, not a query setting, and its default has moved between
16
+ * versions. So `probePermissive()` measures the behaviour before the first policy is
17
+ * created rather than assuming it, and refuses when the server would blindfold everyone.
18
+ */
19
+
20
+ /** Columns a scope may filter on, and the column each room spells time with. */
21
+ const SCOPE_COLUMNS = {
22
+ session: { column: () => 'session_id', quote: true },
23
+ project: { column: () => 'project', quote: true },
24
+ folder: { column: () => 'folder', quote: true },
25
+ host: { column: () => 'host', quote: true },
26
+ source: { column: () => 'source', quote: true },
27
+ // `sessions` has no `ts`; its clock is `created_at`. One literal predicate cannot span
28
+ // the rooms, so time scopes render per room.
29
+ since: { column: (room) => (room === 'sessions' ? 'created_at' : 'ts'), op: '>=' },
30
+ until: { column: (room) => (room === 'sessions' ? 'created_at' : 'ts'), op: '<' },
31
+ };
32
+
33
+ /** `project=memhouse,since=2026-01-01` -> [{key, value}], rejecting anything unknown. */
34
+ function parseScope(raw) {
35
+ const out = [];
36
+ for (const part of String(raw).split(',').map((s) => s.trim()).filter(Boolean)) {
37
+ const m = /^([a-z_]+)\s*=\s*(.+)$/.exec(part);
38
+ if (!m) throw new Error(`scope "${part}" is not <key>=<value>`);
39
+ const key = m[1].toLowerCase();
40
+ if (!SCOPE_COLUMNS[key]) {
41
+ throw new Error(`unknown scope key "${key}" — try: ${Object.keys(SCOPE_COLUMNS).join(', ')}`);
42
+ }
43
+ out.push({ key, value: m[2].trim() });
44
+ }
45
+ if (!out.length) throw new Error('--only was given nothing to scope on');
46
+ return out;
47
+ }
48
+
49
+ /** The WHERE-shaped predicate for one room. Values are escaped, never interpolated raw. */
50
+ function scopePredicate(scope, room, sqlStr) {
51
+ return scope.map(({ key, value }) => {
52
+ const spec = SCOPE_COLUMNS[key];
53
+ const col = spec.column(room);
54
+ if (spec.op) return `${col} ${spec.op} parseDateTimeBestEffort(${sqlStr(value)})`;
55
+ return `${col} = ${sqlStr(value)}`;
56
+ }).join(' AND ');
57
+ }
58
+
59
+ /** Policy name for a grantee's room — predictable, so revoke can find every one. */
60
+ function policyName(user, room) {
61
+ return `mh_share_${user}_${room}`;
62
+ }
63
+
64
+ module.exports = { SCOPE_COLUMNS, parseScope, scopePredicate, policyName };
@@ -63,6 +63,8 @@ const selfSnap = selfUpdate.snapshot(__filename);
63
63
  const {
64
64
  resolveRooms, READ_SETTINGS, ROOM_TYPES, META_TYPES, SCHEMA_VERSION, MEMBER_PIN,
65
65
  SUPPORTED_SCHEMAS, keyProblem,
66
+ sessionStatsStatements, sessionModelStatsStatements, sessionToolStatsStatements,
67
+ SESSION_STATS, MODEL_STATS, TOOL_STATS,
66
68
  } = require('../house/house');
67
69
  // Rooms plus the house's own record of itself. Every table the template declares, which
68
70
  // is what the column healer and the drift warning have to cover — a column added to
@@ -448,6 +450,36 @@ async function ensureSchema(client) {
448
450
  throw e;
449
451
  }
450
452
  }
453
+ // The precomputed rollups — additive, so they belong here and not in a migration (see
454
+ // migrate.js's rule). Three plain tables plus the refreshable views that fill them;
455
+ // the read layer probes for them at startup and falls back to the inline rollup when
456
+ // they are absent, so a denied CREATE here degrades to slow, never to wrong. Built from
457
+ // the SAME rollup text the read layer would otherwise run, so the two cannot drift.
458
+ const statStmts = [
459
+ ...sessionStatsStatements(rooms), ...sessionModelStatsStatements(rooms), ...sessionToolStatsStatements(rooms),
460
+ ];
461
+ for (const q of statStmts) {
462
+ try {
463
+ await client.command({
464
+ query: q,
465
+ // 26.x defaults the flag on; 25.x wants it stated. Query-scoped, like the
466
+ // text-index flag above.
467
+ clickhouse_settings: { async_insert: 0, allow_experimental_refreshable_materialized_view: 1 },
468
+ });
469
+ } catch (e) {
470
+ const m = e && e.message ? e.message : String(e);
471
+ if (/Not enough privileges|ACCESS_DENIED/i.test(m)) { denied++; continue; }
472
+ if (/ALREADY_EXISTS|already exists/i.test(m)) { continue; }
473
+ throw e;
474
+ }
475
+ }
476
+ // Kick the first population now rather than waiting for the schedule: a refreshable
477
+ // view's first refresh lands at its next interval boundary, and until then the read
478
+ // layer (correctly) ignores the empty table and runs the slow rollup. Best effort —
479
+ // the schedule gets there on its own, this just makes install/update fast at once.
480
+ for (const mv of [`${SESSION_STATS}_mv`, `${MODEL_STATS}_mv`, `${TOOL_STATS}_mv`]) {
481
+ try { await client.command({ query: `SYSTEM REFRESH VIEW ${mv}` }); } catch { /* no grant, or not there */ }
482
+ }
451
483
  if (denied) {
452
484
  console.error(`[memhouse] ${denied} schema statement(s) needed rights you do not hold — continuing with what you can do.`);
453
485
  console.error('[memhouse] creating or replacing a ROOM is the house owner\'s job; adding a missing COLUMN is not.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.16.0",
3
+ "version": "0.17.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": {
@@ -1,131 +0,0 @@
1
- ---
2
- name: ask
3
- description: Answer a question FROM conversation memory — retrieve the relevant past sessions out of the memhouse ClickHouse store, read the actual transcripts, and synthesize an answer with citations. Use when the user asks something their past work already answered, e.g. "how did I fix X last time", "what approach did we settle on for Y", "why did we choose Z", "have I solved this error before", "what did the reviewer say about W". Also answers from a friend's house shared with you when they name it ("how did yigit fix X", "ask yigit's memory"). This is the retrieve-and-answer skill; /mem:search is find-the-session, /mem:sql is run-a-query.
4
- user-invocable: true
5
- argument-hint: "<question about past work>"
6
- allowed-tools: Bash
7
- ---
8
-
9
- # /mem:ask — answer from conversation memory
10
-
11
- Answer the user's question from what the house holds, not from what you assume. The
12
- house is the archive that OUTLIVES the transcripts on disk — Claude Code compacts and
13
- deletes local session files, so for anything older than ~30 days memhouse is the only
14
- place the answer still exists.
15
-
16
- The synthesis is yours; the store is not. memhouse itself runs no LLM anywhere in its
17
- write or read path — this skill retrieves rows and YOU read them. Never present a guess
18
- as a retrieval: if the house has nothing on the question, say so.
19
-
20
- ## Connection
21
-
22
- **Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
23
- MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
24
- transcript that memhouse itself ships into the house.
25
-
26
- Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
27
- (default `~/.memhouse/env`) — the same order the `memhouse` CLI uses:
28
-
29
- ```bash
30
- MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
31
- _u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
32
- set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
33
- [ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
34
- [ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
35
- if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
36
- echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
37
- echo "Run: memhouse install" >&2
38
- exit 1
39
- fi
40
-
41
- curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
42
- --data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-$MEMHOUSE_USER}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
43
- <the query>
44
- FORMAT JSONEachRow
45
- SQL
46
- ```
47
-
48
- `readonly=1` is pinned on every request: this skill never writes.
49
-
50
- ## Answering from another person's house (shared with you)
51
-
52
- The house is a ClickHouse database; a share is a read-only GRANT on it. When the user asks
53
- about a FRIEND's memory — "how did yigit fix X", "in yigit's house", "ask yigit's memory",
54
- "from yigit" — answer from THEIR house without changing your credentials:
55
-
56
- 1. **Resolve the house.** `SHOW DATABASES` returns what your credential may read; a name
57
- that is not `system` / `information_schema` / `default` and not your own `$MEMHOUSE_DB`
58
- is a house shared with you. Match the named person to one; set `HOUSE=yigit`. With no
59
- house named, `HOUSE=$MEMHOUSE_DB`.
60
- 2. **Point the connection at it:** replace `database=${MEMHOUSE_DB:-$MEMHOUSE_USER}` with
61
- `database=<HOUSE>` in the recipe above. The rooms (`messages`, `sessions`) resolve by
62
- the connection's database, so retrieval, the transcript read, and the citation all run
63
- against that house — leave the table names bare.
64
- 3. **Cite the house.** Say whose memory the answer came from ("from yigit's house") so the
65
- user never mistakes a friend's session for their own.
66
- 4. **Read-only, thin by design.** A refused SELECT means the house was not shared with you
67
- (or the share was revoked) — say so; never guess another database.
68
-
69
- ## The method — retrieve, read, answer, cite
70
-
71
- **Escaping, before anything touches SQL.** Search terms, session ids and user ids are
72
- arbitrary text going inside single-quoted literals: escape backslashes then single
73
- quotes (`\` → `\\`, `'` → `\'`) in every value you splice in. A term like `O'Reilly`
74
- otherwise breaks the query — and crafted text could reshape it.
75
-
76
- **1. Extract search terms from the question.** Not the question verbatim — the terms
77
- that would appear in the ANSWER. "How did I fix the ClickHouse auth error?" → search
78
- `ClickHouse auth`, `Authentication failed`, `ACCESS_DENIED` — error strings beat
79
- paraphrases, identifiers beat prose. Run 2–3 term variants; the FTS columns are
80
- lowercased, so lowercase your needle.
81
-
82
- ```sql
83
- SELECT session_id, any(folder) AS folder,
84
- formatDateTime(max(ts), '%Y-%m-%d') AS day, count() AS hits,
85
- substring(anyIf(text, positionCaseInsensitive(text, 'NEEDLE') > 0), 1, 300) AS sample
86
- FROM messages
87
- WHERE text_ngram LIKE '%needle%'
88
- GROUP BY session_id ORDER BY max(ts) DESC LIMIT 8
89
- ```
90
-
91
- **2. Read the strongest sessions — the actual turns, not just the hit line.** Pull a
92
- window around the hits so you see the resolution, not the complaint:
93
-
94
- ```sql
95
- SELECT seq, role, substring(text, 1, 2000) AS text
96
- FROM messages
97
- WHERE session_id = '<sid>' AND user_id = '<uid>'
98
- AND seq BETWEEN <hit_seq - 3> AND <hit_seq + 12>
99
- ORDER BY seq
100
- ```
101
-
102
- Join on `(session_id, user_id)` always — two housemates can collide on an
103
- adapter-local session id.
104
-
105
- **3. Answer, then cite.** Lead with the answer; under it list the sessions it came
106
- from — `session_id`, date, project — so the user can reopen one (`memhouse resume
107
- <session_id>` prints the command). Quote the decisive lines verbatim where the exact
108
- wording matters (error text, a command, a config value).
109
-
110
- **4. Nothing found is an answer too.** Say what you searched and where the house's
111
- coverage ends (`SELECT min(ts), max(ts), count() FROM messages`) rather than padding.
112
-
113
- ## What can bite
114
-
115
- - **Rows can hold more than one PARSE of a session** (`origin`/`epoch` are in the key —
116
- the house retains superseded parses instead of deleting them). Counts and rollups must
117
- filter to the current epoch:
118
- ```sql
119
- WHERE origin != 'ship'
120
- OR (session_id, user_id, epoch) IN (
121
- SELECT session_id, user_id, max(epoch) FROM messages
122
- WHERE origin = 'ship' GROUP BY session_id, user_id)
123
- ```
124
- For step 2's transcript reads, PREFER the filter as well — an unfiltered read of a
125
- compacted session interleaves two parses. Drop it deliberately only when the question
126
- is about what a session said BEFORE it was compacted; the superseded epochs are the
127
- one place that content still exists.
128
- - **`text` is truncated at 50,000 chars** and tool args at 20,000 — a missing tail is
129
- truncation, not absence.
130
- - The rollup (`sessions_v`) is a saved query, not a table — get it with
131
- `memhouse sessions-query` if you need per-session totals.
@@ -1,121 +0,0 @@
1
- ---
2
- name: hello
3
- description: Introduce memhouse — what it is, what it remembers, and how to use it from here. Use when the user asks "what is memhouse", "what can my memory do", "how do I use this", invokes /mem:hello, or seems new to the memory system. Also the right first answer when another /mem skill fails because nothing is configured yet.
4
- user-invocable: true
5
- argument-hint: ""
6
- allowed-tools: Bash
7
- ---
8
-
9
- # /mem:hello — welcome to your memory
10
-
11
- **Invoking this skill IS the request. Give the introduction immediately — grounded in
12
- this machine's actual state, not hypotheticals. Nothing to clarify first.**
13
-
14
- ## First: find out what state this machine is in
15
-
16
- RUN this yourself with the Bash tool — do not print it for the user to run, and do not
17
- show the env values it sources. Read its output, then write the introduction using the
18
- ACTUAL numbers it returns (interpolate them; never emit the literal "N sessions").
19
-
20
- ```bash
21
- MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
22
- _u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
23
- set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
24
- [ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
25
- [ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
26
- if [ -n "${MEMHOUSE_URL:-}" ] && [ -n "${MEMHOUSE_USER:-}" ]; then
27
- curl -sS -m 5 --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
28
- --data-binary "SELECT uniqExact(session_id), count(), formatDateTime(min(ts),'%Y-%m-%d') FROM messages" \
29
- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-$MEMHOUSE_USER}&final=1&readonly=1" 2>&1
30
- else
31
- echo "NOT_CONFIGURED"
32
- fi
33
- ```
34
-
35
- (Do not print the env values, and do not narrate that you are withholding them — just use them.)
36
-
37
- ## What memhouse is — the overview (give this in every state, before the tour)
38
-
39
- One paragraph, grounded not sold: **memhouse is your personal, permanent archive of every
40
- AI coding session.** A background shipper reads the session files your editors write —
41
- Claude Code, Codex, Cursor, VS Code, Zed, OpenCode, Gemini CLI and 10 more, 17 in all —
42
- and ships them into a ClickHouse database you own, called a **house**. YOU choose that
43
- ClickHouse: a local one on your laptop, one on your own VM, or any ClickHouse server you
44
- control — managed or remote. memhouse itself is not a hosted service and phones nothing
45
- home; your sessions go only to the house you point it at, and it runs no LLM in its write
46
- or read path — it stores rows, the agent reading them (this chat) does the thinking. It
47
- outlives the transcripts on disk: editors compact and delete their local session files
48
- after weeks, so for anything older than ~30 days the house is the only place it still
49
- exists. One archive spans every editor, every project, every machine you ship from.
50
-
51
- What it gives you, in one breath: **ask** your own history a question and get a cited
52
- answer, **search** for the session you half-remember, **query** the raw data for numbers,
53
- browse a **dashboard**, and **invite** other people onto your ClickHouse so memories can be
54
- **shared** read-only. The tour below names each.
55
-
56
- ## Then introduce it — three states, three openings
57
-
58
- **Configured and answering** — open with the numbers: "Your memory holds N sessions
59
- (M messages) going back to <date> — every conversation you've had with coding agents
60
- (Claude Code, Codex, Cursor, Gemini CLI and 13 more), across all your machines. It
61
- outlives the transcripts on disk: editors delete their session files after weeks; this
62
- doesn't." Then the tour below.
63
-
64
- **Not configured** — say what memhouse IS (one paragraph: a personal, permanent archive
65
- of every AI coding session, stored in a ClickHouse "house" you own), then how to start:
66
- `memhouse onboard` (wizard) or `memhouse install --env <file>` if someone invited them
67
- (the file came from `/mem:invite` on the inviter's side).
68
-
69
- **Configured but unreachable** — say which URL refused, point at `memhouse doctor`,
70
- stop. Never guess another endpoint.
71
-
72
- ## The tour (keep it to one screen)
73
-
74
- What you can DO from this chat:
75
-
76
- - `/mem:ask <question>` — answer from your past work, with citations ("how did I fix X
77
- last time?")
78
- - `/mem:search <terms>` — find the session ("that conversation about Y")
79
- - `/mem:sessions` — browse and filter what's stored
80
- - `/mem:sql <question or SQL>` — ad-hoc analytics (tokens, costs, rankings)
81
- - `/mem:status` — is it healthy, how much is stored, which machines ship into it
82
-
83
- The first three overlap — same words work in each — so give the reader the rule that
84
- separates them: **want a session → search. Want an answer → ask. Want a number → sql.**
85
- `ask` runs a `search` first, then reads the turns it finds and writes you a cited answer;
86
- `search` stops at the ranked list of sessions; `sql` counts and aggregates the columns
87
- rather than hunting text.
88
-
89
- And around other people — **inviting comes before sharing**:
90
-
91
- - `/mem:invite <name>` — mint a friend's account + their own house on your ClickHouse,
92
- hand them one env file; they install with it and have their own memory
93
- - `/mem:share <name>` — once they exist, open a read-only window into YOUR memory
94
- (`/mem:share revoke <name>` closes it); `/mem:users` shows who's around
95
- - **Read a friend's shared memory** — once they've shared theirs with you, name their
96
- house right in the question: `/mem:ask <question> in <their-house>`, or
97
- `/mem:search <terms> in <their-house>` / `/mem:sql … on <their-house>`. It reads THEIR
98
- house read-only, with your own credentials; `/mem:users` lists the houses shared with
99
- you. (A house is a whole ClickHouse database — sharing grants read on it, not on single
100
- sessions.)
101
-
102
- Also worth naming: `memhouse start` serves a dashboard at http://localhost:4640 for
103
- browsing and analytics outside the chat, and everything here is read-only for agents —
104
- only the shipper writes.
105
-
106
- **Freshness check** — quietly compare the installed client to the latest release; mention
107
- it ONLY if there's a real gap (don't clutter the welcome otherwise):
108
-
109
- ```bash
110
- INSTALLED=$(memhouse version 2>/dev/null | head -1 | tr -d '[:space:]')
111
- LATEST=$(curl -sS -m 5 https://registry.npmjs.org/memhouse/latest 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
112
- echo "installed=${INSTALLED:-unknown} latest=${LATEST:-unknown}"
113
- ```
114
-
115
- If the installed version is a MINOR or MAJOR behind (or many patches), add one line:
116
- "heads-up — you're on 0.A.B, 0.C.D is out; `memhouse update` upgrades it and refreshes
117
- these `/mem:*` skills." A patch or two behind isn't worth mentioning here. Never run the
118
- update yourself.
119
-
120
- End by offering one concrete action: a search for something they mentioned recently, or
121
- `/mem:status` if they seem unsure it's working. One offer, not a menu.
@@ -1,130 +0,0 @@
1
- ---
2
- name: invite
3
- description: Invite another person onto this memhouse ClickHouse — mint their user and their own house (database) with the right grants, producing the one env file their install needs. Use when the user says "invite X", "add X to memhouse", "get my friend set up". Inviting comes BEFORE sharing — /mem:share can only grant a user who exists.
4
- user-invocable: true
5
- argument-hint: "<name> [into <db>]"
6
- allowed-tools: Bash
7
- ---
8
-
9
- # /mem:invite — mint a member and hand them one file
10
-
11
- This runs the `memhouse invite` CLI and nothing else. Inviting mints a ClickHouse user
12
- and a house, which takes an ADMIN credential — and an admin password must never enter
13
- this conversation, because memhouse ships the transcript into the house it just
14
- provisioned. The CLI is the only safe path: the admin password is passed to one process
15
- and never echoed. **Do not hand-run `CREATE USER` / `GRANT` with an admin password
16
- substituted into a command — that burns the credential into the transcript on disk.**
17
-
18
- ## Do this
19
-
20
- 1. Confirm the `memhouse` binary is present: `command -v memhouse`. If it is not, tell
21
- the user to install it first (`npm install -g memhouse`) — there is no in-skill
22
- fallback, on purpose (see above).
23
-
24
- 2. Gather ONLY two things by asking:
25
- - the invitee's **name** (validate `[A-Za-z][A-Za-z0-9_]*`; refuse otherwise)
26
- - the **URL the invitee will reach the house at** — NOT localhost (that is the
27
- invitee's own machine). A LAN IP, hostname, or tunnel.
28
-
29
- Do NOT ask for an admin user or password yet — the next step usually needs neither.
30
- Only ask about a **shared house** if the user brings one up; default is the invitee's
31
- own house (omit --db; never pass `--db default` for "the default house").
32
-
33
- 3. TRY IT BARE FIRST — run this via the Bash tool, from the directory the file should
34
- land in (step 4). `memhouse invite` uses the caller's OWN configured credential when
35
- it can manage users, which is the common case for whoever set the house up, so no
36
- admin is needed:
37
-
38
- ```
39
- memhouse invite <name> --url <url> [--db <shared-house>]
40
- ```
41
-
42
- If it prints `invite written: …`, go to step 4 — you are done, and no admin secret
43
- ever touched this conversation.
44
-
45
- Otherwise read WHICH refusal came back; they need different answers.
46
-
47
- **"cannot reach `<url>` …"** — nothing to do with privileges. The address is wrong,
48
- or the house is bound to loopback (`memhouse deploy --local` binds to 127.0.0.1 on
49
- purpose, so no LAN address reaches it and no invitee could either). Ask the user for
50
- the address the invitee will actually use; do not retry with a guess.
51
-
52
- **"`<user>` is a MEMBER of this ClickHouse, not an administrator"** — inviting needs
53
- CREATE USER and CREATE DATABASE, which a member deliberately does not hold. Two cases,
54
- and you must ask which one applies rather than assuming:
55
-
56
- - **They run the ClickHouse.** They have the admin credential; memhouse never stores
57
- it. Do NOT take that password in chat — memhouse archives this transcript. Hand them
58
- the line to run THEMSELVES, and leave `--admin-password` OFF so it is prompted for,
59
- keeping it out of both the process list and shell history:
60
-
61
- ```
62
- memhouse invite <name> --url <url> [--db <shared-house>] --admin-user <admin>
63
- ```
64
-
65
- **You cannot run that line yourself.** The prompt needs a TTY and your Bash tool has
66
- none, so it refuses with "no TTY to prompt on" — and the fix is NOT to add
67
- `--admin-password`, which would put the credential in this transcript. The user runs
68
- it in their own terminal; you wait for them to say it is done.
69
-
70
- - **Somebody else runs it.** They cannot invite, and no flag changes that. Print the
71
- statements for whoever administers the server — this contacts nothing and needs no
72
- credential:
73
-
74
- ```
75
- memhouse invite <name> --url <url> [--db <shared-house>] --print-sql
76
- ```
77
-
78
- The output ends with the four `MEMHOUSE_*` lines the invitee needs. Those lines are
79
- a credential: tell the user to send them over a trusted channel, and **never print
80
- them into the conversation yourself**.
81
-
82
- **If it refuses with "house `<db>` already exists and holds N messages", STOP.** That
83
- house is somebody's memory, and inviting into it would hand the invitee someone else's
84
- sessions. Do not pass `--adopt` on your own initiative — report the row count and the
85
- writer count to the user and let them choose: a different handle, a different `--db`,
86
- or `--adopt` if sharing that house is genuinely what they want.
87
-
88
- 4. **Put the file where the user can attach it.** The CLI writes `invite-<name>.env`
89
- into the CURRENT directory, and the user has to send it by hand — so run the command
90
- from their downloads directory (on macOS and most Linux desktops `~/Downloads`), or
91
- move it there afterwards. Never leave it in a git repo: it is a password, and
92
- `invite-*.env` is only gitignored in this repo, not in whatever checkout the user
93
- happened to be standing in.
94
-
95
- Confirm it is there and still mode `600`, and say the full path. **Never print the
96
- file's contents** — the password inside would land in a transcript memhouse ships.
97
-
98
- 5. **Say whether the house was fresh.** Run
99
- `memhouse stats` or query `SELECT count() FROM <db>.messages` and tell the user
100
- plainly: a brand-new house reads 0. This is a second line of defence behind step 3's
101
- refusal, and it is how the user learns the invitee is starting clean.
102
-
103
- 6. **Write the message the user will send.** They are about to hand over a file plus an
104
- explanation, so compose it for them rather than making them write it. Put it in a
105
- fenced block they can copy whole, and cover:
106
- - what memhouse is, in a sentence or two — the invitee may never have heard of it
107
- - that the attached `invite-<name>.env` IS a password: don't forward it, don't paste
108
- it into chat
109
- - `npm install -g memhouse` (Node 24+, no flags, nothing compiles), then
110
- `memhouse install --env invite-<name>.env`
111
- - that install OFFERS to change the password to one only they know, then deletes the
112
- file — so they should say yes, because the inviter knows the current one
113
- - `memhouse onboard` to start shipping, `memhouse status` to check it worked
114
- - that their house is theirs alone; the inviter cannot read it unless they run
115
- `/mem:share <inviter>`
116
-
117
- If the invitee is REPLACING an existing setup (they were shipping under another
118
- credential), add `memhouse ship --full` after the install — a plain incremental pass
119
- skips sessions the new house has no record of, so their history would not follow them.
120
-
121
- Then: once installed, sharing works both ways — `/mem:share <name>`.
122
-
123
- ## Never
124
-
125
- Never create a ClickHouse user by running raw SQL with an admin password you were given
126
- in chat. Never invent an admin credential. Never suggest granting beyond what `memhouse
127
- invite` does. If the binary is absent, stop and say so — do not improvise the SQL.
128
-
129
- Never print the invite file's contents, and never pass `--adopt` unless the user asked
130
- for it after being told what is already in that house.