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 +5 -3
- package/editors/claude.js +3 -1
- package/editors/codex.js +3 -0
- package/editors/commandcode.js +3 -1
- package/editors/copilot.js +3 -1
- package/editors/cursor-agent.js +3 -1
- package/editors/cursor.js +3 -1
- package/editors/gemini.js +3 -1
- package/editors/goose.js +287 -0
- package/editors/index.js +10 -2
- package/editors/kiro.js +296 -0
- package/editors/opencode.js +149 -68
- package/editors/vscode.js +3 -1
- package/editors/windsurf.js +195 -49
- package/editors/zed.js +45 -20
- package/index.js +7 -20
- package/package.json +1 -1
- package/ui/src/components/EditorIcon.jsx +4 -0
- package/ui/src/lib/constants.js +4 -0
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>
|
|
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
|
+
<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
|
-
|
|
203
|
+
const labels = { 'claude-code': 'Claude Code' };
|
|
204
|
+
|
|
205
|
+
module.exports = { name, labels, getChats, getMessages };
|
package/editors/codex.js
CHANGED
package/editors/commandcode.js
CHANGED
|
@@ -156,4 +156,6 @@ function extractAssistantContent(content) {
|
|
|
156
156
|
return { text: parts.join('\n') || '', toolCalls };
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
const labels = { 'commandcode': 'Command Code' };
|
|
160
|
+
|
|
161
|
+
module.exports = { name, labels, getChats, getMessages };
|
package/editors/copilot.js
CHANGED
|
@@ -171,4 +171,6 @@ function safeParse(str) {
|
|
|
171
171
|
try { return JSON.parse(str); } catch { return {}; }
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
const labels = { 'copilot-cli': 'Copilot CLI' };
|
|
175
|
+
|
|
176
|
+
module.exports = { name, labels, getChats, getMessages };
|
package/editors/cursor-agent.js
CHANGED
package/editors/cursor.js
CHANGED
package/editors/gemini.js
CHANGED
package/editors/goose.js
ADDED
|
@@ -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 };
|