memhouse 0.0.1 → 0.3.1
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/LICENSE +22 -0
- package/README.md +72 -3
- package/bin/memhouse.js +559 -0
- package/cache.js +850 -0
- package/ch-ingest.js +329 -0
- package/ch-schema.js +152 -0
- package/clickhouse.js +87 -0
- package/editors/adapter-errors.js +52 -0
- package/editors/antigravity.js +1048 -0
- package/editors/base.js +321 -0
- package/editors/claude.js +386 -0
- package/editors/codebuff.js +338 -0
- package/editors/codex.js +537 -0
- package/editors/commandcode.js +166 -0
- package/editors/copilot-jetbrains.js +207 -0
- package/editors/copilot.js +260 -0
- package/editors/cursor-agent.js +202 -0
- package/editors/cursor.js +498 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +386 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +274 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +432 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +229 -0
- package/index.js +424 -2
- package/mcp-server.js +294 -0
- package/mem-house/DESIGN.md +119 -0
- package/mem-house/delivery/AGENT-INSTALL.md +87 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +83 -0
- package/mem-house/delivery/plugin/.claude-plugin/plugin.json +8 -0
- package/mem-house/delivery/skills/memhouse-search/SKILL.md +104 -0
- package/mem-house/delivery/skills/memhouse-sessions/SKILL.md +94 -0
- package/mem-house/delivery/skills/memhouse-sql/SKILL.md +115 -0
- package/mem-house/rls.sql +17 -0
- package/mem-house/schema.sql +145 -0
- package/mem-house/server/queries.js +878 -0
- package/mem-house/server/server.js +228 -0
- package/mem-house/shipper/ship.js +461 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +59 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-CCPh7a2Z.js +75 -0
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/index.html +16 -0
- package/relay-client.js +306 -0
- package/relay-server.js +577 -0
- package/server.js +860 -0
- package/share-image.js +367 -0
- package/ui/eslint.config.js +29 -0
- package/ui/index.html +15 -0
- package/ui/package-lock.json +4229 -0
- package/ui/package.json +38 -0
- package/ui/src/App.jsx +364 -0
- package/ui/src/components/ActivityHeatmap.jsx +206 -0
- package/ui/src/components/AiAuditCard.jsx +254 -0
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/components/AnimatedLogo.jsx +96 -0
- package/ui/src/components/ChatSidebar.jsx +226 -0
- package/ui/src/components/DateRangePicker.jsx +104 -0
- package/ui/src/components/EditorDot.jsx +11 -0
- package/ui/src/components/EditorIcon.jsx +117 -0
- package/ui/src/components/KpiCard.jsx +9 -0
- package/ui/src/components/LiveFeed.jsx +128 -0
- package/ui/src/components/LoginScreen.jsx +81 -0
- package/ui/src/components/MessageRenderer.jsx +148 -0
- package/ui/src/components/PageHeader.jsx +11 -0
- package/ui/src/components/SectionTitle.jsx +3 -0
- package/ui/src/components/ShareModal.jsx +305 -0
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/hooks/useLive.jsx +15 -0
- package/ui/src/index.css +83 -0
- package/ui/src/lib/api.js +343 -0
- package/ui/src/lib/constants.js +88 -0
- package/ui/src/lib/theme.jsx +23 -0
- package/ui/src/main.jsx +16 -0
- package/ui/src/pages/Artifacts.jsx +575 -0
- package/ui/src/pages/Compare.jsx +232 -0
- package/ui/src/pages/CostAnalysis.jsx +354 -0
- package/ui/src/pages/Dashboard.jsx +529 -0
- package/ui/src/pages/DeepAnalysis.jsx +495 -0
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/MCPs.jsx +744 -0
- package/ui/src/pages/ProjectDetail.jsx +325 -0
- package/ui/src/pages/Projects.jsx +249 -0
- package/ui/src/pages/RelayDashboard.jsx +590 -0
- package/ui/src/pages/RelayUserDetail.jsx +466 -0
- package/ui/src/pages/Sessions.jsx +535 -0
- package/ui/src/pages/Settings.jsx +241 -0
- package/ui/src/pages/SqlViewer.jsx +370 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Fatih Kadir Akın (agentlytics — original work)
|
|
4
|
+
Copyright (c) 2026 Ramazan Polat (memhouse — derivative work)
|
|
5
|
+
|
|
6
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
7
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
8
|
+
in the Software without restriction, including without limitation the rights
|
|
9
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
10
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
11
|
+
furnished to do so, subject to the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be included in all
|
|
14
|
+
copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
17
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
18
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
19
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
20
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,6 +1,75 @@
|
|
|
1
1
|
# memhouse
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
**memhouse** — agent conversation memory as a product. Every coding-agent session
|
|
4
|
+
on your machines — across the **17 editors** the agentlytics adapters support —
|
|
5
|
+
parsed locally, shipped to one typed ClickHouse store, shareable with a team,
|
|
6
|
+
installable on the ultimagent kernel as an **agency**, and visible through the
|
|
7
|
+
agentlytics dashboard unchanged.
|
|
5
8
|
|
|
6
|
-
|
|
9
|
+
An **agency** in the constellation sense (`TERMINOLOGY.md`): a **house** — the
|
|
10
|
+
`memhouse` database — plus a **resident** working in it, the shipper. The test is
|
|
11
|
+
what writes. The shipper fires on its own loop and puts rows in the house that
|
|
12
|
+
outlive any query; everything else here is only ever read. The house alone would
|
|
13
|
+
hold; the shipper is what makes it act.
|
|
14
|
+
|
|
15
|
+
An alternative agency **competing with memory-house**; if it wins, it becomes
|
|
16
|
+
memory-house v4. Start with `mem-house/DESIGN.md` for the four bets
|
|
17
|
+
(parse-on-client, typed common schema, kernel-agency, borrowed UI).
|
|
18
|
+
|
|
19
|
+
## Layout
|
|
20
|
+
|
|
21
|
+
| Path | What |
|
|
22
|
+
|---|---|
|
|
23
|
+
| `mem-house/` | the product: `DESIGN.md`, `schema.sql`, `rls.sql`, `shipper/`, `server/`, `delivery/` |
|
|
24
|
+
| `editors/` | the 17 editor adapters (inherited from agentlytics; the crown jewels) |
|
|
25
|
+
| `pricing.js` + `pricing.json` | the cost engine |
|
|
26
|
+
| `ui/` | the dashboard SPA (built to `public/`, served unchanged by the memhouse server) |
|
|
27
|
+
| `agency/` | the earlier agentlytics-agency wrap (raw canonical shape) — kept as prior art |
|
|
28
|
+
| `TERMINOLOGY.md` | the constellation terminology canon + how it applies here |
|
|
29
|
+
| everything else at root | upstream agentlytics (see `AGENTLYTICS-README.md`), still runnable |
|
|
30
|
+
|
|
31
|
+
## Quickstart — the `memhouse` CLI
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npm install -g memhouse --allow-scripts=better-sqlite3
|
|
35
|
+
memhouse onboard # wizard: discover → configure → ship → start
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Do not drop `--allow-scripts=better-sqlite3`.** Five adapters — cursor, goose,
|
|
39
|
+
opencode, zed, and antigravity — read sessions out of SQLite files, and
|
|
40
|
+
`better-sqlite3` builds its native binding from an install script. npm 12 blocks
|
|
41
|
+
install scripts by default, so without the flag those five read nothing and you
|
|
42
|
+
silently ship a partial history. `memhouse discover` and `memhouse doctor` both
|
|
43
|
+
say so when the binding is missing.
|
|
44
|
+
|
|
45
|
+
To try it without installing, npx takes the same flag — it has to come before the
|
|
46
|
+
package name:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx --allow-scripts=better-sqlite3 -y memhouse discover
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Working from a checkout instead: `npm install` (the repo's `allowScripts` field
|
|
53
|
+
covers the binding), then `node bin/memhouse.js …`.
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
memhouse onboard | install | setup | discover | uninstall | reset
|
|
57
|
+
memhouse ship [--full|--loop N] | stats | search <terms> | start | stop | status | doctor
|
|
58
|
+
memhouse plugins install claude | prompt
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Every command is dual-mode: interactive for humans, `--yes`/flags/`--json` for
|
|
62
|
+
agents — so an agent can self-install its own memory (`memhouse install --yes …`,
|
|
63
|
+
`memhouse plugins install claude`). Config: flags > `MEMHOUSE_*` env >
|
|
64
|
+
`~/.memhouse/env` > defaults.
|
|
65
|
+
|
|
66
|
+
Deeper docs: `mem-house/delivery/AGENT-INSTALL.md`, kernel install (agency
|
|
67
|
+
`memhouse`, members, own-only RLS): `mem-house/delivery/kernel-install.md`,
|
|
68
|
+
skills/plugin payloads: `mem-house/delivery/`.
|
|
69
|
+
|
|
70
|
+
## Heritage & license
|
|
71
|
+
|
|
72
|
+
Built on [agentlytics](https://github.com/f/agentlytics) by Fatih Kadir Akın (MIT)
|
|
73
|
+
— the adapters, dashboard, and cost engine come from there (this repo's history
|
|
74
|
+
carries the full lineage). The `agentlytics` remote tracks the private working
|
|
75
|
+
mirror for syncing adapter improvements both ways.
|
package/bin/memhouse.js
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// memhouse — the mem-house CLI. One command to install, ship, serve, and query
|
|
3
|
+
// agent conversation memory (see mem-house/DESIGN.md).
|
|
4
|
+
//
|
|
5
|
+
// Thin orchestrator: heavy operations run the existing entrypoints
|
|
6
|
+
// (mem-house/shipper/ship.js, mem-house/server/server.js) as children with the
|
|
7
|
+
// resolved MEMHOUSE_* env, so there is a single source of truth for shipping and
|
|
8
|
+
// serving. The CLI owns: config (~/.memhouse/env), daemons (pidfiles), probes,
|
|
9
|
+
// and the small read-only queries (status/search) done over ClickHouse HTTP.
|
|
10
|
+
//
|
|
11
|
+
// Every command is dual-mode: interactive for humans, flag/env-driven (--yes,
|
|
12
|
+
// --json where relevant) for agents. Config precedence: flags > process env >
|
|
13
|
+
// $MEMHOUSE_HOME/env file > defaults.
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const os = require('os');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { spawn, spawnSync } = require('child_process');
|
|
19
|
+
|
|
20
|
+
const REPO_ROOT = path.join(__dirname, '..');
|
|
21
|
+
const SHIP_JS = path.join(REPO_ROOT, 'mem-house', 'shipper', 'ship.js');
|
|
22
|
+
const SERVER_JS = path.join(REPO_ROOT, 'mem-house', 'server', 'server.js');
|
|
23
|
+
const DELIVERY = path.join(REPO_ROOT, 'mem-house', 'delivery');
|
|
24
|
+
const PKG = require(path.join(REPO_ROOT, 'package.json'));
|
|
25
|
+
|
|
26
|
+
const HOME_DIR = process.env.MEMHOUSE_HOME || path.join(os.homedir(), '.memhouse');
|
|
27
|
+
const ENV_FILE = path.join(HOME_DIR, 'env');
|
|
28
|
+
const RUN_DIR = path.join(HOME_DIR, 'run');
|
|
29
|
+
const LOG_DIR = path.join(HOME_DIR, 'logs');
|
|
30
|
+
|
|
31
|
+
// ── args ────────────────────────────────────────────────────────────────────────
|
|
32
|
+
const argv = process.argv.slice(2);
|
|
33
|
+
const cmd = argv[0] && !argv[0].startsWith('-') ? argv[0] : null;
|
|
34
|
+
const rest = cmd ? argv.slice(1) : argv;
|
|
35
|
+
const flags = {};
|
|
36
|
+
const positional = [];
|
|
37
|
+
for (let i = 0; i < rest.length; i++) {
|
|
38
|
+
const a = rest[i];
|
|
39
|
+
if (a.startsWith('--')) {
|
|
40
|
+
const key = a.slice(2);
|
|
41
|
+
const next = rest[i + 1];
|
|
42
|
+
if (next !== undefined && !next.startsWith('--')) { flags[key] = next; i++; }
|
|
43
|
+
else flags[key] = true;
|
|
44
|
+
} else positional.push(a);
|
|
45
|
+
}
|
|
46
|
+
const JSON_OUT = flags.json === true;
|
|
47
|
+
|
|
48
|
+
// ── config ──────────────────────────────────────────────────────────────────────
|
|
49
|
+
function readEnvFile() {
|
|
50
|
+
const out = {};
|
|
51
|
+
try {
|
|
52
|
+
for (const line of fs.readFileSync(ENV_FILE, 'utf-8').split('\n')) {
|
|
53
|
+
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
|
|
54
|
+
if (m && !line.trim().startsWith('#')) {
|
|
55
|
+
let v = m[2].trim();
|
|
56
|
+
// Unwrap shell quoting (we write single-quoted; tolerate double too).
|
|
57
|
+
if (v.startsWith("'") && v.endsWith("'") && v.length >= 2) {
|
|
58
|
+
v = v.slice(1, -1).replace(/'\\''/g, "'");
|
|
59
|
+
} else if (v.startsWith('"') && v.endsWith('"') && v.length >= 2) {
|
|
60
|
+
v = v.slice(1, -1);
|
|
61
|
+
}
|
|
62
|
+
out[m[1]] = v;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
} catch { /* no env file yet */ }
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveConfig() {
|
|
70
|
+
const file = readEnvFile();
|
|
71
|
+
const pick = (flag, env, dflt) => flags[flag] !== undefined && flags[flag] !== true
|
|
72
|
+
? flags[flag] : (process.env[env] ?? file[env] ?? dflt);
|
|
73
|
+
return {
|
|
74
|
+
url: pick('url', 'MEMHOUSE_URL', 'http://localhost:8123'),
|
|
75
|
+
user: pick('user', 'MEMHOUSE_USER', 'memhouse_root'),
|
|
76
|
+
password: pick('password', 'MEMHOUSE_PASSWORD', ''),
|
|
77
|
+
db: pick('db', 'MEMHOUSE_DB', 'memhouse'),
|
|
78
|
+
port: pick('port', 'MEMHOUSE_PORT', '4640'),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function childEnv(cfg) {
|
|
83
|
+
return {
|
|
84
|
+
...process.env,
|
|
85
|
+
MEMHOUSE_URL: cfg.url, MEMHOUSE_USER: cfg.user, MEMHOUSE_PASSWORD: cfg.password,
|
|
86
|
+
MEMHOUSE_DB: cfg.db, MEMHOUSE_PORT: String(cfg.port), MEMHOUSE_HOME: HOME_DIR,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function writeEnvFile(cfg) {
|
|
91
|
+
fs.mkdirSync(HOME_DIR, { recursive: true });
|
|
92
|
+
// Single-quoted values: this file is also sourced by shells (skills/docs use
|
|
93
|
+
// `. ~/.memhouse/env`), so metacharacters in a password must never be bare.
|
|
94
|
+
const sq = (v) => `'${String(v).replace(/'/g, `'\\''`)}'`;
|
|
95
|
+
const body = [
|
|
96
|
+
'# mem-house connection — written by `memhouse install/setup`',
|
|
97
|
+
`MEMHOUSE_URL=${sq(cfg.url)}`,
|
|
98
|
+
`MEMHOUSE_USER=${sq(cfg.user)}`,
|
|
99
|
+
`MEMHOUSE_PASSWORD=${sq(cfg.password)}`,
|
|
100
|
+
`MEMHOUSE_DB=${sq(cfg.db)}`,
|
|
101
|
+
`MEMHOUSE_PORT=${sq(cfg.port)}`,
|
|
102
|
+
'',
|
|
103
|
+
].join('\n');
|
|
104
|
+
fs.writeFileSync(ENV_FILE, body, { mode: 0o600 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── ClickHouse over HTTP (small read-only queries; heavy ops go via ship.js) ───
|
|
108
|
+
async function ch(cfg, sql, { database = cfg.db } = {}) {
|
|
109
|
+
const params = new URLSearchParams({ final: '1' });
|
|
110
|
+
if (database) params.set('database', database);
|
|
111
|
+
const res = await fetch(`${cfg.url.replace(/\/$/, '')}/?${params}`, {
|
|
112
|
+
method: 'POST',
|
|
113
|
+
body: sql,
|
|
114
|
+
headers: { Authorization: 'Basic ' + Buffer.from(`${cfg.user}:${cfg.password}`).toString('base64') },
|
|
115
|
+
signal: AbortSignal.timeout(30000),
|
|
116
|
+
});
|
|
117
|
+
const text = await res.text();
|
|
118
|
+
if (!res.ok) throw new Error(text.trim().split('\n')[0]);
|
|
119
|
+
return text.trim();
|
|
120
|
+
}
|
|
121
|
+
async function chRows(cfg, sql, opts) {
|
|
122
|
+
const text = await ch(cfg, sql + ' FORMAT JSONEachRow', opts);
|
|
123
|
+
return text ? text.split('\n').map((l) => JSON.parse(l)) : [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ── small helpers ───────────────────────────────────────────────────────────────
|
|
127
|
+
const ok = (s) => ` \x1b[32m✓\x1b[0m ${s}`;
|
|
128
|
+
const bad = (s) => ` \x1b[31m✗\x1b[0m ${s}`;
|
|
129
|
+
const warn = (s) => ` \x1b[33m•\x1b[0m ${s}`;
|
|
130
|
+
|
|
131
|
+
function run(script, args, cfg, { inherit = true } = {}) {
|
|
132
|
+
const r = spawnSync(process.execPath, [script, ...args], {
|
|
133
|
+
env: childEnv(cfg), stdio: inherit ? 'inherit' : 'pipe', encoding: 'utf-8',
|
|
134
|
+
});
|
|
135
|
+
return r.status ?? 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function ask(question, dflt) {
|
|
139
|
+
const rl = require('node:readline/promises').createInterface({ input: process.stdin, output: process.stdout });
|
|
140
|
+
const a = (await rl.question(dflt !== undefined ? `${question} [${dflt}]: ` : `${question}: `)).trim();
|
|
141
|
+
rl.close();
|
|
142
|
+
return a || dflt || '';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function pidOf(name) {
|
|
146
|
+
try {
|
|
147
|
+
const pid = parseInt(fs.readFileSync(path.join(RUN_DIR, name + '.pid'), 'utf-8'), 10);
|
|
148
|
+
process.kill(pid, 0);
|
|
149
|
+
return pid;
|
|
150
|
+
} catch { return null; }
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function ensureUiBuilt() {
|
|
154
|
+
if (fs.existsSync(path.join(REPO_ROOT, 'public', 'index.html'))) return true;
|
|
155
|
+
const uiDir = path.join(REPO_ROOT, 'ui');
|
|
156
|
+
if (!fs.existsSync(uiDir)) return false;
|
|
157
|
+
console.log(' building dashboard UI (first run, ~1-2 min)…');
|
|
158
|
+
for (const args of [['install', '--no-audit', '--no-fund'], ['run', 'build']]) {
|
|
159
|
+
const r = spawnSync('npm', args, { cwd: uiDir, stdio: 'pipe' });
|
|
160
|
+
if (r.status !== 0) { console.log(bad('UI build failed — dashboard will 503; run: cd ui && npm install && npm run build')); return false; }
|
|
161
|
+
}
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── commands ────────────────────────────────────────────────────────────────────
|
|
166
|
+
const HELP = `
|
|
167
|
+
memhouse ${PKG.version} — agent conversation memory across 17 editors
|
|
168
|
+
|
|
169
|
+
Setup onboard interactive wizard: discover → configure → ship → start
|
|
170
|
+
install scriptable setup (--url --user --password --db [--yes] [--no-ship])
|
|
171
|
+
setup (re)write the connection config only (--yes = no prompts)
|
|
172
|
+
discover read-only preflight: editors, sessions, reachable ClickHouses
|
|
173
|
+
uninstall stop daemons + remove ${HOME_DIR.replace(os.homedir(), '~')} (house data untouched)
|
|
174
|
+
reset truncate the house tables and re-ship everything (--yes to skip confirm)
|
|
175
|
+
|
|
176
|
+
Data ship one incremental pass (--full | --loop [sec])
|
|
177
|
+
stats per-source session/message/token counts
|
|
178
|
+
search <terms…> full-text search across all sessions
|
|
179
|
+
start | stop | shipper loop + dashboard as background daemons
|
|
180
|
+
status daemons, connection, counts, freshness (--json)
|
|
181
|
+
doctor diagnose the whole pipeline
|
|
182
|
+
|
|
183
|
+
Agents plugins list | install claude [--target DIR] | remove claude
|
|
184
|
+
prompt print the memory system-prompt snippet
|
|
185
|
+
|
|
186
|
+
Config: flags > MEMHOUSE_* env > ${ENV_FILE.replace(os.homedir(), '~')} > defaults.
|
|
187
|
+
`;
|
|
188
|
+
|
|
189
|
+
// A skipped adapter and an editor the user does not have look identical — both
|
|
190
|
+
// contribute zero sessions. Say which happened, and how to fix the one that is fixable.
|
|
191
|
+
function printAdapterErrors(errors) {
|
|
192
|
+
if (!errors || errors.length === 0) return;
|
|
193
|
+
const blocked = errors.filter((e) => e.missingBinding);
|
|
194
|
+
const other = errors.filter((e) => !e.missingBinding);
|
|
195
|
+
if (blocked.length) {
|
|
196
|
+
console.log(warn(`${blocked.length} adapter${blocked.length > 1 ? 's' : ''} skipped — better-sqlite3 has no native binding: ${blocked.map((e) => e.source).join(', ')}`));
|
|
197
|
+
console.log(' their sessions are NOT being shipped. npm >= 12 blocks install scripts by default; rebuild with:');
|
|
198
|
+
console.log(' npm install -g memhouse --allow-scripts=better-sqlite3');
|
|
199
|
+
console.log(' (from a checkout: npm install --no-audit --no-fund, which package.json already allows)');
|
|
200
|
+
}
|
|
201
|
+
for (const e of other) console.log(warn(`${e.source.padEnd(16)} skipped: ${e.message}`));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function cmdDiscover() {
|
|
205
|
+
const out = { editors: [], clickhouse: [], config: null, memoryHouse: false };
|
|
206
|
+
process.stdout.write(JSON_OUT ? '' : 'Scanning editors (reading local session stores)…\n');
|
|
207
|
+
try {
|
|
208
|
+
const { getAllChats, getAdapterErrors } = require(path.join(REPO_ROOT, 'editors'));
|
|
209
|
+
const counts = {};
|
|
210
|
+
for (const c of getAllChats()) counts[c.source] = (counts[c.source] || 0) + 1;
|
|
211
|
+
out.editors = Object.entries(counts).sort((a, b) => b[1] - a[1]).map(([source, sessions]) => ({ source, sessions }));
|
|
212
|
+
out.adapterErrors = getAdapterErrors();
|
|
213
|
+
} catch (e) { out.editorsError = e.message; }
|
|
214
|
+
|
|
215
|
+
for (const url of [...new Set([resolveConfig().url, 'http://localhost:8123'])]) {
|
|
216
|
+
const probe = { url, reachable: false };
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch(url.replace(/\/$/, '') + '/ping', { signal: AbortSignal.timeout(3000) });
|
|
219
|
+
probe.reachable = res.ok;
|
|
220
|
+
if (res.ok) {
|
|
221
|
+
try {
|
|
222
|
+
const cfg = { ...resolveConfig(), url };
|
|
223
|
+
probe.version = await ch(cfg, 'SELECT version()', { database: '' });
|
|
224
|
+
const dbs = (await chRows(cfg, 'SELECT name FROM system.databases', { database: '' })).map((r) => r.name);
|
|
225
|
+
probe.hasHouse = dbs.includes(cfg.db);
|
|
226
|
+
probe.kernel = dbs.includes('sys');
|
|
227
|
+
} catch { probe.auth = 'needed'; }
|
|
228
|
+
}
|
|
229
|
+
} catch { /* unreachable */ }
|
|
230
|
+
out.clickhouse.push(probe);
|
|
231
|
+
}
|
|
232
|
+
out.config = fs.existsSync(ENV_FILE) ? ENV_FILE : null;
|
|
233
|
+
out.memoryHouse = fs.existsSync(path.join(os.homedir(), '.config', 'memory-house'));
|
|
234
|
+
|
|
235
|
+
if (JSON_OUT) return console.log(JSON.stringify(out, null, 2));
|
|
236
|
+
console.log('\nEditors with sessions on this machine:');
|
|
237
|
+
if (out.editors.length === 0) console.log(warn('none found' + (out.editorsError ? ` (${out.editorsError})` : '')));
|
|
238
|
+
for (const e of out.editors) console.log(ok(`${e.source.padEnd(16)} ${e.sessions} sessions`));
|
|
239
|
+
printAdapterErrors(out.adapterErrors);
|
|
240
|
+
console.log('\nClickHouse endpoints:');
|
|
241
|
+
for (const p of out.clickhouse) {
|
|
242
|
+
if (!p.reachable) console.log(bad(`${p.url} — unreachable`));
|
|
243
|
+
else if (p.auth) console.log(warn(`${p.url} — reachable, credentials needed`));
|
|
244
|
+
else console.log(ok(`${p.url} — v${p.version}${p.hasHouse ? ', house present' : ''}${p.kernel ? ', KERNEL detected' : ''}`));
|
|
245
|
+
}
|
|
246
|
+
console.log('');
|
|
247
|
+
console.log(out.config ? ok(`config: ${out.config}`) : warn('no config yet — run: memhouse install (or onboard)'));
|
|
248
|
+
if (out.memoryHouse) console.log(warn('memory-house detected on this machine (they coexist fine)'));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function cmdInstall({ interactive }) {
|
|
252
|
+
let cfg = resolveConfig();
|
|
253
|
+
const haveAll = flags.yes === true || (flags.url && flags.user !== undefined);
|
|
254
|
+
if (interactive || !haveAll) {
|
|
255
|
+
console.log('mem-house connection (Enter keeps the default):');
|
|
256
|
+
cfg.url = await ask(' ClickHouse URL', cfg.url);
|
|
257
|
+
cfg.user = await ask(' user', cfg.user);
|
|
258
|
+
cfg.password = await ask(' password', cfg.password);
|
|
259
|
+
cfg.db = await ask(' database (the house)', cfg.db);
|
|
260
|
+
cfg.port = await ask(' dashboard port', cfg.port);
|
|
261
|
+
}
|
|
262
|
+
if (!/^[A-Za-z0-9_]{1,64}$/.test(cfg.db)) {
|
|
263
|
+
console.log(bad(`invalid database name '${cfg.db}' — use letters, digits, underscore`));
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
writeEnvFile(cfg);
|
|
267
|
+
console.log(ok(`config written: ${ENV_FILE}`));
|
|
268
|
+
// Preflight WITHOUT selecting the house — on a fresh standalone ClickHouse the
|
|
269
|
+
// database doesn't exist yet, and selecting it would fail before we can create it.
|
|
270
|
+
try { await ch(cfg, 'SELECT 1', { database: '' }); }
|
|
271
|
+
catch (e) { console.log(bad(`connection failed: ${e.message}`)); console.log(' fix the connection, then re-run: memhouse install'); return 1; }
|
|
272
|
+
console.log(ok('connection verified'));
|
|
273
|
+
// Ensure the house exists (standalone path). On a kernel realm the house is
|
|
274
|
+
// provisioned by install-agency and this is a no-op; if the user lacks CREATE
|
|
275
|
+
// DATABASE but the house is already reachable, that's fine too.
|
|
276
|
+
try { await ch(cfg, `CREATE DATABASE IF NOT EXISTS ${cfg.db}`, { database: '' }); }
|
|
277
|
+
catch (e) {
|
|
278
|
+
try { await ch(cfg, 'SELECT 1'); }
|
|
279
|
+
catch { console.log(bad(`house '${cfg.db}' does not exist and cannot be created: ${e.message}`)); return 1; }
|
|
280
|
+
}
|
|
281
|
+
console.log(ok(`house '${cfg.db}' ready`));
|
|
282
|
+
if (run(SHIP_JS, ['--ensure-schema'], cfg) !== 0) return 1;
|
|
283
|
+
if (flags['no-ship'] !== true) {
|
|
284
|
+
if (run(SHIP_JS, [], cfg) !== 0) return 1;
|
|
285
|
+
}
|
|
286
|
+
console.log(ok('installed — next: memhouse start (dashboard + shipper loop)'));
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function cmdOnboard() {
|
|
291
|
+
console.log(`memhouse ${PKG.version} — onboarding\n`);
|
|
292
|
+
await cmdDiscover();
|
|
293
|
+
console.log('');
|
|
294
|
+
const code = await cmdInstall({ interactive: true });
|
|
295
|
+
if (code !== 0) return code;
|
|
296
|
+
const yn = (await ask('Start the daemons now? (Y/n)', 'Y')).toLowerCase();
|
|
297
|
+
if (yn !== 'n' && yn !== 'no') await cmdStart();
|
|
298
|
+
return 0;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function cmdStart() {
|
|
302
|
+
const cfg = resolveConfig();
|
|
303
|
+
fs.mkdirSync(RUN_DIR, { recursive: true });
|
|
304
|
+
fs.mkdirSync(LOG_DIR, { recursive: true });
|
|
305
|
+
ensureUiBuilt();
|
|
306
|
+
const daemons = [
|
|
307
|
+
{ name: 'shipper', script: SHIP_JS, args: ['--loop', String(flags.interval || 300)] },
|
|
308
|
+
{ name: 'dashboard', script: SERVER_JS, args: [] },
|
|
309
|
+
];
|
|
310
|
+
for (const d of daemons) {
|
|
311
|
+
if (pidOf(d.name)) { console.log(warn(`${d.name} already running (pid ${pidOf(d.name)})`)); continue; }
|
|
312
|
+
const log = fs.openSync(path.join(LOG_DIR, d.name + '.log'), 'a');
|
|
313
|
+
const child = spawn(process.execPath, [d.script, ...d.args], {
|
|
314
|
+
env: childEnv(cfg), detached: true, stdio: ['ignore', log, log],
|
|
315
|
+
});
|
|
316
|
+
fs.writeFileSync(path.join(RUN_DIR, d.name + '.pid'), String(child.pid));
|
|
317
|
+
child.unref();
|
|
318
|
+
console.log(ok(`${d.name} started (pid ${child.pid})`));
|
|
319
|
+
}
|
|
320
|
+
console.log(` dashboard → http://localhost:${cfg.port}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function cmdStop() {
|
|
324
|
+
let stopped = 0;
|
|
325
|
+
for (const name of ['shipper', 'dashboard']) {
|
|
326
|
+
const pid = pidOf(name);
|
|
327
|
+
if (pid) { try { process.kill(pid, 'SIGTERM'); console.log(ok(`${name} stopped (pid ${pid})`)); stopped++; } catch { /* raced */ } }
|
|
328
|
+
try { fs.unlinkSync(path.join(RUN_DIR, name + '.pid')); } catch { /* absent */ }
|
|
329
|
+
}
|
|
330
|
+
if (!stopped) console.log(warn('nothing was running'));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
async function cmdStatus() {
|
|
334
|
+
const cfg = resolveConfig();
|
|
335
|
+
const out = {
|
|
336
|
+
config: fs.existsSync(ENV_FILE) ? ENV_FILE : null,
|
|
337
|
+
url: cfg.url, db: cfg.db, user: cfg.user,
|
|
338
|
+
daemons: { shipper: pidOf('shipper'), dashboard: pidOf('dashboard') },
|
|
339
|
+
connected: false,
|
|
340
|
+
};
|
|
341
|
+
try {
|
|
342
|
+
out.connected = true && !!(await ch(cfg, 'SELECT 1'));
|
|
343
|
+
const s = await chRows(cfg, 'SELECT count() AS sessions FROM sessions_v');
|
|
344
|
+
const m = await chRows(cfg, "SELECT count() AS msgs, formatDateTime(max(ingested_at), '%Y-%m-%d %H:%i:%S') AS freshest FROM messages");
|
|
345
|
+
out.sessions = Number(s[0]?.sessions || 0);
|
|
346
|
+
out.messages = Number(m[0]?.msgs || 0);
|
|
347
|
+
out.freshest = m[0]?.freshest || null;
|
|
348
|
+
} catch (e) { out.connected = false; out.error = e.message; }
|
|
349
|
+
|
|
350
|
+
if (JSON_OUT) return console.log(JSON.stringify(out, null, 2));
|
|
351
|
+
console.log(out.config ? ok(`config: ${out.config}`) : warn('no config (memhouse install)'));
|
|
352
|
+
console.log(out.connected ? ok(`connected: ${cfg.url} / ${cfg.db} as ${cfg.user}`) : bad(`not connected: ${out.error || cfg.url}`));
|
|
353
|
+
if (out.connected) console.log(ok(`house: ${out.sessions} sessions, ${out.messages} messages (freshest ingest ${out.freshest} UTC)`));
|
|
354
|
+
for (const [name, pid] of Object.entries(out.daemons)) {
|
|
355
|
+
console.log(pid ? ok(`${name}: running (pid ${pid})`) : warn(`${name}: not running`));
|
|
356
|
+
}
|
|
357
|
+
if (out.daemons.dashboard) console.log(` dashboard → http://localhost:${cfg.port}`);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function cmdDoctor() {
|
|
361
|
+
const cfg = resolveConfig();
|
|
362
|
+
const checks = [];
|
|
363
|
+
const add = (okFlag, label, hint) => checks.push({ ok: okFlag, label, hint });
|
|
364
|
+
|
|
365
|
+
const [major, minor] = process.versions.node.split('.').map(Number);
|
|
366
|
+
add(major > 20 || (major === 20 && minor >= 19), `node ${process.versions.node}`, 'need >= 20.19');
|
|
367
|
+
add(fs.existsSync(ENV_FILE), `config ${ENV_FILE}`, 'run: memhouse install');
|
|
368
|
+
try { await ch(cfg, 'SELECT 1', { database: '' }); add(true, `clickhouse reachable (${cfg.url})`); }
|
|
369
|
+
catch (e) { add(false, `clickhouse reachable (${cfg.url})`, e.message); }
|
|
370
|
+
try {
|
|
371
|
+
const t = (await chRows(cfg, "SELECT name FROM system.tables WHERE database = {db:String} AND name IN ('sessions','messages','tool_calls')".replace('{db:String}', `'${cfg.db}'`), { database: '' })).length;
|
|
372
|
+
add(t === 3, `schema: ${t}/3 tables in '${cfg.db}'`, 'run: memhouse install (ensure-schema)');
|
|
373
|
+
} catch (e) { add(false, 'schema check', e.message); }
|
|
374
|
+
try {
|
|
375
|
+
const u = await chRows(cfg, 'SELECT any(user_id) AS u FROM sessions');
|
|
376
|
+
add((u[0]?.u ?? '') !== '' || (await chRows(cfg, 'SELECT count() AS c FROM sessions'))[0].c === 0,
|
|
377
|
+
`identity stamping (user_id='${u[0]?.u ?? ''}')`, 'writers must use async_insert=0');
|
|
378
|
+
} catch { add(false, 'identity stamping', 'schema missing?'); }
|
|
379
|
+
let adapterErrors = [];
|
|
380
|
+
try {
|
|
381
|
+
const { getAllChats, getAdapterErrors, getMessages } = require(path.join(REPO_ROOT, 'editors'));
|
|
382
|
+
const chats = getAllChats();
|
|
383
|
+
const seen = chats.length;
|
|
384
|
+
// Listing sessions is not the same as being able to read them. Goose, for one,
|
|
385
|
+
// can query `sessions` while its `messages` rows use a schema this parser cannot
|
|
386
|
+
// decode — the shipper would then withhold every one of those sessions while
|
|
387
|
+
// doctor reported a clean bill of health. Read the newest session per source so
|
|
388
|
+
// the message readers actually run; that is one parse per editor, not a full scan.
|
|
389
|
+
// One chat per source is not enough coverage: several adapters mix storage
|
|
390
|
+
// paths, and a probe only exercises the one the sampled chat happens to use.
|
|
391
|
+
// Each such adapter declares which store a chat came from — Cursor as _type
|
|
392
|
+
// (agent-store vs workspace), Goose as _storage (sqlite vs jsonl), OpenCode as
|
|
393
|
+
// _storageType (file vs sqlite) — so probing one chat per declared variant
|
|
394
|
+
// covers every path deterministically.
|
|
395
|
+
//
|
|
396
|
+
// The newest/middle/oldest spread is kept on top of that as a cheap hedge for
|
|
397
|
+
// any split an adapter does NOT declare.
|
|
398
|
+
const picks = new Map();
|
|
399
|
+
const bySource = new Map();
|
|
400
|
+
for (const chat of chats) {
|
|
401
|
+
if (!bySource.has(chat.source)) bySource.set(chat.source, []);
|
|
402
|
+
bySource.get(chat.source).push(chat);
|
|
403
|
+
}
|
|
404
|
+
for (const [source, list] of bySource) {
|
|
405
|
+
for (const i of new Set([0, Math.floor(list.length / 2), list.length - 1])) {
|
|
406
|
+
picks.set(`${source}#${i}`, list[i]);
|
|
407
|
+
}
|
|
408
|
+
for (const chat of list) {
|
|
409
|
+
const variant = `${source}|${chat._type || ''}|${chat._storage || ''}|${chat._storageType || ''}`;
|
|
410
|
+
if (!picks.has(variant)) picks.set(variant, chat);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const thrown = [];
|
|
415
|
+
for (const chat of picks.values()) {
|
|
416
|
+
// Not every reader reports through the sink — some throw instead, e.g.
|
|
417
|
+
// Cursor's agent-store path, where discovery reads `meta` fine but
|
|
418
|
+
// collectStoreMessages() can throw on a corrupt `blobs` table. Swallowing
|
|
419
|
+
// that here would discard the only signal and leave doctor green while the
|
|
420
|
+
// shipper withholds every one of those sessions.
|
|
421
|
+
try { getMessages(chat); }
|
|
422
|
+
catch (e) { thrown.push({ source: chat.source, message: ((e && e.message) || String(e)).split('\n')[0] }); }
|
|
423
|
+
}
|
|
424
|
+
adapterErrors = getAdapterErrors();
|
|
425
|
+
for (const t of thrown) {
|
|
426
|
+
if (!adapterErrors.some((e) => e.source === t.source)) adapterErrors.push({ ...t, missingBinding: false });
|
|
427
|
+
}
|
|
428
|
+
// Any adapter that could not be read is a failure, whatever the cause — a
|
|
429
|
+
// locked or corrupt store loses just as many sessions as a missing binding.
|
|
430
|
+
const blocked = adapterErrors.filter((e) => e.missingBinding).map((e) => e.source);
|
|
431
|
+
const failed = adapterErrors.map((e) => e.source);
|
|
432
|
+
add(adapterErrors.length === 0,
|
|
433
|
+
`adapters: ${seen} sessions visible locally${failed.length ? ` (${failed.length} skipped: ${failed.join(', ')})` : ''}`,
|
|
434
|
+
blocked.length ? 'npm install -g memhouse --allow-scripts=better-sqlite3'
|
|
435
|
+
: adapterErrors.length ? adapterErrors.map((e) => `${e.source}: ${e.message}`).join('; ')
|
|
436
|
+
: undefined);
|
|
437
|
+
} catch (e) { add(false, 'adapters', e.message); }
|
|
438
|
+
add(!!pidOf('shipper'), 'shipper daemon', 'memhouse start');
|
|
439
|
+
add(!!pidOf('dashboard'), 'dashboard daemon', 'memhouse start');
|
|
440
|
+
add(fs.existsSync(path.join(REPO_ROOT, 'public', 'index.html')), 'dashboard UI built', 'built automatically by memhouse start');
|
|
441
|
+
|
|
442
|
+
for (const c of checks) console.log(c.ok ? ok(c.label) : bad(`${c.label}${c.hint ? ` — ${c.hint}` : ''}`));
|
|
443
|
+
return checks.every((c) => c.ok) ? 0 : 1;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function cmdSearch() {
|
|
447
|
+
if (!positional.length) { console.log('usage: memhouse search <terms…>'); return 2; }
|
|
448
|
+
const cfg = resolveConfig();
|
|
449
|
+
const needle = positional.join(' ').toLowerCase().replace(/[%_\\]/g, '\\$&').replace(/'/g, "\\'");
|
|
450
|
+
const rows = await chRows(cfg, `
|
|
451
|
+
SELECT session_id, any(source) AS source, any(project) AS project,
|
|
452
|
+
formatDateTime(max(ts), '%Y-%m-%d %H:%i') AS at, count() AS hits,
|
|
453
|
+
substring(any(text), 1, 150) AS snippet
|
|
454
|
+
FROM messages
|
|
455
|
+
WHERE text_ngram LIKE '%${needle}%'
|
|
456
|
+
GROUP BY session_id ORDER BY max(ts) DESC LIMIT ${Number(flags.limit) || 10}`);
|
|
457
|
+
if (JSON_OUT) return console.log(JSON.stringify(rows, null, 2));
|
|
458
|
+
if (!rows.length) return console.log(warn('no matches'));
|
|
459
|
+
for (const r of rows) {
|
|
460
|
+
console.log(`\x1b[1m${r.session_id.slice(0, 8)}\x1b[0m ${r.source} ${r.project || '-'} ${r.at} (${r.hits} hits)`);
|
|
461
|
+
console.log(` ${r.snippet.replace(/\s+/g, ' ')}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function cmdPlugins() {
|
|
466
|
+
const sub = positional[0] || 'list';
|
|
467
|
+
const target = flags.target || process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
|
|
468
|
+
const skillsSrc = path.join(DELIVERY, 'skills');
|
|
469
|
+
const names = fs.readdirSync(skillsSrc);
|
|
470
|
+
const dstSkills = path.join(target, 'skills');
|
|
471
|
+
if (sub === 'list') {
|
|
472
|
+
console.log(`skills available: ${names.join(', ')}`);
|
|
473
|
+
for (const n of names) {
|
|
474
|
+
console.log(fs.existsSync(path.join(dstSkills, n)) ? ok(`${n} installed in ${dstSkills}`) : warn(`${n} not installed (memhouse plugins install claude)`));
|
|
475
|
+
}
|
|
476
|
+
} else if (sub === 'install' && positional[1] === 'claude') {
|
|
477
|
+
fs.mkdirSync(dstSkills, { recursive: true });
|
|
478
|
+
for (const n of names) fs.cpSync(path.join(skillsSrc, n), path.join(dstSkills, n), { recursive: true });
|
|
479
|
+
console.log(ok(`installed ${names.length} skills into ${dstSkills} (${names.join(', ')})`));
|
|
480
|
+
} else if (sub === 'remove' && positional[1] === 'claude') {
|
|
481
|
+
let removed = 0;
|
|
482
|
+
for (const n of names) { const p = path.join(dstSkills, n); if (fs.existsSync(p)) { fs.rmSync(p, { recursive: true }); removed++; } }
|
|
483
|
+
console.log(ok(`removed ${removed} skills from ${dstSkills}`));
|
|
484
|
+
} else {
|
|
485
|
+
console.log('usage: memhouse plugins [list | install claude | remove claude] [--target DIR]');
|
|
486
|
+
return 2;
|
|
487
|
+
}
|
|
488
|
+
return 0;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
async function cmdReset() {
|
|
492
|
+
const cfg = resolveConfig();
|
|
493
|
+
if (flags.yes !== true) {
|
|
494
|
+
const a = (await ask(`This truncates ALL rows in '${cfg.db}' and re-ships. Continue? (yes/no)`, 'no')).toLowerCase();
|
|
495
|
+
if (a !== 'yes' && a !== 'y') return console.log('aborted'), 1;
|
|
496
|
+
}
|
|
497
|
+
for (const t of ['sessions', 'messages', 'tool_calls']) await ch(cfg, `TRUNCATE TABLE IF EXISTS ${t}`);
|
|
498
|
+
console.log(ok('house truncated'));
|
|
499
|
+
return run(SHIP_JS, ['--full'], cfg);
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function cmdUninstall() {
|
|
503
|
+
cmdStop();
|
|
504
|
+
if (fs.existsSync(HOME_DIR)) fs.rmSync(HOME_DIR, { recursive: true });
|
|
505
|
+
console.log(ok(`removed ${HOME_DIR} (the house data in ClickHouse is untouched)`));
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ── dispatch ────────────────────────────────────────────────────────────────────
|
|
509
|
+
(async () => {
|
|
510
|
+
const cfg = resolveConfig();
|
|
511
|
+
switch (cmd) {
|
|
512
|
+
case null: case 'help': console.log(HELP); break;
|
|
513
|
+
case 'version': console.log(PKG.version); break;
|
|
514
|
+
case 'discover': await cmdDiscover(); break;
|
|
515
|
+
case 'onboard': process.exitCode = await cmdOnboard(); break;
|
|
516
|
+
case 'install': process.exitCode = await cmdInstall({ interactive: false }); break;
|
|
517
|
+
case 'setup': {
|
|
518
|
+
// Dual-mode like install: --yes (with any --url/--user/--password/--db/
|
|
519
|
+
// --port overrides, already resolved into cfg) skips every prompt so
|
|
520
|
+
// agents/CI can rewrite the config non-interactively.
|
|
521
|
+
const c = { ...cfg };
|
|
522
|
+
if (flags.yes !== true) {
|
|
523
|
+
c.url = await ask(' ClickHouse URL', c.url);
|
|
524
|
+
c.user = await ask(' user', c.user);
|
|
525
|
+
c.password = await ask(' password', c.password);
|
|
526
|
+
c.db = await ask(' database (the house)', c.db);
|
|
527
|
+
c.port = await ask(' dashboard port', c.port);
|
|
528
|
+
}
|
|
529
|
+
if (!/^[A-Za-z0-9_]{1,64}$/.test(c.db)) {
|
|
530
|
+
console.log(bad(`invalid database name '${c.db}' — use letters, digits, underscore`));
|
|
531
|
+
process.exitCode = 1;
|
|
532
|
+
break;
|
|
533
|
+
}
|
|
534
|
+
writeEnvFile(c);
|
|
535
|
+
console.log(ok(`config written: ${ENV_FILE}`));
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case 'ship': {
|
|
539
|
+
const args = [];
|
|
540
|
+
if (flags.full === true) args.push('--full');
|
|
541
|
+
if (flags.loop !== undefined) { args.push('--loop'); if (flags.loop !== true) args.push(String(flags.loop)); }
|
|
542
|
+
process.exitCode = run(SHIP_JS, args, cfg);
|
|
543
|
+
break;
|
|
544
|
+
}
|
|
545
|
+
case 'stats': process.exitCode = run(SHIP_JS, ['--stats'], cfg); break;
|
|
546
|
+
case 'start': await cmdStart(); break;
|
|
547
|
+
case 'stop': cmdStop(); break;
|
|
548
|
+
case 'status': await cmdStatus(); break;
|
|
549
|
+
case 'doctor': process.exitCode = await cmdDoctor(); break;
|
|
550
|
+
case 'search': process.exitCode = await cmdSearch(); break;
|
|
551
|
+
case 'plugins': process.exitCode = cmdPlugins(); break;
|
|
552
|
+
case 'prompt': process.stdout.write(fs.readFileSync(path.join(DELIVERY, 'PROMPT.md'), 'utf-8')); break;
|
|
553
|
+
case 'reset': process.exitCode = await cmdReset(); break;
|
|
554
|
+
case 'uninstall': cmdUninstall(); break;
|
|
555
|
+
default:
|
|
556
|
+
console.error(`unknown command: ${cmd}\n${HELP}`);
|
|
557
|
+
process.exitCode = 2;
|
|
558
|
+
}
|
|
559
|
+
})().catch((e) => { console.error(bad(e.message)); process.exit(1); });
|