memhouse 0.16.0 → 0.17.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.
@@ -1,226 +1,105 @@
1
1
  ---
2
2
  name: sql
3
- description: Run free-form read-only SQL against memhouse conversation memory (typed sessions/messages/tool_calls tables on ClickHouse, shared by the house). 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 — over your own house, or a friend's house shared with you when they name it.
3
+ description: Run read-only SQL over memhouse conversation memory for numbers rather than transcripts — token spend, cost by model or editor or project, tool-call rankings, activity over time, busiest days, cache-hit ratios, session length distributions, or any custom aggregate across the typed sessions/messages/tool_calls tables. Use when the user wants a figure, a ranking, a trend or a breakdown - "what did I spend on Opus", "which tools do I use most", "how many sessions per project", "my busiest week", "compare editors". For finding or answering FROM past conversations, use /mem:recall instead. Works over your own house, or a housemate's when they name it.
4
4
  user-invocable: true
5
- argument-hint: "<question or SQL>"
5
+ argument-hint: "<the question, or the SQL>"
6
6
  allowed-tools: Bash
7
7
  ---
8
8
 
9
- # /mem:sql — ad-hoc analytics
9
+ # /mem:sql — analytics over the rooms
10
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.
11
+ For **numbers**. When the answer is a passage from a past conversation, that is
12
+ `/mem:recall` — this skill counts, groups and ranks.
13
13
 
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.
14
+ **Read `../reference/HOUSE.md` first.** Schema, connection, and the three traps. The epoch
15
+ filter in particular is not optional here: this is the skill most likely to produce a
16
+ figure someone acts on, and omitting it over-counted a real house by **34%**.
23
17
 
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:
18
+ ## Read-only, and that is enforced
28
19
 
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
- ```
20
+ Every query goes through `readonly=1`. The shipper is the only writer; a skill that
21
+ mutates the archive is a skill that can destroy it. Never `INSERT`, `ALTER`, `DROP`,
22
+ `TRUNCATE` or `OPTIMIZE` from here — not even "just to tidy up". If the user genuinely
23
+ wants a write, that is `/mem:admin` with its confirmations, not this.
35
24
 
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.
39
-
40
- The recipe below pins `readonly=1` on every request, so a write that slips past the rule
41
- is refused by the server (`Code: 164 … Cannot execute query in readonly mode`) rather
42
- than by good intentions. Two honest limits: it is a **setting, not a grant** — a caller
43
- who writes their own URL can leave it off — and the HTTP interface refuses
44
- multi-statement bodies, so `SET readonly=0;` cannot be smuggled into a query. For
45
- enforcement that does not depend on this file, the house owner can mint a second
46
- `SELECT`-only credential and point the skills at that instead.
47
-
48
- ## Connection
49
-
50
- **Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
51
- MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
52
- transcript that memhouse itself ships into the house.
53
-
54
- Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
55
- (default `~/.memhouse/env`) — the same order the `memhouse` CLI uses. Every query
56
- runs over ClickHouse HTTP with `final=1` and `join_use_nulls=1` (ReplacingMergeTree
57
- keeps stale row versions until merges; `final=1` collapses to latest-wins — always
58
- include it on reads):
59
-
60
- ```bash
61
- # Config lives at $MEMHOUSE_HOME/env (default ~/.memhouse/env). Exported MEMHOUSE_*
62
- # vars WIN over the file — snapshot them, source, then put them back. Sourcing alone
63
- # lets a stale file silently override the house you were pointed at.
64
- MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
65
- _u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
66
- set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
67
- [ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
68
- [ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
69
- # No default URL. localhost:8123 as memhouse_root is a REAL house on many machines,
70
- # usually the pilot's own — guessing it reads someone else's memory and looks like it
71
- # worked. If there is no config, say so and stop.
72
- if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
73
- # An explicit test, not ${VAR:?msg}: zsh does not expand the message, so under the
74
- # shell Claude Code actually uses the refusal read "nothing in $MH_ENV" literally.
75
- echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
76
- echo "Run: memhouse install" >&2
77
- exit 1
78
- fi
79
-
80
- # --user, not a -K config file. The config-file parser treats `"` and `\` specially, so a
81
- # password containing either authenticates fine from the CLI and fails from every skill
82
- # with "password is incorrect" — measured. The argv exposure -K was meant to avoid is not
83
- # observable here either: curl blanks the --user argument before `ps` can read it (0 hits
84
- # in 50 samples). What actually leaks a credential is PRINTING it, which the rule above
85
- # covers.
86
- curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
87
- --data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-$MEMHOUSE_USER}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
88
- <the query>
89
- FORMAT PrettyCompact
90
- SQL
91
- ```
25
+ ## Method
26
+
27
+ 1. **Say what you are counting** before running it — "messages, current parse only, by
28
+ model, this year". Half of wrong analytics is a right query answering a different
29
+ question.
30
+ 2. **Apply the epoch filter to anything aggregating.** Copy it from the reference.
31
+ 3. **Group by `(session_id, user_id)`** wherever a session is the unit.
32
+ 4. **Show the SQL** beside the result. The user can re-run and adjust it; an unexplained
33
+ number is not an answer.
34
+ 5. **`FORMAT PrettyCompact`** for humans, `JSONEachRow` when you need to post-process.
92
35
 
93
- `FORMAT PrettyCompact` for display, `FORMAT JSONEachRow` to parse.
94
-
95
- ## Room names — plain, shared tables
96
-
97
- The house's rooms are three plain tables: `messages`, `sessions`, `tool_calls` — resolved
98
- by the connection's database (`MEMHOUSE_DB`), not by who is asking. Everyone in the house
99
- writes into the same tables; `user_id` (stamped by the server) says whose row it is and
100
- `host` says which machine shipped it. Filter with `WHERE user_id = '<name>'` when you want
101
- one person, or leave it off for the whole house.
102
-
103
- **Another person's house (shared with you).** A house is a database; a share is a
104
- read-only GRANT on it. Because the rooms resolve by the connection's database, you read a
105
- friend's memory by pointing the connection at THEIR house — same credentials, different
106
- `database`. `SHOW DATABASES` lists what you may read; a name that is not `system` /
107
- `information_schema` / `default` and not your own `$MEMHOUSE_DB` is a house shared with
108
- you. When the user names one ("query yigit's house", "on yigit", "from yigit"), replace
109
- `database=${MEMHOUSE_DB:-$MEMHOUSE_USER}` with `database=<house>` in the connection recipe and leave
110
- the table names bare — or, to join across houses in one query, qualify tables explicitly
111
- (`yigit.messages`). Read-only: a refused SELECT means that house was not shared with you.
112
-
113
- **Keep the SQL heredoc quoted (`<<'SQL'`).** This skill once told the agent to substitute
114
- a shell variable into the table names, which cannot expand inside a quoted heredoc — so
115
- the agent unquotes it, and then the SEARCH TERMS expand too. Both instances driving this
116
- skill did exactly that, unprompted, on the first attempt. Measured consequences, from
117
- `system.query_log`:
118
-
119
- - a term containing `$home` became the empty string, so `LIKE '%%'` matched **every row**
120
- and reported hits with exit 0 — silently wrong, not an error;
121
- - a term containing a backtick **executed the command inside it**.
122
-
123
- A user's search term is arbitrary text. With the heredoc quoted, neither can happen.
124
-
125
- **There is no `sessions_v` object.** The session rollup is a saved query over those same
126
- rooms — `memhouse sessions-query` prints it for whoever you are connected as, ready to
127
- paste into a `FROM (...) AS c` position.
128
-
129
- **The rollup is self-contained.** As of 0.4.5 the printed text carries its own `FINAL` on
130
- both rooms and a trailing `SETTINGS join_use_nulls = 1`, so it is correct wherever you
131
- paste it. Both matter: without `FINAL` every message is counted once per undeleted
132
- ReplacingMergeTree version (2x right after a ship, 3x a few ships later — measured, and it
133
- grows); without `join_use_nulls` a session with no messages reports `total_msgs = 1`
134
- rather than 0.
135
-
136
- **If the `memhouse` binary is not on PATH**, you cannot print the rollup — the skills are
137
- installable on their own. Query the rooms directly instead: `sessions` for metadata and
138
- `messages` for counts, each read `FINAL`, joined on `session_id` **and `user_id`** — the
139
- tables are shared, and joining on `session_id` alone can merge two housemates' rows.
140
- Prefer the binary when it is there; a rollup you assemble by hand and one printed by a
141
- DIFFERENT memhouse version are the two ways this goes quietly wrong.
142
-
143
- ## Schema (the house)
144
-
145
- | Object | Kind | Columns |
146
- |---|---|---|
147
- | `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` |
148
- | `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) |
149
- | `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` |
150
- | `house_meta` | table, house's own record | `key, value, updated_at, updated_by, host` — `schema_version`, per-member `client_version:<user>` |
151
- | `house_events` | append-only log | `event_at, kind, id, status, from_version, to_version, actor, host, rows_before, rows_after, detail` — migrations, version changes |
152
- | 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` |
153
-
154
- Notes: `source` = editor id (`claude-code`, `codex`, `cursor`, `cursor-agent`,
155
- `vscode`, `zed`, `opencode`, `gemini-cli`, `windsurf`, `antigravity`, …);
156
- `seq` = message index within its session (0-based); `tool_calls.idx` = call
157
- index within the session; `user_id` is server-stamped (`currentUser()`); `origin` is
158
- `ship` for rows the shipper wrote and anything else for imports; `epoch` is which parse
159
- of the session a row belongs to — see the filter above;
160
- `models` in the rollup is an `Array(String)`. Exclude `''` and
161
- `'<synthetic>'` from model aggregates.
162
-
163
- ## Examples
164
-
165
- Token spend by model:
36
+ ## Shapes worth knowing
37
+
38
+ Cost is **derived**, not stored — `pricing.json` × tokens, per model.
166
39
 
167
40
  ```sql
