agentlytics 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -62
- package/cache.js +221 -5
- package/editors/base.js +1 -1
- package/editors/codebuff.js +338 -0
- package/editors/copilot.js +3 -3
- package/editors/gsd.js +366 -0
- package/editors/index.js +10 -5
- package/editors/windsurf.js +64 -37
- package/index.js +32 -12
- package/package.json +6 -6
- package/public/assets/index-DV6ONi_F.css +2 -0
- package/public/assets/index-SOQVJIDS.js +73 -0
- package/public/index.html +16 -0
- package/relay-client.js +10 -8
- package/server.js +104 -2
- package/share-image.js +9 -7
- package/ui/src/App.jsx +5 -2
- package/ui/src/components/ChatSidebar.jsx +31 -2
- package/ui/src/components/EditorIcon.jsx +60 -11
- package/ui/src/components/TokenTimeline.jsx +258 -0
- package/ui/src/lib/api.js +43 -0
- package/ui/src/lib/constants.js +10 -8
- package/ui/src/pages/Artifacts.jsx +0 -12
- package/ui/src/pages/GSD.jsx +726 -0
- package/ui/src/pages/Settings.jsx +1 -1
- package/ui/src/pages/Subscriptions.jsx +3 -3
- package/deno.json +0 -9
- package/mod.ts +0 -1020
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
// ============================================================
|
|
6
|
+
// Codebuff adapter
|
|
7
|
+
// ------------------------------------------------------------
|
|
8
|
+
// Codebuff persists chats under ~/.config/manicode (the legacy folder name
|
|
9
|
+
// — the product was previously called Manicode). Non-prod builds use
|
|
10
|
+
// manicode-dev / manicode-staging. Layout:
|
|
11
|
+
//
|
|
12
|
+
// ~/.config/manicode/projects/<projectBasename>/chats/<chatId>/
|
|
13
|
+
// ├── chat-messages.json // serialized ChatMessage[]
|
|
14
|
+
// ├── run-state.json // SDK RunState (has real `cwd`)
|
|
15
|
+
// └── log.jsonl // internal logs (ignored)
|
|
16
|
+
//
|
|
17
|
+
// chatId is an ISO timestamp with ':' replaced by '-'. We use
|
|
18
|
+
// "<projectBasename>::<chatId>" as composerId to avoid collisions when
|
|
19
|
+
// two different projects share the same folder basename.
|
|
20
|
+
// ============================================================
|
|
21
|
+
|
|
22
|
+
const HOME = os.homedir();
|
|
23
|
+
|
|
24
|
+
function getProjectRoots() {
|
|
25
|
+
const roots = [];
|
|
26
|
+
for (const variant of ['manicode', 'manicode-dev', 'manicode-staging']) {
|
|
27
|
+
const projectsDir = path.join(HOME, '.config', variant, 'projects');
|
|
28
|
+
if (fs.existsSync(projectsDir)) roots.push({ variant, projectsDir });
|
|
29
|
+
}
|
|
30
|
+
return roots;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function safeReadJson(filePath) {
|
|
34
|
+
try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseChatIdToTs(chatId) {
|
|
38
|
+
// Codebuff chatIds look like "2026-04-21T16-34-12.000Z" — reverse the
|
|
39
|
+
// substitution so we get a real ISO timestamp back.
|
|
40
|
+
if (!chatId) return null;
|
|
41
|
+
const iso = chatId.replace(/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})/, '$1:$2:$3');
|
|
42
|
+
const ts = Date.parse(iso);
|
|
43
|
+
return Number.isFinite(ts) ? ts : null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function cleanPrompt(text) {
|
|
47
|
+
if (!text) return null;
|
|
48
|
+
const clean = String(text).replace(/\s+/g, ' ').trim().substring(0, 120);
|
|
49
|
+
return clean || null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function extractCwdFromRunState(runState) {
|
|
53
|
+
if (!runState) return null;
|
|
54
|
+
// Common shapes: { sessionState: { ... cwd }, output } or { cwd } at root.
|
|
55
|
+
const candidates = [
|
|
56
|
+
runState?.sessionState?.projectContext?.cwd,
|
|
57
|
+
runState?.sessionState?.fileContext?.cwd,
|
|
58
|
+
runState?.sessionState?.cwd,
|
|
59
|
+
runState?.cwd,
|
|
60
|
+
];
|
|
61
|
+
for (const c of candidates) if (typeof c === 'string' && c) return c;
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// --- Best-effort model / token extraction ---
|
|
66
|
+
|
|
67
|
+
function pickNumber(...vals) {
|
|
68
|
+
for (const v of vals) {
|
|
69
|
+
if (typeof v === 'number' && Number.isFinite(v)) return v;
|
|
70
|
+
}
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function extractUsageFromMetadata(meta) {
|
|
75
|
+
if (!meta || typeof meta !== 'object') return {};
|
|
76
|
+
const cb = meta.codebuff && typeof meta.codebuff === 'object' ? meta.codebuff : null;
|
|
77
|
+
const usage = (cb && cb.usage) || meta.usage || null;
|
|
78
|
+
if (!usage || typeof usage !== 'object') return {};
|
|
79
|
+
return {
|
|
80
|
+
inputTokens: pickNumber(usage.inputTokens, usage.promptTokens, usage.prompt_tokens, usage.input_tokens),
|
|
81
|
+
outputTokens: pickNumber(usage.outputTokens, usage.completionTokens, usage.completion_tokens, usage.output_tokens),
|
|
82
|
+
cacheRead: pickNumber(
|
|
83
|
+
usage.cacheReadInputTokens, usage.cache_read_input_tokens,
|
|
84
|
+
usage?.promptTokensDetails?.cachedTokens, usage?.prompt_tokens_details?.cached_tokens,
|
|
85
|
+
),
|
|
86
|
+
cacheWrite: pickNumber(
|
|
87
|
+
usage.cacheCreationInputTokens, usage.cache_creation_input_tokens,
|
|
88
|
+
usage.cachedTokensCreated,
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function extractMessageUsageAndModel(msg) {
|
|
94
|
+
// Codebuff ChatMessage shape allows metadata.runState to stash the SDK
|
|
95
|
+
// RunState after completion. Model + usage isn't guaranteed to be there
|
|
96
|
+
// but we try a few known spots.
|
|
97
|
+
const out = { model: undefined, inputTokens: undefined, outputTokens: undefined, cacheRead: undefined, cacheWrite: undefined };
|
|
98
|
+
const meta = msg?.metadata;
|
|
99
|
+
if (!meta || typeof meta !== 'object') return out;
|
|
100
|
+
|
|
101
|
+
// Direct provider hints some Codebuff builds attach.
|
|
102
|
+
if (typeof meta.model === 'string') out.model = meta.model;
|
|
103
|
+
if (typeof meta.modelId === 'string' && !out.model) out.model = meta.modelId;
|
|
104
|
+
|
|
105
|
+
// Token totals may live on metadata.usage or inside providerMetadata.
|
|
106
|
+
const usageDirect = extractUsageFromMetadata(meta);
|
|
107
|
+
Object.assign(out, {
|
|
108
|
+
inputTokens: out.inputTokens ?? usageDirect.inputTokens,
|
|
109
|
+
outputTokens: out.outputTokens ?? usageDirect.outputTokens,
|
|
110
|
+
cacheRead: out.cacheRead ?? usageDirect.cacheRead,
|
|
111
|
+
cacheWrite: out.cacheWrite ?? usageDirect.cacheWrite,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
// Walk the RunState stash for the most recent assistant message with
|
|
115
|
+
// providerOptions that carry OpenRouter-style usage.
|
|
116
|
+
const rs = meta.runState;
|
|
117
|
+
if (rs && typeof rs === 'object') {
|
|
118
|
+
const history = rs?.sessionState?.mainAgentState?.messageHistory;
|
|
119
|
+
if (Array.isArray(history)) {
|
|
120
|
+
for (let i = history.length - 1; i >= 0; i--) {
|
|
121
|
+
const m = history[i];
|
|
122
|
+
if (m?.role !== 'assistant') continue;
|
|
123
|
+
const po = m.providerOptions;
|
|
124
|
+
const u = extractUsageFromMetadata(po);
|
|
125
|
+
if (u.inputTokens != null || u.outputTokens != null) {
|
|
126
|
+
out.inputTokens = out.inputTokens ?? u.inputTokens;
|
|
127
|
+
out.outputTokens = out.outputTokens ?? u.outputTokens;
|
|
128
|
+
out.cacheRead = out.cacheRead ?? u.cacheRead;
|
|
129
|
+
out.cacheWrite = out.cacheWrite ?? u.cacheWrite;
|
|
130
|
+
if (!out.model && typeof po?.codebuff?.model === 'string') out.model = po.codebuff.model;
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// --- Block flattening: turn Codebuff ChatMessage blocks into a single
|
|
141
|
+
// transcript-style content string + normalized tool-call list. ---
|
|
142
|
+
|
|
143
|
+
function flattenBlocks(blocks, out = { parts: [], toolCalls: [] }, depth = 0) {
|
|
144
|
+
if (!Array.isArray(blocks)) return out;
|
|
145
|
+
const indent = depth ? ' '.repeat(depth) : '';
|
|
146
|
+
for (const block of blocks) {
|
|
147
|
+
if (!block || typeof block !== 'object') continue;
|
|
148
|
+
switch (block.type) {
|
|
149
|
+
case 'text': {
|
|
150
|
+
if (typeof block.content !== 'string' || !block.content) break;
|
|
151
|
+
if (block.textType === 'reasoning') {
|
|
152
|
+
out.parts.push(`${indent}[thinking] ${block.content}`);
|
|
153
|
+
} else {
|
|
154
|
+
out.parts.push(`${indent}${block.content}`);
|
|
155
|
+
}
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
case 'tool': {
|
|
159
|
+
const toolName = block.toolName || 'tool';
|
|
160
|
+
const input = block.input || {};
|
|
161
|
+
const argKeys = (input && typeof input === 'object') ? Object.keys(input).join(', ') : '';
|
|
162
|
+
out.parts.push(`${indent}[tool-call: ${toolName}(${argKeys})]`);
|
|
163
|
+
out.toolCalls.push({ name: toolName, args: input });
|
|
164
|
+
if (typeof block.output === 'string' && block.output) {
|
|
165
|
+
out.parts.push(`${indent}[tool-result] ${block.output.substring(0, 500)}`);
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
case 'agent': {
|
|
170
|
+
const name = block.agentName || block.agentType || 'agent';
|
|
171
|
+
const status = block.status ? ` (${block.status})` : '';
|
|
172
|
+
out.parts.push(`${indent}[subagent: ${name}${status}]`);
|
|
173
|
+
if (typeof block.content === 'string' && block.content) {
|
|
174
|
+
out.parts.push(`${indent} ${block.content}`);
|
|
175
|
+
}
|
|
176
|
+
flattenBlocks(block.blocks, out, depth + 1);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
case 'plan': {
|
|
180
|
+
if (typeof block.content === 'string' && block.content) {
|
|
181
|
+
out.parts.push(`${indent}[plan]\n${block.content}`);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
case 'mode-divider': {
|
|
186
|
+
if (block.mode) out.parts.push(`${indent}[mode: ${block.mode}]`);
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 'ask-user': {
|
|
190
|
+
const qs = Array.isArray(block.questions) ? block.questions : [];
|
|
191
|
+
for (const q of qs) {
|
|
192
|
+
if (q?.question) out.parts.push(`${indent}[ask-user] ${q.question}`);
|
|
193
|
+
}
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
case 'image': {
|
|
197
|
+
out.parts.push(`${indent}[image${block.filename ? `: ${block.filename}` : ''}]`);
|
|
198
|
+
break;
|
|
199
|
+
}
|
|
200
|
+
default:
|
|
201
|
+
break;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return out;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================
|
|
208
|
+
// Adapter interface
|
|
209
|
+
// ============================================================
|
|
210
|
+
|
|
211
|
+
const name = 'codebuff';
|
|
212
|
+
const labels = { 'codebuff': 'Codebuff' };
|
|
213
|
+
|
|
214
|
+
function getChats() {
|
|
215
|
+
const chats = [];
|
|
216
|
+
const roots = getProjectRoots();
|
|
217
|
+
if (roots.length === 0) return chats;
|
|
218
|
+
|
|
219
|
+
for (const { variant, projectsDir } of roots) {
|
|
220
|
+
let projectDirs;
|
|
221
|
+
try { projectDirs = fs.readdirSync(projectsDir); } catch { continue; }
|
|
222
|
+
// Only the non-prod variants get prefixed so the prod composerId stays clean.
|
|
223
|
+
const variantPrefix = variant === 'manicode' ? '' : `${variant}::`;
|
|
224
|
+
|
|
225
|
+
for (const projectBase of projectDirs) {
|
|
226
|
+
const projectDir = path.join(projectsDir, projectBase);
|
|
227
|
+
try { if (!fs.statSync(projectDir).isDirectory()) continue; } catch { continue; }
|
|
228
|
+
|
|
229
|
+
const chatsDir = path.join(projectDir, 'chats');
|
|
230
|
+
if (!fs.existsSync(chatsDir)) continue;
|
|
231
|
+
|
|
232
|
+
let chatIds;
|
|
233
|
+
try { chatIds = fs.readdirSync(chatsDir); } catch { continue; }
|
|
234
|
+
|
|
235
|
+
for (const chatId of chatIds) {
|
|
236
|
+
const chatDir = path.join(chatsDir, chatId);
|
|
237
|
+
let dirStat;
|
|
238
|
+
try { dirStat = fs.statSync(chatDir); } catch { continue; }
|
|
239
|
+
if (!dirStat.isDirectory()) continue;
|
|
240
|
+
|
|
241
|
+
const messagesPath = path.join(chatDir, 'chat-messages.json');
|
|
242
|
+
if (!fs.existsSync(messagesPath)) continue;
|
|
243
|
+
|
|
244
|
+
// Light peek for title + message count — don't hydrate blocks here.
|
|
245
|
+
const messages = safeReadJson(messagesPath);
|
|
246
|
+
if (!Array.isArray(messages) || messages.length === 0) continue;
|
|
247
|
+
|
|
248
|
+
const firstUser = messages.find((m) => m && m.variant === 'user' && typeof m.content === 'string');
|
|
249
|
+
const title = cleanPrompt(firstUser && firstUser.content);
|
|
250
|
+
|
|
251
|
+
// Recover the real cwd so Agentlytics can group by project correctly.
|
|
252
|
+
const runState = safeReadJson(path.join(chatDir, 'run-state.json'));
|
|
253
|
+
const folder = extractCwdFromRunState(runState) || null;
|
|
254
|
+
|
|
255
|
+
chats.push({
|
|
256
|
+
source: 'codebuff',
|
|
257
|
+
composerId: `${variantPrefix}${projectBase}::${chatId}`,
|
|
258
|
+
name: title,
|
|
259
|
+
createdAt: parseChatIdToTs(chatId),
|
|
260
|
+
lastUpdatedAt: dirStat.mtime.getTime(),
|
|
261
|
+
mode: 'codebuff',
|
|
262
|
+
folder,
|
|
263
|
+
encrypted: false,
|
|
264
|
+
bubbleCount: messages.length,
|
|
265
|
+
_fullPath: chatDir,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return chats;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getMessages(chat) {
|
|
275
|
+
const chatDir = chat._fullPath;
|
|
276
|
+
if (!chatDir) return [];
|
|
277
|
+
const messagesPath = path.join(chatDir, 'chat-messages.json');
|
|
278
|
+
if (!fs.existsSync(messagesPath)) return [];
|
|
279
|
+
|
|
280
|
+
const raw = safeReadJson(messagesPath);
|
|
281
|
+
if (!Array.isArray(raw)) return [];
|
|
282
|
+
|
|
283
|
+
const out = [];
|
|
284
|
+
for (const msg of raw) {
|
|
285
|
+
if (!msg || typeof msg !== 'object') continue;
|
|
286
|
+
const variant = msg.variant;
|
|
287
|
+
|
|
288
|
+
if (variant === 'user') {
|
|
289
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
290
|
+
if (content) out.push({ role: 'user', content });
|
|
291
|
+
continue;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (variant === 'error') {
|
|
295
|
+
const content = typeof msg.content === 'string' ? msg.content : '';
|
|
296
|
+
if (content) out.push({ role: 'system', content: `[error] ${content}` });
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (variant === 'ai' || variant === 'agent') {
|
|
301
|
+
const flattened = flattenBlocks(msg.blocks);
|
|
302
|
+
const parts = [];
|
|
303
|
+
if (typeof msg.content === 'string' && msg.content) parts.push(msg.content);
|
|
304
|
+
if (flattened.parts.length) parts.push(flattened.parts.join('\n'));
|
|
305
|
+
const content = parts.join('\n').trim();
|
|
306
|
+
if (!content) continue;
|
|
307
|
+
|
|
308
|
+
const { model, inputTokens, outputTokens, cacheRead, cacheWrite } = extractMessageUsageAndModel(msg);
|
|
309
|
+
|
|
310
|
+
out.push({
|
|
311
|
+
role: 'assistant',
|
|
312
|
+
content,
|
|
313
|
+
_model: model,
|
|
314
|
+
_inputTokens: inputTokens,
|
|
315
|
+
_outputTokens: outputTokens,
|
|
316
|
+
_cacheRead: cacheRead,
|
|
317
|
+
_cacheWrite: cacheWrite,
|
|
318
|
+
_toolCalls: flattened.toolCalls.length ? flattened.toolCalls : undefined,
|
|
319
|
+
_credits: typeof msg.credits === 'number' ? msg.credits : undefined,
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return out;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function getArtifacts(folder) {
|
|
329
|
+
const { scanArtifacts } = require('./base');
|
|
330
|
+
return scanArtifacts(folder, {
|
|
331
|
+
editor: 'codebuff',
|
|
332
|
+
label: 'Codebuff',
|
|
333
|
+
files: ['.codebuffignore', '.manicodeignore', 'knowledge.md'],
|
|
334
|
+
dirs: ['.agents'],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts };
|
package/editors/copilot.js
CHANGED
|
@@ -240,20 +240,20 @@ async function getUsage() {
|
|
|
240
240
|
};
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
const labels = { 'copilot-cli': 'Copilot
|
|
243
|
+
const labels = { 'copilot-cli': 'GitHub Copilot' };
|
|
244
244
|
|
|
245
245
|
function getArtifacts(folder) {
|
|
246
246
|
const { scanArtifacts } = require('./base');
|
|
247
247
|
return scanArtifacts(folder, {
|
|
248
248
|
editor: 'copilot-cli',
|
|
249
|
-
label: 'Copilot',
|
|
249
|
+
label: 'GitHub Copilot',
|
|
250
250
|
files: ['.github/copilot-instructions.md'],
|
|
251
251
|
dirs: [],
|
|
252
252
|
});
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
function getMCPServers() {
|
|
256
|
-
// Copilot
|
|
256
|
+
// GitHub Copilot shares MCP config with VS Code (handled by vscode.js)
|
|
257
257
|
return [];
|
|
258
258
|
}
|
|
259
259
|
|