memhouse 0.0.1 → 0.3.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.
Files changed (96) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +59 -3
  3. package/bin/memhouse.js +487 -0
  4. package/cache.js +850 -0
  5. package/ch-ingest.js +329 -0
  6. package/ch-schema.js +152 -0
  7. package/clickhouse.js +87 -0
  8. package/editors/antigravity.js +1019 -0
  9. package/editors/base.js +321 -0
  10. package/editors/claude.js +386 -0
  11. package/editors/codebuff.js +338 -0
  12. package/editors/codex.js +537 -0
  13. package/editors/commandcode.js +166 -0
  14. package/editors/copilot-jetbrains.js +207 -0
  15. package/editors/copilot.js +260 -0
  16. package/editors/cursor-agent.js +202 -0
  17. package/editors/cursor.js +438 -0
  18. package/editors/gemini.js +231 -0
  19. package/editors/goose.js +358 -0
  20. package/editors/gsd.js +366 -0
  21. package/editors/index.js +179 -0
  22. package/editors/kiro.js +315 -0
  23. package/editors/opencode.js +374 -0
  24. package/editors/vscode.js +413 -0
  25. package/editors/windsurf.js +634 -0
  26. package/editors/zed.js +213 -0
  27. package/index.js +424 -2
  28. package/mcp-server.js +294 -0
  29. package/mem-house/DESIGN.md +119 -0
  30. package/mem-house/delivery/AGENT-INSTALL.md +75 -0
  31. package/mem-house/delivery/PROMPT.md +42 -0
  32. package/mem-house/delivery/install.sh +181 -0
  33. package/mem-house/delivery/kernel-install.md +76 -0
  34. package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
  35. package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
  36. package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
  37. package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
  38. package/mem-house/rls.sql +17 -0
  39. package/mem-house/schema.sql +145 -0
  40. package/mem-house/server/queries.js +878 -0
  41. package/mem-house/server/server.js +228 -0
  42. package/mem-house/shipper/ship.js +402 -0
  43. package/misc/syntax-check.js +38 -0
  44. package/package.json +56 -7
  45. package/pricing.js +88 -0
  46. package/pricing.json +992 -0
  47. package/public/assets/index-BsgjOSTv.js +75 -0
  48. package/public/assets/index-DV6ONi_F.css +2 -0
  49. package/public/index.html +16 -0
  50. package/relay-client.js +306 -0
  51. package/relay-server.js +577 -0
  52. package/server.js +860 -0
  53. package/share-image.js +367 -0
  54. package/ui/eslint.config.js +29 -0
  55. package/ui/index.html +15 -0
  56. package/ui/package-lock.json +4229 -0
  57. package/ui/package.json +38 -0
  58. package/ui/src/App.jsx +364 -0
  59. package/ui/src/components/ActivityHeatmap.jsx +206 -0
  60. package/ui/src/components/AiAuditCard.jsx +254 -0
  61. package/ui/src/components/AnimatedLoader.jsx +14 -0
  62. package/ui/src/components/AnimatedLogo.jsx +96 -0
  63. package/ui/src/components/ChatSidebar.jsx +226 -0
  64. package/ui/src/components/DateRangePicker.jsx +104 -0
  65. package/ui/src/components/EditorDot.jsx +11 -0
  66. package/ui/src/components/EditorIcon.jsx +117 -0
  67. package/ui/src/components/KpiCard.jsx +9 -0
  68. package/ui/src/components/LiveFeed.jsx +128 -0
  69. package/ui/src/components/LoginScreen.jsx +81 -0
  70. package/ui/src/components/MessageRenderer.jsx +148 -0
  71. package/ui/src/components/PageHeader.jsx +11 -0
  72. package/ui/src/components/SectionTitle.jsx +3 -0
  73. package/ui/src/components/ShareModal.jsx +305 -0
  74. package/ui/src/components/TokenTimeline.jsx +258 -0
  75. package/ui/src/hooks/useLive.jsx +15 -0
  76. package/ui/src/index.css +83 -0
  77. package/ui/src/lib/api.js +343 -0
  78. package/ui/src/lib/constants.js +88 -0
  79. package/ui/src/lib/theme.jsx +23 -0
  80. package/ui/src/main.jsx +16 -0
  81. package/ui/src/pages/Artifacts.jsx +575 -0
  82. package/ui/src/pages/Compare.jsx +232 -0
  83. package/ui/src/pages/CostAnalysis.jsx +354 -0
  84. package/ui/src/pages/Dashboard.jsx +529 -0
  85. package/ui/src/pages/DeepAnalysis.jsx +495 -0
  86. package/ui/src/pages/GSD.jsx +726 -0
  87. package/ui/src/pages/MCPs.jsx +744 -0
  88. package/ui/src/pages/ProjectDetail.jsx +325 -0
  89. package/ui/src/pages/Projects.jsx +249 -0
  90. package/ui/src/pages/RelayDashboard.jsx +590 -0
  91. package/ui/src/pages/RelayUserDetail.jsx +466 -0
  92. package/ui/src/pages/Sessions.jsx +535 -0
  93. package/ui/src/pages/Settings.jsx +241 -0
  94. package/ui/src/pages/SqlViewer.jsx +344 -0
  95. package/ui/src/pages/Subscriptions.jsx +411 -0
  96. package/ui/vite.config.js +18 -0
