memhouse 0.0.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -0
- package/README.md +59 -3
- package/bin/memhouse.js +487 -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/antigravity.js +1019 -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 +438 -0
- package/editors/gemini.js +231 -0
- package/editors/goose.js +358 -0
- package/editors/gsd.js +366 -0
- package/editors/index.js +179 -0
- package/editors/kiro.js +315 -0
- package/editors/opencode.js +374 -0
- package/editors/vscode.js +413 -0
- package/editors/windsurf.js +634 -0
- package/editors/zed.js +213 -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 +75 -0
- package/mem-house/delivery/PROMPT.md +42 -0
- package/mem-house/delivery/install.sh +181 -0
- package/mem-house/delivery/kernel-install.md +76 -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 +402 -0
- package/misc/syntax-check.js +38 -0
- package/package.json +56 -7
- package/pricing.js +88 -0
- package/pricing.json +992 -0
- package/public/assets/index-BsgjOSTv.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 +344 -0
- package/ui/src/pages/Subscriptions.jsx +411 -0
- package/ui/vite.config.js +18 -0
package/editors/zed.js
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const Database = require('better-sqlite3');
|
|
6
|
+
|
|
7
|
+
// Zed stores data in different locations depending on the platform
|
|
8
|
+
// - Windows: %LOCALAPPDATA%\Zed (not Roaming)
|
|
9
|
+
// - macOS: ~/Library/Application Support/Zed
|
|
10
|
+
// - Linux: ~/.config/Zed
|
|
11
|
+
function getZedDataPath() {
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
switch (process.platform) {
|
|
14
|
+
case 'win32':
|
|
15
|
+
return path.join(home, 'AppData', 'Local', 'Zed');
|
|
16
|
+
case 'darwin':
|
|
17
|
+
return path.join(home, 'Library', 'Application Support', 'Zed');
|
|
18
|
+
default: // linux, etc.
|
|
19
|
+
return path.join(home, '.config', 'Zed');
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const THREADS_DB = path.join(getZedDataPath(), 'threads', 'threads.db');
|
|
24
|
+
|
|
25
|
+
// ============================================================
|
|
26
|
+
// Decompress zstd blob via CLI (with cross-platform support)
|
|
27
|
+
// ============================================================
|
|
28
|
+
|
|
29
|
+
function decompressZstd(buf) {
|
|
30
|
+
const tmpIn = path.join(os.tmpdir(), `zed_thread_${Date.now()}.zst`);
|
|
31
|
+
const tmpOut = tmpIn.replace('.zst', '.json');
|
|
32
|
+
try {
|
|
33
|
+
fs.writeFileSync(tmpIn, buf);
|
|
34
|
+
|
|
35
|
+
// Try zstd CLI first
|
|
36
|
+
try {
|
|
37
|
+
const { execFileSync } = require('child_process');
|
|
38
|
+
const zstdCmd = process.platform === 'win32' ? 'zstd.exe' : 'zstd';
|
|
39
|
+
execFileSync(zstdCmd, ['-d', '-f', '-q', tmpIn, '-o', tmpOut], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback: try using Node.js zstd library if available
|
|
42
|
+
try {
|
|
43
|
+
const zlib = require('zlib');
|
|
44
|
+
// Check if Node version supports zstd natively (v22+)
|
|
45
|
+
if (zlib.createZstdDecompress) {
|
|
46
|
+
const decompressed = zlib.zstdDecompressSync(buf);
|
|
47
|
+
fs.writeFileSync(tmpOut, decompressed);
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error('zstd not available');
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error('zstd decompression not available on this system');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const data = fs.readFileSync(tmpOut, 'utf-8');
|
|
57
|
+
return data;
|
|
58
|
+
} finally {
|
|
59
|
+
try { fs.unlinkSync(tmpIn); } catch {}
|
|
60
|
+
try { fs.unlinkSync(tmpOut); } catch {}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ============================================================
|
|
65
|
+
// Query SQLite using better-sqlite3 (cross-platform)
|
|
66
|
+
// ============================================================
|
|
67
|
+
|
|
68
|
+
function queryDb(sql) {
|
|
69
|
+
if (!fs.existsSync(THREADS_DB)) return [];
|
|
70
|
+
try {
|
|
71
|
+
const db = new Database(THREADS_DB, { readonly: true });
|
|
72
|
+
const rows = db.prepare(sql).all();
|
|
73
|
+
db.close();
|
|
74
|
+
return rows;
|
|
75
|
+
} catch (e) {
|
|
76
|
+
// Silently fail if database is locked or inaccessible
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function queryBlob(id) {
|
|
82
|
+
if (!fs.existsSync(THREADS_DB)) return null;
|
|
83
|
+
try {
|
|
84
|
+
const db = new Database(THREADS_DB, { readonly: true });
|
|
85
|
+
const row = db.prepare('SELECT data FROM threads WHERE id = ?').get(id);
|
|
86
|
+
db.close();
|
|
87
|
+
return row ? row.data : null;
|
|
88
|
+
} catch {
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================
|
|
94
|
+
// Adapter interface
|
|
95
|
+
// ============================================================
|
|
96
|
+
|
|
97
|
+
const name = 'zed';
|
|
98
|
+
|
|
99
|
+
function getChats() {
|
|
100
|
+
const rows = queryDb(
|
|
101
|
+
'SELECT id, summary, updated_at, data_type, length(data) as data_size, parent_id, worktree_branch FROM threads ORDER BY updated_at DESC'
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
return rows.map(row => ({
|
|
105
|
+
source: 'zed',
|
|
106
|
+
composerId: row.id,
|
|
107
|
+
name: row.summary || null,
|
|
108
|
+
createdAt: row.updated_at ? new Date(row.updated_at).getTime() : null,
|
|
109
|
+
lastUpdatedAt: row.updated_at ? new Date(row.updated_at).getTime() : null,
|
|
110
|
+
mode: 'thread',
|
|
111
|
+
folder: null,
|
|
112
|
+
encrypted: false,
|
|
113
|
+
bubbleCount: 0,
|
|
114
|
+
_dataType: row.data_type,
|
|
115
|
+
_gitBranch: row.worktree_branch,
|
|
116
|
+
}));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getMessages(chat) {
|
|
120
|
+
const blob = queryBlob(chat.composerId);
|
|
121
|
+
if (!blob) return [];
|
|
122
|
+
|
|
123
|
+
let json;
|
|
124
|
+
const dataType = chat._dataType || 'zstd';
|
|
125
|
+
try {
|
|
126
|
+
if (dataType === 'zstd') {
|
|
127
|
+
json = decompressZstd(blob);
|
|
128
|
+
} else {
|
|
129
|
+
json = blob.toString('utf-8');
|
|
130
|
+
}
|
|
131
|
+
} catch (e) {
|
|
132
|
+
// Decompression failed - zstd CLI not available
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
let data;
|
|
137
|
+
try { data = JSON.parse(json); } catch { return []; }
|
|
138
|
+
|
|
139
|
+
const model = data.model?.model || null;
|
|
140
|
+
const messages = [];
|
|
141
|
+
for (const msg of data.messages || []) {
|
|
142
|
+
if (msg.User) {
|
|
143
|
+
const { text } = extractContent(msg.User.content);
|
|
144
|
+
if (text) messages.push({ role: 'user', content: text });
|
|
145
|
+
} else if (msg.Agent) {
|
|
146
|
+
const { text, toolCalls } = extractContent(msg.Agent.content);
|
|
147
|
+
if (text) messages.push({ role: 'assistant', content: text, _model: model, _toolCalls: toolCalls });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return messages;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function extractContent(content) {
|
|
154
|
+
if (!Array.isArray(content)) return { text: '', toolCalls: [] };
|
|
155
|
+
const parts = [];
|
|
156
|
+
const toolCalls = [];
|
|
157
|
+
for (const block of content) {
|
|
158
|
+
if (block.Text) {
|
|
159
|
+
parts.push(block.Text);
|
|
160
|
+
} else if (block.ToolUse) {
|
|
161
|
+
const tu = block.ToolUse;
|
|
162
|
+
let args = {};
|
|
163
|
+
try {
|
|
164
|
+
args = typeof tu.input === 'string' ? JSON.parse(tu.input) : (tu.input || {});
|
|
165
|
+
} catch {}
|
|
166
|
+
const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
|
|
167
|
+
parts.push(`[tool-call: ${tu.name || 'tool'}(${argKeys})]`);
|
|
168
|
+
toolCalls.push({ name: tu.name || 'tool', args });
|
|
169
|
+
} else if (block.ToolResult) {
|
|
170
|
+
const tr = block.ToolResult;
|
|
171
|
+
const preview = (tr.content || tr.output || '').substring(0, 500);
|
|
172
|
+
parts.push(`[tool-result: ${tr.tool_use_id || 'tool'}] ${preview}`);
|
|
173
|
+
} else if (block.Thinking) {
|
|
174
|
+
const text = typeof block.Thinking === 'string' ? block.Thinking : (block.Thinking.text || '');
|
|
175
|
+
if (text) parts.push(`[thinking] ${text}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { text: parts.join('\n') || '', toolCalls };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const labels = { 'zed': 'Zed' };
|
|
182
|
+
|
|
183
|
+
function getMCPServers() {
|
|
184
|
+
const results = [];
|
|
185
|
+
// Zed stores MCP servers in ~/.config/zed/settings.json under context_servers key
|
|
186
|
+
const settingsPath = path.join(getZedDataPath(), 'settings.json');
|
|
187
|
+
if (!fs.existsSync(settingsPath)) return results;
|
|
188
|
+
try {
|
|
189
|
+
const data = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'));
|
|
190
|
+
const servers = data.context_servers || {};
|
|
191
|
+
for (const [name, cfg] of Object.entries(servers)) {
|
|
192
|
+
if (typeof cfg !== 'object') continue;
|
|
193
|
+
const settings = cfg.settings || {};
|
|
194
|
+
results.push({
|
|
195
|
+
name,
|
|
196
|
+
editor: 'zed',
|
|
197
|
+
editorLabel: 'Zed',
|
|
198
|
+
scope: 'global',
|
|
199
|
+
configPath: settingsPath,
|
|
200
|
+
command: settings.command || cfg.command || null,
|
|
201
|
+
args: settings.args || cfg.args || [],
|
|
202
|
+
env: settings.env ? Object.keys(settings.env) : [],
|
|
203
|
+
url: settings.url || cfg.url || null,
|
|
204
|
+
transport: (settings.url || cfg.url) ? 'http' : 'stdio',
|
|
205
|
+
disabled: false,
|
|
206
|
+
disabledTools: [],
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch {}
|
|
210
|
+
return results;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { name, labels, getChats, getMessages, getMCPServers };
|
package/index.js
CHANGED
|
@@ -1,3 +1,425 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
|
|
9
|
+
// ── Node.js version check ─────────────────────────────────
|
|
10
|
+
const nodeVersion = process.versions.node;
|
|
11
|
+
const [nodeMajor, nodeMinor] = nodeVersion.split('.').map(Number);
|
|
12
|
+
const isNodeSupported =
|
|
13
|
+
(nodeMajor === 20 && nodeMinor >= 19) ||
|
|
14
|
+
(nodeMajor === 22 && nodeMinor >= 12) ||
|
|
15
|
+
nodeMajor === 23 || nodeMajor === 24 || nodeMajor >= 25;
|
|
16
|
+
|
|
17
|
+
if (!isNodeSupported) {
|
|
18
|
+
console.error('');
|
|
19
|
+
console.error(` \x1b[31m✗ Unsupported Node.js version: v${nodeVersion}\x1b[0m`);
|
|
20
|
+
console.error('');
|
|
21
|
+
console.error(` \x1b[1mAgentlytics requires Node.js 20.19+ or 22.12+\x1b[0m`);
|
|
22
|
+
console.error(` \x1b[2mYour current version: v${nodeVersion}\x1b[0m`);
|
|
23
|
+
console.error('');
|
|
24
|
+
console.error(` \x1b[2mTo upgrade, visit: https://nodejs.org\x1b[0m`);
|
|
25
|
+
console.error(` \x1b[2mOr use a version manager: nvm install 22\x1b[0m`);
|
|
26
|
+
console.error('');
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const HOME = os.homedir();
|
|
31
|
+
|
|
32
|
+
// ── Detect package manager ─────────────────────────────────
|
|
33
|
+
function detectPackageManager() {
|
|
34
|
+
const ua = process.env.npm_config_user_agent || '';
|
|
35
|
+
if (ua.startsWith('pnpm/')) return 'pnpm';
|
|
36
|
+
if (ua.startsWith('yarn/')) return 'yarn';
|
|
37
|
+
if (ua.startsWith('bun/')) return 'bun';
|
|
38
|
+
return 'npm';
|
|
39
|
+
}
|
|
40
|
+
const PM = detectPackageManager();
|
|
41
|
+
const PM_RUN = PM === 'npm' ? 'npx' : PM === 'pnpm' ? 'pnpm dlx' : PM === 'yarn' ? 'yarn dlx' : 'bunx';
|
|
42
|
+
const PORT = process.env.PORT || 4637;
|
|
43
|
+
const RELAY_PORT = process.env.RELAY_PORT || 4638;
|
|
44
|
+
const noCache = process.argv.includes('--no-cache');
|
|
45
|
+
const collectOnly = process.argv.includes('--collect');
|
|
46
|
+
const isUiDev = process.argv.includes('--ui-dev');
|
|
47
|
+
const isRelay = process.argv.includes('--relay');
|
|
48
|
+
const joinIndex = process.argv.indexOf('--join');
|
|
49
|
+
const isJoin = joinIndex !== -1;
|
|
50
|
+
|
|
51
|
+
// ── Relay mode ───────────────────────────────────────────────
|
|
52
|
+
if (isRelay) {
|
|
53
|
+
// Loopback by default; exposing to the network is an explicit opt-in AND
|
|
54
|
+
// fail-closed: a non-loopback bind without RELAY_PASSWORD would let any
|
|
55
|
+
// network client read all transcripts, upload/overwrite sessions, and merge
|
|
56
|
+
// users — refuse before touching anything.
|
|
57
|
+
const RELAY_HOST = process.env.RELAY_HOST || '127.0.0.1';
|
|
58
|
+
const relayLoopback = RELAY_HOST === '127.0.0.1' || RELAY_HOST === 'localhost' || RELAY_HOST === '::1';
|
|
59
|
+
if (!relayLoopback && !process.env.RELAY_PASSWORD) {
|
|
60
|
+
console.error(chalk.red(` ✗ Refusing to bind relay to ${RELAY_HOST} without RELAY_PASSWORD.`));
|
|
61
|
+
console.error(chalk.dim(' Set RELAY_PASSWORD, or drop RELAY_HOST to stay loopback-only.'));
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { initRelayDb, getRelayDb, createRelayApp } = require('./relay-server');
|
|
66
|
+
const { wireMcpToExpress } = require('./mcp-server');
|
|
67
|
+
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(chalk.bold(' ⚡ Agentlytics Relay'));
|
|
70
|
+
console.log(chalk.dim(' Multi-user context sharing server'));
|
|
71
|
+
console.log('');
|
|
72
|
+
|
|
73
|
+
initRelayDb();
|
|
74
|
+
console.log(chalk.green(' ✓ Relay database initialized'));
|
|
75
|
+
|
|
76
|
+
const app = createRelayApp();
|
|
77
|
+
wireMcpToExpress(app, getRelayDb);
|
|
78
|
+
console.log(chalk.green(' ✓ MCP server registered'));
|
|
79
|
+
|
|
80
|
+
if (process.env.RELAY_PASSWORD) {
|
|
81
|
+
console.log(chalk.green(' ✓ Password protection enabled'));
|
|
82
|
+
} else {
|
|
83
|
+
console.log(chalk.yellow(' ⚠ No password set (loopback-only; set RELAY_PASSWORD before exposing with RELAY_HOST)'));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
app.listen(RELAY_PORT, RELAY_HOST, () => {
|
|
87
|
+
const localIp = relayLoopback ? '127.0.0.1' : getLocalIp();
|
|
88
|
+
const relayUrl = `http://${localIp}:${RELAY_PORT}`;
|
|
89
|
+
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(chalk.green(` ✓ Relay server running on port ${RELAY_PORT}`));
|
|
92
|
+
console.log('');
|
|
93
|
+
console.log(chalk.bold(' Team sharing:'));
|
|
94
|
+
console.log(chalk.dim(' Relay join is not supported in memhouse — members ship with their own'));
|
|
95
|
+
console.log(chalk.dim(' credentials (kernel-based): see mem-house/delivery/kernel-install.md'));
|
|
96
|
+
console.log('');
|
|
97
|
+
console.log(chalk.bold(' MCP server endpoint (add to your AI client):'));
|
|
98
|
+
console.log('');
|
|
99
|
+
console.log(chalk.cyan(` ${relayUrl}/mcp`));
|
|
100
|
+
if (process.env.RELAY_PASSWORD) {
|
|
101
|
+
console.log(chalk.dim(' (password protection is on — clients must send the bearer token from /api/login)'));
|
|
102
|
+
}
|
|
103
|
+
console.log('');
|
|
104
|
+
console.log(chalk.dim(' REST endpoints:'));
|
|
105
|
+
console.log(chalk.dim(` GET ${relayUrl}/relay/health`));
|
|
106
|
+
console.log(chalk.dim(` GET ${relayUrl}/relay/users`));
|
|
107
|
+
console.log(chalk.dim(` GET ${relayUrl}/relay/search?q=<query>`));
|
|
108
|
+
console.log(chalk.dim(` GET ${relayUrl}/relay/activity/<username>`));
|
|
109
|
+
console.log(chalk.dim(` GET ${relayUrl}/relay/session/<chatId>`));
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(chalk.dim(' Press Ctrl+C to stop'));
|
|
112
|
+
console.log('');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Skip the rest of the normal flow
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Join mode ────────────────────────────────────────────────
|
|
120
|
+
if (isJoin) {
|
|
121
|
+
// Fail fast: the relay join client still targets the removed synchronous SQLite
|
|
122
|
+
// cache (relay-client.js pickProjects → cache.getDb), which the ClickHouse
|
|
123
|
+
// cutover eliminated — it would crash after connecting. In memhouse, team
|
|
124
|
+
// sharing is the kernel path (members + owner grants + RLS), not relay.
|
|
125
|
+
console.error(chalk.red('\n ✗ Relay join is not supported in memhouse.'));
|
|
126
|
+
console.error(chalk.dim(' Team sharing here is kernel-based: see mem-house/delivery/kernel-install.md'));
|
|
127
|
+
console.error(chalk.dim(' (register-member + owner GRANT + own-only RLS).\n'));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Helper: get local IP for relay ───────────────────────────
|
|
132
|
+
function getLocalIp() {
|
|
133
|
+
const interfaces = os.networkInterfaces();
|
|
134
|
+
for (const name of Object.keys(interfaces)) {
|
|
135
|
+
for (const iface of interfaces[name]) {
|
|
136
|
+
if (iface.family === 'IPv4' && !iface.internal) return iface.address;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return 'localhost';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ── ASCII banner ─────────────────────────────────────────
|
|
143
|
+
const c1 = chalk.hex('#818cf8'), c2 = chalk.hex('#f472b6'), c3 = chalk.hex('#34d399'), c4 = chalk.hex('#fbbf24');
|
|
144
|
+
console.log('');
|
|
145
|
+
console.log(` ${c1('(● ●)')} ${c2('[● ●]')} ${chalk.bold('Agentlytics')}`);
|
|
146
|
+
console.log(` ${c3('{● ●}')} ${c4('<● ●>')} ${chalk.dim('Unified analytics for your AI coding agents')}`);
|
|
147
|
+
if (collectOnly) console.log(chalk.cyan(' ⟳ Collect-only mode (no server)'));
|
|
148
|
+
console.log('');
|
|
149
|
+
|
|
150
|
+
// ── Build UI if not already built ──────────────────────────
|
|
151
|
+
const publicIndex = path.join(__dirname, 'public', 'index.html');
|
|
152
|
+
const uiDir = path.join(__dirname, 'ui');
|
|
153
|
+
|
|
154
|
+
if (!collectOnly && !isUiDev && !fs.existsSync(publicIndex) && fs.existsSync(uiDir)) {
|
|
155
|
+
console.log(chalk.cyan(' ⟳ Building dashboard UI (first run)...'));
|
|
156
|
+
try {
|
|
157
|
+
const uiModules = path.join(uiDir, 'node_modules');
|
|
158
|
+
if (fs.existsSync(uiModules)) fs.rmSync(uiModules, { recursive: true, force: true });
|
|
159
|
+
console.log(chalk.dim(' Installing UI dependencies...'));
|
|
160
|
+
const installCmd = PM === 'npm' ? 'npm install --no-audit --no-fund'
|
|
161
|
+
: PM === 'pnpm' ? 'pnpm install --no-frozen-lockfile'
|
|
162
|
+
: PM === 'yarn' ? 'yarn install'
|
|
163
|
+
: 'bun install';
|
|
164
|
+
const buildCmd = `${PM} run build`;
|
|
165
|
+
execSync(installCmd, { cwd: uiDir, stdio: 'pipe' });
|
|
166
|
+
console.log(chalk.dim(' Compiling frontend...'));
|
|
167
|
+
execSync(buildCmd, { cwd: uiDir, stdio: 'pipe' });
|
|
168
|
+
console.log(chalk.green(' ✓ UI built successfully'));
|
|
169
|
+
} catch (err) {
|
|
170
|
+
console.error(chalk.red(' ✗ UI build failed:'), err.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
console.log('');
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!collectOnly && !isUiDev && !fs.existsSync(publicIndex)) {
|
|
177
|
+
console.error(chalk.red(' ✗ No built UI found at public/index.html'));
|
|
178
|
+
console.error(chalk.dim(` Run: cd ui && ${PM} install && ${PM} run build`));
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── ClickHouse cache (no native module to build) ────────────
|
|
183
|
+
const cache = require('./cache');
|
|
184
|
+
// --no-cache is handled in the async flow below (ClickHouse truncate needs await).
|
|
185
|
+
|
|
186
|
+
// ── Warn about installed-but-not-running Devin variants (macOS only) ─
|
|
187
|
+
if (process.platform === 'darwin') {
|
|
188
|
+
const DEVIN_DESKTOP_VARIANTS = [
|
|
189
|
+
{
|
|
190
|
+
name: 'Devin',
|
|
191
|
+
apps: ['/Applications/Devin Desktop.app', '/Applications/Devin.app', '/Applications/Windsurf.app'],
|
|
192
|
+
dataDirs: [path.join(HOME, '.windsurf'), path.join(HOME, '.codeium', 'windsurf')],
|
|
193
|
+
ides: ['devin-desktop', 'devin', 'windsurf'],
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
name: 'Devin Next',
|
|
197
|
+
apps: ['/Applications/Devin Desktop Next.app', '/Applications/Devin Next.app', '/Applications/Windsurf Next.app'],
|
|
198
|
+
dataDirs: [path.join(HOME, '.codeium', 'windsurf-next')],
|
|
199
|
+
ides: ['devin-desktop-next', 'devin-next', 'windsurf-next'],
|
|
200
|
+
},
|
|
201
|
+
{ name: 'Antigravity', app: '/Applications/Antigravity.app', dataDir: path.join(HOME, '.codeium', 'antigravity'), ide: 'antigravity' },
|
|
202
|
+
];
|
|
203
|
+
|
|
204
|
+
(() => {
|
|
205
|
+
// Check which language servers are running
|
|
206
|
+
let runningIdes = [];
|
|
207
|
+
try {
|
|
208
|
+
const ps = execSync('ps aux', { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
|
|
209
|
+
for (const line of ps.split('\n')) {
|
|
210
|
+
if (!line.includes('language_server_macos')) continue;
|
|
211
|
+
const ideMatch = line.match(/--ide_name\s+(\S+)/);
|
|
212
|
+
const appDirMatch = line.match(/--app_data_dir\s+(\S+)/);
|
|
213
|
+
if (ideMatch) runningIdes.push(ideMatch[1]);
|
|
214
|
+
if (appDirMatch) runningIdes.push(appDirMatch[1]);
|
|
215
|
+
}
|
|
216
|
+
} catch {}
|
|
217
|
+
|
|
218
|
+
const installedNotRunning = DEVIN_DESKTOP_VARIANTS.filter(v => {
|
|
219
|
+
const apps = v.apps || [v.app];
|
|
220
|
+
const dataDirs = v.dataDirs || [v.dataDir];
|
|
221
|
+
const ides = v.ides || [v.ide];
|
|
222
|
+
const installed = apps.some(app => fs.existsSync(app)) || dataDirs.some(dataDir => fs.existsSync(dataDir));
|
|
223
|
+
const running = runningIdes.some(r => ides.some(ide => r === ide || r.includes(ide)));
|
|
224
|
+
return installed && !running;
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
if (installedNotRunning.length > 0) {
|
|
228
|
+
const names = installedNotRunning.map(v => chalk.bold(v.name)).join(', ');
|
|
229
|
+
console.log(chalk.yellow(` ⚠ ${names} installed but not running`));
|
|
230
|
+
console.log(chalk.dim(' These editors must be open for their sessions to be detected.'));
|
|
231
|
+
console.log('');
|
|
232
|
+
}
|
|
233
|
+
})();
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Cache DB is initialized (and optionally cleared) inside the async flow below.
|
|
237
|
+
|
|
238
|
+
// ── Detect editors & collect sessions ───────────────────────
|
|
239
|
+
const { editors: editorModules, editorLabels } = require('./editors');
|
|
240
|
+
|
|
241
|
+
console.log(chalk.dim(' Looking for AI coding agents...'));
|
|
242
|
+
const allChats = [];
|
|
243
|
+
for (const editor of editorModules) {
|
|
244
|
+
try {
|
|
245
|
+
const chats = editor.getChats();
|
|
246
|
+
allChats.push(...chats);
|
|
247
|
+
} catch { /* skip broken adapters */ }
|
|
248
|
+
}
|
|
249
|
+
allChats.sort((a, b) => (b.lastUpdatedAt || b.createdAt || 0) - (a.lastUpdatedAt || a.createdAt || 0));
|
|
250
|
+
|
|
251
|
+
// Count per source
|
|
252
|
+
const bySource = {};
|
|
253
|
+
for (const chat of allChats) bySource[chat.source] = (bySource[chat.source] || 0) + 1;
|
|
254
|
+
|
|
255
|
+
const displayByLabel = new Map();
|
|
256
|
+
for (const [src, label] of Object.entries(editorLabels)) {
|
|
257
|
+
const existing = displayByLabel.get(label) || { label, count: 0 };
|
|
258
|
+
existing.count += bySource[src] || 0;
|
|
259
|
+
displayByLabel.set(label, existing);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const displayList = Array.from(displayByLabel.values())
|
|
263
|
+
.map(({ label, count }) => [label, count])
|
|
264
|
+
.sort((a, b) => b[1] - a[1]);
|
|
265
|
+
|
|
266
|
+
for (const [label, count] of displayList) {
|
|
267
|
+
if (count > 0) {
|
|
268
|
+
console.log(` ${chalk.green('✓')} ${chalk.bold(label.padEnd(18))} ${chalk.dim(`${count} session${count === 1 ? '' : 's'}`)}`);
|
|
269
|
+
} else {
|
|
270
|
+
console.log(` ${chalk.dim('–')} ${chalk.dim(label.padEnd(18) + '–')}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
console.log('');
|
|
274
|
+
|
|
275
|
+
// ── Analyze sessions with robot animation (async to allow Ctrl+C) ──
|
|
276
|
+
const logUpdate = require('log-update');
|
|
277
|
+
const BOT_STYLES = [
|
|
278
|
+
{ l: '(', r: ')', color: '#818cf8' },
|
|
279
|
+
{ l: '[', r: ']', color: '#f472b6' },
|
|
280
|
+
{ l: '{', r: '}', color: '#34d399' },
|
|
281
|
+
{ l: '<', r: '>', color: '#fbbf24' },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
(async () => {
|
|
285
|
+
// Initialize the ClickHouse cache (ensure DB + schema); optionally clear it.
|
|
286
|
+
await cache.initDb();
|
|
287
|
+
if (noCache) {
|
|
288
|
+
await cache.clearAll();
|
|
289
|
+
console.log(chalk.yellow(' ⟳ Cache cleared (--no-cache)'));
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ── Ask for subscription access permission (first run only) ──
|
|
293
|
+
const CONFIG_DIR = path.join(os.homedir(), '.agentlytics');
|
|
294
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
|
295
|
+
let agentConfig = {};
|
|
296
|
+
try { agentConfig = JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf-8')); } catch {}
|
|
297
|
+
|
|
298
|
+
if (agentConfig.allowSubscriptionAccess === undefined) {
|
|
299
|
+
console.log(chalk.yellow(' ⚠ Subscription & usage details require access to local auth tokens.'));
|
|
300
|
+
console.log('');
|
|
301
|
+
console.log(chalk.dim(' To show your plan and usage info, Agentlytics needs to read'));
|
|
302
|
+
console.log(chalk.dim(' locally stored tokens from the following sources:'));
|
|
303
|
+
console.log('');
|
|
304
|
+
console.log(chalk.dim(' • Claude Code – macOS Keychain / Linux secret-tool'));
|
|
305
|
+
console.log(chalk.dim(' • Cursor – local SQLite (state.vscdb)'));
|
|
306
|
+
console.log(chalk.dim(' • Copilot – ~/.config/github-copilot/apps.json'));
|
|
307
|
+
console.log(chalk.dim(' • VS Code – ~/.config/github-copilot/apps.json'));
|
|
308
|
+
console.log(chalk.dim(' • Codex – local auth.json (JWT decode only)'));
|
|
309
|
+
console.log(chalk.dim(' • Devin – local SQLite (state.vscdb)'));
|
|
310
|
+
console.log('');
|
|
311
|
+
console.log(chalk.dim(' These tokens are used to query each editor\'s own API for'));
|
|
312
|
+
console.log(chalk.dim(' your plan name and usage limits.'));
|
|
313
|
+
console.log('');
|
|
314
|
+
console.log(chalk.bold.white(' → Tokens are kept in-memory only and never sent to any'));
|
|
315
|
+
console.log(chalk.bold.white(' third-party service. They are discarded after the request.'));
|
|
316
|
+
console.log('');
|
|
317
|
+
const readline = require('readline');
|
|
318
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
319
|
+
const answer = await new Promise(r => {
|
|
320
|
+
rl.question(chalk.bold(' Allow local token inspection for subscription details? (y/N) '), (a) => {
|
|
321
|
+
rl.close();
|
|
322
|
+
r(a.trim().toLowerCase());
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
agentConfig.allowSubscriptionAccess = answer === 'y' || answer === 'yes';
|
|
326
|
+
if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
327
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(agentConfig, null, 2));
|
|
328
|
+
if (agentConfig.allowSubscriptionAccess) {
|
|
329
|
+
console.log(chalk.green(' ✓ Subscription access enabled'));
|
|
330
|
+
} else {
|
|
331
|
+
console.log(chalk.dim(' – Subscription access skipped (plan/usage details won\'t be collected)'));
|
|
332
|
+
}
|
|
333
|
+
console.log('');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
let tick = 0;
|
|
337
|
+
const startTime = Date.now();
|
|
338
|
+
const result = await cache.scanAllAsync((p) => {
|
|
339
|
+
tick++;
|
|
340
|
+
if (tick % 5 !== 0) return;
|
|
341
|
+
const frame = Math.floor(tick / 40);
|
|
342
|
+
const b = BOT_STYLES[frame % 4];
|
|
343
|
+
const dots = '.'.repeat((Math.floor(tick / 10) % 3) + 1).padEnd(3);
|
|
344
|
+
logUpdate(` ${chalk.hex(b.color)(`${b.l}● ●${b.r}`)} ${chalk.dim(`Analyzing${dots} ${p.scanned}/${p.total}`)}`);
|
|
345
|
+
}, { chats: allChats });
|
|
346
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
347
|
+
const allFaces = BOT_STYLES.map(b => chalk.hex(b.color)(`${b.l}● ●${b.r}`)).join(' ');
|
|
348
|
+
logUpdate(` ${allFaces} ${chalk.green(`✓ ${result.analyzed} analyzed, ${result.skipped} cached (${elapsed}s)`)}`);
|
|
349
|
+
logUpdate.done();
|
|
350
|
+
console.log('');
|
|
351
|
+
|
|
352
|
+
// In collect-only mode, exit after cache is built
|
|
353
|
+
if (collectOnly) {
|
|
354
|
+
const { config } = require('./clickhouse');
|
|
355
|
+
console.log(chalk.dim(` Cache: ClickHouse ${config.database} @ ${config.url}`));
|
|
356
|
+
console.log('');
|
|
357
|
+
process.exit(0);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Start server (kill stale agentlytics or find free port)
|
|
361
|
+
const app = require('./server');
|
|
362
|
+
const http = require('http');
|
|
363
|
+
const net = require('net');
|
|
364
|
+
|
|
365
|
+
// Pre-cache MCP server configs (runs in background, non-blocking). Live tool
|
|
366
|
+
// discovery — which EXECUTES configured servers — only happens when the user
|
|
367
|
+
// opted in via AGENTLYTICS_MCP_DISCOVERY=1 (see server.js initMcpToolsCache).
|
|
368
|
+
const mcpDiscovery = process.env.AGENTLYTICS_MCP_DISCOVERY === '1'
|
|
369
|
+
|| process.env.AGENTLYTICS_MCP_DISCOVERY === 'true';
|
|
370
|
+
app.initMcpToolsCache().then(() => {
|
|
371
|
+
console.log(chalk.green(mcpDiscovery
|
|
372
|
+
? ' ✓ MCP tools cached'
|
|
373
|
+
: ' ✓ MCP configs listed (tool discovery off — enable with AGENTLYTICS_MCP_DISCOVERY=1)'));
|
|
374
|
+
}).catch(() => {});
|
|
375
|
+
|
|
376
|
+
function isPortFree(port) {
|
|
377
|
+
return new Promise((resolve) => {
|
|
378
|
+
const tester = net.createServer()
|
|
379
|
+
.once('error', () => resolve(false))
|
|
380
|
+
.once('listening', () => tester.close(() => resolve(true)))
|
|
381
|
+
.listen(port, '0.0.0.0');
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function startServer(port) {
|
|
386
|
+
const free = await isPortFree(port);
|
|
387
|
+
if (!free) {
|
|
388
|
+
// Port in use — check if it's a previous agentlytics instance
|
|
389
|
+
try {
|
|
390
|
+
const data = await new Promise((resolve, reject) => {
|
|
391
|
+
const req = http.get(`http://127.0.0.1:${port}/api/ping`, { timeout: 2000 }, (res) => {
|
|
392
|
+
let body = '';
|
|
393
|
+
res.on('data', (d) => body += d);
|
|
394
|
+
res.on('end', () => { try { resolve(JSON.parse(body)); } catch { reject(); } });
|
|
395
|
+
});
|
|
396
|
+
req.on('error', reject);
|
|
397
|
+
});
|
|
398
|
+
if (data.app === 'agentlytics' && data.pid) {
|
|
399
|
+
console.log(chalk.yellow(` ⟳ Killing previous Agentlytics instance (PID ${data.pid})...`));
|
|
400
|
+
try { process.kill(data.pid, 'SIGTERM'); } catch {}
|
|
401
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
402
|
+
return startServer(port);
|
|
403
|
+
}
|
|
404
|
+
} catch {}
|
|
405
|
+
console.log(chalk.yellow(` ⚠ Port ${port} is in use by another app, trying ${port + 1}...`));
|
|
406
|
+
return startServer(port + 1);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Loopback by default — the dashboard exposes transcripts and a SQL console
|
|
410
|
+
// with no auth. LAN exposure is an explicit opt-in (AGENTLYTICS_HOST=0.0.0.0).
|
|
411
|
+
const HOST = process.env.AGENTLYTICS_HOST || '127.0.0.1';
|
|
412
|
+
app.listen(port, HOST, () => {
|
|
413
|
+
if (isUiDev) return;
|
|
414
|
+
const url = `http://localhost:${port}`;
|
|
415
|
+
console.log(chalk.green(` ✓ Dashboard ready at ${chalk.bold.white(url)}`));
|
|
416
|
+
console.log('');
|
|
417
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
418
|
+
|
|
419
|
+
const open = require('open');
|
|
420
|
+
open(url).catch(() => {});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
startServer(parseInt(PORT));
|
|
425
|
+
})();
|