memhouse 0.9.0 → 0.11.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 +39 -20
- package/bin/memhouse.js +969 -64
- package/memhouse/DESIGN.md +37 -12
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +10 -2
- package/memhouse/delivery/AGENT-INSTALL.md +41 -19
- package/memhouse/delivery/PROMPT.md +37 -13
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +14 -5
- package/memhouse/delivery/plugin/skills/ask/SKILL.md +112 -0
- package/memhouse/delivery/plugin/skills/hello/SKILL.md +76 -0
- package/memhouse/delivery/plugin/skills/invite/SKILL.md +65 -0
- package/memhouse/delivery/plugin/skills/search/SKILL.md +1 -1
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +3 -3
- package/memhouse/delivery/plugin/skills/share/SKILL.md +130 -0
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +34 -11
- package/memhouse/delivery/plugin/skills/status/SKILL.md +114 -0
- package/memhouse/delivery/plugin/skills/users/SKILL.md +97 -0
- package/memhouse/house/HOUSE.md +40 -5
- package/memhouse/house/house.js +180 -4
- package/memhouse/house/migrate.js +277 -0
- package/memhouse/house/migrations/0100-epoch-key.js +68 -0
- package/memhouse/house/schema.sql.tpl +96 -25
- package/memhouse/service.js +7 -2
- package/memhouse/shipper/ship.js +464 -123
- package/package.json +2 -1
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: share
|
|
3
|
+
description: Share your conversation memory with another user on the same ClickHouse — grant them read-only access to your house, list who can currently read it, or revoke a share. Use when the user says "share my memory with X", "let X see my sessions", "who can read my memory", "stop sharing with X". Read-only by construction — a share can never let anyone write to or delete your memory.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: "[<user> | revoke <user> | (nothing: list current shares)]"
|
|
6
|
+
allowed-tools: Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /mem:share — a read-only window into your memory
|
|
10
|
+
|
|
11
|
+
Three forms, decided by the arguments:
|
|
12
|
+
|
|
13
|
+
- `/mem:share yigit` — let `yigit` read this house
|
|
14
|
+
- `/mem:share revoke yigit` — close that window
|
|
15
|
+
- `/mem:share` — who can read this house right now
|
|
16
|
+
|
|
17
|
+
Sharing is a ClickHouse GRANT and nothing else — memhouse adds no layer on top, so what
|
|
18
|
+
you grant is exactly what they get: `SELECT`, never write, never delete. Your own grant
|
|
19
|
+
carries `WITH GRANT OPTION` on SELECT only (installs from 0.10.0 on), so you can open
|
|
20
|
+
this window yourself and can hand on nothing more.
|
|
21
|
+
|
|
22
|
+
## Connection
|
|
23
|
+
|
|
24
|
+
**Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
|
|
25
|
+
MEMHOUSE`, no `set -x` around these commands.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
|
|
29
|
+
_u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
|
|
30
|
+
set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
|
|
31
|
+
[ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
|
|
32
|
+
[ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
|
|
33
|
+
if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
|
|
34
|
+
echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
|
|
35
|
+
echo "Run: memhouse install" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# NO readonly=1 on this skill's grant/revoke calls — GRANT is refused under readonly.
|
|
40
|
+
# The list query below still pins it.
|
|
41
|
+
curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
|
|
42
|
+
--data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}" <<'SQL'
|
|
43
|
+
<the statement>
|
|
44
|
+
SQL
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Grant — `/mem:share <user>`
|
|
48
|
+
|
|
49
|
+
The user name goes into SQL unquoted: **refuse anything not matching
|
|
50
|
+
`[A-Za-z][A-Za-z0-9_]*`** rather than quoting it — a name you cannot type bare is a name
|
|
51
|
+
that will be gotten wrong everywhere else too.
|
|
52
|
+
|
|
53
|
+
**Validate `<user>` FIRST, once, and use only the validated token — in the GRANT, the
|
|
54
|
+
REVOKE, and the house_meta record alike.** The record INSERT is a quoted literal;
|
|
55
|
+
anything that has not passed the name check must never reach it.
|
|
56
|
+
|
|
57
|
+
**`<db>` below is the LIVE value of `$MEMHOUSE_DB` — print it with the connection
|
|
58
|
+
(`echo "house: $MEMHOUSE_DB"`) and use THAT in every statement and every piece of
|
|
59
|
+
advice.** Driven live, a model filled `<db>` in the admin advice with the literal
|
|
60
|
+
fallback `mem` while the real house was named differently — advice the admin would have
|
|
61
|
+
run against the wrong database.
|
|
62
|
+
|
|
63
|
+
```sql
|
|
64
|
+
GRANT SELECT ON <db>.* TO <user>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
(`<db>` is `$MEMHOUSE_DB`.) Then RECORD it in the house's own memory — a member cannot
|
|
68
|
+
read `system.grants` (measured: `Not enough privileges … SELECT ON system.grants`), so
|
|
69
|
+
this record is what the list form reads back:
|
|
70
|
+
|
|
71
|
+
```sql
|
|
72
|
+
INSERT INTO house_meta (key, value) VALUES ('share:<user>', 'granted <today, YYYY-MM-DD>')
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Verify the grant took by USING it if you can, or simply report the server accepted it —
|
|
76
|
+
do not call `SHOW GRANTS FOR <user>` (it needs privileges a member does not hold).
|
|
77
|
+
|
|
78
|
+
Two failure shapes, both to report exactly:
|
|
79
|
+
|
|
80
|
+
- `There is no user ... in user directories` — the person has no ClickHouse user on this
|
|
81
|
+
server yet. Joining comes first: they run `memhouse install` against this server (or
|
|
82
|
+
the admin runs `memhouse install --print-sql --member <user> --db <their-house>`).
|
|
83
|
+
A share cannot create a user, by design.
|
|
84
|
+
- `Not enough privileges` — either this house predates 0.10.0 (your own grant carries
|
|
85
|
+
no grant option), or **the configured house is not yours to share** (a team house, or
|
|
86
|
+
someone else's). Only suggest the admin remedy when the house is the user's OWN — the
|
|
87
|
+
grant-option statement hands them standing power to share it, which is the owner's
|
|
88
|
+
decision, not a fix. For their own pre-0.10 house, print for the ADMIN:
|
|
89
|
+
`GRANT SELECT ON <db>.* TO <user>` — and to make future shares self-serve:
|
|
90
|
+
`GRANT SELECT ON <db>.* TO <your-user> WITH GRANT OPTION`.
|
|
91
|
+
For a house that is not theirs, say so and stop: the owner shares it, not you.
|
|
92
|
+
|
|
93
|
+
Tell the user how their friend actually reads it: qualified names —
|
|
94
|
+
`SELECT … FROM <db>.messages …` — from /mem:ask, /mem:search or /mem:sql on the friend's
|
|
95
|
+
side, with the same epoch filter those skills already carry.
|
|
96
|
+
|
|
97
|
+
## Revoke — `/mem:share revoke <user>`
|
|
98
|
+
|
|
99
|
+
```sql
|
|
100
|
+
REVOKE SELECT ON <db>.* FROM <user>
|
|
101
|
+
```
|
|
102
|
+
```sql
|
|
103
|
+
INSERT INTO house_meta (key, value) VALUES ('share:<user>', 'revoked <today, YYYY-MM-DD>')
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Revoking a share that never existed is a no-op, not an error — say which it was (the
|
|
107
|
+
`share:<user>` record answers that).
|
|
108
|
+
|
|
109
|
+
## List — `/mem:share` (no arguments)
|
|
110
|
+
|
|
111
|
+
From the house's own record (append `&readonly=1` to the URL for this one):
|
|
112
|
+
|
|
113
|
+
```sql
|
|
114
|
+
SELECT substring(key, 7) AS user, value AS state,
|
|
115
|
+
formatDateTime(updated_at, '%Y-%m-%d %H:%i') AS at
|
|
116
|
+
FROM house_meta FINAL WHERE key LIKE 'share:%' ORDER BY key
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Latest-wins per user: `granted …` rows are live shares, `revoked …` rows are closed
|
|
120
|
+
ones. Be honest about the record's edge: it shows shares made THROUGH THIS SKILL — a
|
|
121
|
+
grant the admin issued directly will not appear. The authoritative list is the admin's
|
|
122
|
+
one query: `SELECT user_name, access_type FROM system.grants WHERE database = '<db>'`
|
|
123
|
+
(members cannot run it; `system.grants` needs privileges a member does not hold).
|
|
124
|
+
|
|
125
|
+
## What this never does
|
|
126
|
+
|
|
127
|
+
No `GRANT ALL`, no write grants, no `WITH GRANT OPTION` onward, no user creation, no
|
|
128
|
+
grants on any database but your own. If the user asks for more than read access, that is
|
|
129
|
+
the shared-house model (both members in ONE database) — a decision, not a grant; point
|
|
130
|
+
them at `memhouse install --db <shared>` and stop.
|
|
@@ -6,17 +6,36 @@ argument-hint: "<question or SQL>"
|
|
|
6
6
|
allowed-tools: Bash
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
-
# /
|
|
9
|
+
# /mem:sql — ad-hoc analytics
|
|
10
10
|
|
|
11
11
|
If the user gives SQL, run it (append `FORMAT PrettyCompact` if no FORMAT
|
|
12
12
|
given). If they give a question, write the SQL yourself from the schema below.
|
|
13
13
|
|
|
14
|
-
**Read-only rule:** the shipper (`ship.js`) is the only writer
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
14
|
+
**Read-only rule:** the shipper (`ship.js`) is the only writer, and it is INSERT-ONLY: it
|
|
15
|
+
never deletes and never mutates, because a re-parse that shrank or was rewritten is
|
|
16
|
+
written under a new `epoch` instead of over the stored one. Never INSERT/ALTER/DROP from
|
|
17
|
+
here. The shipping path needs `SELECT, INSERT, ALTER ADD COLUMN, OPTIMIZE` and nothing
|
|
18
|
+
more — the `ALTER DELETE` this line used to list was there for the per-session clear that
|
|
19
|
+
no longer exists, so no unattended process can lose you a row. `ALTER DELETE` is still
|
|
20
|
+
needed by the one command that is *meant* to remove rows, `memhouse reset`, which asks
|
|
21
|
+
first. Most houses grant `ALL` on the database anyway. Reads need no scoping clause: the
|
|
22
|
+
house you can name is already yours, and there is no policy to work around.
|
|
23
|
+
|
|
24
|
+
**One session can be in `messages` twice.** A session that Claude Code compacted, or that
|
|
25
|
+
shrank for any other reason, keeps its earlier parse — that is the whole point of `epoch`,
|
|
26
|
+
and it is why nothing is ever deleted. Ad-hoc SQL must filter to the current parse or it
|
|
27
|
+
counts such a session twice:
|
|
28
|
+
|
|
29
|
+
```sql
|
|
30
|
+
WHERE origin != 'ship'
|
|
31
|
+
OR (session_id, user_id, epoch) IN (
|
|
32
|
+
SELECT session_id, user_id, max(epoch) FROM messages
|
|
33
|
+
WHERE origin = 'ship' GROUP BY session_id, user_id)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Drop the filter deliberately when you want the history — "what did this session say before
|
|
37
|
+
it was compacted" is a question only the house can answer, because the transcript on disk
|
|
38
|
+
is gone.
|
|
20
39
|
|
|
21
40
|
The recipe below pins `readonly=1` on every request, so a write that slips past the rule
|
|
22
41
|
is refused by the server (`Code: 164 … Cannot execute query in readonly mode`) rather
|
|
@@ -115,15 +134,19 @@ DIFFERENT memhouse version are the two ways this goes quietly wrong.
|
|
|
115
134
|
|
|
116
135
|
| Object | Kind | Columns |
|
|
117
136
|
|---|---|---|
|
|
118
|
-
| `sessions` | table, 1 row/session | `session_id, source, host, name, mode, folder, project, git_branch, created_at, last_updated_at, message_count, path, extra JSON, user_id, ingested_at` |
|
|
119
|
-
| `messages` | table, 1 row/message | `session_id, seq, source, host, ts, role, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, text, project, folder, is_subagent, extra JSON, line_hash, user_id, ingested_at` + FTS columns `text_ngram`/`text_word` (lowercased; see /
|
|
120
|
-
| `tool_calls` | table, 1 row/tool call | `session_id, seq, idx, source, host, tool_name, args, ts, project, folder, user_id, ingested_at` |
|
|
137
|
+
| `sessions` | table, 1 row/session | `session_id, source, host, name, mode, folder, project, git_branch, created_at, last_updated_at, message_count, path, extra JSON, origin, epoch, user_id, ingested_at` |
|
|
138
|
+
| `messages` | table, 1 row/message **per parse** | `session_id, seq, source, host, ts, role, model, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, text, project, folder, is_subagent, extra JSON, line_hash, origin, epoch, user_id, ingested_at` + FTS columns `text_ngram`/`text_word` (lowercased; see /mem:search) |
|
|
139
|
+
| `tool_calls` | table, 1 row/tool call **per parse** | `session_id, seq, idx, source, host, tool_name, args, ts, project, folder, origin, epoch, user_id, ingested_at` |
|
|
140
|
+
| `house_meta` | table, house's own record | `key, value, updated_at, updated_by, host` — `schema_version`, per-member `client_version:<user>` |
|
|
141
|
+
| `house_events` | append-only log | `event_at, kind, id, status, from_version, to_version, actor, host, rows_before, rows_after, detail` — migrations, version changes |
|
|
121
142
|
| the rollup | **saved query**, not an object — `memhouse sessions-query` prints it | `session_id, source, host, name, mode, folder, project, git_branch, user_id, created_at, last_updated_at, started, ended, duration_sec, total_msgs, user_msgs, assistant_msgs, subagent_msgs, models, input_tokens, output_tokens, cache_read_tokens, cache_write_tokens, user_chars, assistant_chars, first_prompt` |
|
|
122
143
|
|
|
123
144
|
Notes: `source` = editor id (`claude-code`, `codex`, `cursor`, `cursor-agent`,
|
|
124
145
|
`vscode`, `zed`, `opencode`, `gemini-cli`, `windsurf`, `antigravity`, …);
|
|
125
146
|
`seq` = message index within its session (0-based); `tool_calls.idx` = call
|
|
126
|
-
index within the session; `user_id` is server-stamped (`currentUser()`);
|
|
147
|
+
index within the session; `user_id` is server-stamped (`currentUser()`); `origin` is
|
|
148
|
+
`ship` for rows the shipper wrote and anything else for imports; `epoch` is which parse
|
|
149
|
+
of the session a row belongs to — see the filter above;
|
|
127
150
|
`models` in the rollup is an `Array(String)`. Exclude `''` and
|
|
128
151
|
`'<synthetic>'` from model aggregates.
|
|
129
152
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: status
|
|
3
|
+
description: Show the state of the memory system — what the house holds (sessions, messages, tokens per editor), how fresh it is, its schema generation and migration state, and every machine writing into it. Use when the user asks "is my memory working", "how much is stored", "when did it last ship", "what's in the house", "which machines are shipping", or before debugging why a search found nothing.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: ""
|
|
6
|
+
allowed-tools: Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /mem:status — the state of the memory system
|
|
10
|
+
|
|
11
|
+
**Invoking this skill IS the request. Run the report immediately — connect, run the four
|
|
12
|
+
sections below, and present the summary. There are no arguments and nothing to clarify;
|
|
13
|
+
do not describe this skill or ask what to check.** (Driven live, a model did exactly
|
|
14
|
+
that: it summarized the file and asked "is there something specific?" — a status command
|
|
15
|
+
that answers with a question has failed.)
|
|
16
|
+
|
|
17
|
+
One report, four sections: what the house holds, how fresh it is, what generation it is
|
|
18
|
+
at, and who writes into it. Read-only; nothing here changes anything.
|
|
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
|
+
```bash
|
|
27
|
+
MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
|
|
28
|
+
_u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
|
|
29
|
+
set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
|
|
30
|
+
[ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
|
|
31
|
+
[ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
|
|
32
|
+
if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
|
|
33
|
+
echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
|
|
34
|
+
echo "Run: memhouse install" >&2
|
|
35
|
+
exit 1
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
|
|
39
|
+
--data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
|
|
40
|
+
<the query>
|
|
41
|
+
FORMAT PrettyCompact
|
|
42
|
+
SQL
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## The report — run these, then summarize
|
|
46
|
+
|
|
47
|
+
**1. What the house holds** — per editor, CURRENT parses only (the rooms retain
|
|
48
|
+
superseded parses; an unfiltered count over-reports on any compacted session):
|
|
49
|
+
|
|
50
|
+
```sql
|
|
51
|
+
SELECT source,
|
|
52
|
+
uniqExact(session_id) AS sessions,
|
|
53
|
+
count() AS messages,
|
|
54
|
+
sum(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) AS tokens
|
|
55
|
+
FROM messages
|
|
56
|
+
WHERE origin != 'ship'
|
|
57
|
+
OR (session_id, user_id, epoch) IN (
|
|
58
|
+
SELECT session_id, user_id, max(epoch) FROM messages
|
|
59
|
+
WHERE origin = 'ship' GROUP BY session_id, user_id)
|
|
60
|
+
GROUP BY source ORDER BY sessions DESC
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
**2. Freshness and coverage** — when the archive starts, ends, and last received:
|
|
64
|
+
|
|
65
|
+
```sql
|
|
66
|
+
SELECT formatDateTime(min(ts), '%Y-%m-%d') AS oldest,
|
|
67
|
+
formatDateTime(max(ts), '%Y-%m-%d %H:%i') AS newest,
|
|
68
|
+
formatDateTime(max(ingested_at), '%Y-%m-%d %H:%i') AS last_ingest
|
|
69
|
+
FROM messages
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A `last_ingest` hours old with the user actively working means the shipper is not
|
|
73
|
+
running — say so and point at `memhouse status` / `memhouse start` on that machine.
|
|
74
|
+
|
|
75
|
+
**3. Generation and migration state** — the house's record of itself (absent on a
|
|
76
|
+
pre-0.10 house; that absence is itself the answer: say the house predates the record):
|
|
77
|
+
|
|
78
|
+
```sql
|
|
79
|
+
SELECT key, value FROM house_meta FINAL
|
|
80
|
+
WHERE key IN ('schema_version', 'min_writer_schema') ORDER BY key
|
|
81
|
+
```
|
|
82
|
+
```sql
|
|
83
|
+
SELECT id, argMax(status, event_at) AS status,
|
|
84
|
+
formatDateTime(max(event_at), '%Y-%m-%d %H:%i') AS at
|
|
85
|
+
FROM house_events WHERE kind = 'migration' GROUP BY id ORDER BY at DESC LIMIT 3
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A `pending` here with nothing running means an interrupted migration — name
|
|
89
|
+
`memhouse migrate` (it refuses stale leftovers legibly rather than clobbering them).
|
|
90
|
+
|
|
91
|
+
**4. The fleet** — every machine writing into this house, from the DATA (a pre-0.10
|
|
92
|
+
writer records nothing about itself, so rows-without-record is the finding):
|
|
93
|
+
|
|
94
|
+
```sql
|
|
95
|
+
SELECT user_id, host, count() AS rows,
|
|
96
|
+
formatDateTime(max(ingested_at), '%Y-%m-%d %H:%i') AS last_write
|
|
97
|
+
FROM messages GROUP BY user_id, host ORDER BY last_write DESC
|
|
98
|
+
```
|
|
99
|
+
```sql
|
|
100
|
+
SELECT key, value FROM house_meta FINAL
|
|
101
|
+
WHERE key LIKE 'client_version:%' OR key LIKE 'client_schema:%' OR key LIKE 'last_ship:%'
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Match `client_*:<member>@<host>` against the data rows. A `(user_id, host)` pair with
|
|
105
|
+
rows but NO `client_*` record is a **pre-0.10 memhouse still writing** — flag it: its
|
|
106
|
+
re-ships delete retained parses, and the fixes are upgrade it or (as admin)
|
|
107
|
+
`REVOKE ALTER DELETE, ALTER UPDATE ON <db>.* FROM <member>`.
|
|
108
|
+
|
|
109
|
+
## Presenting it
|
|
110
|
+
|
|
111
|
+
Lead with one line of verdict ("healthy: N sessions across M editors, last ingest X
|
|
112
|
+
minutes ago" — or the one thing wrong). Then the per-editor table, then anything from
|
|
113
|
+
sections 3–4 that needs action. Skip empty sections; do not pad. If the house is
|
|
114
|
+
unreachable, say which URL refused and stop — never guess another endpoint.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: users
|
|
3
|
+
description: Show the people around your memory — who writes into your house and from which machines, which other houses on this ClickHouse you can read, and who can read yours. Use when the user asks "who can see my memory", "whose memory can I see", "which houses are on this server", "who else ships here", or before/after sharing with /mem:share.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: ""
|
|
6
|
+
allowed-tools: Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /mem:users — who is around your memory
|
|
10
|
+
|
|
11
|
+
**Invoking this skill IS the request. Run the report immediately and present it; there
|
|
12
|
+
are no arguments and nothing to clarify.** Read-only throughout.
|
|
13
|
+
|
|
14
|
+
Throughout, `<your-db>` is the live `$MEMHOUSE_DB` and `<your-user>` the live
|
|
15
|
+
`$MEMHOUSE_USER` — take both from the connection environment, never from the
|
|
16
|
+
conversation or from examples.
|
|
17
|
+
|
|
18
|
+
Answers three questions, best-effort by design: ClickHouse shows a credential only what
|
|
19
|
+
it may see, so on someone else's server some sections legitimately come back thin — say
|
|
20
|
+
what could not be seen rather than guessing.
|
|
21
|
+
|
|
22
|
+
## Connection
|
|
23
|
+
|
|
24
|
+
**Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
|
|
25
|
+
MEMHOUSE`, no `set -x` around these commands.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
|
|
29
|
+
_u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
|
|
30
|
+
set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
|
|
31
|
+
[ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
|
|
32
|
+
[ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
|
|
33
|
+
if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
|
|
34
|
+
echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
|
|
35
|
+
echo "Run: memhouse install" >&2
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
|
|
40
|
+
--data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}&readonly=1" <<'SQL'
|
|
41
|
+
<the query>
|
|
42
|
+
FORMAT PrettyCompact
|
|
43
|
+
SQL
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The report
|
|
47
|
+
|
|
48
|
+
**1. Who writes into YOUR house** — members × machines, from the data (same ground truth
|
|
49
|
+
as /mem:status's fleet):
|
|
50
|
+
|
|
51
|
+
```sql
|
|
52
|
+
SELECT user_id AS member, host AS machine, count() AS rows,
|
|
53
|
+
formatDateTime(max(ingested_at), '%Y-%m-%d %H:%i') AS last_write
|
|
54
|
+
FROM messages GROUP BY user_id, host ORDER BY last_write DESC
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**2. Whose memory YOU can read** — every database your credential reaches is one someone
|
|
58
|
+
gave you (or your own):
|
|
59
|
+
|
|
60
|
+
```sql
|
|
61
|
+
SHOW DATABASES
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Drop `system`, `information_schema`, `INFORMATION_SCHEMA`, `default` unless they hold
|
|
65
|
+
rooms. For each OTHER house listed, it is readable because of a grant — one line each:
|
|
66
|
+
`house 'polat' — readable (shared with you); query it as polat.messages`.
|
|
67
|
+
|
|
68
|
+
**3. Who can read YOURS** — from the house's own share record (`system.grants` needs
|
|
69
|
+
privileges a member does not hold, so the record /mem:share keeps is the readable
|
|
70
|
+
source; an admin-issued grant will not appear in it, and the report should say so):
|
|
71
|
+
|
|
72
|
+
```sql
|
|
73
|
+
SELECT substring(key, 7) AS user, value AS state
|
|
74
|
+
FROM house_meta FINAL WHERE key LIKE 'share:%' ORDER BY key
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`granted …` = a live read window (`/mem:share`); `revoked …` = closed. If the caller IS
|
|
78
|
+
privileged, prefer the authoritative form:
|
|
79
|
+
`SELECT user_name, groupUniqArray(access_type) FROM system.grants WHERE database = '<your-db>' AND user_name != '<your-user>' GROUP BY user_name`
|
|
80
|
+
— try it first, fall back to the record on refusal.
|
|
81
|
+
|
|
82
|
+
**4. Every user on the server** — usually admin-only; try it, and if refused say
|
|
83
|
+
"(full user list needs admin — showing only what this credential can see)":
|
|
84
|
+
|
|
85
|
+
```sql
|
|
86
|
+
SELECT name FROM system.users ORDER BY name
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Presenting it
|
|
90
|
+
|
|
91
|
+
Four short sections in that order, each with its one-line meaning ("2 machines ship into
|
|
92
|
+
your house", "you can read polat's memory", "yigit can read yours — read-only", …).
|
|
93
|
+
Empty section three means "no shares made through /mem:share" — and ONLY that. The
|
|
94
|
+
record cannot see an admin-issued grant, so never promise "nobody can read your memory"
|
|
95
|
+
from an empty ledger; say "no shares recorded through this skill — the authoritative
|
|
96
|
+
check is the admin's system.grants query" and print it. Point at `/mem:share <user>` /
|
|
97
|
+
`/mem:share revoke <user>` for changes.
|
package/memhouse/house/HOUSE.md
CHANGED
|
@@ -42,9 +42,9 @@ all — but when it does (the same `~/.claude` on two synced machines), both shi
|
|
|
42
42
|
the same `(session_id, user_id)`. The keys deliberately do NOT include `host`, so the
|
|
43
43
|
rows collapse to the LAST shipper's copy: one logical session stays one session, with
|
|
44
44
|
`host` recording who shipped it most recently. Keying on host instead would duplicate
|
|
45
|
-
every such session and double what the rollup counts.
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
every such session and double what the rollup counts. Two shippers interleaving can no
|
|
46
|
+
longer drop rows — neither of them deletes anything — but they can disagree about the
|
|
47
|
+
epoch and fork a session that did not change; the next pass settles it. Truly parallel
|
|
48
48
|
shippers per user are the deferred "ephemeral fingerprint" expert feature, not today's.
|
|
49
49
|
|
|
50
50
|
## The rollup is a saved query, not an object
|
|
@@ -69,6 +69,41 @@ product does not have:
|
|
|
69
69
|
housemates are collaborators, and groups that are not do not share a database.
|
|
70
70
|
|
|
71
71
|
The origin story that survives: rows carry `origin` (in the messages/tool_calls sorting
|
|
72
|
-
keys, deliberately NOT in sessions'),
|
|
73
|
-
|
|
72
|
+
keys, deliberately NOT in sessions'), so ReplacingMergeTree cannot collapse an imported
|
|
73
|
+
row against a shipped one and imported history survives every re-ship.
|
|
74
|
+
|
|
75
|
+
## One session, more than one parse
|
|
76
|
+
|
|
77
|
+
`messages` and `tool_calls` also key on `epoch`. The shipper is insert-only: when a
|
|
78
|
+
re-parse is SHORTER than what the house holds, or differs from it at a `seq` the house
|
|
79
|
+
already has, it writes the new parse under `epoch + 1` and leaves the old one intact.
|
|
80
|
+
Both are ordinary — Claude Code compacts a transcript in place and deletes it after
|
|
81
|
+
`cleanupPeriodDays` (30 by default), so the house is routinely the only surviving copy.
|
|
82
|
+
An unchanged re-ship reuses the epoch and costs nothing.
|
|
83
|
+
|
|
84
|
+
Reads must therefore take the newest epoch per `(session_id, user_id)` for `origin='ship'`
|
|
85
|
+
rows and leave every other origin alone (an import sits at epoch 0 forever). Nobody writes
|
|
86
|
+
that filter by hand: `roomNames()` resolves `messages` and `tool_calls` to a subquery that
|
|
87
|
+
already applies it, and hands out the bare tables only as `messages_raw` / `tool_calls_raw`
|
|
88
|
+
for writes and DDL.
|
|
89
|
+
|
|
90
|
+
## The house's own record
|
|
91
|
+
|
|
92
|
+
Two tables that are not rooms:
|
|
93
|
+
|
|
94
|
+
- `house_meta` — house-wide key/value, latest-wins: `schema_version`, and the memhouse
|
|
95
|
+
version each member last shipped with.
|
|
96
|
+
- `house_events` — append-only: migrations (`pending` → `applied` | `failed`), version
|
|
97
|
+
changes, schema observations, each with actor, host and row counts. Nothing is updated
|
|
98
|
+
in place, so a migration that failed and was retried reads as exactly that.
|
|
99
|
+
|
|
100
|
+
What lives there is what `system.tables` and `system.users` cannot say — intent, sequence,
|
|
101
|
+
outcome. Which rooms and users exist is read live from `system.*`, never mirrored.
|
|
102
|
+
|
|
103
|
+
Sorting keys cannot be altered, so a schema generation change is a room rebuild:
|
|
104
|
+
`memhouse migrate-rooms` copies each room into one with the current key, swaps it in with
|
|
105
|
+
a single `RENAME`, and keeps the old room as `<room>_pre_epoch`. It deletes nothing and
|
|
106
|
+
carries `user_id` across explicitly, so a housemate's rows are not restamped as the
|
|
107
|
+
migrator's.
|
|
108
|
+
|
|
74
109
|
See the comments in `schema.sql.tpl` — they are the authority on the keys.
|