package/mcp-server.js ADDED
@@ -0,0 +1,294 @@
1
+ const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
2
+ const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js');
3
+ const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
4
+ const { z } = require('zod');
5
+ const crypto = require('crypto');
6
+
7
+ /**
8
+ * Creates an MCP server instance wired to the relay database.
9
+ * Returns { mcpServer, transports } — caller wires SSE endpoints into Express.
10
+ */
11
+ function createMcpServer(getDb) {
12
+ const mcpServer = new McpServer({
13
+ name: 'agentlytics-relay',
14
+ version: '1.0.0',
15
+ });
16
+
17
+ // ── Tool: list_users ──
18
+ mcpServer.tool(
19
+ 'list_users',
20
+ 'List all connected users and their shared projects',
21
+ {},
22
+ async () => {
23
+ const db = getDb();
24
+ if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
25
+ const users = db.prepare('SELECT username, last_seen, projects FROM users ORDER BY last_seen DESC').all();
26
+ const result = users.map(u => ({
27
+ username: u.username,
28
+ lastSeen: new Date(u.last_seen).toISOString(),
29
+ projects: JSON.parse(u.projects || '[]'),
30
+ }));
31
+ return {
32
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
33
+ };
34
+ }
35
+ );
36
+
37
+ // ── Tool: search_sessions ──
38
+ mcpServer.tool(
39
+ 'search_sessions',
40
+ 'Search across all users\' chat messages by keyword. Use this to find what someone worked on, or find discussions about a specific file or topic.',
41
+ {
42
+ query: z.string().describe('Search query — keyword, file name, or topic'),
43
+ username: z.string().optional().describe('Filter by specific username'),
44
+ project: z.string().optional().describe('Filter by project folder path (partial match)'),
45
+ limit: z.number().optional().describe('Max results (default 20)'),
46
+ },
47
+ async ({ query, username, project, limit }) => {
48
+ const db = getDb();
49
+ if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
50
+
51
+ let sql = `
52
+ SELECT rm.chat_id, rm.username, rm.role, rm.content, rm.model, rm.seq,
53
+ rc.name as chat_name, rc.source, rc.folder, rc.last_updated_at
54
+ FROM relay_messages rm
55
+ JOIN relay_chats rc ON rm.chat_id = rc.id AND rm.username = rc.username
56
+ WHERE rm.content LIKE ?`;
57
+ const params = [`%${query}%`];
58
+
59
+ if (username) { sql += ' AND rm.username = ?'; params.push(username); }
60
+ if (project) { sql += ' AND rc.folder LIKE ?'; params.push(`%${project}%`); }
61
+ sql += ' ORDER BY rc.last_updated_at DESC LIMIT ?';
62
+ params.push(limit || 20);
63
+
64
+ const rows = db.prepare(sql).all(...params);
65
+
66
+ if (rows.length === 0) {
67
+ return { content: [{ type: 'text', text: `No results found for "${query}"` }] };
68
+ }
69
+
70
+ const results = rows.map(r => ({
71
+ chatId: r.chat_id,
72
+ chatName: r.chat_name,
73
+ username: r.username,
74
+ role: r.role,
75
+ source: r.source,
76
+ folder: r.folder,
77
+ lastUpdated: r.last_updated_at ? new Date(r.last_updated_at).toISOString() : null,
78
+ model: r.model,
79
+ content: r.content.length > 300 ? r.content.substring(0, 300) + '...' : r.content,
80
+ }));
81
+
82
+ return {
83
+ content: [{ type: 'text', text: JSON.stringify(results, null, 2) }],
84
+ };
85
+ }
86
+ );
87
+
88
+ // ── Tool: get_user_activity ──
89
+ mcpServer.tool(
90
+ 'get_user_activity',
91
+ 'Get recent activity for a specific user — their recent sessions, what they worked on, which editors and models they used.',
92
+ {
93
+ username: z.string().describe('Username to look up'),
94
+ project: z.string().optional().describe('Filter by project folder (partial match)'),
95
+ file_path: z.string().optional().describe('Filter by file path mentioned in messages'),
96
+ limit: z.number().optional().describe('Max sessions to return (default 20)'),
97
+ },
98
+ async ({ username, project, file_path, limit }) => {
99
+ const db = getDb();
100
+ if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
101
+
102
+ // First get sessions
103
+ let sql = `
104
+ SELECT rc.*, rcs.total_messages, rcs.models, rcs.tool_calls,
105
+ rcs.total_input_tokens, rcs.total_output_tokens
106
+ FROM relay_chats rc
107
+ LEFT JOIN relay_chat_stats rcs ON rc.id = rcs.chat_id AND rc.username = rcs.username
108
+ WHERE rc.username = ?`;
109
+ const params = [username];
110
+
111
+ if (project) { sql += ' AND rc.folder LIKE ?'; params.push(`%${project}%`); }
112
+ sql += ' ORDER BY rc.last_updated_at DESC LIMIT ?';
113
+ params.push(limit || 20);
114
+
115
+ let sessions = db.prepare(sql).all(...params);
116
+
117
+ // If file_path filter, narrow down to sessions mentioning that file
118
+ if (file_path && sessions.length > 0) {
119
+ const chatIds = sessions.map(s => s.id);
120
+ const placeholders = chatIds.map(() => '?').join(',');
121
+ const fileMatches = db.prepare(`
122
+ SELECT DISTINCT chat_id FROM relay_messages
123
+ WHERE chat_id IN (${placeholders}) AND username = ? AND content LIKE ?
124
+ `).all(...chatIds, username, `%${file_path}%`);
125
+ const matchingIds = new Set(fileMatches.map(m => m.chat_id));
126
+ sessions = sessions.filter(s => matchingIds.has(s.id));
127
+ }
128
+
129
+ if (sessions.length === 0) {
130
+ return { content: [{ type: 'text', text: `No activity found for user "${username}"` }] };
131
+ }
132
+
133
+ const result = sessions.map(s => ({
134
+ id: s.id,
135
+ name: s.name,
136
+ source: s.source,
137
+ mode: s.mode,
138
+ folder: s.folder,
139
+ lastUpdated: s.last_updated_at ? new Date(s.last_updated_at).toISOString() : null,
140
+ totalMessages: s.total_messages,
141
+ models: s.models ? [...new Set(JSON.parse(s.models))].slice(0, 5) : [],
142
+ toolCalls: s.tool_calls ? JSON.parse(s.tool_calls).length : 0,
143
+ totalInputTokens: s.total_input_tokens,
144
+ totalOutputTokens: s.total_output_tokens,
145
+ }));
146
+
147
+ return {
148
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
149
+ };
150
+ }
151
+ );
152
+
153
+ // ── Tool: get_session_detail ──
154
+ mcpServer.tool(
155
+ 'get_session_detail',
156
+ 'Get the full conversation messages for a specific session. Use the session ID from search_sessions or get_user_activity results.',
157
+ {
158
+ session_id: z.string().describe('The chat/session ID'),
159
+ username: z.string().optional().describe('Username who owns the session (optional, auto-detected if unique)'),
160
+ },
161
+ async ({ session_id, username }) => {
162
+ const db = getDb();
163
+ if (!db) return { content: [{ type: 'text', text: 'Relay database not initialized' }] };
164
+
165
+ let chatSql = 'SELECT * FROM relay_chats WHERE id = ?';
166
+ const chatParams = [session_id];
167
+ if (username) { chatSql += ' AND username = ?'; chatParams.push(username); }
168
+ chatSql += ' LIMIT 1';
169
+
170
+ const chat = db.prepare(chatSql).get(...chatParams);
171
+ if (!chat) {
172
+ return { content: [{ type: 'text', text: `Session "${session_id}" not found` }] };
173
+ }
174
+
175
+ const messages = db.prepare(
176
+ 'SELECT seq, role, content, model FROM relay_messages WHERE chat_id = ? AND username = ? ORDER BY seq'
177
+ ).all(chat.id, chat.username);
178
+
179
+ const formatted = messages.map(m => {
180
+ const label = m.role === 'user' ? 'User' : m.role === 'assistant' ? 'Assistant' : m.role;
181
+ const modelTag = m.model ? ` (${m.model})` : '';
182
+ const content = m.content.length > 2000 ? m.content.substring(0, 2000) + '\n... [truncated]' : m.content;
183
+ return `## ${label}${modelTag}\n\n${content}`;
184
+ }).join('\n\n---\n\n');
185
+
186
+ const header = `# ${chat.name || 'Untitled Session'}\n**User:** ${chat.username} | **Editor:** ${chat.source} | **Project:** ${chat.folder || 'N/A'}\n\n---\n\n`;
187
+
188
+ return {
189
+ content: [{ type: 'text', text: header + formatted }],
190
+ };
191
+ }
192
+ );
193
+
194
+ return mcpServer;
195
+ }
196
+
197
+ /**
198
+ * Wire MCP SSE transport into an Express app.
199
+ * GET /mcp → establishes SSE connection
200
+ * POST /mcp → receives messages from MCP client
201
+ */
202
+ function wireMcpToExpress(app, getDb) {
203
+ const sseTransports = {};
204
+ const httpTransports = {};
205
+
206
+ // Same auth gate as the relay's /relay/* routes (same token derivation as
207
+ // createRelayApp): when RELAY_PASSWORD is set, /mcp must not expose
208
+ // search_sessions / get_session_detail to unauthenticated clients.
209
+ const RELAY_PASSWORD = process.env.RELAY_PASSWORD || null;
210
+ const AUTH_TOKEN = RELAY_PASSWORD
211
+ ? require('crypto').createHmac('sha256', 'agentlytics-relay').update(RELAY_PASSWORD).digest('hex')
212
+ : null;
213
+ function requireAuth(req, res, next) {
214
+ if (!AUTH_TOKEN) return next();
215
+ const header = req.headers.authorization || '';
216
+ const token = header.startsWith('Bearer ') ? header.slice(7) : null;
217
+ if (token === AUTH_TOKEN) return next();
218
+ res.status(401).json({ error: 'Unauthorized' });
219
+ }
220
+
221
+ // SSE: GET /mcp establishes SSE stream
222
+ app.get('/mcp', requireAuth, async (req, res) => {
223
+ const sessionId = req.headers['mcp-session-id'];
224
+
225
+ // Streamable HTTP GET for SSE stream resumption
226
+ if (sessionId && httpTransports[sessionId]) {
227
+ const transport = httpTransports[sessionId];
228
+ await transport.handleRequest(req, res);
229
+ return;
230
+ }
231
+
232
+ // Legacy SSE transport
233
+ const transport = new SSEServerTransport('/mcp', res);
234
+ sseTransports[transport.sessionId] = transport;
235
+
236
+ const mcpServer = createMcpServer(getDb);
237
+
238
+ res.on('close', () => {
239
+ delete sseTransports[transport.sessionId];
240
+ mcpServer.close().catch(() => {});
241
+ });
242
+
243
+ await mcpServer.connect(transport);
244
+ });
245
+
246
+ // POST /mcp handles both SSE messages and Streamable HTTP
247
+ app.post('/mcp', requireAuth, async (req, res) => {
248
+ // Check for SSE session first
249
+ const sseSessionId = req.query.sessionId;
250
+ if (sseSessionId && sseTransports[sseSessionId]) {
251
+ await sseTransports[sseSessionId].handlePostMessage(req, res);
252
+ return;
253
+ }
254
+
255
+ // Streamable HTTP: check for existing session
256
+ const sessionId = req.headers['mcp-session-id'];
257
+ if (sessionId && httpTransports[sessionId]) {
258
+ await httpTransports[sessionId].handleRequest(req, res, req.body);
259
+ return;
260
+ }
261
+
262
+ // New Streamable HTTP session (initialization request)
263
+ const transport = new StreamableHTTPServerTransport({
264
+ sessionIdGenerator: () => crypto.randomUUID(),
265
+ onsessioninitialized: (id) => {
266
+ httpTransports[id] = transport;
267
+ },
268
+ });
269
+
270
+ transport.onclose = () => {
271
+ if (transport.sessionId) {
272
+ delete httpTransports[transport.sessionId];
273
+ }
274
+ };
275
+
276
+ const mcpServer = createMcpServer(getDb);
277
+ await mcpServer.connect(transport);
278
+ await transport.handleRequest(req, res, req.body);
279
+ });
280
+
281
+ // DELETE /mcp for session cleanup
282
+ app.delete('/mcp', requireAuth, async (req, res) => {
283
+ const sessionId = req.headers['mcp-session-id'];
284
+ if (sessionId && httpTransports[sessionId]) {
285
+ await httpTransports[sessionId].handleRequest(req, res);
286
+ return;
287
+ }
288
+ res.status(404).end();
289
+ });
290
+
291
+ return { sseTransports, httpTransports };
292
+ }
293
+
294
+ module.exports = { createMcpServer, wireMcpToExpress };
@@ -0,0 +1,119 @@
1
+ # mem-house — design
2
+
3
+ **mem-house** is agent conversation memory as a product: every coding-agent session
4
+ on your machines — across **all 17 editors** agentlytics supports — parsed locally,
5
+ shipped to one typed ClickHouse store, shareable with a team, installable on the
6
+ ultimagent kernel as an **agency**, and visible through the agentlytics dashboard.
7
+
8
+ **Agency, precisely.** In constellation terms (`../TERMINOLOGY.md`) mem-house is a
9
+ **house** — the `memhouse` database — **plus a resident**: the shipper. That pairing
10
+ is what the word *agency* means; a house on its own only holds.
11
+
12
+ The test is mechanical — **residents write, routines read**. The shipper fires on a
13
+ trigger no query supplies (its own loop) and produces writes that outlive the call:
14
+
15
+ ```toml
16
+ [[resident]]
17
+ kind = "worker" # deterministic code, no LLM
18
+ on = "loop" # a daemon — not an insert trigger, not a schedule
19
+ ```
20
+
21
+ `sessions_v` is a plain view: computed during your query, for your query, writing
22
+ nothing. It is a **routine** — house machinery, not a second resident. mem-house has
23
+ **no materialized views at all**, so the shipper is not merely a resident, it is the
24
+ only candidate in the tree. Strip it and every remaining moving part is a routine
25
+ over rows nobody is writing any more.
26
+
27
+ Note that `user_id MATERIALIZED currentUser()` is a materialized *column*: it computes
28
+ during your own insert and is part of the table's definition. It makes own-only RLS
29
+ work and it records who wrote each row, but it is an identity property, not what makes
30
+ this an agency.
31
+
32
+ **Positioning:** an alternative agency **competing with memory-house**. It borrows
33
+ memory-house's proven ideas (server-stamped identity, own-only RLS, idempotent
34
+ shipping, skills/plugin delivery) and agentlytics' proven assets (adapters, UI,
35
+ cost engine). If it wins, it can become memory-house v4; until then the two run
36
+ side by side as separate agencies, each in its own house. Naming: the product is
37
+ **mem-house**; ClickHouse identifiers use **`memhouse`** (agency/house name, owner
38
+ `memhouse_root`) since identifiers can't carry a dash.
39
+
40
+ ## The four bets
41
+
42
+ 1. **Parse on the client.** memory-house ships raw transcript lines and parses in
43
+ views — workable only because its 4 agents store sessions as JSON/JSONL files.
44
+ agentlytics' breadth includes sqlite-backed editors (Cursor, VS Code, Zed,
45
+ Devin…) where no "raw line" exists; the adapter must crack a DB. So client-side
46
+ parsing is not a style choice — it is what unlocks 17-editor coverage. The
47
+ shipper runs the adapters (`editors/`) and ships **typed rows**. On the canon's
48
+ write axis this bet is precisely *moving work from routine to resident*:
49
+ memory-house parses when you query, mem-house parses before anyone asks and
50
+ writes the result down.
51
+ 2. **Typed common schema** (`schema.sql`). Physical typed columns (what
52
+ memory-house derives in views) + `tool_calls` (memory-house has none) + one
53
+ `extra JSON` escape hatch per table so unnormalized adapter fields are never
54
+ lost. `ReplacingMergeTree(ingested_at)`; `messages` keyed `(session_id, seq)` so
55
+ a re-ship of a grown/changed session replaces stale rows (latest-wins). FTS text
56
+ indexes built in (CH ≥ 26.2).
57
+ 3. **Kernel-installable agency.** Same install path proven for agentlytics-agency:
58
+ `install-agency{memhouse}` → house + `memhouse_root` + credential; members via
59
+ `register-member` + owner `GRANT`; own-only visibility via the row policy
60
+ (`rls.sql`) applied by the ego (the `kernel` user) or the mayor. The shipper is
61
+ the resident that lands with it. Identity is `user_id MATERIALIZED currentUser()`
62
+ (requires `async_insert=0`). Sharing modes: own-only (policy TO member) or
63
+ team-pool (no policy).
64
+ 4. **Borrowed UI, zero fork.** The React SPA consumes REST JSON, not tables. The
65
+ mem-house server implements the **same `/api/*` contract** as agentlytics'
66
+ `server.js` (same routes, same response shapes) over the mem-house schema, and
67
+ serves the same built `public/` bundle. UI changes: none.
68
+
69
+ ## Components (this directory)
70
+
71
+ | Path | What | Notes |
72
+ |---|---|---|
73
+ | `schema.sql` | the house schema (typed) | unqualified names; owner applies in its house |
74
+ | `rls.sql` | own-only row policies | the ego (`kernel`) or the mayor applies (owner lacks ACCESS MANAGEMENT) |
75
+ | `shipper/ship.js` | parse-on-client shipper CLI — the resident (`worker`) | reuses `../../editors`; incremental; idempotent |
76
+ | `server/server.js` | REST API + dashboard | same API contract as agentlytics; serves `../../public` |
77
+ | `delivery/` | delivery kit | installer, skills, plugin, AGENT-INSTALL.md, prompt |
78
+
79
+ ## Environment contract
80
+
81
+ | Var | Default | Meaning |
82
+ |---|---|---|
83
+ | `MEMHOUSE_URL` | `http://localhost:8123` | ClickHouse HTTP(S) endpoint |
84
+ | `MEMHOUSE_USER` | `memhouse_root` | CH user (owner, or a member's own credential) |
85
+ | `MEMHOUSE_PASSWORD` | *(empty)* | credential |
86
+ | `MEMHOUSE_DB` | `memhouse` | the house |
87
+ | `MEMHOUSE_PORT` | `4640` | dashboard/API port |
88
+
89
+ Config file: `~/.memhouse/config.json` (`hiddenProjects`, future prefs).
90
+
91
+ ## Data-plane rules (binding for the shipper and server)
92
+
93
+ - Inserts: `JSONEachRow`, batches ≤ 2000 rows, **`async_insert=0` always** (else
94
+ `user_id` stamping breaks). Reads: set `final: 1` so ReplacingMergeTree collapses.
95
+ - All Int64-bound values coerced to integers (some adapters emit fractional ms).
96
+ - Per-message `ts`: interpolated across the session's `[createdAt, lastUpdatedAt]`
97
+ (adapters don't expose per-message timestamps; monotonic by `seq`; documented
98
+ approximation, refine per-adapter later).
99
+ - `seq` is the message index within the session (0-based). `tool_calls.idx` is the
100
+ call index within the session. `is_subagent` = message content begins with
101
+ `[subagent]` (the fold marker from the Claude adapter).
102
+ - Incremental shipping: read existing `(session_id, last_updated_at, message_count)`
103
+ (FINAL) and skip chats that haven't grown/changed; a full re-ship must remain
104
+ safe (ReplacingMergeTree collapses).
105
+ - Costs are computed in the server from `pricing.js` (repo root) over per-model
106
+ token sums; `<synthetic>` and empty models excluded from model lists.
107
+
108
+ ## Delivery kit (mirrors memory-house)
109
+
110
+ `install.sh` (standalone), `AGENT-INSTALL.md` (agent-facing install steps),
111
+ `skills/` (Claude Code skills to query/search mem-house), `plugin/` (Claude Code
112
+ plugin wrapping the skills), `PROMPT.md` (system-prompt snippet teaching an agent
113
+ to use mem-house as memory), `kernel-install.md` (provision as agency `memhouse`).
114
+
115
+ ## Non-goals (v0)
116
+
117
+ Relay/team-server (the kernel + RLS already covers sharing), launchd daemonization
118
+ (ship `--loop` instead), migration tooling from memory-house's `memory` db, and
119
+ per-adapter true message timestamps.
@@ -0,0 +1,75 @@
1
+ # mem-house — agent install
2
+
3
+ Imperative steps for an AI coding agent installing mem-house on a machine. Execute
4
+ in order; every command is copy-pasteable. Prereqs: Node >= 20.19, a reachable
5
+ ClickHouse >= 26.2 (local server, Cloud, or a kernel house — see
6
+ `kernel-install.md` for the kernel path), and this repo checked out.
7
+
8
+ ## 1. Install
9
+
10
+ ```bash
11
+ cd <repo> # the memhouse checkout
12
+ npm install --no-audit --no-fund
13
+ ```
14
+
15
+ ## 2. Configure the connection
16
+
17
+ Write `~/.memhouse/env` (the installer can do this interactively — `bash
18
+ mem-house/delivery/install.sh` — or write it yourself):
19
+
20
+ ```bash
21
+ mkdir -p ~/.memhouse
22
+ cat > ~/.memhouse/env <<'EOF'
23
+ MEMHOUSE_URL=http://localhost:8123
24
+ MEMHOUSE_USER=memhouse_root
25
+ MEMHOUSE_PASSWORD=<credential>
26
+ MEMHOUSE_DB=memhouse
27
+ EOF
28
+ chmod 600 ~/.memhouse/env
29
+ ```
30
+
31
+ ## 3. Create the schema and run the first ship
32
+
33
+ ```bash
34
+ set -a; source ~/.memhouse/env; set +a
35
+ node mem-house/shipper/ship.js --ensure-schema
36
+ node mem-house/shipper/ship.js # first full parse-on-client ship
37
+ ```
38
+
39
+ Expected output shape: `[mem-house] shipped N sessions (0 skipped) → M msg rows, T
40
+ tool rows in Xs` with N in the hundreds on a machine with real agent usage.
41
+
42
+ ## 4. Verify
43
+
44
+ ```bash
45
+ node mem-house/shipper/ship.js --stats
46
+ ```
47
+
48
+ Expect one row per source (claude-code, codex, gemini-cli, …) with non-zero
49
+ sessions/messages. Or verify by SQL:
50
+
51
+ ```bash
52
+ curl -s -u "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" "$MEMHOUSE_URL/?database=$MEMHOUSE_DB" \
53
+ --data-binary "SELECT source, count() FROM sessions_v GROUP BY source SETTINGS final=1 FORMAT PrettyCompact"
54
+ ```
55
+
56
+ ## 5. Start the dashboard (optional)
57
+
58
+ ```bash
59
+ cd ui && npm install && npm run build && cd .. # first time only (builds public/)
60
+ node mem-house/server/server.js # → http://localhost:4640
61
+ ```
62
+
63
+ ## 6. Keep it fresh
64
+
65
+ Re-ship on a schedule (incremental — cheap):
66
+
67
+ ```bash
68
+ node mem-house/shipper/ship.js --loop 300 &
69
+ ```
70
+
71
+ ## 7. Install the skills (optional, for Claude Code)
72
+
73
+ Copy `mem-house/delivery/skills/*` into the target agent's skills directory
74
+ (e.g. `$CLAUDE_CONFIG_DIR/skills/`), or install `mem-house/delivery/plugin/` as a
75
+ Claude Code plugin. The skills read the connection from `~/.memhouse/env`.
@@ -0,0 +1,42 @@
1
+ # mem-house — system-prompt snippet
2
+
3
+ Paste (or import) this into an agent's system prompt to make mem-house its
4
+ long-term conversation memory.
5
+
6
+ ---
7
+
8
+ ## Conversation memory (mem-house)
9
+
10
+ You have persistent memory of past agent sessions — every conversation this user
11
+ has had with coding agents (Claude Code, Codex, Gemini CLI, Cursor, and other
12
+ editors), across machines, stored in a ClickHouse database called **mem-house**.
13
+ Connection: read `~/.memhouse/env` (`MEMHOUSE_URL/USER/PASSWORD/DB`); query over
14
+ HTTP with `curl -u "$MEMHOUSE_USER:$MEMHOUSE_PASSWORD" "$MEMHOUSE_URL/?database=$MEMHOUSE_DB"`
15
+ and always add `SETTINGS final=1` to reads.
16
+
17
+ **When to reach for it — before claiming ignorance.** If the user refers to past
18
+ work that is not in your current context ("that session where…", "how did I solve
19
+ X before", "did we ever…", "what was I working on last week"), search mem-house
20
+ FIRST. Only say you don't know after a search comes back empty.
21
+
22
+ **How to query.** Prefer the installed skills when present: `/memhouse:search`
23
+ (full-text over messages), `/memhouse:sessions` (list/filter sessions),
24
+ `/memhouse:sql` (free-form read-only SQL). Without skills, query directly:
25
+
26
+ - Find sessions about a topic (FTS, lowercase your terms):
27
+ `SELECT DISTINCT session_id, any(project), min(ts) FROM messages
28
+ WHERE hasToken(text_word, 'clickhouse') GROUP BY session_id
29
+ ORDER BY 3 DESC LIMIT 10 SETTINGS final=1 FORMAT PrettyCompact`
30
+ - Recent sessions: `SELECT session_id, source, project, started, first_prompt
31
+ FROM sessions_v ORDER BY started DESC LIMIT 20 SETTINGS final=1`
32
+ - Replay one session: `SELECT role, text FROM messages
33
+ WHERE session_id = '<id>' ORDER BY seq SETTINGS final=1`
34
+
35
+ **Rules.**
36
+ - Memory is READ-ONLY for you. Never INSERT/ALTER/DROP — ingestion belongs to the
37
+ mem-house shipper alone.
38
+ - Quote retrieved content as *the user's past sessions*, and cite the session_id
39
+ when the user may want to dig deeper.
40
+ - Other members' sessions may be invisible to you (row-level security). An empty
41
+ result means "nothing visible", not "nothing ever happened".
42
+ - Do not paste credentials from `~/.memhouse/env` into responses, commits, or logs.