168
- SELECT model, sum(input_tokens) AS in_tok, sum(output_tokens) AS out_tok,
169
- sum(cache_read_tokens) AS cache_read
41
+ -- spend by model, current parse only
42
+ SELECT model,
43
+ count() AS messages,
44
+ sum(input_tokens) AS in_tok,
45
+ sum(output_tokens) AS out_tok,
46
+ sum(cache_read_tokens) AS cache_read,
47
+ sum(cache_write_tokens) AS cache_write
170
48
  FROM messages
171
- WHERE model NOT IN ('', '<synthetic>')
172
- GROUP BY model
173
- ORDER BY out_tok DESC
174
- FORMAT PrettyCompact
49
+ WHERE model != ''
50
+ AND (origin != 'ship'
51
+ OR (session_id, user_id, epoch) IN (
52
+ SELECT session_id, user_id, max(epoch) FROM messages
53
+ WHERE origin = 'ship' GROUP BY session_id, user_id))
54
+ GROUP BY model ORDER BY out_tok DESC
175
55
  ```
176
56
 
177
- Which tools do I use most (memhouse exclusive — memory-house has no tool table):
178
-
179
57
  ```sql
58
+ -- which tools, how often
180
59
  SELECT tool_name, count() AS calls, uniqExact(session_id) AS sessions
