memhouse 0.16.0 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -31
- package/bin/memhouse.js +657 -187
- package/editors/antigravity.js +2 -0
- package/editors/claude.js +3 -1
- package/editors/codebuff.js +2 -0
- package/editors/codex.js +4 -1
- package/editors/commandcode.js +2 -1
- package/editors/copilot-jetbrains.js +2 -1
- package/editors/copilot.js +2 -1
- package/editors/cursor-agent.js +2 -1
- package/editors/cursor.js +2 -0
- package/editors/gemini.js +2 -1
- package/editors/goose.js +2 -1
- package/editors/index.js +8 -1
- package/editors/kiro.js +3 -2
- package/editors/opencode.js +2 -1
- package/editors/scope.js +126 -0
- package/editors/vscode.js +2 -0
- package/editors/zed.js +2 -1
- package/memhouse/DESIGN.md +31 -8
- package/memhouse/channel.js +50 -0
- package/memhouse/delivery/AGENT-INSTALL-PROMPT.md +4 -3
- package/memhouse/delivery/AGENT-INSTALL.md +3 -3
- package/memhouse/delivery/INVITATION.md.tpl +130 -0
- package/memhouse/delivery/PROMPT.md +21 -14
- package/memhouse/delivery/kernel-install.md +4 -2
- package/memhouse/delivery/plugin/.claude-plugin/plugin.json +2 -2
- package/memhouse/delivery/plugin/reference/HOUSE.md +173 -0
- package/memhouse/delivery/plugin/skills/access/SKILL.md +148 -0
- package/memhouse/delivery/plugin/skills/admin/SKILL.md +30 -1
- package/memhouse/delivery/plugin/skills/house/SKILL.md +99 -0
- package/memhouse/delivery/plugin/skills/recall/SKILL.md +119 -0
- package/memhouse/delivery/plugin/skills/sql/SKILL.md +73 -194
- package/memhouse/flags.js +6 -4
- package/memhouse/house/HOUSE.md +25 -11
- package/memhouse/house/house.js +110 -27
- package/memhouse/house/migrate.js +7 -6
- package/memhouse/house/relocate.js +2 -2
- package/memhouse/house/schema.sql.tpl +3 -3
- package/memhouse/provision.js +100 -0
- package/memhouse/share.js +64 -0
- package/memhouse/shipper/ship.js +87 -28
- package/package.json +1 -1
- package/memhouse/delivery/plugin/skills/ask/SKILL.md +0 -131
- package/memhouse/delivery/plugin/skills/hello/SKILL.md +0 -121
- package/memhouse/delivery/plugin/skills/invite/SKILL.md +0 -130
- package/memhouse/delivery/plugin/skills/search/SKILL.md +0 -186
- package/memhouse/delivery/plugin/skills/sessions/SKILL.md +0 -164
- package/memhouse/delivery/plugin/skills/share/SKILL.md +0 -130
- package/memhouse/delivery/plugin/skills/status/SKILL.md +0 -139
- package/memhouse/delivery/plugin/skills/users/SKILL.md +0 -97
package/memhouse/shipper/ship.js
CHANGED
|
@@ -61,12 +61,11 @@ const selfUpdate = require('../self-update');
|
|
|
61
61
|
// gets. Taking it later would record whatever an upgrade had already replaced.
|
|
62
62
|
const selfSnap = selfUpdate.snapshot(__filename);
|
|
63
63
|
const {
|
|
64
|
-
resolveRooms, READ_SETTINGS, ROOM_TYPES, META_TYPES, SCHEMA_VERSION, MEMBER_PIN,
|
|
65
|
-
SUPPORTED_SCHEMAS, keyProblem,
|
|
66
|
-
} = require('../house/house');
|
|
64
|
+
resolveRooms, READ_SETTINGS, ROOM_TYPES, META_TYPES, SCHEMA_VERSION, MEMBER_PIN, createStatement,
|
|
65
|
+
SUPPORTED_SCHEMAS, keyProblem, legacyTextIndexDialect, isTextIndexGrammarRefusal, textIndexDialectFor } = require('../house/house');
|
|
67
66
|
// Rooms plus the house's own record of itself. Every table the template declares, which
|
|
68
67
|
// is what the column healer and the drift warning have to cover — a column added to
|
|
69
|
-
//
|
|
68
|
+
// events would otherwise roll out to nobody.
|
|
70
69
|
const ALL_TABLES = [...ROOM_TYPES, ...META_TYPES];
|
|
71
70
|
|
|
72
71
|
const BATCH_ROWS = 2000; // insert batch ceiling by ROW COUNT
|
|
@@ -410,17 +409,25 @@ async function assertRoomKeys(client, rooms) {
|
|
|
410
409
|
}
|
|
411
410
|
}
|
|
412
411
|
|
|
412
|
+
// The shipper reads the prefix straight from its environment: it is spawned with the
|
|
413
|
+
// resolved MEMHOUSE_* env by the CLI, and every room name it touches comes from
|
|
414
|
+
// resolveRooms, so this is the only line that needs to know.
|
|
415
|
+
|
|
413
416
|
async function ensureSchema(client) {
|
|
414
417
|
const rooms = await resolveRooms(client);
|
|
415
418
|
// BEFORE the CREATEs and ALTERs, not beside the end-of-function asserts. A house whose
|
|
416
419
|
// record says a newer release moved it forward may have columns this template does not
|
|
417
420
|
// know; running this template's DDL first could add back what that release removed —
|
|
418
421
|
// the exact write the guard exists to prevent.
|
|
422
|
+
await assertNotLegacyLayout(client, rooms);
|
|
419
423
|
await assertWriterSupported(client, rooms);
|
|
420
424
|
const tpl = fs.readFileSync(path.join(__dirname, '..', 'house', 'schema.sql.tpl'), 'utf-8');
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
425
|
+
// Rendered per room rather than taken as one blob, because a prefixed house names its
|
|
426
|
+
// rooms `<prefix>_messages` and the template says `messages`. With no prefix the two
|
|
427
|
+
// are identical, so this is the same statements in the same order as before.
|
|
428
|
+
const stmts = [...ROOM_TYPES, ...META_TYPES]
|
|
429
|
+
.map((t) => createStatement(tpl, t, rooms.physical ? rooms.physical[t] : t)
|
|
430
|
+
.replace('CREATE TABLE ', 'CREATE TABLE IF NOT EXISTS '));
|
|
424
431
|
// Two failure shapes are survivable here and both are skipped rather than fatal:
|
|
425
432
|
//
|
|
426
433
|
// - a permission denial. A read-only credential can still run --ensure-schema for
|
|
@@ -431,16 +438,24 @@ async function ensureSchema(client) {
|
|
|
431
438
|
// the loser can get TABLE_ALREADY_EXISTS or a metadata-file collision. The table
|
|
432
439
|
// exists either way, which is the outcome this function wants.
|
|
433
440
|
let denied = 0;
|
|
434
|
-
|
|
441
|
+
// The text-index grammar changed at 25.10 and neither side parses the other (measured:
|
|
442
|
+
// 25.8/25.9 take the quoted form, 25.10+ the function form). Pick by the server's own
|
|
443
|
+
// version, then keep one fallback: if the chosen form is refused on grammar alone —
|
|
444
|
+
// never on privilege — send the other. A wrong guess costs a round-trip, not the install.
|
|
445
|
+
const version = await serverVersion(client);
|
|
446
|
+
const dialect = textIndexDialectFor(version);
|
|
447
|
+
if (dialect === 'legacy') console.error(`[memhouse] ClickHouse ${version}: text indexes in the pre-25.10 grammar`);
|
|
448
|
+
const create = (q) => client.command({ query: q, clickhouse_settings: { async_insert: 0, allow_experimental_full_text_index: 1 } });
|
|
449
|
+
for (const modern of stmts) {
|
|
450
|
+
const legacy = legacyTextIndexDialect(modern);
|
|
451
|
+
const [first, second] = dialect === 'legacy' ? [legacy, modern] : [modern, legacy];
|
|
435
452
|
try {
|
|
436
|
-
await
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
clickhouse_settings: { async_insert: 0, allow_experimental_full_text_index: 1 },
|
|
443
|
-
});
|
|
453
|
+
try { await create(first); }
|
|
454
|
+
catch (e) {
|
|
455
|
+
const m = e && e.message ? e.message : String(e);
|
|
456
|
+
if (!(first !== second && isTextIndexGrammarRefusal(m))) throw e;
|
|
457
|
+
await create(second);
|
|
458
|
+
}
|
|
444
459
|
} catch (e) {
|
|
445
460
|
const m = e && e.message ? e.message : String(e);
|
|
446
461
|
if (/Not enough privileges|ACCESS_DENIED/i.test(m)) { denied++; continue; }
|
|
@@ -449,8 +464,16 @@ async function ensureSchema(client) {
|
|
|
449
464
|
}
|
|
450
465
|
}
|
|
451
466
|
if (denied) {
|
|
452
|
-
|
|
453
|
-
|
|
467
|
+
// In a SHARED house this is the designed state, not a shortfall: the operator creates
|
|
468
|
+
// each member's rooms during `invite` precisely so the member cannot — a member who
|
|
469
|
+
// could CREATE TABLE here could add tables beside every housemate's. Saying "you do
|
|
470
|
+
// not hold these rights" and then "schema ensured" about the same statements reads as
|
|
471
|
+
// a contradiction on every routine ship, which is how often a member sees it.
|
|
472
|
+
// A member holds CREATE TABLE on their own name pattern, so this is no longer the
|
|
473
|
+
// ordinary path — it is a credential provisioned outside memhouse with less than the
|
|
474
|
+
// plan gives. Say what to ask for rather than whose job it is.
|
|
475
|
+
console.error(`[memhouse] ${denied} schema statement(s) needed rights this credential does not hold — continuing with what it can do.`);
|
|
476
|
+
console.error('[memhouse] the memhouse grant is one line — `memhouse install --print-sql` shows it; ask whoever administers the house to run it.');
|
|
454
477
|
}
|
|
455
478
|
// A house created before origin existed has no such column, and every read and write
|
|
456
479
|
// scopes by it. Add it in place; ReplacingMergeTree backfills the DEFAULT, so every
|
|
@@ -503,7 +526,7 @@ async function ensureSchema(client) {
|
|
|
503
526
|
// There used to be a second, hardcoded `ADD COLUMN … origin` here, from when the healer
|
|
504
527
|
// above could only add that one column. It is redundant now that the generic loop reads
|
|
505
528
|
// the template correctly — and it would have been actively wrong once this loop covered
|
|
506
|
-
//
|
|
529
|
+
// meta and events, which have no origin and want none.
|
|
507
530
|
|
|
508
531
|
await assertRoomsExist(client, rooms);
|
|
509
532
|
await assertRoomKeys(client, rooms);
|
|
@@ -534,7 +557,7 @@ async function ensureSchema(client) {
|
|
|
534
557
|
* Keep the house's record of itself current: which schema generation it is at, and which
|
|
535
558
|
* memhouse version is writing into it.
|
|
536
559
|
*
|
|
537
|
-
* Written only when something CHANGED. A row per pass would turn
|
|
560
|
+
* Written only when something CHANGED. A row per pass would turn events into a
|
|
538
561
|
* heartbeat log — under `--loop` at the default interval that is 288 rows a day per
|
|
539
562
|
* machine, and the one question the table exists to answer ("when did this house move,
|
|
540
563
|
* and who moved it") would be buried in noise.
|
|
@@ -548,7 +571,7 @@ async function recordHouseState(client, rooms, host) {
|
|
|
548
571
|
const writer = `${rooms.user}@${host}`;
|
|
549
572
|
try {
|
|
550
573
|
const rs = await client.query({
|
|
551
|
-
query: `SELECT key, value FROM ${rooms.
|
|
574
|
+
query: `SELECT key, value FROM ${rooms.meta} FINAL WHERE key IN ('schema_version', {ck:String}, {cs:String})`,
|
|
552
575
|
// Keyed per member AND host. Keyed by member alone, two machines of one member
|
|
553
576
|
// overwrote each other's entry on every pass, so the fleet view could only ever
|
|
554
577
|
// show the machine that shipped last — the exact machine that needs no attention.
|
|
@@ -588,15 +611,15 @@ async function recordHouseState(client, rooms, host) {
|
|
|
588
611
|
}
|
|
589
612
|
// The heartbeat, EVERY pass — the fleet view's health column. Latest-wins on the key,
|
|
590
613
|
// so it is one live row per writer however often it fires; version/schema above stay
|
|
591
|
-
// change-only so
|
|
614
|
+
// change-only so events remains a record of moves, not a pulse trace.
|
|
592
615
|
metas.push({ key: `last_ship:${writer}`, value: new Date().toISOString(), host });
|
|
593
616
|
await client.insert({
|
|
594
|
-
table: rooms.
|
|
617
|
+
table: rooms.meta_raw, values: metas, format: 'JSONEachRow',
|
|
595
618
|
clickhouse_settings: { async_insert: 0 },
|
|
596
619
|
});
|
|
597
620
|
if (events.length) {
|
|
598
621
|
await client.insert({
|
|
599
|
-
table: rooms.
|
|
622
|
+
table: rooms.events_raw, values: events, format: 'JSONEachRow',
|
|
600
623
|
clickhouse_settings: { async_insert: 0 },
|
|
601
624
|
});
|
|
602
625
|
}
|
|
@@ -609,8 +632,8 @@ async function recordHouseState(client, rooms, host) {
|
|
|
609
632
|
*
|
|
610
633
|
* Distinct from assertRoomKeys, which inspects key SHAPES — a future generation could be
|
|
611
634
|
* a data transform the keys do not show. This reads what the house says about itself:
|
|
612
|
-
*
|
|
613
|
-
*
|
|
635
|
+
* meta['schema_version'] (what generation the rooms are at) and
|
|
636
|
+
* meta['min_writer_schema'] (the floor `memhouse migrate` sets under writers).
|
|
614
637
|
* Too new -> the fix is on THIS machine: memhouse update. Below the floor -> same.
|
|
615
638
|
* (A house OLDER than this release is not an error here — the room checks catch it and
|
|
616
639
|
* name `memhouse migrate`; this guard must not fire on a pre-0.10 house that has no
|
|
@@ -619,17 +642,52 @@ async function recordHouseState(client, rooms, host) {
|
|
|
619
642
|
* Releases before 0.10.0 never read this — for them the floor is enforced by the
|
|
620
643
|
* pilot's GRANTs, not by code.
|
|
621
644
|
*/
|
|
645
|
+
/**
|
|
646
|
+
* A house from before the one-layout holds plain rooms — `messages`, not `<member>_messages`.
|
|
647
|
+
* Shipping into it would quietly create a second, empty set of rooms beside the full ones
|
|
648
|
+
* and write there: every past session invisible to the member's own reads, the dashboard
|
|
649
|
+
* and the skills, with nothing anywhere saying why. Refuse, and say exactly what moves the
|
|
650
|
+
* old rooms across — a RENAME, instant, nothing copied.
|
|
651
|
+
*
|
|
652
|
+
* Trips only when the member's rooms are ABSENT and the plain ones PRESENT; a house holding
|
|
653
|
+
* both is mid-conversion and is left alone.
|
|
654
|
+
*/
|
|
655
|
+
async function serverVersion(client) {
|
|
656
|
+
try {
|
|
657
|
+
const rs = await client.query({ query: 'SELECT version() AS v', format: 'JSONEachRow' });
|
|
658
|
+
return String(((await rs.json())[0] || {}).v || '');
|
|
659
|
+
} catch { return ''; }
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
async function assertNotLegacyLayout(client, rooms) {
|
|
663
|
+
const rs = await client.query({
|
|
664
|
+
query: `SELECT name FROM system.tables WHERE database = currentDatabase() AND name IN ('messages', '${rooms.physical.messages}')`,
|
|
665
|
+
format: 'JSONEachRow',
|
|
666
|
+
});
|
|
667
|
+
const names = new Set((await rs.json()).map((r) => r.name));
|
|
668
|
+
if (names.has(rooms.physical.messages) || !names.has('messages')) return;
|
|
669
|
+
// The plain rooms of a pre-one-layout house: the transcript rooms by type name, and the
|
|
670
|
+
// two bookkeeping rooms under the names they had then.
|
|
671
|
+
const legacyName = { meta: 'house_meta', events: 'house_events' };
|
|
672
|
+
const moves = [...ROOM_TYPES, ...META_TYPES].map((t) => `RENAME TABLE ${legacyName[t] || t} TO ${rooms.physical[t]};`).join('\n ');
|
|
673
|
+
throw new Error(
|
|
674
|
+
'this house holds plain rooms (messages, sessions, tool_calls) — the layout before rooms were named for their member.\n'
|
|
675
|
+
+ ` Shipping now would create empty ${rooms.pattern} rooms beside them and write there, hiding every past session.\n`
|
|
676
|
+
+ ` Move the old rooms across (a rename — instant, nothing copied), then ship again:\n ${moves}\n`
|
|
677
|
+
+ ` Then replace the database-wide grant with the one-layout grant: memhouse install --print-sql --member ${rooms.member}`);
|
|
678
|
+
}
|
|
679
|
+
|
|
622
680
|
async function assertWriterSupported(client, rooms) {
|
|
623
681
|
let have;
|
|
624
682
|
try {
|
|
625
683
|
const rs = await client.query({
|
|
626
|
-
query: `SELECT key, value FROM ${rooms.
|
|
684
|
+
query: `SELECT key, value FROM ${rooms.meta} FINAL WHERE key IN ('schema_version', 'min_writer_schema')`,
|
|
627
685
|
format: 'JSONEachRow',
|
|
628
686
|
});
|
|
629
687
|
have = new Map((await rs.json()).map((r) => [r.key, toInt(r.value)]));
|
|
630
688
|
} catch (e) {
|
|
631
689
|
// ONLY a missing table means a pre-0.10 house (the room checks own that case). Any
|
|
632
|
-
// other failure — an ACCESS_DENIED on
|
|
690
|
+
// other failure — an ACCESS_DENIED on meta, a timeout — used to fall through
|
|
633
691
|
// here too, and a writer that merely could not READ the record was treated as if the
|
|
634
692
|
// record did not exist: on a newer-schema house whose key shapes happen to match,
|
|
635
693
|
// that bypassed the whole compatibility guard. Not being able to check is a reason
|
|
@@ -1004,6 +1062,7 @@ async function runShip(client, opts = {}) {
|
|
|
1004
1062
|
// shape: writing at the wrong epoch either forks a session that did not change, or
|
|
1005
1063
|
// overwrites a stored parse that did.
|
|
1006
1064
|
const rooms = await resolveRooms(client);
|
|
1065
|
+
await assertNotLegacyLayout(client, rooms);
|
|
1007
1066
|
await assertWriterSupported(client, rooms);
|
|
1008
1067
|
await assertRoomsExist(client, rooms);
|
|
1009
1068
|
await assertRoomKeys(client, rooms);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "memhouse",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.0",
|
|
4
4
|
"description": "memhouse \u2014 agent conversation memory across 17 editors: parse-on-client shippers, a typed ClickHouse store with one set of rooms per member, kernel-agency install, and the agentlytics dashboard",
|
|
5
5
|
"main": "bin/memhouse.js",
|
|
6
6
|
"bin": {
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: ask
|
|
3
|
-
description: Answer a question FROM conversation memory — retrieve the relevant past sessions out of the memhouse ClickHouse store, read the actual transcripts, and synthesize an answer with citations. Use when the user asks something their past work already answered, e.g. "how did I fix X last time", "what approach did we settle on for Y", "why did we choose Z", "have I solved this error before", "what did the reviewer say about W". Also answers from a friend's house shared with you when they name it ("how did yigit fix X", "ask yigit's memory"). This is the retrieve-and-answer skill; /mem:search is find-the-session, /mem:sql is run-a-query.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
argument-hint: "<question about past work>"
|
|
6
|
-
allowed-tools: Bash
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /mem:ask — answer from conversation memory
|
|
10
|
-
|
|
11
|
-
Answer the user's question from what the house holds, not from what you assume. The
|
|
12
|
-
house is the archive that OUTLIVES the transcripts on disk — Claude Code compacts and
|
|
13
|
-
deletes local session files, so for anything older than ~30 days memhouse is the only
|
|
14
|
-
place the answer still exists.
|
|
15
|
-
|
|
16
|
-
The synthesis is yours; the store is not. memhouse itself runs no LLM anywhere in its
|
|
17
|
-
write or read path — this skill retrieves rows and YOU read them. Never present a guess
|
|
18
|
-
as a retrieval: if the house has nothing on the question, say so.
|
|
19
|
-
|
|
20
|
-
## Connection
|
|
21
|
-
|
|
22
|
-
**Never print this file or the variables in it.** No `cat "$MH_ENV"`, no `env | grep
|
|
23
|
-
MEMHOUSE`, no `set -x` around these commands. Anything you print becomes part of a
|
|
24
|
-
transcript that memhouse itself ships into the house.
|
|
25
|
-
|
|
26
|
-
Credentials resolve as **flags > exported `MEMHOUSE_*` > `$MEMHOUSE_HOME/env`**
|
|
27
|
-
(default `~/.memhouse/env`) — the same order the `memhouse` CLI uses:
|
|
28
|
-
|
|
29
|
-
```bash
|
|
30
|
-
MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
|
|
31
|
-
_u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
|
|
32
|
-
set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
|
|
33
|
-
[ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
|
|
34
|
-
[ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
|
|
35
|
-
if [ -z "${MEMHOUSE_URL:-}" ] || [ -z "${MEMHOUSE_USER:-}" ]; then
|
|
36
|
-
echo "no memhouse house configured — nothing in $MH_ENV and no MEMHOUSE_URL/MEMHOUSE_USER set." >&2
|
|
37
|
-
echo "Run: memhouse install" >&2
|
|
38
|
-
exit 1
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
curl -sS --fail-with-body --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
|
|
42
|
-
--data-binary @- "$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-$MEMHOUSE_USER}&final=1&join_use_nulls=1&readonly=1" <<'SQL'
|
|
43
|
-
<the query>
|
|
44
|
-
FORMAT JSONEachRow
|
|
45
|
-
SQL
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
`readonly=1` is pinned on every request: this skill never writes.
|
|
49
|
-
|
|
50
|
-
## Answering from another person's house (shared with you)
|
|
51
|
-
|
|
52
|
-
The house is a ClickHouse database; a share is a read-only GRANT on it. When the user asks
|
|
53
|
-
about a FRIEND's memory — "how did yigit fix X", "in yigit's house", "ask yigit's memory",
|
|
54
|
-
"from yigit" — answer from THEIR house without changing your credentials:
|
|
55
|
-
|
|
56
|
-
1. **Resolve the house.** `SHOW DATABASES` returns what your credential may read; a name
|
|
57
|
-
that is not `system` / `information_schema` / `default` and not your own `$MEMHOUSE_DB`
|
|
58
|
-
is a house shared with you. Match the named person to one; set `HOUSE=yigit`. With no
|
|
59
|
-
house named, `HOUSE=$MEMHOUSE_DB`.
|
|
60
|
-
2. **Point the connection at it:** replace `database=${MEMHOUSE_DB:-$MEMHOUSE_USER}` with
|
|
61
|
-
`database=<HOUSE>` in the recipe above. The rooms (`messages`, `sessions`) resolve by
|
|
62
|
-
the connection's database, so retrieval, the transcript read, and the citation all run
|
|
63
|
-
against that house — leave the table names bare.
|
|
64
|
-
3. **Cite the house.** Say whose memory the answer came from ("from yigit's house") so the
|
|
65
|
-
user never mistakes a friend's session for their own.
|
|
66
|
-
4. **Read-only, thin by design.** A refused SELECT means the house was not shared with you
|
|
67
|
-
(or the share was revoked) — say so; never guess another database.
|
|
68
|
-
|
|
69
|
-
## The method — retrieve, read, answer, cite
|
|
70
|
-
|
|
71
|
-
**Escaping, before anything touches SQL.** Search terms, session ids and user ids are
|
|
72
|
-
arbitrary text going inside single-quoted literals: escape backslashes then single
|
|
73
|
-
quotes (`\` → `\\`, `'` → `\'`) in every value you splice in. A term like `O'Reilly`
|
|
74
|
-
otherwise breaks the query — and crafted text could reshape it.
|
|
75
|
-
|
|
76
|
-
**1. Extract search terms from the question.** Not the question verbatim — the terms
|
|
77
|
-
that would appear in the ANSWER. "How did I fix the ClickHouse auth error?" → search
|
|
78
|
-
`ClickHouse auth`, `Authentication failed`, `ACCESS_DENIED` — error strings beat
|
|
79
|
-
paraphrases, identifiers beat prose. Run 2–3 term variants; the FTS columns are
|
|
80
|
-
lowercased, so lowercase your needle.
|
|
81
|
-
|
|
82
|
-
```sql
|
|
83
|
-
SELECT session_id, any(folder) AS folder,
|
|
84
|
-
formatDateTime(max(ts), '%Y-%m-%d') AS day, count() AS hits,
|
|
85
|
-
substring(anyIf(text, positionCaseInsensitive(text, 'NEEDLE') > 0), 1, 300) AS sample
|
|
86
|
-
FROM messages
|
|
87
|
-
WHERE text_ngram LIKE '%needle%'
|
|
88
|
-
GROUP BY session_id ORDER BY max(ts) DESC LIMIT 8
|
|
89
|
-
```
|
|
90
|
-
|
|
91
|
-
**2. Read the strongest sessions — the actual turns, not just the hit line.** Pull a
|
|
92
|
-
window around the hits so you see the resolution, not the complaint:
|
|
93
|
-
|
|
94
|
-
```sql
|
|
95
|
-
SELECT seq, role, substring(text, 1, 2000) AS text
|
|
96
|
-
FROM messages
|
|
97
|
-
WHERE session_id = '<sid>' AND user_id = '<uid>'
|
|
98
|
-
AND seq BETWEEN <hit_seq - 3> AND <hit_seq + 12>
|
|
99
|
-
ORDER BY seq
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
Join on `(session_id, user_id)` always — two housemates can collide on an
|
|
103
|
-
adapter-local session id.
|
|
104
|
-
|
|
105
|
-
**3. Answer, then cite.** Lead with the answer; under it list the sessions it came
|
|
106
|
-
from — `session_id`, date, project — so the user can reopen one (`memhouse resume
|
|
107
|
-
<session_id>` prints the command). Quote the decisive lines verbatim where the exact
|
|
108
|
-
wording matters (error text, a command, a config value).
|
|
109
|
-
|
|
110
|
-
**4. Nothing found is an answer too.** Say what you searched and where the house's
|
|
111
|
-
coverage ends (`SELECT min(ts), max(ts), count() FROM messages`) rather than padding.
|
|
112
|
-
|
|
113
|
-
## What can bite
|
|
114
|
-
|
|
115
|
-
- **Rows can hold more than one PARSE of a session** (`origin`/`epoch` are in the key —
|
|
116
|
-
the house retains superseded parses instead of deleting them). Counts and rollups must
|
|
117
|
-
filter to the current epoch:
|
|
118
|
-
```sql
|
|
119
|
-
WHERE origin != 'ship'
|
|
120
|
-
OR (session_id, user_id, epoch) IN (
|
|
121
|
-
SELECT session_id, user_id, max(epoch) FROM messages
|
|
122
|
-
WHERE origin = 'ship' GROUP BY session_id, user_id)
|
|
123
|
-
```
|
|
124
|
-
For step 2's transcript reads, PREFER the filter as well — an unfiltered read of a
|
|
125
|
-
compacted session interleaves two parses. Drop it deliberately only when the question
|
|
126
|
-
is about what a session said BEFORE it was compacted; the superseded epochs are the
|
|
127
|
-
one place that content still exists.
|
|
128
|
-
- **`text` is truncated at 50,000 chars** and tool args at 20,000 — a missing tail is
|
|
129
|
-
truncation, not absence.
|
|
130
|
-
- The rollup (`sessions_v`) is a saved query, not a table — get it with
|
|
131
|
-
`memhouse sessions-query` if you need per-session totals.
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: hello
|
|
3
|
-
description: Introduce memhouse — what it is, what it remembers, and how to use it from here. Use when the user asks "what is memhouse", "what can my memory do", "how do I use this", invokes /mem:hello, or seems new to the memory system. Also the right first answer when another /mem skill fails because nothing is configured yet.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
argument-hint: ""
|
|
6
|
-
allowed-tools: Bash
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /mem:hello — welcome to your memory
|
|
10
|
-
|
|
11
|
-
**Invoking this skill IS the request. Give the introduction immediately — grounded in
|
|
12
|
-
this machine's actual state, not hypotheticals. Nothing to clarify first.**
|
|
13
|
-
|
|
14
|
-
## First: find out what state this machine is in
|
|
15
|
-
|
|
16
|
-
RUN this yourself with the Bash tool — do not print it for the user to run, and do not
|
|
17
|
-
show the env values it sources. Read its output, then write the introduction using the
|
|
18
|
-
ACTUAL numbers it returns (interpolate them; never emit the literal "N sessions").
|
|
19
|
-
|
|
20
|
-
```bash
|
|
21
|
-
MH_ENV="${MEMHOUSE_HOME:-$HOME/.memhouse}/env"
|
|
22
|
-
_u=${MEMHOUSE_URL-}; _s=${MEMHOUSE_USER-}; _p=${MEMHOUSE_PASSWORD-}; _d=${MEMHOUSE_DB-}
|
|
23
|
-
set -a; [ -f "$MH_ENV" ] && . "$MH_ENV"; set +a
|
|
24
|
-
[ -n "$_u" ] && MEMHOUSE_URL=$_u; [ -n "$_s" ] && MEMHOUSE_USER=$_s
|
|
25
|
-
[ -n "$_p" ] && MEMHOUSE_PASSWORD=$_p; [ -n "$_d" ] && MEMHOUSE_DB=$_d
|
|
26
|
-
if [ -n "${MEMHOUSE_URL:-}" ] && [ -n "${MEMHOUSE_USER:-}" ]; then
|
|
27
|
-
curl -sS -m 5 --user "$MEMHOUSE_USER:${MEMHOUSE_PASSWORD:-}" \
|
|
28
|
-
--data-binary "SELECT uniqExact(session_id), count(), formatDateTime(min(ts),'%Y-%m-%d') FROM messages" \
|
|
29
|
-
"$MEMHOUSE_URL/?database=${MEMHOUSE_DB:-$MEMHOUSE_USER}&final=1&readonly=1" 2>&1
|
|
30
|
-
else
|
|
31
|
-
echo "NOT_CONFIGURED"
|
|
32
|
-
fi
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
(Do not print the env values, and do not narrate that you are withholding them — just use them.)
|
|
36
|
-
|
|
37
|
-
## What memhouse is — the overview (give this in every state, before the tour)
|
|
38
|
-
|
|
39
|
-
One paragraph, grounded not sold: **memhouse is your personal, permanent archive of every
|
|
40
|
-
AI coding session.** A background shipper reads the session files your editors write —
|
|
41
|
-
Claude Code, Codex, Cursor, VS Code, Zed, OpenCode, Gemini CLI and 10 more, 17 in all —
|
|
42
|
-
and ships them into a ClickHouse database you own, called a **house**. YOU choose that
|
|
43
|
-
ClickHouse: a local one on your laptop, one on your own VM, or any ClickHouse server you
|
|
44
|
-
control — managed or remote. memhouse itself is not a hosted service and phones nothing
|
|
45
|
-
home; your sessions go only to the house you point it at, and it runs no LLM in its write
|
|
46
|
-
or read path — it stores rows, the agent reading them (this chat) does the thinking. It
|
|
47
|
-
outlives the transcripts on disk: editors compact and delete their local session files
|
|
48
|
-
after weeks, so for anything older than ~30 days the house is the only place it still
|
|
49
|
-
exists. One archive spans every editor, every project, every machine you ship from.
|
|
50
|
-
|
|
51
|
-
What it gives you, in one breath: **ask** your own history a question and get a cited
|
|
52
|
-
answer, **search** for the session you half-remember, **query** the raw data for numbers,
|
|
53
|
-
browse a **dashboard**, and **invite** other people onto your ClickHouse so memories can be
|
|
54
|
-
**shared** read-only. The tour below names each.
|
|
55
|
-
|
|
56
|
-
## Then introduce it — three states, three openings
|
|
57
|
-
|
|
58
|
-
**Configured and answering** — open with the numbers: "Your memory holds N sessions
|
|
59
|
-
(M messages) going back to <date> — every conversation you've had with coding agents
|
|
60
|
-
(Claude Code, Codex, Cursor, Gemini CLI and 13 more), across all your machines. It
|
|
61
|
-
outlives the transcripts on disk: editors delete their session files after weeks; this
|
|
62
|
-
doesn't." Then the tour below.
|
|
63
|
-
|
|
64
|
-
**Not configured** — say what memhouse IS (one paragraph: a personal, permanent archive
|
|
65
|
-
of every AI coding session, stored in a ClickHouse "house" you own), then how to start:
|
|
66
|
-
`memhouse onboard` (wizard) or `memhouse install --env <file>` if someone invited them
|
|
67
|
-
(the file came from `/mem:invite` on the inviter's side).
|
|
68
|
-
|
|
69
|
-
**Configured but unreachable** — say which URL refused, point at `memhouse doctor`,
|
|
70
|
-
stop. Never guess another endpoint.
|
|
71
|
-
|
|
72
|
-
## The tour (keep it to one screen)
|
|
73
|
-
|
|
74
|
-
What you can DO from this chat:
|
|
75
|
-
|
|
76
|
-
- `/mem:ask <question>` — answer from your past work, with citations ("how did I fix X
|
|
77
|
-
last time?")
|
|
78
|
-
- `/mem:search <terms>` — find the session ("that conversation about Y")
|
|
79
|
-
- `/mem:sessions` — browse and filter what's stored
|
|
80
|
-
- `/mem:sql <question or SQL>` — ad-hoc analytics (tokens, costs, rankings)
|
|
81
|
-
- `/mem:status` — is it healthy, how much is stored, which machines ship into it
|
|
82
|
-
|
|
83
|
-
The first three overlap — same words work in each — so give the reader the rule that
|
|
84
|
-
separates them: **want a session → search. Want an answer → ask. Want a number → sql.**
|
|
85
|
-
`ask` runs a `search` first, then reads the turns it finds and writes you a cited answer;
|
|
86
|
-
`search` stops at the ranked list of sessions; `sql` counts and aggregates the columns
|
|
87
|
-
rather than hunting text.
|
|
88
|
-
|
|
89
|
-
And around other people — **inviting comes before sharing**:
|
|
90
|
-
|
|
91
|
-
- `/mem:invite <name>` — mint a friend's account + their own house on your ClickHouse,
|
|
92
|
-
hand them one env file; they install with it and have their own memory
|
|
93
|
-
- `/mem:share <name>` — once they exist, open a read-only window into YOUR memory
|
|
94
|
-
(`/mem:share revoke <name>` closes it); `/mem:users` shows who's around
|
|
95
|
-
- **Read a friend's shared memory** — once they've shared theirs with you, name their
|
|
96
|
-
house right in the question: `/mem:ask <question> in <their-house>`, or
|
|
97
|
-
`/mem:search <terms> in <their-house>` / `/mem:sql … on <their-house>`. It reads THEIR
|
|
98
|
-
house read-only, with your own credentials; `/mem:users` lists the houses shared with
|
|
99
|
-
you. (A house is a whole ClickHouse database — sharing grants read on it, not on single
|
|
100
|
-
sessions.)
|
|
101
|
-
|
|
102
|
-
Also worth naming: `memhouse start` serves a dashboard at http://localhost:4640 for
|
|
103
|
-
browsing and analytics outside the chat, and everything here is read-only for agents —
|
|
104
|
-
only the shipper writes.
|
|
105
|
-
|
|
106
|
-
**Freshness check** — quietly compare the installed client to the latest release; mention
|
|
107
|
-
it ONLY if there's a real gap (don't clutter the welcome otherwise):
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
INSTALLED=$(memhouse version 2>/dev/null | head -1 | tr -d '[:space:]')
|
|
111
|
-
LATEST=$(curl -sS -m 5 https://registry.npmjs.org/memhouse/latest 2>/dev/null | sed -n 's/.*"version":"\([^"]*\)".*/\1/p')
|
|
112
|
-
echo "installed=${INSTALLED:-unknown} latest=${LATEST:-unknown}"
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
If the installed version is a MINOR or MAJOR behind (or many patches), add one line:
|
|
116
|
-
"heads-up — you're on 0.A.B, 0.C.D is out; `memhouse update` upgrades it and refreshes
|
|
117
|
-
these `/mem:*` skills." A patch or two behind isn't worth mentioning here. Never run the
|
|
118
|
-
update yourself.
|
|
119
|
-
|
|
120
|
-
End by offering one concrete action: a search for something they mentioned recently, or
|
|
121
|
-
`/mem:status` if they seem unsure it's working. One offer, not a menu.
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: invite
|
|
3
|
-
description: Invite another person onto this memhouse ClickHouse — mint their user and their own house (database) with the right grants, producing the one env file their install needs. Use when the user says "invite X", "add X to memhouse", "get my friend set up". Inviting comes BEFORE sharing — /mem:share can only grant a user who exists.
|
|
4
|
-
user-invocable: true
|
|
5
|
-
argument-hint: "<name> [into <db>]"
|
|
6
|
-
allowed-tools: Bash
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
# /mem:invite — mint a member and hand them one file
|
|
10
|
-
|
|
11
|
-
This runs the `memhouse invite` CLI and nothing else. Inviting mints a ClickHouse user
|
|
12
|
-
and a house, which takes an ADMIN credential — and an admin password must never enter
|
|
13
|
-
this conversation, because memhouse ships the transcript into the house it just
|
|
14
|
-
provisioned. The CLI is the only safe path: the admin password is passed to one process
|
|
15
|
-
and never echoed. **Do not hand-run `CREATE USER` / `GRANT` with an admin password
|
|
16
|
-
substituted into a command — that burns the credential into the transcript on disk.**
|
|
17
|
-
|
|
18
|
-
## Do this
|
|
19
|
-
|
|
20
|
-
1. Confirm the `memhouse` binary is present: `command -v memhouse`. If it is not, tell
|
|
21
|
-
the user to install it first (`npm install -g memhouse`) — there is no in-skill
|
|
22
|
-
fallback, on purpose (see above).
|
|
23
|
-
|
|
24
|
-
2. Gather ONLY two things by asking:
|
|
25
|
-
- the invitee's **name** (validate `[A-Za-z][A-Za-z0-9_]*`; refuse otherwise)
|
|
26
|
-
- the **URL the invitee will reach the house at** — NOT localhost (that is the
|
|
27
|
-
invitee's own machine). A LAN IP, hostname, or tunnel.
|
|
28
|
-
|
|
29
|
-
Do NOT ask for an admin user or password yet — the next step usually needs neither.
|
|
30
|
-
Only ask about a **shared house** if the user brings one up; default is the invitee's
|
|
31
|
-
own house (omit --db; never pass `--db default` for "the default house").
|
|
32
|
-
|
|
33
|
-
3. TRY IT BARE FIRST — run this via the Bash tool, from the directory the file should
|
|
34
|
-
land in (step 4). `memhouse invite` uses the caller's OWN configured credential when
|
|
35
|
-
it can manage users, which is the common case for whoever set the house up, so no
|
|
36
|
-
admin is needed:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
memhouse invite <name> --url <url> [--db <shared-house>]
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
If it prints `invite written: …`, go to step 4 — you are done, and no admin secret
|
|
43
|
-
ever touched this conversation.
|
|
44
|
-
|
|
45
|
-
Otherwise read WHICH refusal came back; they need different answers.
|
|
46
|
-
|
|
47
|
-
**"cannot reach `<url>` …"** — nothing to do with privileges. The address is wrong,
|
|
48
|
-
or the house is bound to loopback (`memhouse deploy --local` binds to 127.0.0.1 on
|
|
49
|
-
purpose, so no LAN address reaches it and no invitee could either). Ask the user for
|
|
50
|
-
the address the invitee will actually use; do not retry with a guess.
|
|
51
|
-
|
|
52
|
-
**"`<user>` is a MEMBER of this ClickHouse, not an administrator"** — inviting needs
|
|
53
|
-
CREATE USER and CREATE DATABASE, which a member deliberately does not hold. Two cases,
|
|
54
|
-
and you must ask which one applies rather than assuming:
|
|
55
|
-
|
|
56
|
-
- **They run the ClickHouse.** They have the admin credential; memhouse never stores
|
|
57
|
-
it. Do NOT take that password in chat — memhouse archives this transcript. Hand them
|
|
58
|
-
the line to run THEMSELVES, and leave `--admin-password` OFF so it is prompted for,
|
|
59
|
-
keeping it out of both the process list and shell history:
|
|
60
|
-
|
|
61
|
-
```
|
|
62
|
-
memhouse invite <name> --url <url> [--db <shared-house>] --admin-user <admin>
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
**You cannot run that line yourself.** The prompt needs a TTY and your Bash tool has
|
|
66
|
-
none, so it refuses with "no TTY to prompt on" — and the fix is NOT to add
|
|
67
|
-
`--admin-password`, which would put the credential in this transcript. The user runs
|
|
68
|
-
it in their own terminal; you wait for them to say it is done.
|
|
69
|
-
|
|
70
|
-
- **Somebody else runs it.** They cannot invite, and no flag changes that. Print the
|
|
71
|
-
statements for whoever administers the server — this contacts nothing and needs no
|
|
72
|
-
credential:
|
|
73
|
-
|
|
74
|
-
```
|
|
75
|
-
memhouse invite <name> --url <url> [--db <shared-house>] --print-sql
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
The output ends with the four `MEMHOUSE_*` lines the invitee needs. Those lines are
|
|
79
|
-
a credential: tell the user to send them over a trusted channel, and **never print
|
|
80
|
-
them into the conversation yourself**.
|
|
81
|
-
|
|
82
|
-
**If it refuses with "house `<db>` already exists and holds N messages", STOP.** That
|
|
83
|
-
house is somebody's memory, and inviting into it would hand the invitee someone else's
|
|
84
|
-
sessions. Do not pass `--adopt` on your own initiative — report the row count and the
|
|
85
|
-
writer count to the user and let them choose: a different handle, a different `--db`,
|
|
86
|
-
or `--adopt` if sharing that house is genuinely what they want.
|
|
87
|
-
|
|
88
|
-
4. **Put the file where the user can attach it.** The CLI writes `invite-<name>.env`
|
|
89
|
-
into the CURRENT directory, and the user has to send it by hand — so run the command
|
|
90
|
-
from their downloads directory (on macOS and most Linux desktops `~/Downloads`), or
|
|
91
|
-
move it there afterwards. Never leave it in a git repo: it is a password, and
|
|
92
|
-
`invite-*.env` is only gitignored in this repo, not in whatever checkout the user
|
|
93
|
-
happened to be standing in.
|
|
94
|
-
|
|
95
|
-
Confirm it is there and still mode `600`, and say the full path. **Never print the
|
|
96
|
-
file's contents** — the password inside would land in a transcript memhouse ships.
|
|
97
|
-
|
|
98
|
-
5. **Say whether the house was fresh.** Run
|
|
99
|
-
`memhouse stats` or query `SELECT count() FROM <db>.messages` and tell the user
|
|
100
|
-
plainly: a brand-new house reads 0. This is a second line of defence behind step 3's
|
|
101
|
-
refusal, and it is how the user learns the invitee is starting clean.
|
|
102
|
-
|
|
103
|
-
6. **Write the message the user will send.** They are about to hand over a file plus an
|
|
104
|
-
explanation, so compose it for them rather than making them write it. Put it in a
|
|
105
|
-
fenced block they can copy whole, and cover:
|
|
106
|
-
- what memhouse is, in a sentence or two — the invitee may never have heard of it
|
|
107
|
-
- that the attached `invite-<name>.env` IS a password: don't forward it, don't paste
|
|
108
|
-
it into chat
|
|
109
|
-
- `npm install -g memhouse` (Node 24+, no flags, nothing compiles), then
|
|
110
|
-
`memhouse install --env invite-<name>.env`
|
|
111
|
-
- that install OFFERS to change the password to one only they know, then deletes the
|
|
112
|
-
file — so they should say yes, because the inviter knows the current one
|
|
113
|
-
- `memhouse onboard` to start shipping, `memhouse status` to check it worked
|
|
114
|
-
- that their house is theirs alone; the inviter cannot read it unless they run
|
|
115
|
-
`/mem:share <inviter>`
|
|
116
|
-
|
|
117
|
-
If the invitee is REPLACING an existing setup (they were shipping under another
|
|
118
|
-
credential), add `memhouse ship --full` after the install — a plain incremental pass
|
|
119
|
-
skips sessions the new house has no record of, so their history would not follow them.
|
|
120
|
-
|
|
121
|
-
Then: once installed, sharing works both ways — `/mem:share <name>`.
|
|
122
|
-
|
|
123
|
-
## Never
|
|
124
|
-
|
|
125
|
-
Never create a ClickHouse user by running raw SQL with an admin password you were given
|
|
126
|
-
in chat. Never invent an admin credential. Never suggest granting beyond what `memhouse
|
|
127
|
-
invite` does. If the binary is absent, stop and say so — do not improvise the SQL.
|
|
128
|
-
|
|
129
|
-
Never print the invite file's contents, and never pass `--adopt` unless the user asked
|
|
130
|
-
for it after being told what is already in that house.
|