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.
@@ -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 };
@@ -240,20 +240,20 @@ async function getUsage() {
240
240
  };
241
241
  }
242
242
 
243
- const labels = { 'copilot-cli': 'Copilot CLI' };
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 CLI shares MCP config with VS Code (handled by vscode.js)
256
+ // GitHub Copilot shares MCP config with VS Code (handled by vscode.js)
257
257
  return [];
258
258
  }
259
259