memhouse 0.16.0 → 0.18.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 +97 -31
- package/bin/memhouse.js +657 -187
- package/editors/antigravity.js +2 -0
- package/editors/claude.js +3 -1
- package/editors/codebuff.js +2 -0
- package/editors/codex.js +4 -1
- package/editors/commandcode.js +2 -1
- package/editors/copilot-jetbrains.js +2 -1
- package/editors/copilot.js +2 -1
- package/editors/cursor-agent.js +2 -1
- package/editors/cursor.js +2 -0
- package/editors/gemini.js +2 -1
- package/editors/goose.js +2 -1
- package/editors/index.js +8 -1
- package/editors/kiro.js +3 -2
- package/editors/opencode.js +2 -1
- package/editors/scope.js +126 -0
- package/editors/vscode.js +2 -0
- package/editors/zed.js +2 -1
- package/memhouse/DESIGN.md +31 -8
- package/memhouse/channel.js +50 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +4 -3
- package/memhouse/delivery/AGENT-INSTALL.md +3 -3
- package/memhouse/delivery/INVITATION.md.tpl +130 -0
- package/memhouse/delivery/PROMPT.md +21 -14
- package/memhouse/delivery/kernel-install.md +4 -2
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +173 -0
- package/memhouse/delivery/plugin/skills/access/SKILL.md +148 -0
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +30 -1
- 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 +6 -4
- package/memhouse/house/HOUSE.md +25 -11
- package/memhouse/house/house.js +110 -27
- package/memhouse/house/migrate.js +7 -6
- package/memhouse/house/relocate.js +2 -2
- package/memhouse/house/schema.sql.tpl +3 -3
- package/memhouse/provision.js +100 -0
- package/memhouse/share.js +64 -0
- package/memhouse/shipper/ship.js +87 -28
- 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
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: house
|
|
3
|
+
description: The state of your memory and who is around it — what memhouse is and how to use it, whether shipping is working, what the house holds (sessions, messages, tokens per editor), how fresh it is, which machines write into it, which houses you can read, and who can read yours. Use for "what is memhouse", "how do I use this", "is my memory working", "how much is stored", "when did it last ship", "which machines are shipping", "who can see my memory", "whose memory can I see", "who else is on this server" — and as the first answer when another /mem skill fails because nothing is configured yet, or before debugging why a search found nothing.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: ""
|
|
6
|
+
allowed-tools: Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /mem:house — what this is, whether it works, and who is around it
|
|
10
|
+
|
|
11
|
+
**Invoking this IS the request. Run the report and present it; there is nothing to
|
|
12
|
+
clarify.** Read-only throughout.
|
|
13
|
+
|
|
14
|
+
**Read `../reference/HOUSE.md` first** — connection, schema, and the epoch filter every
|
|
15
|
+
count below depends on.
|
|
16
|
+
|
|
17
|
+
## Nothing configured yet? Answer that instead
|
|
18
|
+
|
|
19
|
+
If the connection block finds no house, do not report an error — explain the product and
|
|
20
|
+
stop:
|
|
21
|
+
|
|
22
|
+
> **memhouse** gives your coding agents a memory. A local shipper parses your sessions
|
|
23
|
+
> from 17 editors — Claude Code, Codex, Cursor, Zed, Gemini CLI and more — and writes
|
|
24
|
+
> them as typed rows into a ClickHouse database **you own**. Nothing is embedded or
|
|
25
|
+
> summarised; the transcripts are kept verbatim, and the house outlives the local files,
|
|
26
|
+
> which most editors delete after about 30 days.
|
|
27
|
+
>
|
|
28
|
+
> Then any agent can read it: `/mem:recall` answers from your own history instead of
|
|
29
|
+
> starting from zero.
|
|
30
|
+
>
|
|
31
|
+
> To set it up: `npm install -g memhouse` then `memhouse onboard`. No ClickHouse yet?
|
|
32
|
+
> `memhouse deploy --local` stands one up bound to loopback. Someone invited you?
|
|
33
|
+
> `memhouse install --env invite-<you>.env`.
|
|
34
|
+
|
|
35
|
+
## The report
|
|
36
|
+
|
|
37
|
+
Lead with the one-line verdict — *working and current*, *working but stale*, or *not
|
|
38
|
+
shipping* — then the detail.
|
|
39
|
+
|
|
40
|
+
**1. Identity and reach.** `memhouse whoami` — who this credential is and what it may do.
|
|
41
|
+
`memhouse status --json` — daemons, connection, counts, freshness. Prefer these over
|
|
42
|
+
hand-rolled queries; they already know the schema.
|
|
43
|
+
|
|
44
|
+
**2. What the house holds**, per editor. Epoch filter required or every number is inflated:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
SELECT source,
|
|
48
|
+
uniqExact(session_id) AS sessions,
|
|
49
|
+
count() AS messages,
|
|
50
|
+
sum(input_tokens + output_tokens + cache_read_tokens + cache_write_tokens) AS tokens,
|
|
51
|
+
formatDateTime(max(ts), '%Y-%m-%d %H:%i') AS newest
|
|
52
|
+
FROM messages
|
|
53
|
+
WHERE origin != 'ship'
|
|
54
|
+
OR (session_id, user_id, epoch) IN (
|
|
55
|
+
SELECT session_id, user_id, max(epoch) FROM messages
|
|
56
|
+
WHERE origin = 'ship' GROUP BY session_id, user_id)
|
|
57
|
+
GROUP BY source ORDER BY messages DESC
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**A source shipping zero tokens is not free — it is unmeasured.** `kiro`, `vscode`, `zed`
|
|
61
|
+
and one of `cursor`'s two storage paths extract no usage, so their cost reads `$0`. Say
|
|
62
|
+
"unknown", never "$0", and point at `memhouse doctor`.
|
|
63
|
+
|
|
64
|
+
**3. Which machines write here**, and whether this one is among them:
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
SELECT user_id AS member, host AS machine, count() AS rows,
|
|
68
|
+
formatDateTime(max(ingested_at), '%Y-%m-%d %H:%i') AS last_write
|
|
69
|
+
FROM messages GROUP BY user_id, host ORDER BY last_write DESC
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A machine silent for days is the usual reason a search finds nothing. Note that the
|
|
73
|
+
*fleet heartbeat* records that a pass CONNECTED, not that rows landed — so treat
|
|
74
|
+
`last_write` from the data above as the truth, and be sceptical of a healthy-looking
|
|
75
|
+
heartbeat beside a stale `last_write`.
|
|
76
|
+
|
|
77
|
+
**4. Who can read your house, and whose you can read.** Best-effort by design — ClickHouse
|
|
78
|
+
shows a credential only what it may see, so on someone else's server sections come back
|
|
79
|
+
thin. Say what could not be seen rather than guessing.
|
|
80
|
+
|
|
81
|
+
```sql
|
|
82
|
+
-- houses your credential can reach; anything that is not yours was shared with you
|
|
83
|
+
SHOW DATABASES
|
|
84
|
+
-- who you have granted (memhouse's own record of it)
|
|
85
|
+
SELECT substring(key, 7) AS user, value AS state
|
|
86
|
+
FROM meta FINAL WHERE key LIKE 'share:%' ORDER BY key
|
|
87
|
+
-- everyone on the server (names only — every member holds SHOW USERS)
|
|
88
|
+
SELECT name FROM system.users ORDER BY name
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
That `meta` list is memhouse's own note-keeping, not ClickHouse's grant table —
|
|
92
|
+
a member cannot read `system.grants`. Present it as "what memhouse recorded", and say
|
|
93
|
+
that a grant made by hand would not appear.
|
|
94
|
+
|
|
95
|
+
## Closing
|
|
96
|
+
|
|
97
|
+
End with the one useful next step, not a menu: reopen a session (`memhouse resume <id>`),
|
|
98
|
+
fix a stale machine (`memhouse update` there), start shipping (`memhouse onboard`), or
|
|
99
|
+
bring someone in (`/mem:access`). If everything is healthy, say so in one line and stop.
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: recall
|
|
3
|
+
description: Retrieve from past agent conversations and answer from them — search every session ever shipped from all 17 editors (Claude Code, Codex, Cursor, VS Code, Zed, Gemini CLI, …) across every project and machine, read the actual transcripts, and answer with citations. Use whenever the user refers to something not in the current context - "how did I fix X last time", "what approach did we settle on", "why did we choose Z", "find that conversation about Y", "did I ever try W", "what was I working on", "show my recent sessions", "sessions from last week", "remind me how I did it". Reach for this BEFORE saying you do not know about prior work. Also reads a housemate's memory when they name it ("how did yigit fix X"). For numbers rather than transcripts — spend, model mix, tool rankings — use /mem:sql.
|
|
4
|
+
user-invocable: true
|
|
5
|
+
argument-hint: "<question, terms, or 'recent sessions'>"
|
|
6
|
+
allowed-tools: Bash
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
# /mem:recall — find it, read it, answer from it
|
|
10
|
+
|
|
11
|
+
The house is the archive that OUTLIVES the transcripts on disk. Claude Code compacts and
|
|
12
|
+
deletes local sessions, so for anything older than ~30 days this is the only place the
|
|
13
|
+
answer still exists.
|
|
14
|
+
|
|
15
|
+
**The synthesis is yours; the store is not.** memhouse runs no LLM in its write or read
|
|
16
|
+
path — this skill retrieves rows and YOU read them. Never present a guess as a retrieval:
|
|
17
|
+
if the house has nothing, say so.
|
|
18
|
+
|
|
19
|
+
**Read `../reference/HOUSE.md` first** — connection, schema, and the three traps
|
|
20
|
+
(epoch filtering, `(session_id, user_id)` joins, escaping). Every query below assumes it.
|
|
21
|
+
|
|
22
|
+
## Which shape is being asked for
|
|
23
|
+
|
|
24
|
+
One skill, three depths. Pick by what the user wants back, not by which words they used.
|
|
25
|
+
|
|
26
|
+
- **A question** ("how did I fix X") → retrieve, read the turns, answer with citations.
|
|
27
|
+
- **A conversation** ("find the chat about Y") → locate sessions, describe them, offer
|
|
28
|
+
to read one.
|
|
29
|
+
- **An orientation** ("what was I working on") → list recent sessions and stop.
|
|
30
|
+
|
|
31
|
+
When it is ambiguous, do the deepest one that fits — an answer with citations also tells
|
|
32
|
+
them which session it was, whereas a bare list does not answer anything.
|
|
33
|
+
|
|
34
|
+
## Answering a question
|
|
35
|
+
|
|
36
|
+
**1. Extract terms that would appear in the ANSWER, not the question.** "How did I fix
|
|
37
|
+
the ClickHouse auth error?" → `ClickHouse auth`, `Authentication failed`, `ACCESS_DENIED`.
|
|
38
|
+
Error strings beat paraphrases; identifiers beat prose. Run 2–3 variants — and search
|
|
39
|
+
each term SEPARATELY, because the columns hold text, not concepts: a needle of
|
|
40
|
+
`clickhouse ttl` matches only that exact adjacency. On a real house `%clickhouse ttl%`
|
|
41
|
+
found 8 messages where `%clickhouse%` AND `%ttl%` found 1,461.
|
|
42
|
+
|
|
43
|
+
```sql
|
|
44
|
+
SELECT session_id, user_id, any(folder) AS folder,
|
|
45
|
+
formatDateTime(max(ts), '%Y-%m-%d') AS day, count() AS hits,
|
|
46
|
+
substring(anyIf(text, positionCaseInsensitive(text, 'NEEDLE') > 0), 1, 300) AS sample
|
|
47
|
+
FROM messages
|
|
48
|
+
WHERE text_ngram LIKE '%needle%'
|
|
49
|
+
AND (origin != 'ship'
|
|
50
|
+
OR (session_id, user_id, epoch) IN (
|
|
51
|
+
SELECT session_id, user_id, max(epoch) FROM messages
|
|
52
|
+
WHERE origin = 'ship' GROUP BY session_id, user_id))
|
|
53
|
+
GROUP BY session_id, user_id
|
|
54
|
+
ORDER BY hits DESC, max(ts) DESC
|
|
55
|
+
LIMIT 8
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
`anyIf(...)` so the sample is a line that actually matched — `any(text)` returns an
|
|
59
|
+
arbitrary message from the session and misled two readers in three. **Order by `hits`,
|
|
60
|
+
not just recency**: a session mentioning the term 197 times outranks yesterday's passing
|
|
61
|
+
reference.
|
|
62
|
+
|
|
63
|
+
**2. Read the strongest sessions — the turns, not the hit line.** Pull a window around
|
|
64
|
+
the match so you see the resolution, not the complaint:
|
|
65
|
+
|
|
66
|
+
```sql
|
|
67
|
+
SELECT seq, role, substring(text, 1, 2000) AS text
|
|
68
|
+
FROM messages
|
|
69
|
+
WHERE session_id = '<sid>' AND user_id = '<uid>'
|
|
70
|
+
AND seq BETWEEN <hit_seq - 3> AND <hit_seq + 12>
|
|
71
|
+
ORDER BY seq
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Apply the epoch filter here too — an unfiltered read of a compacted session interleaves
|
|
75
|
+
two parses of the same conversation.
|
|
76
|
+
|
|
77
|
+
**3. Answer, then cite.** Lead with the answer. Under it list the sessions it came from —
|
|
78
|
+
`session_id`, date, project — so the user can reopen one (`memhouse resume <session_id>`
|
|
79
|
+
prints the command). Quote decisive lines verbatim where exact wording matters: an error
|
|
80
|
+
string, a command, a config value.
|
|
81
|
+
|
|
82
|
+
**4. Nothing found is an answer.** Say what you searched and where coverage ends
|
|
83
|
+
(`SELECT min(ts), max(ts), count() FROM messages`) rather than padding. Check the obvious
|
|
84
|
+
causes first: wrong case (the columns are lowercased), a phrase that never occurred
|
|
85
|
+
adjacently, or a house that simply has not shipped that project.
|
|
86
|
+
|
|
87
|
+
## Finding a conversation
|
|
88
|
+
|
|
89
|
+
Same first query, then describe what you found — project, editor, date, how many hits,
|
|
90
|
+
one representative line each. Offer to read the strongest. Do not dump ten rows and stop.
|
|
91
|
+
|
|
92
|
+
## Listing recent sessions
|
|
93
|
+
|
|
94
|
+
`memhouse sessions-query` prints a self-contained rollup (it carries its own `FINAL`);
|
|
95
|
+
prefer it over hand-joining. For a plain list:
|
|
96
|
+
|
|
97
|
+
```sql
|
|
98
|
+
SELECT session_id, name, source, host, project,
|
|
99
|
+
formatDateTime(started_at, '%Y-%m-%d %H:%i') AS started, message_count
|
|
100
|
+
FROM sessions
|
|
101
|
+
ORDER BY started_at DESC
|
|
102
|
+
LIMIT 20
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Narrow with `WHERE project = '…'`, `source = 'cursor'`, `host = '…'`, or a date range as
|
|
106
|
+
asked. In a shared house every member's sessions are here — add `AND user_id = '<name>'`
|
|
107
|
+
only when the user asks for one person's.
|
|
108
|
+
|
|
109
|
+
## What can bite
|
|
110
|
+
|
|
111
|
+
Beyond the three in the reference:
|
|
112
|
+
|
|
113
|
+
- **Ranking is yours.** There is no relevance score in the store. `hits` plus recency is
|
|
114
|
+
the whole of it — say so rather than implying the top row is "the best match".
|
|
115
|
+
- **Tool *results* are not stored.** "What did that command print" is unanswerable from
|
|
116
|
+
the house; only the invocation and its arguments survive.
|
|
117
|
+
- **`memhouse search`** exists as a CLI, but it joins your terms into ONE literal phrase
|
|
118
|
+
and orders by date alone. Prefer the SQL above; reach for the CLI only for a quick
|
|
119
|
+
one-word look.
|
|
@@ -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,16 +15,18 @@ const GLOBAL_FLAGS = ['json', 'yes', 'help', 'version'];
|
|
|
15
15
|
const COMMAND_FLAGS = {
|
|
16
16
|
null: [], help: [], version: [],
|
|
17
17
|
whoami: ['admin'],
|
|
18
|
-
|
|
18
|
+
share: ['only', 'revoke', 'list'],
|
|
19
|
+
discover: [], stats: [], stop: [], doctor: [], 'sessions-query': [], rooms: [], members: ['db','admin-user','admin-password'],
|
|
19
20
|
status: [], start: [],
|
|
20
21
|
onboard: ['url', 'user', 'password', 'db', 'port', 'no-ship', 'target', 'house-port', 'tag', 'local'],
|
|
21
22
|
install: ['url', 'user', 'password', 'db', 'port', 'no-ship', 'env', 'print-sql', 'member',
|
|
22
|
-
'member-password', 'admin-user', 'admin-password', 'ensure-schema', 'rotate-password', 'force'
|
|
23
|
-
|
|
23
|
+
'member-password', 'admin-user', 'admin-password', 'ensure-schema', 'rotate-password', 'force',
|
|
24
|
+
'editors', 'claude-roots', 'channel'],
|
|
25
|
+
setup: ['url', 'user', 'password', 'db', 'port', 'admin-user', 'admin-password', 'editors', 'claude-roots', 'channel'],
|
|
24
26
|
ship: ['full', 'loop', 'ensure-schema'],
|
|
25
27
|
search: ['limit'],
|
|
26
28
|
resume: [],
|
|
27
|
-
update: ['check', 'migrate', 'no-install'],
|
|
29
|
+
update: ['check', 'migrate', 'no-install', 'channel'],
|
|
28
30
|
reset: ['all-origins'],
|
|
29
31
|
invite: ['url', 'db', 'out', 'admin-user', 'admin-password', 'adopt', 'print-sql',
|
|
30
32
|
'allow-local', 'member-password'],
|
package/memhouse/house/HOUSE.md
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
# The house — a database,
|
|
1
|
+
# The house — a database, and the rooms in it
|
|
2
2
|
|
|
3
3
|
**A house is a ClickHouse database.** Any database, named whatever its people name it —
|
|
4
|
-
`polat`, `team_a`, even `default`. Its rooms are
|
|
5
|
-
`
|
|
6
|
-
Merge rooms, no views, no settings profile.
|
|
4
|
+
`polat`, `team_a`, even `default`. Its rooms are tables: `sessions`, `messages`,
|
|
5
|
+
`tool_calls`. No views, no Merge rooms, no settings profile.
|
|
7
6
|
|
|
8
|
-
**
|
|
9
|
-
|
|
7
|
+
**Every member's rooms are named for them, and one grant covers them.** `mem.polat_*`
|
|
8
|
+
for polat, `mem.alice_*` for alice, in the same database — `mem` unless somebody has a
|
|
9
|
+
reason. The grant is `GRANT … ON mem.polat_* TO polat WITH GRANT OPTION`: it lets the
|
|
10
|
+
member create and rebuild their own rooms, share them, and reach nothing else. Nobody is
|
|
11
|
+
granted the database itself. A house with one member is a house of one, not a different
|
|
12
|
+
kind of house — a colleague joins as one more member and nothing about the first changes.
|
|
13
|
+
`roomNames()` in `house.js` is the only place a table name is produced.
|
|
14
|
+
|
|
15
|
+
Two columns say where every row came from:
|
|
10
16
|
|
|
11
17
|
| Column | Meaning | Comes from |
|
|
12
18
|
|---|---|---|
|
|
@@ -31,9 +37,17 @@ shipper create and evolve the rooms (`ship.js --ensure-schema`) — no admin has
|
|
|
31
37
|
memhouse code. Joining a ClickHouse that happens to run a kernel is the same three
|
|
32
38
|
statements: a database and a credential, nothing else.
|
|
33
39
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
40
|
+
**Housemates do not have to trust each other with the house.** They used to: everyone
|
|
41
|
+
held `ALL` on the database, and the isolation mechanism between parties who should not
|
|
42
|
+
see each other was a separate house. That shape leaked — one member could `GRANT SELECT
|
|
43
|
+
ON db.* TO <outsider>` and hand over a housemate's transcripts, with no admin and no
|
|
44
|
+
notification.
|
|
45
|
+
|
|
46
|
+
In a shared house each member is granted their own rooms and nothing else, so isolation
|
|
47
|
+
lives INSIDE one house and is checkable: `SHOW GRANTS` lists what you hold, and `SHOW
|
|
48
|
+
TABLES` does not list what you do not. A separate house is still the boundary between
|
|
49
|
+
groups that should not even know of each other — names leak across a shared house even
|
|
50
|
+
when content does not.
|
|
37
51
|
|
|
38
52
|
## One user on two machines, same session id
|
|
39
53
|
|
|
@@ -91,9 +105,9 @@ for writes and DDL.
|
|
|
91
105
|
|
|
92
106
|
Two tables that are not rooms:
|
|
93
107
|
|
|
94
|
-
- `
|
|
108
|
+
- `meta` — house-wide key/value, latest-wins: `schema_version`, and the memhouse
|
|
95
109
|
version each member last shipped with.
|
|
96
|
-
- `
|
|
110
|
+
- `events` — append-only: migrations (`pending` → `applied` | `failed`), version
|
|
97
111
|
changes, schema observations, each with actor, host and row counts. Nothing is updated
|
|
98
112
|
in place, so a migration that failed and was retried reads as exactly that.
|
|
99
113
|
|