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/editors/antigravity.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { execSync, execFileSync } = require('child_process');
|
|
2
2
|
const fs = require('fs');
|
|
3
|
+
const scope = require('./scope');
|
|
3
4
|
const path = require('path');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const { openReadOnly, textOf } = require('./sqlite');
|
|
@@ -10,6 +11,7 @@ const HOME = os.homedir();
|
|
|
10
11
|
const ANTIGRAVITY_USER_DIR = path.join(getAppDataPath('Antigravity'), 'User');
|
|
11
12
|
const ANTIGRAVITY_GLOBAL_STORAGE_DB = path.join(ANTIGRAVITY_USER_DIR, 'globalStorage', 'state.vscdb');
|
|
12
13
|
const ANTIGRAVITY_BRAIN_DIR = path.join(HOME, '.gemini', 'antigravity', 'brain');
|
|
14
|
+
scope.fixed('antigravity', [ANTIGRAVITY_USER_DIR, ANTIGRAVITY_BRAIN_DIR]);
|
|
13
15
|
const OFFLINE_TRAJECTORY_SUMMARIES_KEYS = [
|
|
14
16
|
'antigravityUnifiedStateSync.trajectorySummaries',
|
|
15
17
|
'unifiedStateSync.trajectorySummaries',
|
package/editors/claude.js
CHANGED
|
@@ -36,7 +36,9 @@ function discoverClaudeRoots() {
|
|
|
36
36
|
seen.add(rp);
|
|
37
37
|
roots.push(d);
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
// MEMHOUSE_CLAUDE_ROOTS replaces discovery with an explicit list — one Claude Code
|
|
40
|
+
// instance out of several on a machine. Each path is checked the same way as above.
|
|
41
|
+
return require('./scope').roots('claude', roots, process.env.MEMHOUSE_CLAUDE_ROOTS);
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
// ============================================================
|
package/editors/codebuff.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
@@ -29,6 +30,7 @@ function getProjectRoots() {
|
|
|
29
30
|
}
|
|
30
31
|
return roots;
|
|
31
32
|
}
|
|
33
|
+
scope.fixed('codebuff', getProjectRoots().map((r) => r.projectsDir));
|
|
32
34
|
|
|
33
35
|
function safeReadJson(filePath) {
|
|
34
36
|
try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
|
package/editors/codex.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
@@ -33,11 +34,13 @@ function getMessages(chat) {
|
|
|
33
34
|
return parseSessionMessages(filePath);
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function
|
|
37
|
+
function getCodexHomeDefault() {
|
|
37
38
|
return process.env.CODEX_HOME && process.env.CODEX_HOME.trim()
|
|
38
39
|
? path.resolve(process.env.CODEX_HOME.trim())
|
|
39
40
|
: DEFAULT_CODEX_HOME;
|
|
40
41
|
}
|
|
42
|
+
// CODEX_HOME is Codex's own variable; MEMHOUSE_CODEX_ROOTS is memhouse's, and wins.
|
|
43
|
+
function getCodexHome() { return scope.root('codex', getCodexHomeDefault()); }
|
|
41
44
|
|
|
42
45
|
function getSessionsDir() {
|
|
43
46
|
return path.join(getCodexHome(), SESSION_SUBDIR);
|
package/editors/commandcode.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
|
-
const COMMANDCODE_DIR = path.join(os.homedir(), '.commandcode');
|
|
6
|
+
const COMMANDCODE_DIR = scope.root('commandcode', path.join(os.homedir(), '.commandcode'));
|
|
6
7
|
const PROJECTS_DIR = path.join(COMMANDCODE_DIR, 'projects');
|
|
7
8
|
|
|
8
9
|
// ============================================================
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
|
-
const JB_DIR = path.join(os.homedir(), '.copilot', 'jb');
|
|
6
|
+
const JB_DIR = scope.root('copilot-jetbrains', path.join(os.homedir(), '.copilot', 'jb'));
|
|
6
7
|
|
|
7
8
|
// ============================================================
|
|
8
9
|
// Adapter interface
|
package/editors/copilot.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
|
-
const COPILOT_DIR = path.join(os.homedir(), '.copilot');
|
|
6
|
+
const COPILOT_DIR = scope.root('copilot-cli', path.join(os.homedir(), '.copilot'));
|
|
6
7
|
const SESSION_STATE_DIR = path.join(COPILOT_DIR, 'session-state');
|
|
7
8
|
|
|
8
9
|
// ============================================================
|
package/editors/cursor-agent.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
|
-
const CURSOR_PROJECTS_DIR = path.join(os.homedir(), '.cursor', 'projects');
|
|
6
|
+
const CURSOR_PROJECTS_DIR = scope.root('cursor-agent', path.join(os.homedir(), '.cursor', 'projects'));
|
|
6
7
|
|
|
7
8
|
// ============================================================
|
|
8
9
|
// Adapter interface
|
package/editors/cursor.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { openReadOnly, textOf, bytesOf } = require('./sqlite');
|
|
2
2
|
const path = require('path');
|
|
3
|
+
const scope = require('./scope');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const os = require('os');
|
|
5
6
|
const { getAppDataPath } = require('./base');
|
|
@@ -10,6 +11,7 @@ const CURSOR_CHATS_DIR = path.join(HOME, '.cursor', 'chats');
|
|
|
10
11
|
const CURSOR_USER_DIR = path.join(getAppDataPath('Cursor'), 'User');
|
|
11
12
|
const WORKSPACE_STORAGE_DIR = path.join(CURSOR_USER_DIR, 'workspaceStorage');
|
|
12
13
|
const GLOBAL_STORAGE_DB = path.join(CURSOR_USER_DIR, 'globalStorage', 'state.vscdb');
|
|
14
|
+
scope.fixed('cursor', [CURSOR_USER_DIR, CURSOR_CHATS_DIR]);
|
|
13
15
|
|
|
14
16
|
// ============================================================
|
|
15
17
|
// Source 1: ~/.cursor/chats/<hash>/<chatId>/store.db (agent KV)
|
package/editors/gemini.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
|
-
const GEMINI_DIR = path.join(os.homedir(), '.gemini');
|
|
6
|
+
const GEMINI_DIR = scope.root('gemini-cli', path.join(os.homedir(), '.gemini'));
|
|
6
7
|
const TMP_DIR = path.join(GEMINI_DIR, 'tmp');
|
|
7
8
|
const PROJECTS_JSON = path.join(GEMINI_DIR, 'projects.json');
|
|
8
9
|
|
package/editors/goose.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
5
6
|
const { openReadOnly, textOf } = require('./sqlite');
|
|
6
7
|
const adapterErrors = require('./adapter-errors');
|
|
7
8
|
|
|
8
|
-
const GOOSE_DIR = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions');
|
|
9
|
+
const GOOSE_DIR = scope.root('goose', path.join(os.homedir(), '.local', 'share', 'goose', 'sessions'));
|
|
9
10
|
const DB_PATH = path.join(GOOSE_DIR, 'sessions.db');
|
|
10
11
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'goose', 'config.yaml');
|
|
11
12
|
|
package/editors/index.js
CHANGED
|
@@ -15,6 +15,7 @@ const goose = require('./goose');
|
|
|
15
15
|
const kiro = require('./kiro');
|
|
16
16
|
const codebuff = require('./codebuff');
|
|
17
17
|
const adapterErrorSink = require('./adapter-errors');
|
|
18
|
+
const scope = require('./scope');
|
|
18
19
|
|
|
19
20
|
const editors = [cursor, devin, antigravity, claude, vscode, zed, opencode, codex, gemini, copilot, copilotJetbrains, cursorAgent, commandcode, goose, kiro, codebuff];
|
|
20
21
|
|
|
@@ -47,7 +48,13 @@ function getAllChats() {
|
|
|
47
48
|
const chats = [];
|
|
48
49
|
adapterErrors = [];
|
|
49
50
|
adapterErrorSink.reset();
|
|
50
|
-
|
|
51
|
+
// MEMHOUSE_EDITORS narrows which adapters run; an unknown name throws rather than
|
|
52
|
+
// shipping nothing (or everything) behind a typo. See scope.js.
|
|
53
|
+
for (const editor of scope.selectEditors(editors, process.env.MEMHOUSE_EDITORS)) {
|
|
54
|
+
// A refused MEMHOUSE_<EDITOR>_ROOTS is recorded at load; surface it here as this
|
|
55
|
+
// adapter's error and skip it, rather than letting it scan nowhere and report zero.
|
|
56
|
+
const w = scope.watching().find((x) => x.name === editor.name);
|
|
57
|
+
if (w && w.error) { adapterErrors.push({ source: editor.name, message: w.error }); continue; }
|
|
51
58
|
try {
|
|
52
59
|
const editorChats = editor.getChats();
|
|
53
60
|
chats.push(...editorChats);
|
package/editors/kiro.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const { getAppDataPath } = require('./base');
|
|
@@ -9,9 +10,9 @@ const { getAppDataPath } = require('./base');
|
|
|
9
10
|
|
|
10
11
|
const name = 'kiro';
|
|
11
12
|
|
|
12
|
-
const KIRO_AGENT_DIR = path.join(
|
|
13
|
+
const KIRO_AGENT_DIR = scope.root('kiro', path.join(
|
|
13
14
|
getAppDataPath('Kiro'), 'User', 'globalStorage', 'kiro.kiroagent'
|
|
14
|
-
);
|
|
15
|
+
));
|
|
15
16
|
const WORKSPACE_SESSIONS_DIR = path.join(KIRO_AGENT_DIR, 'workspace-sessions');
|
|
16
17
|
|
|
17
18
|
function getChats() {
|
package/editors/opencode.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const { openReadOnly, textOf } = require('./sqlite');
|
|
@@ -6,7 +7,7 @@ const adapterErrors = require('./adapter-errors');
|
|
|
6
7
|
|
|
7
8
|
// OpenCode stores data in XDG-style paths across all platforms
|
|
8
9
|
function getOpenCodeStoragePath() {
|
|
9
|
-
return path.join(os.homedir(), '.local', 'share', 'opencode', 'storage');
|
|
10
|
+
return scope.root('opencode', path.join(os.homedir(), '.local', 'share', 'opencode', 'storage'));
|
|
10
11
|
}
|
|
11
12
|
|
|
12
13
|
const STORAGE_DIR = getOpenCodeStoragePath();
|
package/editors/scope.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
// Which of this machine's sessions ship — the scope.
|
|
2
|
+
//
|
|
3
|
+
// By default every adapter runs and the Claude adapter reads every config directory it
|
|
4
|
+
// can find (~/.claude and each ~/.claude-playbooks/<name>). A machine that keeps several
|
|
5
|
+
// Claude Code instances, or an operator who wants one instance's memory in a team house
|
|
6
|
+
// and the rest of their work elsewhere, needs to say which. Two knobs, both plain text in
|
|
7
|
+
// the env file so a person can read what a house will receive:
|
|
8
|
+
//
|
|
9
|
+
// MEMHOUSE_EDITORS comma-separated adapter names: `claude`, `codex`, … Empty = all.
|
|
10
|
+
// MEMHOUSE_<EDITOR>_ROOTS the directory that adapter reads instead of its default —
|
|
11
|
+
// MEMHOUSE_CODEX_ROOTS='~/work/.codex'. The name is the adapter's,
|
|
12
|
+
// upper-cased, dashes to underscores: MEMHOUSE_GEMINI_CLI_ROOTS.
|
|
13
|
+
// Claude is the one adapter with MANY roots (each Claude Code
|
|
14
|
+
// config dir); every other adapter has one store, so one path.
|
|
15
|
+
//
|
|
16
|
+
// `memhouse discover` prints, per adapter, what it is watching and the variable that
|
|
17
|
+
// changes it. Adapters register here at load, so the list is what actually ran.
|
|
18
|
+
//
|
|
19
|
+
// Both FAIL LOUDLY on a name or path that does not exist. A typo that silently shipped
|
|
20
|
+
// nothing would look like a working install with an empty house; one that silently
|
|
21
|
+
// shipped everything would defeat the point of setting it.
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const os = require('os');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
function parseList(value) {
|
|
28
|
+
return String(value || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The adapters that should run, in the order given. */
|
|
32
|
+
function selectEditors(editors, value) {
|
|
33
|
+
const wanted = parseList(value);
|
|
34
|
+
if (!wanted.length) return editors;
|
|
35
|
+
const byName = new Map(editors.map((e) => [e.name, e]));
|
|
36
|
+
const unknown = wanted.filter((n) => !byName.has(n));
|
|
37
|
+
if (unknown.length) {
|
|
38
|
+
throw new Error(`MEMHOUSE_EDITORS names an adapter that does not exist: ${unknown.join(', ')}. `
|
|
39
|
+
+ `Known: ${editors.map((e) => e.name).join(', ')}`);
|
|
40
|
+
}
|
|
41
|
+
return wanted.map((n) => byName.get(n));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function expandHome(p) {
|
|
45
|
+
return p === '~' ? os.homedir() : p.startsWith('~/') ? path.join(os.homedir(), p.slice(2)) : p;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The Claude roots to read. With nothing set, the discovered list as-is. With a list set,
|
|
50
|
+
* exactly those directories — each must exist and hold a `projects/` dir or a
|
|
51
|
+
* `history.jsonl`, the same test discovery applies, so a path that is real but not a
|
|
52
|
+
* Claude config dir is refused rather than silently contributing nothing.
|
|
53
|
+
*/
|
|
54
|
+
function selectClaudeRoots(discovered, value, { exists = fs.existsSync, isDir = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } } } = {}) {
|
|
55
|
+
const wanted = parseList(value).map(expandHome);
|
|
56
|
+
if (!wanted.length) return discovered;
|
|
57
|
+
const bad = [];
|
|
58
|
+
for (const r of wanted) {
|
|
59
|
+
if (!isDir(r)) { bad.push(`${r} (not a directory)`); continue; }
|
|
60
|
+
if (!isDir(path.join(r, 'projects')) && !exists(path.join(r, 'history.jsonl'))) bad.push(`${r} (no projects/ and no history.jsonl — not a Claude Code config dir)`);
|
|
61
|
+
}
|
|
62
|
+
if (bad.length) throw new Error(`MEMHOUSE_CLAUDE_ROOTS: ${bad.join('; ')}`);
|
|
63
|
+
return wanted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** One line for status/discover: what this install ships, or 'everything'. */
|
|
67
|
+
function describe(env = process.env) {
|
|
68
|
+
const parts = [];
|
|
69
|
+
if (parseList(env.MEMHOUSE_EDITORS).length) parts.push(`editors: ${parseList(env.MEMHOUSE_EDITORS).join(', ')}`);
|
|
70
|
+
for (const k of Object.keys(env).filter((k) => /^MEMHOUSE_[A-Z_]+_ROOTS$/.test(k)).sort()) {
|
|
71
|
+
if (parseList(env[k]).length) parts.push(`${k.slice(9, -6).toLowerCase().replace(/_/g, '-')} roots: ${parseList(env[k]).join(', ')}`);
|
|
72
|
+
}
|
|
73
|
+
return parts.length ? parts.join('; ') : 'everything this machine has';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The env variable that overrides an adapter's root(s). */
|
|
77
|
+
function keyFor(name) { return `MEMHOUSE_${String(name).toUpperCase().replace(/-/g, '_')}_ROOTS`; }
|
|
78
|
+
|
|
79
|
+
const isDirFs = (p) => { try { return fs.statSync(p).isDirectory(); } catch { return false; } };
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Pure core for a single-store adapter: the override if one is set, else the default.
|
|
83
|
+
* An override must be exactly one existing directory. Errors are RETURNED, not thrown, so
|
|
84
|
+
* a bad value for one adapter skips that adapter with its reason and never takes the other
|
|
85
|
+
* sixteen down with it at module load.
|
|
86
|
+
*/
|
|
87
|
+
function selectRoot(dflt, value, { isDir = isDirFs } = {}) {
|
|
88
|
+
const wanted = parseList(value).map(expandHome);
|
|
89
|
+
if (!wanted.length) return { root: dflt, error: null };
|
|
90
|
+
if (wanted.length > 1) return { root: null, error: `reads one directory, ${wanted.length} given` };
|
|
91
|
+
if (!isDir(wanted[0])) return { root: null, error: `${wanted[0]} is not a directory` };
|
|
92
|
+
return { root: wanted[0], error: null };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// What each adapter is watching, recorded as adapters load. `key` is null for an adapter
|
|
96
|
+
// whose location is not overridable yet; `error` is why an override was refused.
|
|
97
|
+
const watched = new Map();
|
|
98
|
+
const NOWHERE = path.join(os.tmpdir(), 'memhouse-scope-refused-' + process.pid);
|
|
99
|
+
|
|
100
|
+
function root(name, dflt, env = process.env) {
|
|
101
|
+
const key = keyFor(name);
|
|
102
|
+
const { root: r, error } = selectRoot(dflt, env[key]);
|
|
103
|
+
watched.set(name, { roots: [error ? null : r].filter(Boolean), key, error: error ? `${key}: ${error}` : null });
|
|
104
|
+
return error ? NOWHERE : r;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function roots(name, defaults, value, opts) {
|
|
108
|
+
const key = keyFor(name);
|
|
109
|
+
try {
|
|
110
|
+
const r = selectClaudeRoots(defaults, value, opts);
|
|
111
|
+
watched.set(name, { roots: r, key, error: null });
|
|
112
|
+
return r;
|
|
113
|
+
} catch (e) {
|
|
114
|
+
watched.set(name, { roots: [], key, error: String(e.message || e) });
|
|
115
|
+
throw e;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Register an adapter whose location is built in and not (yet) overridable. */
|
|
120
|
+
function fixed(name, dirs) { watched.set(name, { roots: dirs.filter(Boolean), key: null, error: null }); }
|
|
121
|
+
|
|
122
|
+
function watching() {
|
|
123
|
+
return [...watched.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, w]) => ({ name, ...w }));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
module.exports = { parseList, selectEditors, selectClaudeRoots, selectRoot, expandHome, describe, keyFor, root, roots, fixed, watching };
|
package/editors/vscode.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
const { getAppDataPath } = require('./base');
|
|
@@ -14,6 +15,7 @@ const VARIANTS = [
|
|
|
14
15
|
appSupport: getAppDataPath('Code - Insiders'),
|
|
15
16
|
},
|
|
16
17
|
];
|
|
18
|
+
scope.fixed('vscode', VARIANTS.map((v) => v.appSupport));
|
|
17
19
|
|
|
18
20
|
// ============================================================
|
|
19
21
|
// JSONL reconstruction: kind:0 = init, kind:1 = patch at key path
|
package/editors/zed.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
|
+
const scope = require('./scope');
|
|
2
3
|
const fs = require('fs');
|
|
3
4
|
const os = require('os');
|
|
4
5
|
|
|
@@ -21,7 +22,7 @@ function getZedDataPath() {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
const THREADS_DB = path.join(getZedDataPath(), 'threads', 'threads.db');
|
|
25
|
+
const THREADS_DB = path.join(scope.root('zed', getZedDataPath()), 'threads', 'threads.db');
|
|
25
26
|
|
|
26
27
|
// ============================================================
|
|
27
28
|
// Decompress zstd blob via CLI (with cross-platform support)
|
package/memhouse/DESIGN.md
CHANGED
|
@@ -5,12 +5,35 @@ on your machines — across **all 17 editors** agentlytics supports — parsed l
|
|
|
5
5
|
shipped to a typed ClickHouse store, shareable with a team, installable on the
|
|
6
6
|
ultimagent kernel as an **agency**, and visible through the agentlytics dashboard.
|
|
7
7
|
|
|
8
|
-
**A house is a database; its rooms are
|
|
9
|
-
`
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
8
|
+
**A house is a database; its rooms are tables** — `sessions`, `messages`, `tool_calls`.
|
|
9
|
+
The server-stamped `user_id` says who wrote a row, the install fingerprint `host` says
|
|
10
|
+
which machine.
|
|
11
|
+
|
|
12
|
+
**What a member owns inside a house is the layout's whole question, and it has three
|
|
13
|
+
answers, two of them retired.** Shared tables with row policies fell in 0.4.0 (policies
|
|
14
|
+
are permissive and OR'd — a catch-all fails OPEN). Per-member suffixed rooms fell in
|
|
15
|
+
0.8.0, on the argument that isolation between housemates solved an adversarial problem
|
|
16
|
+
the product did not have. That argument was wrong, and the counter-example is measured:
|
|
17
|
+
with everyone holding `ALL ON db.* WITH GRANT OPTION`, one member ran
|
|
18
|
+
`GRANT SELECT ON db.* TO <outsider>` and handed over a housemate's transcripts — no
|
|
19
|
+
admin, no notification.
|
|
20
|
+
|
|
21
|
+
So the layout today is per-member rooms again, prefixed rather than suffixed, and this
|
|
22
|
+
time there is only one of it: **every member's rooms are named for them, and one wildcard
|
|
23
|
+
grant — `ON mem.<name>_*` — is the whole of what they hold.** Nobody is granted the
|
|
24
|
+
database. A house with one member is a house of one, not a different layout; standalone,
|
|
25
|
+
team and kernel are the same statements. `roomNames()` in `house/house.js` is the only
|
|
26
|
+
place a table name is produced and `provision.js` the only description of the grant.
|
|
27
|
+
|
|
28
|
+
The two objections that retired the 0.8.0 layout were real and are paid again here:
|
|
29
|
+
clients must resolve names before querying (`memhouse rooms`, and the `q()` helper the
|
|
30
|
+
skills route through), and reading across members needs a `UNION ALL` rather than one
|
|
31
|
+
table. Both are cheaper than the leak.
|
|
32
|
+
|
|
33
|
+
**Why grants and not policies.** A grant is something a colleague can verify for
|
|
34
|
+
themselves — `SHOW GRANTS` lists their rooms, and `SHOW TABLES` does not show them what
|
|
35
|
+
they were not granted. "Trust the row policy protecting your rows" is a sentence you
|
|
36
|
+
cannot ask a teammate to accept when "here are your grants, check them" is available.
|
|
14
37
|
|
|
15
38
|
**Agency, precisely.** In constellation terms (`../TERMINOLOGY.md`) memhouse is a
|
|
16
39
|
**house** — the `mem` database — **plus a resident**: the shipper. That pairing
|
|
@@ -60,7 +83,7 @@ them free and further along; **bet 3 is the one nobody else attempts.**
|
|
|
60
83
|
write axis this bet is precisely *moving work from routine to resident*:
|
|
61
84
|
memory-house parses when you query, memhouse parses before anyone asks and
|
|
62
85
|
writes the result down.
|
|
63
|
-
2. **Typed common schema** (`house/schema.sql.tpl` —
|
|
86
|
+
2. **Typed common schema** (`house/schema.sql.tpl` — the rooms, rendered per member).
|
|
64
87
|
Physical typed columns (what memory-house derives in views) + `tool_calls`
|
|
65
88
|
(memory-house has no tool table) + one `extra JSON` escape hatch per room so
|
|
66
89
|
unnormalized adapter fields are never lost. `ReplacingMergeTree(ingested_at)`;
|
|
@@ -84,7 +107,7 @@ them free and further along; **bet 3 is the one nobody else attempts.**
|
|
|
84
107
|
| Path | What | Notes |
|
|
85
108
|
|---|---|---|
|
|
86
109
|
| `house/house.js` | room names + the session rollup + the user pin | one rule for both transports |
|
|
87
|
-
| `house/schema.sql.tpl` | the
|
|
110
|
+
| `house/schema.sql.tpl` | the rooms (typed) | applied by the member's shipper, or by the operator at invite in a shared house |
|
|
88
111
|
| `house/HOUSE.md` | the model, and what it replaced | |
|
|
89
112
|
| `shipper/ship.js` | parse-on-client shipper CLI — the resident (`worker`) | reuses `../../editors`; incremental; idempotent |
|
|
90
113
|
| `server/server.js` | REST API + dashboard | same API contract as agentlytics; serves `../../public` |
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Which npm channel this install follows, and what `memhouse update` may move it to.
|
|
2
|
+
//
|
|
3
|
+
// `update` used to install `memhouse@latest`, full stop. That is right for a house that
|
|
4
|
+
// tracks the published release and wrong for every other kind of install: a build
|
|
5
|
+
// published under another dist-tag (`memhouse@team`, so a team on a newer layout does not
|
|
6
|
+
// drag every other house along) would be DOWNGRADED to latest on its next update, and a
|
|
7
|
+
// tarball install has no tag at all and would be replaced by whatever latest is.
|
|
8
|
+
//
|
|
9
|
+
// So the channel is resolved, in this order:
|
|
10
|
+
// 1. pinned — MEMHOUSE_CHANNEL in the env file, or --channel on the command line
|
|
11
|
+
// 2. inferred — the dist-tag whose version equals the one installed (latest preferred)
|
|
12
|
+
// 3. a pre-release version matching no tag is a tarball: no automatic update at all
|
|
13
|
+
// 4. otherwise latest
|
|
14
|
+
// Pure, so the decision is unit-tested against every shape of registry answer.
|
|
15
|
+
|
|
16
|
+
function isPrerelease(version) { return /-/.test(String(version || '')); }
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {{version:string, pinned?:string|null, tags:Record<string,string>|null}} p
|
|
20
|
+
* @returns {{channel:string|null, target:string|null, reason:string}}
|
|
21
|
+
*/
|
|
22
|
+
function pickChannel({ version, pinned = null, tags = null }) {
|
|
23
|
+
const t = tags || {};
|
|
24
|
+
if (pinned) {
|
|
25
|
+
return t[pinned]
|
|
26
|
+
? { channel: pinned, target: t[pinned], reason: `pinned by MEMHOUSE_CHANNEL` }
|
|
27
|
+
: { channel: pinned, target: null, reason: `pinned to '${pinned}', which is not a tag on the registry${tags ? '' : ' (or the registry did not answer)'}` };
|
|
28
|
+
}
|
|
29
|
+
const matching = Object.entries(t).filter(([, v]) => v === version).map(([k]) => k);
|
|
30
|
+
if (matching.includes('latest')) return { channel: 'latest', target: t.latest, reason: 'installed version is latest' };
|
|
31
|
+
if (matching.length) return { channel: matching[0], target: t[matching[0]], reason: `installed version is published as '${matching[0]}'` };
|
|
32
|
+
if (isPrerelease(version)) {
|
|
33
|
+
return { channel: null, target: null, reason: `${version} is on no registry tag — a tarball or checkout build; update it by installing a newer tarball, or pin a channel: memhouse update --channel <tag>` };
|
|
34
|
+
}
|
|
35
|
+
return { channel: 'latest', target: t.latest || null, reason: tags ? 'following latest' : 'following latest (registry did not answer)' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The registry's dist-tags, or null when it cannot be reached. Abbreviated document, small. */
|
|
39
|
+
async function fetchTags(fetchImpl = globalThis.fetch) {
|
|
40
|
+
try {
|
|
41
|
+
const res = await fetchImpl('https://registry.npmjs.org/memhouse', {
|
|
42
|
+
headers: { accept: 'application/vnd.npm.install-v1+json' }, signal: AbortSignal.timeout(8000),
|
|
43
|
+
});
|
|
44
|
+
if (!res.ok) return null;
|
|
45
|
+
const j = await res.json();
|
|
46
|
+
return j && j['dist-tags'] ? j['dist-tags'] : null;
|
|
47
|
+
} catch { return null; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = { pickChannel, fetchTags, isPrerelease };
|
|
@@ -43,7 +43,8 @@ already exist.
|
|
|
43
43
|
- If a command fails, read what it printed before retrying. memhouse's failures name the
|
|
44
44
|
cause and usually name the fix — it is not a generic tool that fails generically.
|
|
45
45
|
- The house's rooms are three PLAIN, SHARED tables — `sessions`, `messages`,
|
|
46
|
-
`tool_calls` — in the connection's database
|
|
46
|
+
`tool_calls` — in the connection's database, carrying your own prefix if this is a
|
|
47
|
+
shared house (`alice_messages`). `memhouse rooms` prints what yours are called.
|
|
47
48
|
Direct reads of `messages`/`tool_calls` must filter to the current parse (the shipper
|
|
48
49
|
retains superseded parses under an `epoch` column); the installed skills carry the
|
|
49
50
|
filter, so prefer them for queries.
|
|
@@ -58,8 +59,8 @@ mark. Then tell them:
|
|
|
58
59
|
|
|
59
60
|
- where the dashboard is (`http://localhost:{{PORT}}`) and that `memhouse start` runs it
|
|
60
61
|
- that `memhouse search <terms>` searches every past session from every editor
|
|
61
|
-
- that `memhouse plugins install claude` gives their agents /mem:
|
|
62
|
-
/mem:
|
|
62
|
+
- that `memhouse plugins install claude` gives their agents /mem:recall, /mem:sql,
|
|
63
|
+
/mem:house, /mem:access and /mem:admin — offer to run it
|
|
63
64
|
- that you can now search their history yourself, and offer to try one query
|
|
64
65
|
|
|
65
66
|
If `doctor` reports a failure, say which line failed and what memhouse suggested. Do not
|
|
@@ -87,7 +87,7 @@ no longer exists, so anything named here is a single editor's own store (locked
|
|
|
87
87
|
running editor, corrupt, or on a schema this parser does not know).
|
|
88
88
|
|
|
89
89
|
`stats` should show one row per source (claude-code, codex, gemini-cli, …) with
|
|
90
|
-
non-zero sessions/messages. Or verify by SQL — the rooms are
|
|
90
|
+
non-zero sessions/messages. Or verify by SQL — the rooms are the ones `memhouse rooms` names
|
|
91
91
|
(`sessions`, `messages`, `tool_calls`) and the session rollup is a saved query rather
|
|
92
92
|
than an object, so `memhouse sessions-query` prints it (it also applies the
|
|
93
93
|
current-parse filter that any hand-written read of `messages` needs — the shipper
|
|
@@ -115,8 +115,8 @@ memhouse plugins install claude
|
|
|
115
115
|
```
|
|
116
116
|
|
|
117
117
|
Installs the memhouse plugin into `<claude-config>/skills/mem`, which loads next
|
|
118
|
-
session as `mem@skills-dir` and exposes `/mem:
|
|
119
|
-
`/mem:
|
|
118
|
+
session as `mem@skills-dir` and exposes `/mem:recall`, `/mem:sql`, `/mem:house`,
|
|
119
|
+
`/mem:access` and `/mem:admin`. They read the connection from
|
|
120
120
|
`$MEMHOUSE_HOME/env` (default
|
|
121
121
|
`~/.memhouse/env`), with exported `MEMHOUSE_*` vars taking precedence, and refuse to run
|
|
122
122
|
rather than guessing a URL when neither is set. `memhouse plugins list` shows what is
|