181
60
  FROM tool_calls
182
- GROUP BY tool_name
183
- ORDER BY calls DESC
184
- LIMIT 20
185
- FORMAT PrettyCompact
61
+ WHERE origin != 'ship'
62
+ OR (session_id, user_id, epoch) IN (
63
+ SELECT session_id, user_id, max(epoch) FROM messages
64
+ WHERE origin = 'ship' GROUP BY session_id, user_id)
65
+ GROUP BY tool_name ORDER BY calls DESC LIMIT 25
186
66
  ```
187
67
 
188
- Busiest days, last two weeks:
68
+ Note `tool_calls` takes its epoch from **`messages`** — both rooms are written by the same
69
+ pass at the same epoch, and a parse that produced messages but no tool calls would
70
+ otherwise resolve to a superseded epoch and serve the wrong parse's calls.
189
71
 
190
72
  ```sql
191
- SELECT toDate(ts) AS day, uniqExact(session_id) AS sessions, count() AS msgs
73
+ -- activity by day, one machine
74
+ SELECT toDate(ts) AS day, uniqExact(session_id) AS sessions, count() AS messages
192
75
  FROM messages
193
- WHERE ts > now() - INTERVAL 14 DAY
194
- GROUP BY day
195
- ORDER BY day DESC
196
- FORMAT PrettyCompact
76
+ WHERE host = '<host>'
77
+ AND (origin != 'ship'
78
+ OR (session_id, user_id, epoch) IN (
79
+ SELECT session_id, user_id, max(epoch) FROM messages
80
+ WHERE origin = 'ship' GROUP BY session_id, user_id))
81
+ GROUP BY day ORDER BY day DESC LIMIT 30
197
82
  ```
198
83
 
199
- Activity by editor this week / subagent share:
84
+ For per-session totals, `memhouse sessions-query` prints a self-contained rollup that
85
+ already carries `FINAL` — prefer it to hand-joining `sessions` and `messages`.
200
86
 
201
- ```sql
202
- SELECT source, count() AS msgs, countIf(is_subagent) AS subagent_msgs
203
- FROM messages
204
- WHERE ts > now() - INTERVAL 7 DAY
205
- GROUP BY source
206
- ORDER BY msgs DESC
207
- FORMAT PrettyCompact
208
- ```
87
+ ## Caveat the answer, every time it needs it
209
88
 
210
- Longest sessions by wall-clock:
89
+ These are properties of the data, not of your query, and a number presented without them
90
+ misleads:
211
91
 
212
- ```sql
213
- SELECT name, project, source, duration_sec, total_msgs, output_tokens
214
- FROM (the rollup from `memhouse sessions-query`)
215
- ORDER BY duration_sec DESC
216
- LIMIT 10
217
- FORMAT PrettyCompact
218
- ```
92
+ - **Several adapters ship no tokens at all** — `kiro`, `vscode`, `zed`, and `cursor` on
93
+ one storage path. Their cost reads `$0`, which means *unknown*. Exclude them or label
94
+ them; never let them dilute an average silently.
95
+ - **Per-message timestamps are interpolated** for most adapters — only session bounds are
96
+ real. Any hour-of-day or "peak time" result inherits that and should say so.
97
+ - **`text` truncates at 50,000 chars**, `args` at 20,000, so length statistics have a
98
+ ceiling.
99
+ - **Tool results are not stored**, so nothing here can measure what a command returned.
219
100
 
220
- ## Output
101
+ ## Someone else's house
221
102
 
222
- Run the query, present the result as a compact table, and add one or two lines
223
- of interpretation. Offer a natural follow-up cut (by source, by project, by
224
- host, by token volume) when relevant. Dollar costs are computed by the
225
- dashboard server from pricing.js — token sums here are the raw material, not
226
- dollars.
103
+ `SHOW DATABASES` lists what your credential may read; anything that is not `system`,
104
+ `information_schema`, `default` or your own is a share. Point `database=` at it, leave
105
+ table names bare, and say whose house the number came from.
package/memhouse/flags.js CHANGED
@@ -15,6 +15,7 @@ const GLOBAL_FLAGS = ['json', 'yes', 'help', 'version'];
15
15
  const COMMAND_FLAGS = {
16
16
  null: [], help: [], version: [],
17
17
  whoami: ['admin'],
18
+ share: ['only', 'revoke', 'list'],
18
19
  discover: [], stats: [], stop: [], doctor: [], 'sessions-query': [],
19
20
  status: [], start: [],
20
21
  onboard: ['url', 'user', 'password', 'db', 'port', 'no-ship', 'target', 'house-port', 'tag', 'local'],
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memhouse",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
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.