agentlytics 0.1.13 → 0.1.15

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 CHANGED
@@ -6,12 +6,12 @@
6
6
 
7
7
  <p align="center">
8
8
  <strong>Your Cursor, Windsurf, Claude Code sessions — analyzed, unified, tracked.</strong><br>
9
- <sub>One command to turn scattered AI conversations from <b>14 editors</b> into a unified analytics dashboard.<br>Sessions, costs, models, tools — finally in one place. 100% local.</sub>
9
+ <sub>One command to turn scattered AI conversations from <b>16 editors</b> into a unified analytics dashboard.<br>Sessions, costs, models, tools — finally in one place. 100% local.</sub>
10
10
  </p>
11
11
 
12
12
  <p align="center">
13
13
  <a href="https://www.npmjs.com/package/agentlytics"><img src="https://img.shields.io/npm/v/agentlytics?color=6366f1&label=npm" alt="npm"></a>
14
- <a href="#supported-editors"><img src="https://img.shields.io/badge/editors-14-818cf8" alt="editors"></a>
14
+ <a href="#supported-editors"><img src="https://img.shields.io/badge/editors-16-818cf8" alt="editors"></a>
15
15
  <a href="#license"><img src="https://img.shields.io/badge/license-MIT-green" alt="license"></a>
16
16
  <a href="https://nodejs.org"><img src="https://img.shields.io/badge/node-%E2%89%A520.19%20%7C%20%E2%89%A522.12-brightgreen" alt="node"></a>
17
17
  </p>
@@ -83,7 +83,7 @@ npx agentlytics --collect
83
83
 
84
84
  | Editor | Msgs | Tools | Models | Tokens |
85
85
  |--------|:----:|:-----:|:------:|:------:|
86
- | **Cursor** | ✅ | ✅ | ⚠️ | ⚠️ |
86
+ | **Cursor** | ✅ | ✅ | | |
87
87
  | **Windsurf** | ✅ | ✅ | ✅ | ✅ |
88
88
  | **Windsurf Next** | ✅ | ✅ | ✅ | ✅ |
89
89
  | **Antigravity** | ✅ | ✅ | ✅ | ✅ |
@@ -97,6 +97,8 @@ npx agentlytics --collect
97
97
  | **Copilot CLI** | ✅ | ✅ | ✅ | ✅ |
98
98
  | **Cursor Agent** | ✅ | ❌ | ❌ | ❌ |
99
99
  | **Command Code** | ✅ | ✅ | ❌ | ❌ |
100
+ | **Goose** | ✅ | ✅ | ✅ | ❌ |
101
+ | **Kiro** | ✅ | ✅ | ✅ | ❌ |
100
102
 
101
103
  > Windsurf, Windsurf Next, and Antigravity must be running during scan.
102
104
 
package/editors/claude.js CHANGED
@@ -200,4 +200,6 @@ function extractAssistantContent(content) {
200
200
  return { text: parts.join('\n') || '', toolCalls };
201
201
  }
202
202
 
203
- module.exports = { name, getChats, getMessages };
203
+ const labels = { 'claude-code': 'Claude Code' };
204
+
205
+ module.exports = { name, labels, getChats, getMessages };
package/editors/codex.js CHANGED
@@ -435,8 +435,11 @@ function safeParseJson(value) {
435
435
  }
436
436
  }
437
437
 
438
+ const labels = { 'codex': 'Codex' };
439
+
438
440
  module.exports = {
439
441
  name,
442
+ labels,
440
443
  getChats,
441
444
  getMessages,
442
445
  };
@@ -156,4 +156,6 @@ function extractAssistantContent(content) {
156
156
  return { text: parts.join('\n') || '', toolCalls };
157
157
  }
158
158
 
159
- module.exports = { name, getChats, getMessages };
159
+ const labels = { 'commandcode': 'Command Code' };
160
+
161
+ module.exports = { name, labels, getChats, getMessages };
@@ -171,4 +171,6 @@ function safeParse(str) {
171
171
  try { return JSON.parse(str); } catch { return {}; }
172
172
  }
173
173
 
174
- module.exports = { name, getChats, getMessages };
174
+ const labels = { 'copilot-cli': 'Copilot CLI' };
175
+
176
+ module.exports = { name, labels, getChats, getMessages };
@@ -192,4 +192,6 @@ function getMessages(chat) {
192
192
  return result;
193
193
  }
194
194
 
