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