memhouse 0.4.4 → 0.7.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.
Files changed (63) hide show
  1. package/README.md +244 -114
  2. package/bin/memhouse.js +1162 -230
  3. package/editors/base.js +6 -0
  4. package/editors/claude.js +44 -9
  5. package/editors/goose.js +4 -0
  6. package/editors/index.js +0 -0
  7. package/editors/opencode.js +3 -0
  8. package/{mem-house → memhouse}/COMPETITION.md +5 -5
  9. package/{mem-house → memhouse}/DESIGN.md +18 -12
  10. package/memhouse/RESUME-AND-UPDATE.md +185 -0
  11. package/{mem-house → memhouse}/delivery/AGENT-INSTALL-PROMPT.md +1 -1
  12. package/{mem-house → memhouse}/delivery/AGENT-INSTALL.md +9 -7
  13. package/{mem-house → memhouse}/delivery/PROMPT.md +8 -8
  14. package/{mem-house → memhouse}/delivery/kernel-install.md +3 -3
  15. package/{mem-house → memhouse}/delivery/plugin/.claude-plugin/plugin.json +1 -1
  16. package/{mem-house/delivery/skills/memhouse-search → memhouse/delivery/plugin/skills/search}/SKILL.md +60 -18
  17. package/memhouse/delivery/plugin/skills/sessions/SKILL.md +177 -0
  18. package/memhouse/delivery/plugin/skills/sql/SKILL.md +206 -0
  19. package/memhouse/host.js +101 -0
  20. package/{mem-house → memhouse}/per-member/INSTALL.md +59 -14
  21. package/{mem-house → memhouse}/per-member/SCHEMA.md +47 -0
  22. package/memhouse/per-member/provision.js +265 -0
  23. package/memhouse/per-member/rooms.js +235 -0
  24. package/{mem-house → memhouse}/per-member/schema-member.sql.tpl +18 -14
  25. package/{mem-house → memhouse}/per-member/schema-merge.sql.tpl +3 -3
  26. package/memhouse/resume.js +150 -0
  27. package/memhouse/self-update.js +150 -0
  28. package/{mem-house → memhouse}/server/queries.js +80 -34
  29. package/memhouse/server/server.js +451 -0
  30. package/{mem-house → memhouse}/service.js +48 -29
  31. package/memhouse/shipper/ship.js +949 -0
  32. package/package.json +5 -20
  33. package/public/assets/{index-CtH-5i95.js → index-CnWzrjF3.js} +10 -7
  34. package/public/index.html +2 -2
  35. package/ui/index.html +1 -1
  36. package/ui/src/App.jsx +1 -1
  37. package/ui/src/lib/api.js +29 -13
  38. package/ui/src/pages/Dashboard.jsx +8 -5
  39. package/ui/src/pages/Sessions.jsx +3 -1
  40. package/ui/src/pages/SqlViewer.jsx +6 -23
  41. package/cache.js +0 -850
  42. package/ch-ingest.js +0 -329
  43. package/ch-schema.js +0 -152
  44. package/clickhouse.js +0 -87
  45. package/index.js +0 -425
  46. package/mcp-server.js +0 -294
  47. package/mem-house/delivery/install.sh +0 -183
  48. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +0 -125
  49. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +0 -146
  50. package/mem-house/per-member/provision.js +0 -142
  51. package/mem-house/per-member/rooms.js +0 -132
  52. package/mem-house/server/server.js +0 -228
  53. package/mem-house/shipper/ship.js +0 -566
  54. package/relay-client.js +0 -306
  55. package/relay-server.js +0 -577
  56. package/server.js +0 -860
  57. package/share-image.js +0 -367
  58. /package/{mem-house → memhouse}/deploy.js +0 -0
  59. /package/{mem-house → memhouse}/envfile.js +0 -0
  60. /package/{mem-house → memhouse}/per-member/PLAN.md +0 -0
  61. /package/{mem-house → memhouse}/per-member/PROVISIONING.md +0 -0
  62. /package/{mem-house → memhouse}/per-member/README.md +0 -0
  63. /package/{mem-house → memhouse}/per-member/SHARING.md +0 -0
@@ -1,28 +1,56 @@
1
1
  ---
2
- name: memhouse-search
2
+ name: search
3
3
  description: Full-text search across ALL shipped agent conversations — every session from all 17 supported editors (Claude Code, Codex, Cursor, VS Code, Zed, OpenCode, Gemini CLI, …), every project, every machine — stored in the memhouse ClickHouse. Use whenever the user refers to something from the past that isn't in the current context, e.g. "what did I say about X", "find that conversation about Y", "when did I work on Z", "did I ever try W", "the chat where we discussed it", "remind me how I did it". Reach for this before saying you don't know about prior work.