195
- module.exports = { name, getChats, getMessages };
195
+ const labels = { 'cursor-agent': 'Cursor Agent' };
196
+
197
+ module.exports = { name, labels, getChats, getMessages };
package/editors/cursor.js CHANGED
@@ -341,4 +341,6 @@ function getMessages(chat) {
341
341
  return msgs;
342
342
  }
343
343
 
344
- module.exports = { name, getChats, getMessages };
344
+ const labels = { 'cursor': 'Cursor' };
345
+
346
+ module.exports = { name, labels, getChats, getMessages };
package/editors/gemini.js CHANGED
@@ -171,4 +171,6 @@ function getMessages(chat) {
171
171
  return result;
172
172
  }
173
173
 
174
- module.exports = { name, getChats, getMessages };
174
+ const labels = { 'gemini-cli': 'Gemini CLI' };
175
+
176
+ module.exports = { name, labels, getChats, getMessages };
@@ -0,0 +1,287 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+ const { execSync } = require('child_process');
5
+
6
+ const GOOSE_DIR = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions');
7
+ const DB_PATH = path.join(GOOSE_DIR, 'sessions.db');
8
+ const CONFIG_PATH = path.join(os.homedir(), '.config', 'goose', 'config.yaml');
9
+
10
+ // ============================================================
11
+ // Query SQLite via CLI
12
+ // ============================================================
13
+
14
+ function queryDb(sql) {
15
+ if (!fs.existsSync(DB_PATH)) return [];
16
+ try {
17
+ const raw = execSync(
18
+ `sqlite3 -json ${JSON.stringify(DB_PATH)} ${JSON.stringify(sql)}`,
19
+ { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
20
+ );
21
+ return JSON.parse(raw);
22
+ } catch { return []; }
23
+ }
24
+
25
+ // ============================================================
26
+ // Adapter interface
27
+ // ============================================================
28
+
29
+ const name = 'goose';
30
+
31
+ let _configModel = undefined; // lazy-loaded
32
+
33
+ function getConfigModel() {
34
+ if (_configModel !== undefined) return _configModel;
35
+ _configModel = null;
36
+ try {
37
+ const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
38
+ const modelMatch = raw.match(/^GOOSE_MODEL:\s*(.+)$/m);
39
+ if (modelMatch) _configModel = modelMatch[1].trim();
40
+ } catch {}
41
+ return _configModel;
42
+ }
43
+
44
+ function getChats() {
45
+ const chats = [];
46
+
47
+ const configModel = getConfigModel();
48
+
49
+ // --- SQLite sessions (v1.10.0+) ---
50
+ const dbSessions = queryDb(
51
+ `SELECT id, name, description, working_dir, created_at, updated_at,
52
+ total_tokens, input_tokens, output_tokens, provider_name, model_config_json,
53
+ (SELECT count(*) FROM messages m WHERE m.session_id = s.id) as msg_count
54
+ FROM sessions s ORDER BY updated_at DESC`
55
+ );
56
+
57
+ const dbSessionIds = new Set();
58
+ for (const row of dbSessions) {
59
+ dbSessionIds.add(row.id);
60
+ chats.push({
61
+ source: 'goose',
62
+ composerId: row.id,
63
+ name: cleanTitle(row.name || row.description),
64
+ createdAt: parseTimestamp(row.created_at),
65
+ lastUpdatedAt: parseTimestamp(row.updated_at),
66
+ mode: 'goose',
67
+ folder: row.working_dir || null,
68
+ encrypted: false,
69
+ bubbleCount: row.msg_count || 0,
70
+ _storage: 'db',
71
+ _inputTokens: row.input_tokens,
72
+ _outputTokens: row.output_tokens,
73
+ _model: extractSessionModel(row) || configModel,
74
+ });
75
+ }
76
+
77
+ // --- Legacy JSONL files ---
78
+ if (fs.existsSync(GOOSE_DIR)) {
79
+ let files;
80
+ try { files = fs.readdirSync(GOOSE_DIR).filter(f => f.endsWith('.jsonl')); } catch { files = []; }
81
+
82
+ for (const file of files) {
83
+ const sessionId = file.replace('.jsonl', '');
84
+ if (dbSessionIds.has(sessionId)) continue; // already in DB
85
+
86
+ const fullPath = path.join(GOOSE_DIR, file);
87
+ try {
88
+ const stat = fs.statSync(fullPath);
89
+ const meta = peekJsonlMeta(fullPath);
90
+ chats.push({
91
+ source: 'goose',
92
+ composerId: sessionId,
93
+ name: meta.firstPrompt ? cleanTitle(meta.firstPrompt) : null,
94
+ createdAt: meta.timestamp || stat.birthtime.getTime(),
95
+ lastUpdatedAt: stat.mtime.getTime(),
96
+ mode: 'goose',
97
+ folder: meta.workingDir || null,
98
+ encrypted: false,
99
+ _storage: 'jsonl',
100
+ _fullPath: fullPath,
101
+ _model: configModel,
102
+ });
103
+ } catch { /* skip */ }
104
+ }
105
+ }
106
+
107
+ return chats;
108
+ }
109
+
110
+ function parseTimestamp(ts) {
111
+ if (!ts) return null;
112
+ if (typeof ts === 'number') return ts;
113
+ const d = new Date(ts);
114
+ return isNaN(d.getTime()) ? null : d.getTime();
115
+ }
116
+
117
+ function cleanTitle(title) {
118
+ if (!title) return null;
119
+ return title.substring(0, 120).trim() || null;
120
+ }
121
+
122
+ function peekJsonlMeta(filePath) {
123
+ const meta = { firstPrompt: null, workingDir: null, timestamp: null };
124
+ try {
125
+ const buf = fs.readFileSync(filePath, 'utf-8');
126
+ for (const line of buf.split('\n')) {
127
+ if (!line) continue;
128
+ const obj = JSON.parse(line);
129
+
130
+ if (!meta.timestamp && obj.created) {
131
+ meta.timestamp = parseTimestamp(obj.created);
132
+ }
133
+
134
+ if (!meta.workingDir && obj.working_dir) {
135
+ meta.workingDir = obj.working_dir;
136
+ }
137
+
138
+ // First user text message
139
+ if (!meta.firstPrompt && obj.role === 'user' && obj.content) {
140
+ let parts;
141
+ try { parts = typeof obj.content === 'string' ? JSON.parse(obj.content) : obj.content; } catch { continue; }
142
+ if (Array.isArray(parts)) {
143
+ const text = parts.filter(p => p.type === 'text').map(p => p.text).join(' ');
144
+ if (text) meta.firstPrompt = text.substring(0, 200);
145
+ }
146
+ }
147
+
148
+ if (meta.firstPrompt && meta.workingDir) break;
149
+ }
150
+ } catch {}
151
+ return meta;
152
+ }
153
+
154
+ function getMessages(chat) {
155
+ if (chat._storage === 'db') return getMessagesFromDb(chat);
156
+ if (chat._storage === 'jsonl') return getMessagesFromJsonl(chat);
157
+ // Try DB first, then JSONL
158
+ const dbMessages = getMessagesFromDb(chat);
159
+ if (dbMessages.length) return dbMessages;
160
+ return getMessagesFromJsonl(chat);
161
+ }
162
+
163
+ function getMessagesFromDb(chat) {
164
+ const rows = queryDb(
165
+ `SELECT role, content_json, created_timestamp FROM messages
166
+ WHERE session_id = '${chat.composerId}' ORDER BY created_timestamp ASC`
167
+ );
168
+
169
+ const result = [];
170
+ for (const row of rows) {
171
+ let parts;
172
+ try { parts = JSON.parse(row.content_json); } catch { continue; }
173
+ if (!Array.isArray(parts)) continue;
174
+
175
+ const role = row.role;
176
+ const contentParts = [];
177
+ const toolCalls = [];
178
+
179
+ for (const part of parts) {
180
+ if (part.type === 'text' && part.text) {
181
+ contentParts.push(part.text);
182
+ } else if (part.type === 'toolRequest' && part.toolCall) {
183
+ const tc = part.toolCall.value || {};
184
+ const toolName = tc.name || 'tool';
185
+ let argKeys = '';
186
+ try { argKeys = Object.keys(tc.arguments || {}).join(', '); } catch {}
187
+ contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
188
+ toolCalls.push({ name: toolName, args: tc.arguments || {} });
189
+ } else if (part.type === 'toolResponse' && part.toolResult) {
190
+ const tr = part.toolResult;
191
+ let preview = '';
192
+ if (tr.value && Array.isArray(tr.value)) {
193
+ preview = tr.value
194
+ .filter(v => v.type === 'text')
195
+ .map(v => v.text)
196
+ .join('\n')
197
+ .substring(0, 500);
198
+ }
199
+ contentParts.push(`[tool-result: ${tr.status || 'done'}] ${preview}`);
200
+ }
201
+ }
202
+
203
+ const content = contentParts.join('\n');
204
+ if (!content) continue;
205
+
206
+ const mappedRole = role === 'user' ? 'user' : role === 'assistant' ? 'assistant' : role;
207
+ const msg = { role: mappedRole, content };
208
+ if (mappedRole === 'assistant' && chat._model) msg._model = chat._model;
209
+ if (toolCalls.length) msg._toolCalls = toolCalls;
210
+ result.push(msg);
211
+ }
212
+
213
+ return result;
214
+ }
215
+
216
+ function getMessagesFromJsonl(chat) {
217
+ const filePath = chat._fullPath || path.join(GOOSE_DIR, chat.composerId + '.jsonl');
218
+ if (!fs.existsSync(filePath)) return [];
219
+
220
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
221
+ const result = [];
222
+
223
+ for (const line of lines) {
224
+ let obj;
225
+ try { obj = JSON.parse(line); } catch { continue; }
226
+
227
+ if (!obj.role) continue;
228
+
229
+ let parts;
230
+ try {
231
+ parts = typeof obj.content === 'string' ? JSON.parse(obj.content) : obj.content;
232
+ } catch {
233
+ // content might be plain text
234
+ if (typeof obj.content === 'string' && obj.content) {
235
+ result.push({ role: obj.role, content: obj.content });
236
+ }
237
+ continue;
238
+ }
239
+
240
+ if (!Array.isArray(parts)) continue;
241
+
242
+ const contentParts = [];
243
+ for (const part of parts) {
244
+ if (part.type === 'text' && part.text) {
245
+ contentParts.push(part.text);
246
+ } else if (part.type === 'toolRequest') {
247
+ const tc = part.toolCall?.value || {};
248
+ contentParts.push(`[tool-call: ${tc.name || 'tool'}(${Object.keys(tc.arguments || {}).join(', ')})]`);
249
+ } else if (part.type === 'toolResponse') {
250
+ const tr = part.toolResult || {};
251
+ let preview = '';
252
+ if (Array.isArray(tr.value)) {
253
+ preview = tr.value.filter(v => v.type === 'text').map(v => v.text).join('\n').substring(0, 500);
254
+ }
255
+ contentParts.push(`[tool-result] ${preview}`);
256
+ }
257
+ }
258
+
259
+ const content = contentParts.join('\n');
260
+ if (content) {
261
+ const msg = { role: obj.role, content };
262
+ if (obj.role === 'assistant' && chat._model) msg._model = chat._model;
263
+ result.push(msg);
264
+ }
265
+ }
266
+
267
+ return result;
268
+ }
269
+
270
+ function extractSessionModel(row) {
271
+ if (row.model_config_json) {
272
+ try {
273
+ const cfg = JSON.parse(row.model_config_json);
274
+ if (cfg.model) return cfg.model;
275
+ if (cfg.model_id) return cfg.model_id;
276
+ } catch {}
277
+ }
278
+ return null;
279
+ }
280
+
281
+ function resetCache() {
282
+ _configModel = undefined;
283
+ }
284
+
285
+ const labels = { 'goose': 'Goose' };
286
+
287
+ module.exports = { name, labels, getChats, getMessages, resetCache };
package/editors/index.js CHANGED
@@ -9,8 +9,16 @@ const gemini = require('./gemini');
9
9
  const copilot = require('./copilot');
10
10
  const cursorAgent = require('./cursor-agent');
11
11
  const commandcode = require('./commandcode');
12
+ const goose = require('./goose');
13
+ const kiro = require('./kiro');
12
14
 
13
- const editors = [cursor, windsurf, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode];
15
+ const editors = [cursor, windsurf, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode, goose, kiro];
16
+
17
+ // Build a unified source → display-label map from all editor modules
18
+ const editorLabels = {};
19
+ for (const editor of editors) {
20
+ if (editor.labels) Object.assign(editorLabels, editor.labels);
21
+ }
14
22
 
15
23
  /**
16
24
  * Get all chats from all editor adapters, sorted by most recent first.
@@ -52,4 +60,4 @@ function resetCaches() {
52
60
  }
53
61
  }
54
62
 
55
- module.exports = { getAllChats, getMessages, editors, resetCaches };
63
+ module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches };