memhouse 0.18.9 → 0.18.11
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/AGENTS.md +194 -0
- package/CHANGELOG.md +1097 -0
- package/README.md +54 -423
- package/SECURITY.md +68 -0
- package/bin/memhouse.js +299 -133
- package/docs/README.md +46 -0
- package/docs/guides/admin-password.md +95 -0
- package/docs/guides/configuration.md +109 -0
- package/docs/guides/daemons.md +53 -0
- package/docs/guides/install.md +128 -0
- package/docs/guides/invite.md +87 -0
- package/docs/guides/operate.md +112 -0
- package/docs/guides/search-and-resume.md +95 -0
- package/docs/guides/share.md +102 -0
- package/docs/guides/storage.md +93 -0
- package/docs/guides/troubleshooting.md +78 -0
- package/docs/guides/uninstall.md +37 -0
- package/docs/guides/update.md +68 -0
- package/docs/releases/README.md +11 -0
- package/docs/releases/v0.18.10.md +226 -0
- package/docs/releases/v0.18.11.md +191 -0
- package/docs/tutorials/01-first-house.md +132 -0
- package/docs/tutorials/02-join-a-house.md +91 -0
- package/docs/tutorials/03-share-your-memory.md +96 -0
- package/editors/claude.js +68 -10
- package/examples/01-local-one-machine/README.md +99 -0
- package/examples/01-local-one-machine/run.sh +61 -0
- package/examples/02-existing-clickhouse/README.md +121 -0
- package/examples/02-existing-clickhouse/run.sh +71 -0
- package/examples/03-team-house/README.md +129 -0
- package/examples/03-team-house/run.sh +124 -0
- package/examples/04-self-hosted-team/.env.example +22 -0
- package/examples/04-self-hosted-team/README.md +187 -0
- package/examples/04-self-hosted-team/compose.yml +158 -0
- package/examples/04-self-hosted-team/smoke.sh +113 -0
- package/examples/README.md +42 -0
- package/examples/fixtures/README.md +24 -0
- package/examples/fixtures/make-sessions.js +63 -0
- package/memhouse/DESIGN.md +9 -2
- package/memhouse/admin-secret.js +81 -0
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +1 -1
- package/memhouse/delivery/plugin/reference/HOUSE.md +18 -6
- package/memhouse/delivery/plugin/skills/access/SKILL.md +1 -1
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +1 -1
- package/memhouse/delivery/plugin/skills/house/SKILL.md +1 -1
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +34 -14
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +1 -1
- package/memhouse/deploy.js +4 -2
- package/memhouse/flags.js +32 -5
- package/memhouse/host.js +11 -6
- package/memhouse/house/HOUSE.md +1 -1
- package/memhouse/house/house.js +27 -1
- package/memhouse/invite-target.js +37 -0
- package/memhouse/provision.js +12 -3
- package/memhouse/server/queries.js +9 -4
- package/memhouse/service.js +5 -0
- package/memhouse/shipper/ship.js +148 -31
- package/package.json +10 -2
- package/public/THIRD_PARTY_NOTICES.txt +2114 -0
- package/ui/vite.config.js +4 -1
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Tutorial 3: share your memory
|
|
2
|
+
|
|
3
|
+
Two members of one house: `alice` and `bob`. Each owns their rooms, and neither can read
|
|
4
|
+
the other's. In this tutorial, alice lets bob read one project's history, then everything,
|
|
5
|
+
then takes it back. Bob reads it from his own agent.
|
|
6
|
+
|
|
7
|
+
**You need:** two members of the same house. If you have one house of your own, see
|
|
8
|
+
[Invite members](../guides/invite.md) to add a second.
|
|
9
|
+
|
|
10
|
+
## 1. Alice shares one project
|
|
11
|
+
|
|
12
|
+
On alice's machine:
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
memhouse share bob --only project=memhouse
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
What this does:
|
|
19
|
+
|
|
20
|
+
- grants bob `SELECT` on exactly three of alice's rooms: `alice_sessions`,
|
|
21
|
+
`alice_messages` and `alice_tool_calls`;
|
|
22
|
+
- adds row policies so that, in those rooms, bob sees only rows where
|
|
23
|
+
`project = 'memhouse'`.
|
|
24
|
+
|
|
25
|
+
Bob gets nothing else. Alice's stats tables, `meta` and `events` stay closed to him, as
|
|
26
|
+
does any room alice gains later.
|
|
27
|
+
|
|
28
|
+
Other scopes work the same way:
|
|
29
|
+
|
|
30
|
+
- `session=…`
|
|
31
|
+
- `host=…`
|
|
32
|
+
- `source=…` (an editor, as stored: `claude-code`, `codex`, …)
|
|
33
|
+
- `folder=…`
|
|
34
|
+
- `since=…` and `until=…` (dates)
|
|
35
|
+
|
|
36
|
+
Before it creates the first policy, `share` checks how the server treats readers that
|
|
37
|
+
have no policy of their own: alice, and anyone with a full share. On some server
|
|
38
|
+
configurations, a table's first row policy blinds those readers to every row. On such a
|
|
39
|
+
server, `share` refuses rather than lock alice out of her own rooms.
|
|
40
|
+
|
|
41
|
+
## 2. Bob reads it
|
|
42
|
+
|
|
43
|
+
Bob's credential can now see three of alice's rooms. From any ClickHouse client, or
|
|
44
|
+
`/mem:sql` in bob's agent:
|
|
45
|
+
|
|
46
|
+
```sql
|
|
47
|
+
SELECT project, count() FROM mem.alice_messages GROUP BY project
|
|
48
|
+
-- one row: memhouse
|
|
49
|
+
|
|
50
|
+
SELECT count() FROM mem.alice_session_stats
|
|
51
|
+
-- refused: bob holds no grant on it
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
A query that strays outside the scope returns nothing, or is refused for the tables bob
|
|
55
|
+
was never granted. It never returns more than the scope.
|
|
56
|
+
|
|
57
|
+
## 3. Alice widens it to everything
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
memhouse share bob
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
A **full share** with no `--only`. Bob can now read all of `mem.alice_*`, including the
|
|
64
|
+
stats, `meta` (alice's other share records among it) and `events`. The command says so
|
|
65
|
+
when it runs. Use a full share for someone you would hand your laptop to.
|
|
66
|
+
|
|
67
|
+
Going back from full to scoped is the same `--only` command. It takes the wildcard grant
|
|
68
|
+
back first, so a failure part-way leaves bob with less access, never more.
|
|
69
|
+
|
|
70
|
+
## 4. See who can read what
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
memhouse share --list
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
This prints memhouse's own record of the shares: each user, full or scoped, and the scope.
|
|
77
|
+
A grant someone made by hand in SQL does not appear.
|
|
78
|
+
|
|
79
|
+
## 5. Take it back
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
memhouse share bob --revoke
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
This withdraws the grants first, then drops the row policies. If the revoke is refused
|
|
86
|
+
part-way, the policies stay, so bob never ends up with an unfiltered share.
|
|
87
|
+
|
|
88
|
+
## What sharing cannot do
|
|
89
|
+
|
|
90
|
+
- **Recall what was already read.** Bob may have copied rows while he could read them.
|
|
91
|
+
- **Write.** A share is read-only. Only alice writes into alice's rooms.
|
|
92
|
+
- **End with a password change.** Grants belong to the user, not the password. To cut
|
|
93
|
+
bob off, revoke.
|
|
94
|
+
|
|
95
|
+
More detail, including what a full share exposes and why scoped shares are table by
|
|
96
|
+
table: [Share and revoke](../guides/share.md).
|
package/editors/claude.js
CHANGED
|
@@ -74,13 +74,18 @@ function getChats() {
|
|
|
74
74
|
} catch { /* no index */ }
|
|
75
75
|
|
|
76
76
|
// Scan all .jsonl files on disk (some may not be in the index)
|
|
77
|
-
let
|
|
78
|
-
try {
|
|
77
|
+
let names;
|
|
78
|
+
try { names = fs.readdirSync(dir); } catch { continue; }
|
|
79
|
+
const files = names.filter(f => f.endsWith('.jsonl'));
|
|
80
|
+
// A session with subagents also has a directory named for it; one without has none, so
|
|
81
|
+
// this listing is how the mtime check below stays free for the sessions it cannot help.
|
|
82
|
+
const sessionDirs = new Set(names.filter(f => !f.endsWith('.jsonl')));
|
|
79
83
|
|
|
80
84
|
for (const file of files) {
|
|
81
85
|
const sessionId = file.replace('.jsonl', '');
|
|
82
86
|
const fullPath = path.join(dir, file);
|
|
83
87
|
const entry = indexed.get(sessionId);
|
|
88
|
+
const subTouched = sessionDirs.has(sessionId) ? newestSubagentMtime(path.join(dir, sessionId, 'subagents')) : 0;
|
|
84
89
|
|
|
85
90
|
if (entry) {
|
|
86
91
|
// Use index metadata
|
|
@@ -89,7 +94,7 @@ function getChats() {
|
|
|
89
94
|
composerId: sessionId,
|
|
90
95
|
name: cleanPrompt(entry.firstPrompt),
|
|
91
96
|
createdAt: entry.created ? new Date(entry.created).getTime() : null,
|
|
92
|
-
lastUpdatedAt: entry.modified ? new Date(entry.modified).getTime() : null,
|
|
97
|
+
lastUpdatedAt: latest(entry.modified ? new Date(entry.modified).getTime() : null, subTouched),
|
|
93
98
|
mode: 'claude',
|
|
94
99
|
folder: entry.projectPath || decodedFolder,
|
|
95
100
|
encrypted: false,
|
|
@@ -107,7 +112,7 @@ function getChats() {
|
|
|
107
112
|
composerId: sessionId,
|
|
108
113
|
name: meta.firstPrompt ? cleanPrompt(meta.firstPrompt) : null,
|
|
109
114
|
createdAt: meta.timestamp || stat.birthtime.getTime(),
|
|
110
|
-
lastUpdatedAt: stat.mtime.getTime(),
|
|
115
|
+
lastUpdatedAt: latest(stat.mtime.getTime(), subTouched),
|
|
111
116
|
mode: 'claude',
|
|
112
117
|
folder: meta.cwd || decodedFolder,
|
|
113
118
|
encrypted: false,
|
|
@@ -195,7 +200,7 @@ function cleanPrompt(prompt) {
|
|
|
195
200
|
// Parse one Claude session .jsonl into the adapter's message shape. When
|
|
196
201
|
// `isSubagent` is set, each message is tagged `[subagent]` so folded subagent turns
|
|
197
202
|
// are clearly attributed in the transcript.
|
|
198
|
-
function parseSessionFile(filePath, isSubagent, agent = null) {
|
|
203
|
+
function parseSessionFile(filePath, isSubagent, agent = null, info = null) {
|
|
199
204
|
const messages = [];
|
|
200
205
|
let lines;
|
|
201
206
|
try { lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean); } catch { return messages; }
|
|
@@ -205,7 +210,9 @@ function parseSessionFile(filePath, isSubagent, agent = null) {
|
|
|
205
210
|
// cited, or told apart from a sibling's. An agent looking for the fork as its own session
|
|
206
211
|
// concluded it was never shipped. Every turn now carries the agent's id, the description
|
|
207
212
|
// the parent gave it, and its position within that subagent — in `extra`, which is not
|
|
208
|
-
// part of the line hash
|
|
213
|
+
// part of the line hash. The position is not only a label any more: the shipper numbers
|
|
214
|
+
// a subagent's rows from it (seq = block + turn, see subagentSeq in ship.js), so `turn`
|
|
215
|
+
// must count exactly the turns this parse keeps, in order.
|
|
209
216
|
let turn = 0;
|
|
210
217
|
const stamp = (m) => { if (agent) { m._agent = { ...agent, turn: turn++ }; } return m; };
|
|
211
218
|
|
|
@@ -228,6 +235,7 @@ function parseSessionFile(filePath, isSubagent, agent = null) {
|
|
|
228
235
|
// time simply omit it.
|
|
229
236
|
const _ts = obj.timestamp ? Date.parse(obj.timestamp) : undefined;
|
|
230
237
|
const at = Number.isFinite(_ts) ? _ts : undefined;
|
|
238
|
+
if (info && info.firstTs === undefined && at !== undefined) info.firstTs = at;
|
|
231
239
|
|
|
232
240
|
if (obj.type === 'user' && obj.message) {
|
|
233
241
|
const content = extractContent(obj.message.content);
|
|
@@ -249,6 +257,10 @@ function parseSessionFile(filePath, isSubagent, agent = null) {
|
|
|
249
257
|
return messages;
|
|
250
258
|
}
|
|
251
259
|
|
|
260
|
+
// How long a subagent transcript without a single timestamped line may hold back its
|
|
261
|
+
// session (see getMessages). A real one gets its first line within seconds.
|
|
262
|
+
const SUBAGENT_STAMP_GRACE_MS = 10 * 60 * 1000;
|
|
263
|
+
|
|
252
264
|
function getMessages(chat) {
|
|
253
265
|
const filePath = chat._fullPath;
|
|
254
266
|
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
@@ -262,30 +274,76 @@ function getMessages(chat) {
|
|
|
262
274
|
const subagentsDir = path.join(filePath.replace(/\.jsonl$/, ''), 'subagents');
|
|
263
275
|
const subFiles = subagentFiles(subagentsDir);
|
|
264
276
|
const named = subFiles.length ? subagentNames(filePath) : new Map();
|
|
277
|
+
const folded = [];
|
|
278
|
+
let unstamped = null;
|
|
265
279
|
for (const { file, workflow } of subFiles) {
|
|
266
280
|
const id = path.basename(file).replace(/^agent-/, '').replace(/\.jsonl$/, '');
|
|
267
281
|
const meta = named.get(id) || {};
|
|
268
282
|
const agent = { id, description: meta.description || '', type: meta.type || (workflow ? 'workflow-subagent' : ''), file: path.basename(file) };
|
|
269
283
|
if (workflow) agent.workflow = workflow;
|
|
270
|
-
const
|
|
284
|
+
const info = {};
|
|
285
|
+
const turns = parseSessionFile(file, true, agent, info);
|
|
271
286
|
// A fork the parent never named (a Workflow-run agent, or a parent from before the
|
|
272
287
|
// ids were reported) is described by its own first prompt — what it was told to do.
|
|
273
288
|
if (!agent.description) {
|
|
274
289
|
const first = turns.find((m) => m.role === 'user');
|
|
275
290
|
if (first) { const d = String(first.content).replace(/^\[subagent\] /, '').replace(/\s+/g, ' ').trim().slice(0, 160); for (const m of turns) m._agent.description = d; }
|
|
276
291
|
}
|
|
277
|
-
|
|
292
|
+
folded.push({ key: path.relative(subagentsDir, file), firstTs: info.firstTs, turns });
|
|
293
|
+
// A subagent caught before its first line is written has no start time, so it ranks
|
|
294
|
+
// last — and when its first line lands with an EARLIER timestamp than a sibling's, it
|
|
295
|
+
// moves, renumbers every sibling after it, and the session forks and re-ships whole
|
|
296
|
+
// (GLM-F1). So a fresh one defers the whole session to the next pass, when it has a
|
|
297
|
+
// time. Bounded by age: a file that never gets a timestamped line stops deferring after
|
|
298
|
+
// SUBAGENT_STAMP_GRACE_MS and ranks last, as before.
|
|
299
|
+
if (info.firstTs === undefined && !unstamped) {
|
|
300
|
+
let age = Infinity;
|
|
301
|
+
try { age = Date.now() - fs.statSync(file).mtimeMs; } catch { /* gone: nothing to wait for */ }
|
|
302
|
+
if (age < SUBAGENT_STAMP_GRACE_MS) unstamped = path.relative(subagentsDir, file);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
// Each subagent gets a SLOT, and the shipper numbers its rows from that slot rather than
|
|
306
|
+
// from where they happen to land in this array (see subagentSeq in ship.js). The slot is
|
|
307
|
+
// the subagent's rank by when it started — the timestamp on its transcript's first line —
|
|
308
|
+
// because that is the one order a new subagent cannot disturb: it started after every
|
|
309
|
+
// subagent already on disk, so it ranks last. The file-name order this used to fold in
|
|
310
|
+
// does not have that property: agent ids are random, so a new fork sorted into the middle
|
|
311
|
+
// and pushed every later subagent's rows down, and a session with subagents re-shipped
|
|
312
|
+
// whole on every pass (one 6,120-line session was stored 60 times in a day). A file with
|
|
313
|
+
// no timestamp yet ranks after every one that has one; the path breaks ties.
|
|
314
|
+
folded.sort((a, b) => (a.firstTs === undefined) - (b.firstTs === undefined)
|
|
315
|
+
|| (a.firstTs || 0) - (b.firstTs || 0) || (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
316
|
+
folded.forEach(({ turns }, slot) => { for (const m of turns) m._agent.slot = slot; messages.push(...turns); });
|
|
317
|
+
if (unstamped) {
|
|
318
|
+
Object.defineProperty(messages, '_defer', { value: `subagent ${unstamped} has no timestamped line yet`, enumerable: false });
|
|
278
319
|
}
|
|
279
320
|
|
|
280
321
|
return messages;
|
|
281
322
|
}
|
|
282
323
|
|
|
324
|
+
/**
|
|
325
|
+
* When a session was last written, counting its subagents. The shipper's incremental skip
|
|
326
|
+
* compares `lastUpdatedAt` with what the house recorded, and a subagent writes only its own
|
|
327
|
+
* file: a background agent that runs on after its parent goes quiet left the parent's mtime
|
|
328
|
+
* (and the index's `modified`) where they were, so its turns shipped at the parent's next
|
|
329
|
+
* write, or never. Stat calls only, and only for a session that has a directory.
|
|
330
|
+
*/
|
|
331
|
+
function newestSubagentMtime(subagentsDir) {
|
|
332
|
+
let newest = 0;
|
|
333
|
+
for (const { file } of subagentFiles(subagentsDir)) {
|
|
334
|
+
try { newest = Math.max(newest, fs.statSync(file).mtimeMs); } catch { /* vanished mid-pass */ }
|
|
335
|
+
}
|
|
336
|
+
return Math.floor(newest);
|
|
337
|
+
}
|
|
338
|
+
function latest(a, b) { return b && (!a || b > a) ? b : a; }
|
|
339
|
+
|
|
283
340
|
/**
|
|
284
341
|
* Every subagent transcript under a session, wherever Claude Code put it:
|
|
285
342
|
* <session>/subagents/agent-<id>.jsonl — Agent tool forks
|
|
286
343
|
* <session>/subagents/workflows/wf_<id>/agent-<id>.jsonl — Workflow-run agents
|
|
287
|
-
* journal.jsonl and *.meta.json are bookkeeping, not transcripts. Sorted
|
|
288
|
-
*
|
|
344
|
+
* journal.jsonl and *.meta.json are bookkeeping, not transcripts. Sorted so the listing is
|
|
345
|
+
* deterministic; the fold ORDER is set by start time in getMessages, not by this.
|
|
346
|
+
* On real data the workflow layer held 100 of 165 forks and shipped none.
|
|
289
347
|
*/
|
|
290
348
|
function subagentFiles(subagentsDir) {
|
|
291
349
|
const out = [];
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# 01 — one machine
|
|
2
|
+
|
|
3
|
+
The smallest memhouse: one person, one laptop, a ClickHouse in a local container. It shows
|
|
4
|
+
the whole loop — a house, a shipping pass, the rows in it, a search that finds a past
|
|
5
|
+
conversation — in about a minute.
|
|
6
|
+
|
|
7
|
+
## What it does
|
|
8
|
+
|
|
9
|
+
1. `memhouse deploy --local` starts ClickHouse in Docker or Podman, bound to `127.0.0.1`
|
|
10
|
+
only, with a generated password. It creates two accounts: an administrator, and **you**
|
|
11
|
+
as a member who holds `mem.<you>_*` and nothing else. Both credentials go into the
|
|
12
|
+
memhouse home's `env` file (mode 600); nothing is typed or shown.
|
|
13
|
+
2. It installs memhouse against that house and starts a background shipper.
|
|
14
|
+
3. `memhouse ship` runs one pass in the foreground.
|
|
15
|
+
4. `memhouse status` shows what the house holds; `memhouse search` finds a session.
|
|
16
|
+
|
|
17
|
+
The script ships two made-up sessions from [`../fixtures`](../fixtures/), never yours, and
|
|
18
|
+
works in its own home (`~/.memhouse-example-01`), so an install you already have is left
|
|
19
|
+
alone.
|
|
20
|
+
|
|
21
|
+
## Needs
|
|
22
|
+
|
|
23
|
+
- Node.js 24 or newer, and `npm install -g memhouse`
|
|
24
|
+
- Docker or Podman, and port 8123 free (or set `HOUSE_PORT`)
|
|
25
|
+
- No earlier `memhouse deploy --local` on this machine. The container and volume names are
|
|
26
|
+
fixed (`memhouse-clickhouse`, `memhouse-data`), so a second local house refuses to start.
|
|
27
|
+
If you have one, you already have this example's result; try [02](../02-existing-clickhouse/).
|
|
28
|
+
|
|
29
|
+
## Run
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
./run.sh
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
| Variable | Default | |
|
|
36
|
+
|---|---|---|
|
|
37
|
+
| `MEMHOUSE_BIN` | `memhouse` | the command to run, e.g. `node ~/memhouse/bin/memhouse.js` |
|
|
38
|
+
| `EXAMPLE_HOME` | `~/.memhouse-example-01` | this example's memhouse home |
|
|
39
|
+
| `HOUSE_PORT` | `8123` | the local port ClickHouse binds |
|
|
40
|
+
|
|
41
|
+
## What you should see
|
|
42
|
+
|
|
43
|
+
Trimmed from a run on a Linux VM with Docker, ClickHouse 26.7 and memhouse 0.18.9:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
== 1. a house: ClickHouse in a container, bound to loopback, plus the install
|
|
47
|
+
✓ ClickHouse starting via docker on http://localhost:8123 (loopback only)
|
|
48
|
+
✓ ClickHouse ready
|
|
49
|
+
✓ member 'polat' holds mem.polat_* and nothing else; 'memhouse_root' administers the house
|
|
50
|
+
✓ rooms for 'polat': polat_sessions, polat_messages, polat_tool_calls
|
|
51
|
+
✓ installed
|
|
52
|
+
✓ shipper started in the background (pid 617049) — loading your history now
|
|
53
|
+
|
|
54
|
+
== 2. one shipping pass
|
|
55
|
+
[memhouse] shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.7s
|
|
56
|
+
|
|
57
|
+
== 3. what the house holds
|
|
58
|
+
✓ connected: http://localhost:8123 / mem as polat
|
|
59
|
+
✓ house: 2 sessions, 6 messages (freshest ingest 2026-10-07 21:08:06 UTC)
|
|
60
|
+
✓ fleet: 1 active writer(s)
|
|
61
|
+
polat@testbed-7edeca38 0.18.9 last ship 0m ago
|
|
62
|
+
✓ shipper: running — daemon (pid 617049)
|
|
63
|
+
|
|
64
|
+
== 4. find a past conversation
|
|
65
|
+
claude-code:421460e4-5e66-4c1c-afa4-f1b198c28c25 claude-code alpha 2026-10-07 19:07 (1 hit)
|
|
66
|
+
The deploy fails with ClickHouse error ACCESS_DENIED on INSERT. Why?
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Three messages per session in the house, not four: the tool result is folded into the
|
|
70
|
+
tool call it answers.
|
|
71
|
+
|
|
72
|
+
To look around, start the dashboard in the example's home (pick another port if 4640 is
|
|
73
|
+
taken by a real install):
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
MEMHOUSE_HOME=~/.memhouse-example-01 MEMHOUSE_PORT=4641 memhouse start
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Teardown
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
./run.sh --teardown
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Stops the shipper (by the pid memhouse recorded), removes the container **and its data
|
|
86
|
+
volume**, and deletes the example's home.
|
|
87
|
+
|
|
88
|
+
## For real
|
|
89
|
+
|
|
90
|
+
The same thing with your own sessions is two commands, in your default home:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
npm install -g memhouse
|
|
94
|
+
memhouse deploy --local
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Every editor memhouse knows is read (`memhouse discover` lists what it finds first). Then
|
|
98
|
+
`memhouse start` for the dashboard and `memhouse plugins install claude` to give your agents
|
|
99
|
+
`/mem:recall` and the other skills.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Example 01 — one person, one machine.
|
|
3
|
+
#
|
|
4
|
+
# A house in a local container, two made-up sessions shipped into it, then read back.
|
|
5
|
+
# Runs in its own memhouse home, so an install you already have is never touched.
|
|
6
|
+
#
|
|
7
|
+
# ./run.sh build it and show it
|
|
8
|
+
# ./run.sh --teardown stop the shipper, remove the container and its volume, delete the home
|
|
9
|
+
#
|
|
10
|
+
# Environment (all optional):
|
|
11
|
+
# MEMHOUSE_BIN the memhouse command (default: memhouse)
|
|
12
|
+
# EXAMPLE_HOME where this example's install lives (default: ~/.memhouse-example-01)
|
|
13
|
+
# HOUSE_PORT the local port ClickHouse binds (default: 8123)
|
|
14
|
+
set -euo pipefail
|
|
15
|
+
|
|
16
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
17
|
+
MH=${MEMHOUSE_BIN:-memhouse}
|
|
18
|
+
mh() { $MH "$@"; }
|
|
19
|
+
|
|
20
|
+
# Only this example's home speaks. An exported credential from another install would
|
|
21
|
+
# otherwise outrank the home's own env file (flags > MEMHOUSE_* > env file).
|
|
22
|
+
unset MEMHOUSE_URL MEMHOUSE_USER MEMHOUSE_PASSWORD MEMHOUSE_DB MEMHOUSE_ADMIN_USER MEMHOUSE_ADMIN_PASSWORD
|
|
23
|
+
export MEMHOUSE_HOME=${EXAMPLE_HOME:-$HOME/.memhouse-example-01}
|
|
24
|
+
HOUSE_PORT=${HOUSE_PORT:-8123}
|
|
25
|
+
|
|
26
|
+
if [ "${1:-}" = "--teardown" ]; then
|
|
27
|
+
mh stop || true
|
|
28
|
+
mh deploy --down --yes
|
|
29
|
+
rm -rf "$MEMHOUSE_HOME"
|
|
30
|
+
echo "example 01 removed"
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
mkdir -p "$MEMHOUSE_HOME"
|
|
35
|
+
|
|
36
|
+
# Made-up sessions, and nothing else: the shipper reads only this directory.
|
|
37
|
+
FIX="$MEMHOUSE_HOME/example-sessions"
|
|
38
|
+
if [ ! -d "$FIX/projects" ]; then
|
|
39
|
+
echo "== writing two made-up Claude Code sessions to $FIX"
|
|
40
|
+
node "$HERE/../fixtures/make-sessions.js" "$FIX" me alpha beta
|
|
41
|
+
fi
|
|
42
|
+
export MEMHOUSE_EDITORS=claude MEMHOUSE_CLAUDE_ROOTS="$FIX"
|
|
43
|
+
|
|
44
|
+
echo "== 1. a house: ClickHouse in a container, bound to loopback, plus the install"
|
|
45
|
+
mh deploy --local --house-port "$HOUSE_PORT"
|
|
46
|
+
|
|
47
|
+
echo
|
|
48
|
+
echo "== 2. one shipping pass (deploy also started a background shipper; this one is in the foreground)"
|
|
49
|
+
mh ship
|
|
50
|
+
|
|
51
|
+
echo
|
|
52
|
+
echo "== 3. what the house holds"
|
|
53
|
+
mh status
|
|
54
|
+
|
|
55
|
+
echo
|
|
56
|
+
echo "== 4. find a past conversation"
|
|
57
|
+
mh search ACCESS_DENIED
|
|
58
|
+
|
|
59
|
+
echo
|
|
60
|
+
echo "Done. Open the dashboard: MEMHOUSE_HOME=$MEMHOUSE_HOME $MH start (then http://localhost:4640)"
|
|
61
|
+
echo "Remove everything: $0 --teardown"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# 02 — an existing ClickHouse
|
|
2
|
+
|
|
3
|
+
You already run ClickHouse — a server of your own, your company's cluster, or ClickHouse
|
|
4
|
+
Cloud — and want memhouse to keep its house there. You hold an account on it that can
|
|
5
|
+
create users. This example builds a **member** account with it, installs as that member,
|
|
6
|
+
and keeps only the member's credential on disk.
|
|
7
|
+
|
|
8
|
+
## What it does
|
|
9
|
+
|
|
10
|
+
1. `memhouse whoami --admin` checks that the admin credential really can create users and
|
|
11
|
+
grant, before anything is changed.
|
|
12
|
+
2. `memhouse install --admin-user …` creates the member (default: your login name), grants
|
|
13
|
+
it `mem.<member>_*` and nothing else, creates its rooms, and verifies the install by
|
|
14
|
+
connecting **as the member**. The admin credential is used for this one command and not
|
|
15
|
+
saved (add `--keep-admin` if this machine should be able to invite people later).
|
|
16
|
+
3. `memhouse ship --ensure-schema` creates any room or stat table that is missing — safe to
|
|
17
|
+
re-run, and the thing to run after an upgrade that adds one — then `memhouse ship`
|
|
18
|
+
ships two made-up sessions.
|
|
19
|
+
4. `memhouse whoami` and `memhouse status` show the credential this machine now keeps and
|
|
20
|
+
what the house holds.
|
|
21
|
+
|
|
22
|
+
## Needs
|
|
23
|
+
|
|
24
|
+
- Node.js 24 or newer, and `npm install -g memhouse`
|
|
25
|
+
- The ClickHouse HTTP endpoint, e.g. `https://ch.example.com:8443` (ClickHouse Cloud:
|
|
26
|
+
the HTTPS endpoint from the console). memhouse's CI runs on ClickHouse 25.11 and 26.7.
|
|
27
|
+
- An account that can create users and databases and grant what it holds. A server's
|
|
28
|
+
first administrator qualifies: the `default` user of a fresh server, or the user the
|
|
29
|
+
Docker image creates with `CLICKHOUSE_DEFAULT_ACCESS_MANAGEMENT=1`. Step 1 tells you
|
|
30
|
+
whether yours does before anything changes.
|
|
31
|
+
- The admin password where a tool can lend it to one command. It never goes on a command
|
|
32
|
+
line: `ps` and shell history would keep it.
|
|
33
|
+
|
|
34
|
+
## Run
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
export MEMHOUSE_URL=https://ch.example.com:8443
|
|
38
|
+
export MEMHOUSE_ADMIN_USER=default
|
|
39
|
+
with-secret MEMHOUSE_ADMIN_PASSWORD=keychain:clickhouse-admin -- ./run.sh
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
No secret tool? Read it into this shell without echo:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
read -rs MEMHOUSE_ADMIN_PASSWORD && export MEMHOUSE_ADMIN_PASSWORD
|
|
46
|
+
./run.sh
|
|
47
|
+
unset MEMHOUSE_ADMIN_PASSWORD
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
| Variable | Default | |
|
|
51
|
+
|---|---|---|
|
|
52
|
+
| `MEMHOUSE_URL` | (required) | the ClickHouse HTTP(S) endpoint |
|
|
53
|
+
| `MEMHOUSE_ADMIN_USER` | (required) | the account that creates the member |
|
|
54
|
+
| `MEMHOUSE_ADMIN_PASSWORD` | (required) | its password, lent by a secret tool |
|
|
55
|
+
| `MEMBER` | your login name | the member to create |
|
|
56
|
+
| `MEMHOUSE_BIN` | `memhouse` | the command to run |
|
|
57
|
+
| `EXAMPLE_HOME` | `~/.memhouse-example-02` | this example's memhouse home |
|
|
58
|
+
|
|
59
|
+
The same install without the script, giving the password on stdin instead of the
|
|
60
|
+
environment:
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
printf '%s' "$PASSWORD" | memhouse install --url "$MEMHOUSE_URL" --admin-user default \
|
|
64
|
+
--member alice --admin-password-file - --yes
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## What you should see
|
|
68
|
+
|
|
69
|
+
Trimmed from a run against ClickHouse 26.7.1.1315 with memhouse 0.18.9 (the test endpoint is shown as `ch.example.com`):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
== 1. whoami --admin: is this credential really an administrator?
|
|
73
|
+
✓ admin at https://ch.example.com:8443 — administrator
|
|
74
|
+
credential from: MEMHOUSE_ADMIN_* environment
|
|
75
|
+
may create users: yes
|
|
76
|
+
|
|
77
|
+
== 2. install: build member 'polat' with the admin credential, then verify as the member
|
|
78
|
+
✓ created ClickHouse user 'polat'
|
|
79
|
+
✓ granted 'polat' mem.polat_* — their rooms and nothing else; async_insert pinned
|
|
80
|
+
✓ verified as 'polat' with the member credential
|
|
81
|
+
✓ installed
|
|
82
|
+
the admin credential was used for this install and not saved. To invite later:
|
|
83
|
+
with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- memhouse invite <name> --url … --admin-user admin
|
|
84
|
+
|
|
85
|
+
== 3. make sure every room and stat table exists, then ship once
|
|
86
|
+
[memhouse] schema ensured (5 statements)
|
|
87
|
+
[memhouse] shipped 2 sessions (0 skipped) → 6 msg rows, 2 tool rows in 0.2s
|
|
88
|
+
|
|
89
|
+
== 4. whoami: the credential this machine keeps, and what it may do
|
|
90
|
+
✓ polat at https://ch.example.com:8443 — member
|
|
91
|
+
may create users: no
|
|
92
|
+
|
|
93
|
+
== 5. status
|
|
94
|
+
✓ house: 2 sessions, 6 messages
|
|
95
|
+
✓ fleet: 1 active writer(s)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Step 2 generates the member password and writes it only to the home's `env` file (mode
|
|
99
|
+
600). From 0.18.10 no command prints it; earlier versions showed it once on this step.
|
|
100
|
+
|
|
101
|
+
## Teardown
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
./run.sh --teardown
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Deletes the example's home. The member and its rooms stay in the house: dropping them is
|
|
108
|
+
an administrator's decision. As the admin:
|
|
109
|
+
|
|
110
|
+
```sql
|
|
111
|
+
DROP USER polat;
|
|
112
|
+
-- then each of mem.polat_sessions, _messages, _tool_calls, _meta, _events,
|
|
113
|
+
-- _session_stats, _session_model_stats, _session_tool_stats
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## For real
|
|
117
|
+
|
|
118
|
+
Leave out the two fixture flags (`--editors claude --claude-roots …`) and the
|
|
119
|
+
`EXAMPLE_HOME`, and every editor on the machine ships into the house. Your other machines
|
|
120
|
+
join the same rooms by installing with the same member credential (`memhouse install --url
|
|
121
|
+
… --user <member> --password …`, or an invite file — see [03](../03-team-house/)).
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Example 02 — point memhouse at a ClickHouse you already run.
|
|
3
|
+
#
|
|
4
|
+
# The admin credential builds a member account (rooms mem.<member>_*, one grant on exactly
|
|
5
|
+
# those), the install is verified AS that member, and the admin credential is then dropped:
|
|
6
|
+
# what stays on disk is the member's. The admin password never touches the command line.
|
|
7
|
+
#
|
|
8
|
+
# with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- ./run.sh
|
|
9
|
+
# ./run.sh --teardown stop anything this home started and delete the home
|
|
10
|
+
#
|
|
11
|
+
# Environment:
|
|
12
|
+
# MEMHOUSE_URL required the ClickHouse HTTP endpoint, e.g. https://ch.example.com:8443
|
|
13
|
+
# MEMHOUSE_ADMIN_USER required an account that can create users and grant
|
|
14
|
+
# MEMHOUSE_ADMIN_PASSWORD required its password — lent by with-secret, never typed into argv
|
|
15
|
+
# MEMBER optional the member to create (default: your login name)
|
|
16
|
+
# MEMHOUSE_BIN optional the memhouse command (default: memhouse)
|
|
17
|
+
# EXAMPLE_HOME optional where this install lives (default: ~/.memhouse-example-02)
|
|
18
|
+
set -euo pipefail
|
|
19
|
+
|
|
20
|
+
HERE=$(cd "$(dirname "$0")" && pwd)
|
|
21
|
+
MH=${MEMHOUSE_BIN:-memhouse}
|
|
22
|
+
mh() { $MH "$@"; }
|
|
23
|
+
export MEMHOUSE_HOME=${EXAMPLE_HOME:-$HOME/.memhouse-example-02}
|
|
24
|
+
|
|
25
|
+
if [ "${1:-}" = "--teardown" ]; then
|
|
26
|
+
mh stop || true
|
|
27
|
+
rm -rf "$MEMHOUSE_HOME"
|
|
28
|
+
echo "example 02 home removed. The member and its rooms stay in the house; dropping them is an admin's call."
|
|
29
|
+
exit 0
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
: "${MEMHOUSE_URL:?set MEMHOUSE_URL to your ClickHouse HTTP endpoint}"
|
|
33
|
+
: "${MEMHOUSE_ADMIN_USER:?set MEMHOUSE_ADMIN_USER to an account that can create users}"
|
|
34
|
+
: "${MEMHOUSE_ADMIN_PASSWORD:?lend it with: with-secret MEMHOUSE_ADMIN_PASSWORD=<reference> -- $0}"
|
|
35
|
+
# A login name like "first.last" is not a ClickHouse user name memhouse accepts.
|
|
36
|
+
MEMBER=${MEMBER:-$(id -un | tr -c 'A-Za-z0-9_\n' '_' | sed 's/^[^A-Za-z]*//')}
|
|
37
|
+
# Only the admin variables and the URL come from outside. A member credential exported
|
|
38
|
+
# by some other install would outrank this home's env file.
|
|
39
|
+
unset MEMHOUSE_USER MEMHOUSE_PASSWORD MEMHOUSE_DB
|
|
40
|
+
|
|
41
|
+
mkdir -p "$MEMHOUSE_HOME"
|
|
42
|
+
FIX="$MEMHOUSE_HOME/example-sessions"
|
|
43
|
+
[ -d "$FIX/projects" ] || node "$HERE/../fixtures/make-sessions.js" "$FIX" "$MEMBER" alpha beta >/dev/null
|
|
44
|
+
|
|
45
|
+
echo "== 1. whoami --admin: is this credential really an administrator?"
|
|
46
|
+
mh whoami --admin
|
|
47
|
+
|
|
48
|
+
echo
|
|
49
|
+
echo "== 2. install: build member '$MEMBER' with the admin credential, then verify as the member"
|
|
50
|
+
# --editors/--claude-roots keep this example to the made-up sessions above. Drop both to
|
|
51
|
+
# ship your real ones. --no-ship: nothing runs in the background; step 3 ships once.
|
|
52
|
+
mh install --url "$MEMHOUSE_URL" --admin-user "$MEMHOUSE_ADMIN_USER" --member "$MEMBER" \
|
|
53
|
+
--editors claude --claude-roots "$FIX" --yes --no-ship
|
|
54
|
+
|
|
55
|
+
# From here on the member credential in the env file is the only one in play.
|
|
56
|
+
unset MEMHOUSE_ADMIN_USER MEMHOUSE_ADMIN_PASSWORD
|
|
57
|
+
|
|
58
|
+
echo
|
|
59
|
+
echo "== 3. make sure every room and stat table exists, then ship once"
|
|
60
|
+
# --ensure-schema only creates what is missing, as the member, and ships nothing; it is
|
|
61
|
+
# safe to re-run, and the thing to run after an upgrade that adds a table.
|
|
62
|
+
mh ship --ensure-schema
|
|
63
|
+
mh ship
|
|
64
|
+
|
|
65
|
+
echo
|
|
66
|
+
echo "== 4. whoami: the credential this machine keeps, and what it may do"
|
|
67
|
+
mh whoami
|
|
68
|
+
|
|
69
|
+
echo
|
|
70
|
+
echo "== 5. status"
|
|
71
|
+
mh status
|