4
4
  user-invocable: true
5
5
  argument-hint: "<search terms> [in <project>] [last <N> days] [from <editor>]"
6
- allowed-tools: Bash(set -a*), Bash(. *), Bash(curl*)
6
+ allowed-tools: Bash
7
7
  ---
8
8
 
9
- # memhouse-search — search conversation memory
9
+ # /memhouse:search — search conversation memory
10
10
 
11
11
  Search the full message history in the memhouse house. Every room is named for your
12
12
  ClickHouse user — `messages_<you>`, `sessions_<you>` — see **Room names** below.
13
13
 
14
14
  ## Connection
15
15
 
16
- Credentials come from `~/.memhouse/env` (or already-exported `MEMHOUSE_*` vars).
17
- Every query runs over ClickHouse HTTP with `final=1` and `join_use_nulls=1`
18
- (ReplacingMergeTree keeps stale row versions until merges; `final=1` collapses to
19
- latest-wins — always
16
+ **Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
17
+ MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
18
+ transcript that memhouse itself ships into the house.
19
+
20
+ Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
21
+ (default `~/.memhouse/env`) — the same order the `memhouse` CLI uses. Every query
22
+ runs over ClickHouse HTTP with `final=1` and `join_use_nulls=1` (ReplacingMergeTree
23
+ keeps stale row versions until merges; `final=1` collapses to latest-wins — always
20
24
  include it on reads):
21
25
 
22
26
  ```bash
23
- set -a; [ -f ~/.memhouse/env ] && . ~/.memhouse/env; set +a
24
- curl -sS --fail-with-body --user "${MEMHOUSE_USER:-memhouse_root}:${MEMHOUSE_PASSWORD:-}" \
25
- --data-binary @- "${MEMHOUSE_URL:-http://localhost:8123}/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1" <<'SQL'
27
+ # Config lives at $MEMHOUSE_HOME/env (default ~/.memhouse/env). Exported MEMHOUSE_*
28
+ # vars WIN over the file — snapshot them, source, then put them back. Sourcing alone
29
+ # lets a stale file silently override the house you were pointed at.
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
+ # No default URL. localhost:8123 as memhouse_root is a REAL house on many machines,
36
+ # usually the pilot's own — guessing it reads someone else's memory and looks like it
37
+ # worked. If there is no config, say so and stop.
38
+ if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
39
+ # An explicit test, not ${VAR:?msg}: zsh does not expand the message, so under the
40
+ # shell Claude Code actually uses the refusal read "nothing in $MH_ENV" literally.
41
+ echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
42
+ echo "Run: memhouse install" >&2
43
+ exit 1
44
+ fi
45
+
46
+ # --user, not a -K config file. The config-file parser treats `"` and `\` specially, so a
47
+ # password containing either authenticates fine from the CLI and fails from every skill
48
+ # with "password is incorrect" — measured. The argv exposure -K was meant to avoid is not
49
+ # observable here either: curl blanks the --user argument before `ps` can read it (0 hits
50
+ # in 50 samples). What actually leaks a credential is PRINTING it, which the rule above
51
+ # covers.
52
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
53
+ --data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
26
54
  <the query>
27
55
  FORMAT PrettyCompact
28
56
  SQL
@@ -38,23 +66,37 @@ Every member owns their own rooms, named for their ClickHouse user:
38
66
  the bare names fail with `UNKNOWN_TABLE` rather than returning nothing. You hold no grant
39
67
  on anyone else's rooms, so isolation is not something a query can work around.
40
68
 
41
- Resolve your own name once and substitute it into every table name below:
69
+ Ask the server your name once, then **type the literal room name into your SQL** — do not
70
+ reference a shell variable inside the query:
42
71
 
43
72
  ```bash
44
- MEM_ME="$(curl -sS --fail-with-body --user "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" \
73
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
45
74
  --data-binary "SELECT currentUser() FORMAT TabSeparated" \
46
- "${MEMHOUSE_URL:-http://localhost:8123}/" | tr -d '\r\n')"
47
- # rooms: messages_$MEM_ME, sessions_$MEM_ME, tool_calls_$MEM_ME
75
+ "$MEMHOUSE_URL/?readonly=1"
76
+ # prints e.g. alice → your rooms are messages_alice, sessions_alice, tool_calls_alice
48
77
  ```
49
78
 
79
+ **Keep the SQL heredoc quoted (`<<'SQL'`) and put the real name in the text.** This used to
80
+ say "substitute it into every table name below" and show `messages_$MEM_ME`, which cannot
81
+ expand inside a quoted heredoc — so the agent unquotes it, and then the SEARCH TERMS expand
82
+ too. Both instances driving this skill did exactly that, unprompted, on the first attempt.
83
+ Measured consequences, from `system.query_log`:
84
+
85
+ - a term containing `$home` became the empty string, so `LIKE '%%'` matched **every row**
86
+ and reported hits with exit 0 — silently wrong, not an error;
87
+ - a term containing a backtick **executed the command inside it**.
88
+
89
+ A user's search term is arbitrary text. With the heredoc quoted and the room name written
90
+ out, neither can happen.
91
+
50
92
  **There is no `sessions_v` object.** The session rollup is a saved query over those same
51
93
  rooms — `memhouse sessions-query` prints it for whoever you are connected as, ready to
52
94
  paste into a `FROM (...) AS c` position.
53
95
 
54
- **Read the rollup with `join_use_nulls=1`.** The connection recipe above sets it. Without
55
- it, ClickHouse gives an unmatched `m.seq` a default instead of NULL, so a session with no
56
- messages reports `total_msgs = 1` rather than 0 — measured, not theoretical. A subquery
57
- has no `SETTINGS` clause of its own, so the setting has to come from the caller.
96
+ **The rollup is self-contained.** As of 0.4.5 the printed text carries its own `FINAL` on
97
+ both rooms and a trailing `SETTINGS join_use_nulls = 1`, so it is correct wherever you
98
+ paste it. `final=1` on the connection still matters for reads of the rooms THEMSELVES,
99
+ which carry no FINAL of their own.
58
100
 
59
101
  If you hold `SELECT` on them, `all_messages` / `all_sessions` / `all_tool_calls` read
60
102
  across every member at once, narrowed to whatever grants you actually have — a Merge room
@@ -0,0 +1,177 @@
1
+ ---
2
+ name: sessions
3
+ description: List recent agent sessions from memhouse conversation memory — names, projects, editors, hosts, times, message and token counts across all 17 supported editors. Use for "what was I working on", "show my recent sessions", "sessions from last week", "what did I do in project X", "list my cursor sessions", or to orient before drilling into one session.
4
+ user-invocable: true
5
+ argument-hint: "[N] [project <name>] [from <editor>] [today|week|month]"
6
+ allowed-tools: Bash
7
+ ---
8
+
9
+ # /memhouse:sessions — browse session history
10
+
11
+ List/filter sessions from the session rollup — a saved query over your own rooms, not an
12
+ object; `memhouse sessions-query` prints it (session metadata +
13
+ message aggregates) in the memhouse house.
14
+
15
+ ## When to use
16
+
17
+ - The user wants an overview of recent activity rather than a specific quote.
18
+ - As a first step before digging into one session, when they don't have an
19
+ id/name yet.
20
+
21
+ ## Connection
22
+
23
+ **Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
24
+ MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
25
+ transcript that memhouse itself ships into the house.
26
+
27
+ Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
28
+ (default `~/.memhouse/env`) — the same order the `memhouse` CLI uses. Every query
29
+ runs over ClickHouse HTTP with `final=1` and `join_use_nulls=1` (ReplacingMergeTree
30
+ keeps stale row versions until merges; `final=1` collapses to latest-wins — always
31
+ include it on reads):
32
+
33
+ ```bash
34
+ # Config lives at $MEMHOUSE_HOME/env (default ~/.memhouse/env). Exported MEMHOUSE_*
35
+ # vars WIN over the file — snapshot them, source, then put them back. Sourcing alone
36
+ # lets a stale file silently override the house you were pointed at.
37
+ MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
38
+ _u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
39
+ set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
40
+ [ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
41
+ [ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
42
+ # No default URL. localhost:8123 as memhouse_root is a REAL house on many machines,
43
+ # usually the pilot's own — guessing it reads someone else's memory and looks like it
44
+ # worked. If there is no config, say so and stop.
45
+ if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
46
+ # An explicit test, not ${VAR:?msg}: zsh does not expand the message, so under the
47
+ # shell Claude Code actually uses the refusal read "nothing in $MH_ENV" literally.
48
+ echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
49
+ echo "Run: memhouse install" >&2
50
+ exit 1
51
+ fi
52
+
53
+ # --user, not a -K config file. The config-file parser treats `"` and `\` specially, so a
54
+ # password containing either authenticates fine from the CLI and fails from every skill
55
+ # with "password is incorrect" — measured. The argv exposure -K was meant to avoid is not
56
+ # observable here either: curl blanks the --user argument before `ps` can read it (0 hits
57
+ # in 50 samples). What actually leaks a credential is PRINTING it, which the rule above
58
+ # covers.
59
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
60
+ --data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
61
+ <the query>
62
+ FORMAT PrettyCompact
63
+ SQL
64
+ ```
65
+
66
+ ## Room names — always suffixed
67
+
68
+ Every member owns their own rooms, named for their ClickHouse user:
69
+ `messages_<you>`, `sessions_<you>`, `tool_calls_<you>`. There are no unsuffixed rooms;
70
+ the bare names fail with `UNKNOWN_TABLE` rather than returning nothing. You hold no grant
71
+ on anyone else's rooms, so isolation is not something a query can work around.
72
+
73
+ Ask the server your name once, then **type the literal room name into your SQL** — do not
74
+ reference a shell variable inside the query:
75
+
76
+ ```bash
77
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
78
+ --data-binary "SELECT currentUser() FORMAT TabSeparated" \
79
+ "$MEMHOUSE_URL/?readonly=1"
80
+ # prints e.g. alice → your rooms are messages_alice, sessions_alice, tool_calls_alice
81
+ ```
82
+
83
+ **Keep the SQL heredoc quoted (`<<'SQL'`) and put the real name in the text.** This used to
84
+ say "substitute it into every table name below" and show `messages_$MEM_ME`, which cannot
85
+ expand inside a quoted heredoc — so the agent unquotes it, and then the SEARCH TERMS expand
86
+ too. Both instances driving this skill did exactly that, unprompted, on the first attempt.
87
+ Measured consequences, from `system.query_log`:
88
+
89
+ - a term containing `$home` became the empty string, so `LIKE '%%'` matched **every row**
90
+ and reported hits with exit 0 — silently wrong, not an error;
91
+ - a term containing a backtick **executed the command inside it**.
92
+
93
+ A user's search term is arbitrary text. With the heredoc quoted and the room name written
94
+ out, neither can happen.
95
+
96
+ **There is no `sessions_v` object.** The session rollup is a saved query over those same
97
+ rooms — `memhouse sessions-query` prints it for whoever you are connected as, ready to
98
+ paste into a `FROM (...) AS c` position.
99
+
100
+ **The rollup is self-contained.** As of 0.4.5 the printed text carries its own `FINAL` on
101
+ both rooms and a trailing `SETTINGS join_use_nulls = 1`, so it is correct wherever you
102
+ paste it. Both matter: without `FINAL` every message is counted once per undeleted
103
+ ReplacingMergeTree version (2x right after a ship, 3x a few ships later — measured, and it
104
+ grows); without `join_use_nulls` a session with no messages reports `total_msgs = 1`
105
+ rather than 0.
106
+
107
+ **If the `memhouse` binary is not on PATH**, you cannot print the rollup — the skills are
108
+ installable on their own. Query the rooms directly instead: `sessions_<you>` for metadata
109
+ and `messages_<you>` for counts, each read `FINAL`, joined on `session_id` and `user_id`.
110
+ Prefer the binary when it is there; a rollup you assemble by hand and one printed by a
111
+ DIFFERENT memhouse version are the two ways this goes quietly wrong.
112
+
113
+ If you hold `SELECT` on them, `all_messages` / `all_sessions` / `all_tool_calls` read
114
+ across every member at once, narrowed to whatever grants you actually have — a Merge room
115
+ reduces to the rooms the caller can read, so it fails closed rather than denying outright.
116
+
117
+ ## Default listing
118
+
119
+ ```sql
120
+ SELECT session_id, name, source, host, project,
121
+ started, ended, total_msgs, user_msgs, assistant_msgs,
122
+ output_tokens,
123
+ substring(first_prompt, 1, 80) AS first_prompt
124
+ FROM (the rollup from `memhouse sessions-query`)
125
+ ORDER BY ended DESC
126
+ LIMIT 20
127
+ FORMAT PrettyCompact
128
+ ```
129
+
130
+ - Default `LIMIT 20`; `[N]` overrides.
131
+ - `today` / `week` / `month` → `WHERE ended > now() - INTERVAL 1 DAY`
132
+ (`7 DAY` / `30 DAY`).
133
+ - `project <name>` → `WHERE project ILIKE '%<name>%'`.
134
+ - `from <editor>` → `WHERE source = '<id>'` (`claude-code`, `codex`, `cursor`,
135
+ `vscode`, `zed`, `opencode`, `gemini-cli`, `windsurf`, …).
136
+ - Unnamed sessions (`name = ''`): fall back to `first_prompt` as the label.
137
+
138
+ ## More cuts
139
+
140
+ Per-editor rollup ("where has my work been happening?"):
141
+
142
+ ```sql
143
+ SELECT source, count() AS sessions, sum(total_msgs) AS msgs,
144
+ sum(output_tokens) AS out_tokens, max(ended) AS last_activity
145
+ FROM (the rollup from `memhouse sessions-query`)
146
+ GROUP BY source
147
+ ORDER BY sessions DESC
148
+ FORMAT PrettyCompact
149
+ ```
150
+
151
+ Busiest projects this month:
152
+
153
+ ```sql
154
+ SELECT project, count() AS sessions, sum(assistant_msgs) AS assistant_msgs
155
+ FROM (the rollup from `memhouse sessions-query`)
156
+ WHERE ended > now() - INTERVAL 30 DAY AND project != ''
157
+ GROUP BY project
158
+ ORDER BY sessions DESC
159
+ LIMIT 15
160
+ FORMAT PrettyCompact
161
+ ```
162
+
163
+ ## Examples
164
+
165
+ | User says | What you do |
166
+ |---|---|
167
+ | "what was I working on yesterday?" | `WHERE ended > now() - INTERVAL 1 DAY` |
168
+ | "show my last 10 sessions in agentlytics" | `LIMIT 10 … WHERE project ILIKE '%agentlytics%'` |
169
+ | "list my cursor sessions this month" | `WHERE source = 'cursor' AND ended > now() - INTERVAL 30 DAY` |
170
+ | "which machine did I do the schema work on?" | search via /memhouse:search, or filter `host` here |
171
+
172
+ ## Output
173
+
174
+ A compact table: when, name (or first prompt), source, host, project, msgs,
175
+ tokens. Offer to pull a full transcript next
176
+ (`SELECT role, text FROM messages_<you> WHERE session_id = '…' ORDER BY seq`) or to
177
+ run /memhouse:search for a specific quote.
@@ -0,0 +1,206 @@
1
+ ---
2
+ name: sql
3
+ description: Run free-form read-only SQL against memhouse conversation memory (typed sessions/messages/tool_calls rooms on ClickHouse, one set per member). Use for ad-hoc analytics the other memhouse skills don't cover — token spend, model/editor usage, tool rankings, activity heatmaps, busiest days/projects, cache-hit ratios, or any custom question over conversation data.
4
+ user-invocable: true
5
+ argument-hint: "<question or SQL>"
6
+ allowed-tools: Bash
7
+ ---
8
+
9
+ # /memhouse:sql — ad-hoc analytics
10
+
11
+ If the user gives SQL, run it (append `FORMAT PrettyCompact` if no FORMAT
12
+ given). If they give a question, write the SQL yourself from the schema below.
13
+
14
+ **Read-only rule:** the shipper (`ship.js`) is the only writer. Never INSERT/ALTER/DROP
15
+ from here. A member credential holds `SELECT, INSERT, ALTER UPDATE, ALTER DELETE, ALTER ADD
16
+ COLUMN, OPTIMIZE` on its **own three rooms** — enough to destroy the rows, though not
17
+ the rooms — and nothing at all on anyone else's. Reads need no scoping
18
+ clause: the rooms you can name are already only yours, and there is no policy to work
19
+ around.
20
+
21
+ The recipe below pins `readonly=1` on every request, so a write that slips past the rule
22
+ is refused by the server (`Code: 164 … Cannot execute query in readonly mode`) rather
23
+ than by good intentions. Two honest limits: it is a **setting, not a grant** — a caller
24
+ who writes their own URL can leave it off — and the HTTP interface refuses
25
+ multi-statement bodies, so `SET readonly=0;` cannot be smuggled into a query. For
26
+ enforcement that does not depend on this file, the house owner can mint a second
27
+ `SELECT`-only credential and point the skills at that instead.
28
+
29
+ ## Connection
30
+
31
+ **Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
32
+ MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
33
+ transcript that memhouse itself ships into the house.
34
+
35
+ Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
36
+ (default `~/.memhouse/env`) — the same order the `memhouse` CLI uses. Every query
37
+ runs over ClickHouse HTTP with `final=1` and `join_use_nulls=1` (ReplacingMergeTree
38
+ keeps stale row versions until merges; `final=1` collapses to latest-wins — always
39
+ include it on reads):
40
+
41
+ ```bash
42
+ # Config lives at $MEMHOUSE_HOME/env (default ~/.memhouse/env). Exported MEMHOUSE_*
43
+ # vars WIN over the file — snapshot them, source, then put them back. Sourcing alone
44
+ # lets a stale file silently override the house you were pointed at.
45
+ MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
46
+ _u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
47
+ set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
48
+ [ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
49
+ [ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
50
+ # No default URL. localhost:8123 as memhouse_root is a REAL house on many machines,
51
+ # usually the pilot's own — guessing it reads someone else's memory and looks like it
52
+ # worked. If there is no config, say so and stop.
53
+ if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
54
+ # An explicit test, not ${VAR:?msg}: zsh does not expand the message, so under the
55
+ # shell Claude Code actually uses the refusal read "nothing in $MH_ENV" literally.
56
+ echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
57
+ echo "Run: memhouse install" >&2
58
+ exit 1
59
+ fi
60
+
61
+ # --user, not a -K config file. The config-file parser treats `"` and `\` specially, so a
62
+ # password containing either authenticates fine from the CLI and fails from every skill
63
+ # with "password is incorrect" — measured. The argv exposure -K was meant to avoid is not
64
+ # observable here either: curl blanks the --user argument before `ps` can read it (0 hits
65
+ # in 50 samples). What actually leaks a credential is PRINTING it, which the rule above
66
+ # covers.
67
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
68
+ --data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-mem}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
69
+ <the query>
70
+ FORMAT PrettyCompact
71
+ SQL
72
+ ```
73
+
74
+ `FORMAT PrettyCompact` for display, `FORMAT JSONEachRow` to parse.
75
+
76
+ ## Room names — always suffixed
77
+
78
+ Every member owns their own rooms, named for their ClickHouse user:
79
+ `messages_<you>`, `sessions_<you>`, `tool_calls_<you>`. There are no unsuffixed rooms;
80
+ the bare names fail with `UNKNOWN_TABLE` rather than returning nothing. You hold no grant
81
+ on anyone else's rooms, so isolation is not something a query can work around.
82
+
83
+ Ask the server your name once, then **type the literal room name into your SQL** — do not
84
+ reference a shell variable inside the query:
85
+
86
+ ```bash
87
+ curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
88
+ --data-binary "SELECT currentUser() FORMAT TabSeparated" \
89
+ "$MEMHOUSE_URL/?readonly=1"
90
+ # prints e.g. alice → your rooms are messages_alice, sessions_alice, tool_calls_alice
91
+ ```
92
+
93
+ **Keep the SQL heredoc quoted (`<<'SQL'`) and put the real name in the text.** This used to
94
+ say "substitute it into every table name below" and show `messages_$MEM_ME`, which cannot
95
+ expand inside a quoted heredoc — so the agent unquotes it, and then the SEARCH TERMS expand
96
+ too. Both instances driving this skill did exactly that, unprompted, on the first attempt.
97
+ Measured consequences, from `system.query_log`:
98
+
99
+ - a term containing `$home` became the empty string, so `LIKE '%%'` matched **every row**
100
+ and reported hits with exit 0 — silently wrong, not an error;
101
+ - a term containing a backtick **executed the command inside it**.
102
+
103
+ A user's search term is arbitrary text. With the heredoc quoted and the room name written
104
+ out, neither can happen.
105
+
106
+ **There is no `sessions_v` object.** The session rollup is a saved query over those same
107
+ rooms — `memhouse sessions-query` prints it for whoever you are connected as, ready to
108
+ paste into a `FROM (...) AS c` position.
109
+
110
+ **The rollup is self-contained.** As of 0.4.5 the printed text carries its own `FINAL` on
111
+ both rooms and a trailing `SETTINGS join_use_nulls = 1`, so it is correct wherever you
112
+ paste it. Both matter: without `FINAL` every message is counted once per undeleted
113
+ ReplacingMergeTree version (2x right after a ship, 3x a few ships later — measured, and it
114
+ grows); without `join_use_nulls` a session with no messages reports `total_msgs = 1`
115
+ rather than 0.
116
+
117
+ **If the `memhouse` binary is not on PATH**, you cannot print the rollup — the skills are
118
+ installable on their own. Query the rooms directly instead: `sessions_<you>` for metadata
119
+ and `messages_<you>` for counts, each read `FINAL`, joined on `session_id` and `user_id`.
120
+ Prefer the binary when it is there; a rollup you assemble by hand and one printed by a
121
+ DIFFERENT memhouse version are the two ways this goes quietly wrong.
122
+
123
+ If you hold `SELECT` on them, `all_messages` / `all_sessions` / `all_tool_calls` read
124
+ across every member at once, narrowed to whatever grants you actually have — a Merge room
125
+ reduces to the rooms the caller can read, so it fails closed rather than denying outright.
126
+
127
+ ## Schema (the house)
128
+
129
+ | Object | Kind | Columns |
130
+ |---|---|---|
131
+ | `sessions_<you>` | 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` |
132
+ | `messages_<you>` | 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 /memhouse:search) |
133
+ | `tool_calls_<you>` | table, 1 row/tool call | `session_id, seq, idx, source, host, tool_name, args, ts, project, folder, user_id, ingested_at` |
134
+ | 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` |
135
+
136
+ Notes: `source` = editor id (`claude-code`, `codex`, `cursor`, `cursor-agent`,
137
+ `vscode`, `zed`, `opencode`, `gemini-cli`, `windsurf`, `antigravity`, …);
138
+ `seq` = message index within its session (0-based); `tool_calls.idx` = call
139
+ index within the session; `user_id` is server-stamped (`currentUser()`);
140
+ `models` in the rollup is an `Array(String)`. Exclude `''` and
141
+ `'<synthetic>'` from model aggregates.
142
+
143
+ ## Examples
144
+
145
+ Token spend by model:
146
+
147
+ ```sql
148
+ SELECT model, sum(input_tokens) AS in_tok, sum(output_tokens) AS out_tok,
149
+ sum(cache_read_tokens) AS cache_read
150
+ FROM messages_<you>
151
+ WHERE model NOT IN ('', '<synthetic>')
152
+ GROUP BY model
153
+ ORDER BY out_tok DESC
154
+ FORMAT PrettyCompact
155
+ ```
156
+
157
+ Which tools do I use most (memhouse exclusive — memory-house has no tool table):
158
+
159
+ ```sql
160
+ SELECT tool_name, count() AS calls, uniqExact(session_id) AS sessions
161
+ FROM tool_calls_<you>
162
+ GROUP BY tool_name
163
+ ORDER BY calls DESC
164
+ LIMIT 20
165
+ FORMAT PrettyCompact
166
+ ```
167
+
168
+ Busiest days, last two weeks:
169
+
170
+ ```sql
171
+ SELECT toDate(ts) AS day, uniqExact(session_id) AS sessions, count() AS msgs
172
+ FROM messages_<you>
173
+ WHERE ts > now() - INTERVAL 14 DAY
174
+ GROUP BY day
175
+ ORDER BY day DESC
176
+ FORMAT PrettyCompact
177
+ ```
178
+
179
+ Activity by editor this week / subagent share:
180
+
181
+ ```sql
182
+ SELECT source, count() AS msgs, countIf(is_subagent) AS subagent_msgs
183
+ FROM messages_<you>
184
+ WHERE ts > now() - INTERVAL 7 DAY
185
+ GROUP BY source
186
+ ORDER BY msgs DESC
187
+ FORMAT PrettyCompact
188
+ ```
189
+
190
+ Longest sessions by wall-clock:
191
+
192
+ ```sql
193
+ SELECT name, project, source, duration_sec, total_msgs, output_tokens
194
+ FROM (the rollup from `memhouse sessions-query`)
195
+ ORDER BY duration_sec DESC
196
+ LIMIT 10
197
+ FORMAT PrettyCompact
198
+ ```
199
+
200
+ ## Output
201
+
202
+ Run the query, present the result as a compact table, and add one or two lines
203
+ of interpretation. Offer a natural follow-up cut (by source, by project, by
204
+ host, by token volume) when relevant. Dollar costs are computed by the
205
+ dashboard server from pricing.js — token sums here are the raw material, not
206
+ dollars.
@@ -0,0 +1,101 @@
1
+ // Which machine shipped this row.
2
+ //
3
+ // All of one member's machines ship into that member's rooms — that is the point of the
4
+ // per-member layout — and the `host` column is the only thing telling them apart. So the
5
+ // value has to be BOTH unique per machine and stable across that machine's lifetime.
6
+ //
7
+ // It used to be derived, `sha256(hostname|platform|arch)`, and that fails in both
8
+ // directions:
9
+ //
10
+ // * Two machines collide. A pilot with two MacBooks that both answer to
11
+ // `MacBook-Pro` on the same darwin/arm64 produce the SAME id — their sessions merge
12
+ // into one apparent host and neither can be told from the other. Default hostnames
13
+ // are exactly the case where a person owns several.
14
+ // * One machine splits. Rename the box and every row shipped afterwards lands under a
15
+ // new host, so the machine's own history appears to end and a stranger's to begin.
16
+ //
17
+ // A random fingerprint, written once and kept, fixes both: nothing derives it, so nothing
18
+ // can collide with it, and nothing about the machine changing can move it.
19
+ //
20
+ // It lives in MEMHOUSE_HOME and is deliberately NOT removed by a plain `memhouse
21
+ // uninstall` — reinstalling on the same machine should continue that machine's history,
22
+ // not start a second one beside it. `uninstall --full-removal` drops it, and then the
23
+ // machine genuinely is a new host next time; that is the honest meaning of full removal.
24
+
25
+ const fs = require('fs');
26
+ const os = require('os');
27
+ const path = require('path');
28
+ const crypto = require('crypto');
29
+
30
+ const FILE = 'host.json';
31
+
32
+ function homeDir() {
33
+ return process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
34
+ }
35
+
36
+ function filePath(home = homeDir()) {
37
+ return path.join(home, FILE);
38
+ }
39
+
40
+ // `<short-hostname>-<8 hex>`: the same SHAPE the derived id had, so dashboards and
41
+ // existing queries read the same, but the hex half now comes from the stored fingerprint
42
+ // rather than from a hash of facts that can collide or change.
43
+ function idFrom(hostname, fingerprint) {
44
+ const short = (hostname || 'host').split('.')[0].replace(/[^A-Za-z0-9_-]/g, '') || 'host';
45
+ return `${short}-${fingerprint.slice(0, 8)}`;
46
+ }
47
+
48
+ function read(home = homeDir()) {
49
+ try {
50
+ const raw = JSON.parse(fs.readFileSync(filePath(home), 'utf-8'));
51
+ if (raw && typeof raw.fingerprint === 'string' && raw.fingerprint.length >= 8) return raw;
52
+ } catch { /* absent or unreadable — treated as "not yet created" */ }
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * The identity of this machine, creating it on first use.
58
+ *
59
+ * Every entry point calls this rather than only `install`, because a checkout can ship
60
+ * without ever running install — and a shipper that could not name its host would either
61
+ * refuse to run or invent a different name each pass.
62
+ *
63
+ * @param {string} [home]
64
+ * @returns {{id: string, fingerprint: string, hostname: string, created: string,
65
+ * renamed: boolean, platform: string, arch: string}}
66
+ */
67
+ function identity(home = homeDir()) {
68
+ const hostname = os.hostname();
69
+ let rec = read(home);
70
+ if (!rec) {
71
+ rec = {
72
+ fingerprint: crypto.randomBytes(16).toString('hex'),
73
+ hostname,
74
+ platform: os.platform(),
75
+ arch: os.arch(),
76
+ // Stamped once. Never rewritten, so it says when this machine joined, not when the
77
+ // file was last touched.
78
+ created: new Date().toISOString(),
79
+ };
80
+ rec.id = idFrom(hostname, rec.fingerprint);
81
+ try {
82
+ fs.mkdirSync(home, { recursive: true });
83
+ // Written with the same 0600 the credential gets. It is not a secret, but it is an
84
+ // identity: anything that can read it can impersonate this host's rows.
85
+ fs.writeFileSync(filePath(home), JSON.stringify(rec, null, 2) + '\n', { mode: 0o600 });
86
+ } catch { /* read-only home: fall through with an in-memory identity for this run */ }
87
+ }
88
+ // The id is frozen at creation, INCLUDING the hostname baked into it. A rename must not
89
+ // move the id — that is the split this exists to prevent — so the current hostname is
90
+ // reported alongside instead, and `renamed` lets callers say so out loud rather than
91
+ // printing a name the machine no longer answers to and leaving the reader to wonder.
92
+ return {
93
+ ...rec,
94
+ id: rec.id || idFrom(rec.hostname, rec.fingerprint),
95
+ hostname: rec.hostname,
96
+ current_hostname: hostname,
97
+ renamed: rec.hostname !== hostname,
98
+ };
99
+ }
100
+
101
+ module.exports = { identity, read, filePath, idFrom, FILE };