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/base.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const os = require('os');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
|
|
7
|
+
// --- Permission check ---
|
|
8
|
+
|
|
9
|
+
function isSubscriptionAccessAllowed() {
|
|
10
|
+
try {
|
|
11
|
+
const configPath = path.join(HOME, '.agentlytics', 'config.json');
|
|
12
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
13
|
+
return config.allowSubscriptionAccess === true;
|
|
14
|
+
} catch { return false; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// --- Platform utilities ---
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get platform-specific app data directory path for VS Code-like editors.
|
|
21
|
+
* - macOS: ~/Library/Application Support/{appName}/User/...
|
|
22
|
+
* - Windows: ~/AppData/Roaming/{appName}/User/...
|
|
23
|
+
* - Linux: ~/.config/{appName}/User/...
|
|
24
|
+
*/
|
|
25
|
+
function getAppDataPath(appName) {
|
|
26
|
+
switch (process.platform) {
|
|
27
|
+
case 'darwin':
|
|
28
|
+
return path.join(HOME, 'Library', 'Application Support', appName);
|
|
29
|
+
case 'win32':
|
|
30
|
+
return path.join(HOME, 'AppData', 'Roaming', appName);
|
|
31
|
+
default: // linux, etc.
|
|
32
|
+
return path.join(HOME, '.config', appName);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Every editor adapter must implement:
|
|
38
|
+
*
|
|
39
|
+
* name - string identifier (e.g. 'cursor', 'devin')
|
|
40
|
+
* getChats() - returns array of chat objects:
|
|
41
|
+
* { source, composerId, name, createdAt, lastUpdatedAt, mode, folder, bubbleCount, encrypted }
|
|
42
|
+
* getMessages(chat) - returns array of message objects:
|
|
43
|
+
* { role: 'user'|'assistant'|'system'|'tool', content: string|Array }
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Scan a project folder for artifact files.
|
|
48
|
+
*
|
|
49
|
+
* @param {string} folder - Absolute path to the project folder
|
|
50
|
+
* @param {Object} opts
|
|
51
|
+
* @param {string} opts.editor - Editor identifier (e.g. 'cursor', 'claude-code')
|
|
52
|
+
* @param {string} opts.label - Display label (e.g. 'Cursor', 'Claude Code')
|
|
53
|
+
* @param {string[]} [opts.files] - Relative file paths to check (e.g. ['CLAUDE.md'])
|
|
54
|
+
* @param {string[]} [opts.dirs] - Relative directories to scan for .md/.yaml/.yml/.json files
|
|
55
|
+
* @returns {Array} Array of artifact objects
|
|
56
|
+
*/
|
|
57
|
+
function scanArtifacts(folder, { editor, label, files = [], dirs = [] }) {
|
|
58
|
+
const fs = require('fs');
|
|
59
|
+
const artifacts = [];
|
|
60
|
+
if (!folder || !fs.existsSync(folder)) return artifacts;
|
|
61
|
+
|
|
62
|
+
for (const relPath of files) {
|
|
63
|
+
const filePath = path.join(folder, relPath);
|
|
64
|
+
try {
|
|
65
|
+
const stat = fs.statSync(filePath);
|
|
66
|
+
if (!stat.isFile()) continue;
|
|
67
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
68
|
+
artifacts.push({
|
|
69
|
+
editor,
|
|
70
|
+
editorLabel: label,
|
|
71
|
+
name: relPath,
|
|
72
|
+
path: filePath,
|
|
73
|
+
relativePath: relPath,
|
|
74
|
+
size: stat.size,
|
|
75
|
+
modifiedAt: stat.mtime.getTime(),
|
|
76
|
+
preview: content.substring(0, 500),
|
|
77
|
+
lines: content.split('\n').length,
|
|
78
|
+
});
|
|
79
|
+
} catch { /* skip */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const isArtifactFile = (f) =>
|
|
83
|
+
f.endsWith('.md') || f.endsWith('.mdc') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json');
|
|
84
|
+
|
|
85
|
+
const addFile = (filePath, relPath, fileName) => {
|
|
86
|
+
try {
|
|
87
|
+
const fstat = fs.statSync(filePath);
|
|
88
|
+
if (!fstat.isFile()) return;
|
|
89
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
90
|
+
artifacts.push({
|
|
91
|
+
editor,
|
|
92
|
+
editorLabel: label,
|
|
93
|
+
name: fileName,
|
|
94
|
+
path: filePath,
|
|
95
|
+
relativePath: relPath,
|
|
96
|
+
size: fstat.size,
|
|
97
|
+
modifiedAt: fstat.mtime.getTime(),
|
|
98
|
+
preview: content.substring(0, 500),
|
|
99
|
+
lines: content.split('\n').length,
|
|
100
|
+
});
|
|
101
|
+
} catch { /* skip */ }
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (const dir of dirs) {
|
|
105
|
+
const dirPath = path.join(folder, dir);
|
|
106
|
+
try {
|
|
107
|
+
const stat = fs.statSync(dirPath);
|
|
108
|
+
if (!stat.isDirectory()) continue;
|
|
109
|
+
const entries = fs.readdirSync(dirPath);
|
|
110
|
+
for (const entry of entries) {
|
|
111
|
+
const entryPath = path.join(dirPath, entry);
|
|
112
|
+
if (isArtifactFile(entry)) {
|
|
113
|
+
addFile(entryPath, path.join(dir, entry), entry);
|
|
114
|
+
} else {
|
|
115
|
+
// Recurse one level into subdirectories (e.g. .kiro/specs/<name>/, .windsurf/skills/<name>/)
|
|
116
|
+
try {
|
|
117
|
+
const eStat = fs.statSync(entryPath);
|
|
118
|
+
if (!eStat.isDirectory()) continue;
|
|
119
|
+
const subEntries = fs.readdirSync(entryPath).filter(isArtifactFile);
|
|
120
|
+
for (const subFile of subEntries) {
|
|
121
|
+
addFile(path.join(entryPath, subFile), path.join(dir, entry, subFile), subFile);
|
|
122
|
+
}
|
|
123
|
+
} catch { /* skip */ }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
} catch { /* skip */ }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return artifacts;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Parse a standard MCP config JSON file (mcpServers format).
|
|
134
|
+
* Returns array of { name, command, args, env, envKeys, url, transport, disabled }.
|
|
135
|
+
*/
|
|
136
|
+
function parseMcpConfigFile(filePath, { editor, label, scope }) {
|
|
137
|
+
const fs = require('fs');
|
|
138
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
139
|
+
try {
|
|
140
|
+
const raw = fs.readFileSync(filePath, 'utf-8');
|
|
141
|
+
const data = JSON.parse(raw);
|
|
142
|
+
const servers = data.mcpServers || data.mcp_servers || data.servers || {};
|
|
143
|
+
return Object.entries(servers).map(([name, cfg]) => ({
|
|
144
|
+
name,
|
|
145
|
+
editor,
|
|
146
|
+
editorLabel: label,
|
|
147
|
+
scope,
|
|
148
|
+
configPath: filePath,
|
|
149
|
+
command: cfg.command || null,
|
|
150
|
+
args: cfg.args || [],
|
|
151
|
+
_env: cfg.env || {},
|
|
152
|
+
env: cfg.env ? Object.keys(cfg.env) : [],
|
|
153
|
+
url: cfg.url || null,
|
|
154
|
+
transport: cfg.url ? (cfg.transport || 'http') : (cfg.transport || 'stdio'),
|
|
155
|
+
disabled: cfg.disabled || false,
|
|
156
|
+
disabledTools: cfg.disabledTools || [],
|
|
157
|
+
}));
|
|
158
|
+
} catch { return []; }
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Query an MCP server for its tools list via JSON-RPC 2.0.
|
|
163
|
+
* For stdio servers: spawns the command and communicates via stdin/stdout.
|
|
164
|
+
* For HTTP servers: sends POST to the URL.
|
|
165
|
+
* Returns a Promise<string[]> of tool names, or [] on failure.
|
|
166
|
+
* Timeout: 10s per server.
|
|
167
|
+
*/
|
|
168
|
+
function queryMcpServerTools(server) {
|
|
169
|
+
const TIMEOUT = 10000;
|
|
170
|
+
|
|
171
|
+
if (server.url && !server.command) {
|
|
172
|
+
// HTTP/SSE transport — send JSON-RPC via POST
|
|
173
|
+
return queryMcpServerToolsHttp(server.url, TIMEOUT);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (!server.command) return Promise.resolve([]);
|
|
177
|
+
|
|
178
|
+
// stdio transport — spawn the process
|
|
179
|
+
return queryMcpServerToolsStdio(server, TIMEOUT);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function queryMcpServerToolsHttp(url, timeout) {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
const controller = new AbortController();
|
|
185
|
+
const timer = setTimeout(() => { controller.abort(); resolve([]); }, timeout);
|
|
186
|
+
const headers = { 'Content-Type': 'application/json', 'Accept': 'application/json, text/event-stream' };
|
|
187
|
+
|
|
188
|
+
const parseToolsFromResponse = async (r) => {
|
|
189
|
+
const ct = r.headers.get('content-type') || '';
|
|
190
|
+
const text = await r.text();
|
|
191
|
+
if (ct.includes('text/event-stream')) {
|
|
192
|
+
// Parse SSE: lines starting with "data: "
|
|
193
|
+
for (const line of text.split('\n')) {
|
|
194
|
+
if (!line.startsWith('data: ')) continue;
|
|
195
|
+
try {
|
|
196
|
+
const msg = JSON.parse(line.slice(6));
|
|
197
|
+
if (msg.result && msg.result.tools) return msg.result.tools.map(t => t.name);
|
|
198
|
+
} catch {}
|
|
199
|
+
}
|
|
200
|
+
return [];
|
|
201
|
+
}
|
|
202
|
+
try {
|
|
203
|
+
const msg = JSON.parse(text);
|
|
204
|
+
return (msg.result && msg.result.tools) ? msg.result.tools.map(t => t.name) : [];
|
|
205
|
+
} catch { return []; }
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// 1. Initialize
|
|
209
|
+
fetch(url, {
|
|
210
|
+
method: 'POST', headers,
|
|
211
|
+
body: JSON.stringify({
|
|
212
|
+
jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
213
|
+
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'agentlytics', version: '1.0.0' } },
|
|
214
|
+
}),
|
|
215
|
+
signal: controller.signal,
|
|
216
|
+
})
|
|
217
|
+
.then(async (r) => {
|
|
218
|
+
const sessionId = r.headers.get('mcp-session-id');
|
|
219
|
+
const h = { ...headers };
|
|
220
|
+
if (sessionId) h['mcp-session-id'] = sessionId;
|
|
221
|
+
await r.text(); // consume body
|
|
222
|
+
|
|
223
|
+
// 2. Send initialized notification
|
|
224
|
+
await fetch(url, {
|
|
225
|
+
method: 'POST', headers: h,
|
|
226
|
+
body: JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }),
|
|
227
|
+
signal: controller.signal,
|
|
228
|
+
}).then(r2 => r2.text());
|
|
229
|
+
|
|
230
|
+
// 3. Request tools/list
|
|
231
|
+
const r3 = await fetch(url, {
|
|
232
|
+
method: 'POST', headers: h,
|
|
233
|
+
body: JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }),
|
|
234
|
+
signal: controller.signal,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
resolve(await parseToolsFromResponse(r3));
|
|
239
|
+
})
|
|
240
|
+
.catch(() => { clearTimeout(timer); resolve([]); });
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function queryMcpServerToolsStdio(server, timeout) {
|
|
245
|
+
const { spawn } = require('child_process');
|
|
246
|
+
|
|
247
|
+
return new Promise((resolve) => {
|
|
248
|
+
const env = { ...process.env, ...(server._env || {}) };
|
|
249
|
+
let child;
|
|
250
|
+
try {
|
|
251
|
+
child = spawn(server.command, server.args || [], {
|
|
252
|
+
env, stdio: ['pipe', 'pipe', 'pipe'],
|
|
253
|
+
});
|
|
254
|
+
} catch { return resolve([]); }
|
|
255
|
+
|
|
256
|
+
let stdout = '';
|
|
257
|
+
let done = false;
|
|
258
|
+
let initReceived = false;
|
|
259
|
+
|
|
260
|
+
const finish = (tools) => {
|
|
261
|
+
if (done) return;
|
|
262
|
+
done = true;
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
try { child.kill(); } catch {}
|
|
265
|
+
resolve(tools);
|
|
266
|
+
};
|
|
267
|
+
|
|
268
|
+
const timer = setTimeout(() => finish([]), timeout);
|
|
269
|
+
|
|
270
|
+
child.stdout.on('data', (chunk) => {
|
|
271
|
+
stdout += chunk.toString();
|
|
272
|
+
// Parse newline-delimited JSON-RPC responses
|
|
273
|
+
const lines = stdout.split('\n');
|
|
274
|
+
// Keep the last (possibly incomplete) line in the buffer
|
|
275
|
+
stdout = lines.pop() || '';
|
|
276
|
+
for (const line of lines) {
|
|
277
|
+
const trimmed = line.trim();
|
|
278
|
+
if (!trimmed) continue;
|
|
279
|
+
try {
|
|
280
|
+
const msg = JSON.parse(trimmed);
|
|
281
|
+
// Got initialize response — now send initialized + tools/list
|
|
282
|
+
if (msg.id === 1 && msg.result && !initReceived) {
|
|
283
|
+
initReceived = true;
|
|
284
|
+
try {
|
|
285
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', method: 'notifications/initialized' }) + '\n');
|
|
286
|
+
child.stdin.write(JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} }) + '\n');
|
|
287
|
+
} catch { finish([]); }
|
|
288
|
+
}
|
|
289
|
+
// Got tools/list response
|
|
290
|
+
if (msg.id === 2 && msg.result && msg.result.tools) {
|
|
291
|
+
finish(msg.result.tools.map(t => t.name));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
} catch { /* incomplete JSON, skip */ }
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
child.on('error', () => finish([]));
|
|
299
|
+
child.on('exit', () => finish([]));
|
|
300
|
+
|
|
301
|
+
// Send initialize
|
|
302
|
+
const initMsg = JSON.stringify({
|
|
303
|
+
jsonrpc: '2.0', id: 1, method: 'initialize',
|
|
304
|
+
params: {
|
|
305
|
+
protocolVersion: '2024-11-05',
|
|
306
|
+
capabilities: {},
|
|
307
|
+
clientInfo: { name: 'agentlytics', version: '1.0.0' },
|
|
308
|
+
},
|
|
309
|
+
}) + '\n';
|
|
310
|
+
|
|
311
|
+
try { child.stdin.write(initMsg); } catch { finish([]); }
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
module.exports = {
|
|
316
|
+
getAppDataPath,
|
|
317
|
+
isSubscriptionAccessAllowed,
|
|
318
|
+
scanArtifacts,
|
|
319
|
+
parseMcpConfigFile,
|
|
320
|
+
queryMcpServerTools,
|
|
321
|
+
};
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const HOME = os.homedir();
|
|
6
|
+
|
|
7
|
+
// Discover all Claude Code config roots — ports memory-house agent-sync's
|
|
8
|
+
// discover_roots("claude-code"). Claude sessions don't live only under ~/.claude:
|
|
9
|
+
// every CLAUDE_CONFIG_DIR (e.g. Kommander playbook installs under
|
|
10
|
+
// ~/.claude-playbooks/<name>[/playbook]) has its own projects/ dir. A dir qualifies
|
|
11
|
+
// as a root if it holds a projects/ subdir or a history.jsonl. Deduped by realpath.
|
|
12
|
+
function discoverClaudeRoots() {
|
|
13
|
+
const cands = [path.join(HOME, '.claude')];
|
|
14
|
+
const pbBase = path.join(HOME, '.claude-playbooks');
|
|
15
|
+
try {
|
|
16
|
+
for (const name of fs.readdirSync(pbBase)) {
|
|
17
|
+
if (name.startsWith('.')) continue; // match glob('*') — skip dotfiles/backups (.bak)
|
|
18
|
+
cands.push(path.join(pbBase, name));
|
|
19
|
+
cands.push(path.join(pbBase, name, 'playbook')); // legacy playbook/ layout
|
|
20
|
+
}
|
|
21
|
+
} catch { /* no ~/.claude-playbooks */ }
|
|
22
|
+
|
|
23
|
+
const roots = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (const d of cands) {
|
|
26
|
+
let isDir = false;
|
|
27
|
+
try { isDir = fs.statSync(d).isDirectory(); } catch { /* missing */ }
|
|
28
|
+
if (!isDir) continue;
|
|
29
|
+
let hasProjects = false;
|
|
30
|
+
try { hasProjects = fs.statSync(path.join(d, 'projects')).isDirectory(); } catch { /* none */ }
|
|
31
|
+
const hasHistory = fs.existsSync(path.join(d, 'history.jsonl'));
|
|
32
|
+
if (!hasProjects && !hasHistory) continue;
|
|
33
|
+
let rp; try { rp = fs.realpathSync(d); } catch { rp = d; }
|
|
34
|
+
if (seen.has(rp)) continue;
|
|
35
|
+
seen.add(rp);
|
|
36
|
+
roots.push(d);
|
|
37
|
+
}
|
|
38
|
+
return roots;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ============================================================
|
|
42
|
+
// Adapter interface
|
|
43
|
+
// ============================================================
|
|
44
|
+
|
|
45
|
+
const name = 'claude';
|
|
46
|
+
|
|
47
|
+
function getChats() {
|
|
48
|
+
const chats = [];
|
|
49
|
+
|
|
50
|
+
for (const root of discoverClaudeRoots()) {
|
|
51
|
+
const PROJECTS_DIR = path.join(root, 'projects');
|
|
52
|
+
if (!fs.existsSync(PROJECTS_DIR)) continue;
|
|
53
|
+
|
|
54
|
+
let projDirs;
|
|
55
|
+
try { projDirs = fs.readdirSync(PROJECTS_DIR); } catch { continue; }
|
|
56
|
+
for (const projDir of projDirs) {
|
|
57
|
+
const dir = path.join(PROJECTS_DIR, projDir);
|
|
58
|
+
try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
|
|
59
|
+
|
|
60
|
+
// Decode folder path from dir name (e.g. -Users-fka-Code-foo -> /Users/fka/Code/foo)
|
|
61
|
+
const decodedFolder = projDir.replace(/-/g, '/');
|
|
62
|
+
|
|
63
|
+
// Read sessions-index.json for indexed sessions
|
|
64
|
+
const indexPath = path.join(dir, 'sessions-index.json');
|
|
65
|
+
const indexed = new Map();
|
|
66
|
+
try {
|
|
67
|
+
const index = JSON.parse(fs.readFileSync(indexPath, 'utf-8'));
|
|
68
|
+
for (const entry of index.entries || []) {
|
|
69
|
+
indexed.set(entry.sessionId, entry);
|
|
70
|
+
}
|
|
71
|
+
} catch { /* no index */ }
|
|
72
|
+
|
|
73
|
+
// Scan all .jsonl files on disk (some may not be in the index)
|
|
74
|
+
let files;
|
|
75
|
+
try { files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl')); } catch { continue; }
|
|
76
|
+
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
const sessionId = file.replace('.jsonl', '');
|
|
79
|
+
const fullPath = path.join(dir, file);
|
|
80
|
+
const entry = indexed.get(sessionId);
|
|
81
|
+
|
|
82
|
+
if (entry) {
|
|
83
|
+
// Use index metadata
|
|
84
|
+
chats.push({
|
|
85
|
+
source: 'claude-code',
|
|
86
|
+
composerId: sessionId,
|
|
87
|
+
name: cleanPrompt(entry.firstPrompt),
|
|
88
|
+
createdAt: entry.created ? new Date(entry.created).getTime() : null,
|
|
89
|
+
lastUpdatedAt: entry.modified ? new Date(entry.modified).getTime() : null,
|
|
90
|
+
mode: 'claude',
|
|
91
|
+
folder: entry.projectPath || decodedFolder,
|
|
92
|
+
encrypted: false,
|
|
93
|
+
bubbleCount: entry.messageCount || 0,
|
|
94
|
+
_fullPath: fullPath,
|
|
95
|
+
_gitBranch: entry.gitBranch,
|
|
96
|
+
});
|
|
97
|
+
} else {
|
|
98
|
+
// Orphan .jsonl — extract metadata from file content
|
|
99
|
+
try {
|
|
100
|
+
const stat = fs.statSync(fullPath);
|
|
101
|
+
const meta = peekSessionMeta(fullPath);
|
|
102
|
+
chats.push({
|
|
103
|
+
source: 'claude-code',
|
|
104
|
+
composerId: sessionId,
|
|
105
|
+
name: meta.firstPrompt ? cleanPrompt(meta.firstPrompt) : null,
|
|
106
|
+
createdAt: meta.timestamp || stat.birthtime.getTime(),
|
|
107
|
+
lastUpdatedAt: stat.mtime.getTime(),
|
|
108
|
+
mode: 'claude',
|
|
109
|
+
folder: meta.cwd || decodedFolder,
|
|
110
|
+
encrypted: false,
|
|
111
|
+
_fullPath: fullPath,
|
|
112
|
+
});
|
|
113
|
+
} catch { /* skip */ }
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Remove from indexed so we know what's left
|
|
117
|
+
indexed.delete(sessionId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Add indexed sessions whose .jsonl files no longer exist (show as unavailable)
|
|
121
|
+
for (const [sessionId, entry] of indexed) {
|
|
122
|
+
if (!entry.fullPath || !fs.existsSync(entry.fullPath)) continue;
|
|
123
|
+
chats.push({
|
|
124
|
+
source: 'claude-code',
|
|
125
|
+
composerId: sessionId,
|
|
126
|
+
name: cleanPrompt(entry.firstPrompt),
|
|
127
|
+
createdAt: entry.created ? new Date(entry.created).getTime() : null,
|
|
128
|
+
lastUpdatedAt: entry.modified ? new Date(entry.modified).getTime() : null,
|
|
129
|
+
mode: 'claude',
|
|
130
|
+
folder: entry.projectPath || decodedFolder,
|
|
131
|
+
encrypted: false,
|
|
132
|
+
bubbleCount: entry.messageCount || 0,
|
|
133
|
+
_fullPath: entry.fullPath,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return chats;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function peekSessionMeta(filePath) {
|
|
143
|
+
const meta = { firstPrompt: null, cwd: null, timestamp: null };
|
|
144
|
+
try {
|
|
145
|
+
const buf = fs.readFileSync(filePath, 'utf-8');
|
|
146
|
+
for (const line of buf.split('\n')) {
|
|
147
|
+
if (!line) continue;
|
|
148
|
+
const obj = JSON.parse(line);
|
|
149
|
+
if (!meta.cwd && obj.cwd) meta.cwd = obj.cwd;
|
|
150
|
+
if (!meta.timestamp && obj.timestamp) {
|
|
151
|
+
meta.timestamp = typeof obj.timestamp === 'string'
|
|
152
|
+
? new Date(obj.timestamp).getTime() : obj.timestamp;
|
|
153
|
+
}
|
|
154
|
+
if (!meta.firstPrompt && obj.type === 'user' && obj.message?.content) {
|
|
155
|
+
const text = typeof obj.message.content === 'string'
|
|
156
|
+
? obj.message.content
|
|
157
|
+
: obj.message.content.filter(c => c.type === 'text').map(c => c.text).join(' ');
|
|
158
|
+
meta.firstPrompt = text.substring(0, 200);
|
|
159
|
+
}
|
|
160
|
+
if (meta.cwd && meta.firstPrompt) break;
|
|
161
|
+
}
|
|
162
|
+
} catch {}
|
|
163
|
+
return meta;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function cleanPrompt(prompt) {
|
|
167
|
+
if (!prompt || prompt === 'No prompt') return null;
|
|
168
|
+
// Strip XML tags and system-reminder blocks
|
|
169
|
+
let clean = prompt
|
|
170
|
+
.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, '')
|
|
171
|
+
.replace(/<[^>]+>/g, '')
|
|
172
|
+
.replace(/\s+/g, ' ')
|
|
173
|
+
.trim()
|
|
174
|
+
.substring(0, 120);
|
|
175
|
+
return clean || null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Parse one Claude session .jsonl into the adapter's message shape. When
|
|
179
|
+
// `isSubagent` is set, each message is tagged `[subagent]` so folded subagent turns
|
|
180
|
+
// are clearly attributed in the transcript.
|
|
181
|
+
function parseSessionFile(filePath, isSubagent) {
|
|
182
|
+
const messages = [];
|
|
183
|
+
let lines;
|
|
184
|
+
try { lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean); } catch { return messages; }
|
|
185
|
+
const tag = isSubagent ? '[subagent] ' : '';
|
|
186
|
+
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
let obj;
|
|
189
|
+
try { obj = JSON.parse(line); } catch { continue; }
|
|
190
|
+
|
|
191
|
+
if (obj.type === 'user' && obj.message) {
|
|
192
|
+
const content = extractContent(obj.message.content);
|
|
193
|
+
if (content) messages.push({ role: 'user', content: tag + content });
|
|
194
|
+
} else if (obj.type === 'assistant' && obj.message) {
|
|
195
|
+
const { text, toolCalls } = extractAssistantContent(obj.message.content);
|
|
196
|
+
const usage = obj.message.usage;
|
|
197
|
+
if (text) messages.push({
|
|
198
|
+
role: 'assistant', content: tag + text, _model: obj.message.model,
|
|
199
|
+
_inputTokens: usage?.input_tokens, _outputTokens: usage?.output_tokens,
|
|
200
|
+
_cacheRead: usage?.cache_read_input_tokens, _cacheWrite: usage?.cache_creation_input_tokens,
|
|
201
|
+
_toolCalls: toolCalls,
|
|
202
|
+
});
|
|
203
|
+
} else if (obj.type === 'system') {
|
|
204
|
+
const text = typeof obj.message?.content === 'string' ? obj.message.content : '';
|
|
205
|
+
if (text) messages.push({ role: 'system', content: tag + text });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return messages;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function getMessages(chat) {
|
|
212
|
+
const filePath = chat._fullPath;
|
|
213
|
+
if (!filePath || !fs.existsSync(filePath)) return [];
|
|
214
|
+
|
|
215
|
+
const messages = parseSessionFile(filePath, false);
|
|
216
|
+
|
|
217
|
+
// Fold in subagent transcripts — projects/<enc>/<uuid>/subagents/agent-*.jsonl.
|
|
218
|
+
// Their turns + token/tool usage belong to THIS parent session; appended (marked)
|
|
219
|
+
// after the parent's turns, so nothing is lost and subagents are never
|
|
220
|
+
// double-counted as standalone sessions.
|
|
221
|
+
const subagentsDir = path.join(filePath.replace(/\.jsonl$/, ''), 'subagents');
|
|
222
|
+
try {
|
|
223
|
+
const files = fs.readdirSync(subagentsDir).filter(f => f.endsWith('.jsonl')).sort();
|
|
224
|
+
for (const f of files) messages.push(...parseSessionFile(path.join(subagentsDir, f), true));
|
|
225
|
+
} catch { /* no subagents for this session */ }
|
|
226
|
+
|
|
227
|
+
return messages;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function extractContent(content) {
|
|
231
|
+
if (typeof content === 'string') return content;
|
|
232
|
+
if (!Array.isArray(content)) return '';
|
|
233
|
+
return content
|
|
234
|
+
.filter(c => c.type === 'text')
|
|
235
|
+
.map(c => c.text)
|
|
236
|
+
.join('\n') || '';
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function extractAssistantContent(content) {
|
|
240
|
+
if (typeof content === 'string') return { text: content, toolCalls: [] };
|
|
241
|
+
if (!Array.isArray(content)) return { text: '', toolCalls: [] };
|
|
242
|
+
const parts = [];
|
|
243
|
+
const toolCalls = [];
|
|
244
|
+
for (const block of content) {
|
|
245
|
+
if (block.type === 'thinking' && block.thinking) {
|
|
246
|
+
parts.push(`[thinking] ${block.thinking}`);
|
|
247
|
+
} else if (block.type === 'text' && block.text) {
|
|
248
|
+
parts.push(block.text);
|
|
249
|
+
} else if (block.type === 'tool_use') {
|
|
250
|
+
const args = block.input || {};
|
|
251
|
+
const argKeys = Object.keys(args).join(', ');
|
|
252
|
+
parts.push(`[tool-call: ${block.name || 'unknown'}(${argKeys})]`);
|
|
253
|
+
toolCalls.push({ name: block.name || 'unknown', args });
|
|
254
|
+
} else if (block.type === 'tool_result') {
|
|
255
|
+
const text = typeof block.content === 'string' ? block.content : '';
|
|
256
|
+
parts.push(`[tool-result: ${block.name || 'tool'}] ${text.substring(0, 500)}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return { text: parts.join('\n') || '', toolCalls };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ============================================================
|
|
263
|
+
// Usage / quota data from Anthropic OAuth API
|
|
264
|
+
// ============================================================
|
|
265
|
+
|
|
266
|
+
function getClaudeCredentials() {
|
|
267
|
+
// macOS: Keychain; Linux: secret-tool; Windows: not yet supported
|
|
268
|
+
// Requires explicit user permission (allowSubscriptionAccess in config)
|
|
269
|
+
const { isSubscriptionAccessAllowed } = require('./base');
|
|
270
|
+
if (!isSubscriptionAccessAllowed()) return null;
|
|
271
|
+
try {
|
|
272
|
+
const { execSync } = require('child_process');
|
|
273
|
+
let raw;
|
|
274
|
+
if (process.platform === 'darwin') {
|
|
275
|
+
raw = execSync('security find-generic-password -s "Claude Code-credentials" -w', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
276
|
+
} else if (process.platform === 'linux') {
|
|
277
|
+
raw = execSync('secret-tool lookup service "Claude Code-credentials"', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
278
|
+
} else {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const creds = JSON.parse(raw);
|
|
282
|
+
const oauth = creds.claudeAiOauth;
|
|
283
|
+
if (!oauth || !oauth.accessToken) return null;
|
|
284
|
+
return oauth;
|
|
285
|
+
} catch { return null; }
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function claudeApiFetch(token) {
|
|
289
|
+
return new Promise((resolve) => {
|
|
290
|
+
const https = require('https');
|
|
291
|
+
const req = https.get('https://api.anthropic.com/api/oauth/usage', {
|
|
292
|
+
headers: {
|
|
293
|
+
'Accept': 'application/json',
|
|
294
|
+
'Content-Type': 'application/json',
|
|
295
|
+
'User-Agent': 'agentlytics/1.0',
|
|
296
|
+
'Authorization': `Bearer ${token}`,
|
|
297
|
+
'anthropic-beta': 'oauth-2025-04-20',
|
|
298
|
+
},
|
|
299
|
+
timeout: 10000,
|
|
300
|
+
}, (res) => {
|
|
301
|
+
let data = '';
|
|
302
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
303
|
+
res.on('end', () => {
|
|
304
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
req.on('error', () => resolve(null));
|
|
308
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function getUsage() {
|
|
313
|
+
const creds = getClaudeCredentials();
|
|
314
|
+
if (!creds) return null;
|
|
315
|
+
|
|
316
|
+
const usage = await claudeApiFetch(creds.accessToken);
|
|
317
|
+
if (!usage) return null;
|
|
318
|
+
|
|
319
|
+
const result = {
|
|
320
|
+
source: 'claude-code',
|
|
321
|
+
plan: {
|
|
322
|
+
name: creds.subscriptionType || null,
|
|
323
|
+
},
|
|
324
|
+
usage: {},
|
|
325
|
+
extraUsage: null,
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
if (usage.five_hour) {
|
|
329
|
+
result.usage.fiveHour = {
|
|
330
|
+
utilization: usage.five_hour.utilization,
|
|
331
|
+
resetsAt: usage.five_hour.resets_at || null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (usage.seven_day) {
|
|
335
|
+
result.usage.sevenDay = {
|
|
336
|
+
utilization: usage.seven_day.utilization,
|
|
337
|
+
resetsAt: usage.seven_day.resets_at || null,
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
if (usage.seven_day_sonnet) {
|
|
341
|
+
result.usage.sevenDaySonnet = {
|
|
342
|
+
utilization: usage.seven_day_sonnet.utilization,
|
|
343
|
+
resetsAt: usage.seven_day_sonnet.resets_at || null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
if (usage.seven_day_opus) {
|
|
347
|
+
result.usage.sevenDayOpus = {
|
|
348
|
+
utilization: usage.seven_day_opus.utilization,
|
|
349
|
+
resetsAt: usage.seven_day_opus.resets_at || null,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
if (usage.extra_usage) {
|
|
353
|
+
result.extraUsage = {
|
|
354
|
+
isEnabled: usage.extra_usage.is_enabled || false,
|
|
355
|
+
monthlyLimit: usage.extra_usage.monthly_limit || null,
|
|
356
|
+
usedCredits: usage.extra_usage.used_credits || null,
|
|
357
|
+
utilization: usage.extra_usage.utilization || null,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
return result;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const labels = { 'claude-code': 'Claude Code' };
|
|
365
|
+
|
|
366
|
+
function getArtifacts(folder) {
|
|
367
|
+
const { scanArtifacts } = require('./base');
|
|
368
|
+
return scanArtifacts(folder, {
|
|
369
|
+
editor: 'claude-code',
|
|
370
|
+
label: 'Claude Code',
|
|
371
|
+
files: ['CLAUDE.md', '.claude/settings.json', '.claude/settings.local.json', '.mcp.json'],
|
|
372
|
+
dirs: ['.claude/commands'],
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function getMCPServers() {
|
|
377
|
+
const { parseMcpConfigFile } = require('./base');
|
|
378
|
+
const results = [];
|
|
379
|
+
// Global: ~/.claude.json (has mcpServers key)
|
|
380
|
+
const globalFile = path.join(os.homedir(), '.claude.json');
|
|
381
|
+
results.push(...parseMcpConfigFile(globalFile, { editor: 'claude-code', label: 'Claude Code', scope: 'global' }));
|
|
382
|
+
// Project-level: .mcp.json (scanned per-project later via getAllMCPServers)
|
|
383
|
+
return results;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts, getMCPServers };
|