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.
- package/README.md +1 -1
- package/bin/memhouse.js +270 -22
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +2 -2
- package/memhouse/delivery/AGENT-INSTALL.md +2 -2
- package/memhouse/delivery/PROMPT.md +15 -9
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +154 -0
- package/memhouse/delivery/plugin/skills/access/SKILL.md +148 -0
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +29 -0
- package/memhouse/delivery/plugin/skills/house/SKILL.md +99 -0
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +119 -0
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +73 -194
- package/memhouse/flags.js +1 -0
- package/memhouse/share.js +64 -0
- package/package.json +1 -1
- package/memhouse/delivery/plugin/skills/ask/SKILL.md +0 -131
- package/memhouse/delivery/plugin/skills/hello/SKILL.md +0 -121
- package/memhouse/delivery/plugin/skills/invite/SKILL.md +0 -130
- package/memhouse/delivery/plugin/skills/search/SKILL.md +0 -186
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +0 -164
- package/memhouse/delivery/plugin/skills/share/SKILL.md +0 -130
- package/memhouse/delivery/plugin/skills/status/SKILL.md +0 -139
- package/memhouse/delivery/plugin/skills/users/SKILL.md +0 -97
|
@@ -1,226 +1,105 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sql
|
|
3
|
-
description: Run
|
|
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 —
|
|
9
|
+
# /mem:sql — analytics over the rooms
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
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
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
169
|
-
|
|
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
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
89
|
+
These are properties of the data, not of your query, and a number presented without them
|
|
90
|
+
misleads:
|
|
211
91
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
##
|
|
101
|
+
## Someone else's house
|
|
221
102
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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.
|
|
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.
|