agentlytics 0.1.14 → 0.1.16
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 -2
- package/editors/antigravity.js +507 -0
- package/editors/claude.js +100 -1
- package/editors/codex.js +61 -0
- package/editors/copilot.js +68 -1
- package/editors/cursor.js +73 -1
- package/editors/index.js +23 -2
- package/editors/kiro.js +296 -0
- package/editors/opencode.js +146 -67
- package/editors/vscode.js +70 -1
- package/editors/windsurf.js +289 -61
- package/editors/zed.js +42 -19
- package/package.json +1 -1
- package/server.js +10 -0
- package/ui/src/App.jsx +4 -1
- package/ui/src/components/EditorIcon.jsx +2 -0
- package/ui/src/lib/api.js +5 -0
- package/ui/src/lib/constants.js +2 -0
- package/ui/src/pages/Subscriptions.jsx +413 -0
package/editors/opencode.js
CHANGED
|
@@ -1,103 +1,182 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const os = require('os');
|
|
4
|
-
const { execSync } = require('child_process');
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
// OpenCode stores data in different locations depending on the platform
|
|
6
|
+
// - Windows: %LOCALAPPDATA%\opencode\storage (not Roaming)
|
|
7
|
+
// - macOS/Linux: ~/.local/share/opencode/storage (XDG path)
|
|
8
|
+
function getOpenCodeStoragePath() {
|
|
9
|
+
const home = os.homedir();
|
|
10
|
+
switch (process.platform) {
|
|
11
|
+
case 'win32':
|
|
12
|
+
return path.join(home, 'AppData', 'Local', 'opencode', 'storage');
|
|
13
|
+
case 'darwin':
|
|
14
|
+
case 'linux':
|
|
15
|
+
default:
|
|
16
|
+
return path.join(home, '.local', 'share', 'opencode', 'storage');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const STORAGE_DIR = getOpenCodeStoragePath();
|
|
21
|
+
const SESSION_DIR = path.join(STORAGE_DIR, 'session');
|
|
22
|
+
const MESSAGE_DIR = path.join(STORAGE_DIR, 'message');
|
|
23
|
+
const PART_DIR = path.join(STORAGE_DIR, 'part');
|
|
7
24
|
|
|
8
25
|
// ============================================================
|
|
9
|
-
//
|
|
26
|
+
// Scan JSON files from OpenCode storage
|
|
10
27
|
// ============================================================
|
|
11
28
|
|
|
12
|
-
function
|
|
13
|
-
if (!fs.existsSync(DB_PATH)) return [];
|
|
29
|
+
function readJson(filePath) {
|
|
14
30
|
try {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
18
|
-
);
|
|
19
|
-
return JSON.parse(raw);
|
|
20
|
-
} catch { return []; }
|
|
31
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
32
|
+
} catch { return null; }
|
|
21
33
|
}
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
35
|
+
function getAllSessions() {
|
|
36
|
+
const sessions = [];
|
|
37
|
+
if (!fs.existsSync(SESSION_DIR)) return sessions;
|
|
26
38
|
|
|
27
|
-
const
|
|
39
|
+
for (const projectHash of fs.readdirSync(SESSION_DIR)) {
|
|
40
|
+
const projectDir = path.join(SESSION_DIR, projectHash);
|
|
41
|
+
if (!fs.statSync(projectDir).isDirectory()) continue;
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
|
|
43
|
+
let files;
|
|
44
|
+
try { files = fs.readdirSync(projectDir).filter(f => f.startsWith('ses_') && f.endsWith('.json')); } catch { continue; }
|
|
31
45
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
bubbleCount: row.msg_count || 0,
|
|
42
|
-
}));
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
const filePath = path.join(projectDir, file);
|
|
48
|
+
const data = readJson(filePath);
|
|
49
|
+
if (data && data.id) {
|
|
50
|
+
sessions.push({ ...data, _filePath: filePath });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return sessions;
|
|
43
55
|
}
|
|
44
56
|
|
|
45
|
-
function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
57
|
+
function getMessageCount(sessionId) {
|
|
58
|
+
const sessionMsgDir = path.join(MESSAGE_DIR, sessionId);
|
|
59
|
+
if (!fs.existsSync(sessionMsgDir)) return 0;
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return fs.readdirSync(sessionMsgDir).filter(f => f.startsWith('msg_') && f.endsWith('.json')).length;
|
|
63
|
+
} catch { return 0; }
|
|
50
64
|
}
|
|
51
65
|
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
function getMessagesForSession(sessionId) {
|
|
67
|
+
const sessionMsgDir = path.join(MESSAGE_DIR, sessionId);
|
|
68
|
+
if (!fs.existsSync(sessionMsgDir)) return [];
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
let msgData;
|
|
59
|
-
try { msgData = JSON.parse(msg.msg_data); } catch { continue; }
|
|
70
|
+
let files;
|
|
71
|
+
try { files = fs.readdirSync(sessionMsgDir).filter(f => f.startsWith('msg_') && f.endsWith('.json')); } catch { return []; }
|
|
60
72
|
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
const messages = [];
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const msgPath = path.join(sessionMsgDir, file);
|
|
76
|
+
const msg = readJson(msgPath);
|
|
77
|
+
if (!msg || !msg.id) continue;
|
|
63
78
|
|
|
64
79
|
// Get parts for this message
|
|
65
|
-
const
|
|
80
|
+
const msgPartDir = path.join(PART_DIR, msg.id);
|
|
81
|
+
const parts = [];
|
|
82
|
+
if (fs.existsSync(msgPartDir)) {
|
|
83
|
+
try {
|
|
84
|
+
const partFiles = fs.readdirSync(msgPartDir).filter(f => f.startsWith('prt_') && f.endsWith('.json'));
|
|
85
|
+
for (const partFile of partFiles) {
|
|
86
|
+
const part = readJson(path.join(msgPartDir, partFile));
|
|
87
|
+
if (part) parts.push(part);
|
|
88
|
+
}
|
|
89
|
+
} catch { /* skip */ }
|
|
90
|
+
}
|
|
66
91
|
|
|
92
|
+
// Build content from parts
|
|
67
93
|
const contentParts = [];
|
|
68
94
|
for (const part of parts) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
argKeys = Object.keys(input).join(', ');
|
|
80
|
-
} catch {}
|
|
95
|
+
const type = part.type;
|
|
96
|
+
|
|
97
|
+
if (type === 'text' && part.text) {
|
|
98
|
+
contentParts.push(part.text);
|
|
99
|
+
} else if (type === 'thinking' || type === 'reasoning') {
|
|
100
|
+
if (part.text) contentParts.push(`[thinking] ${part.text}`);
|
|
101
|
+
} else if (type === 'tool-call' || type === 'tool_use' || type === 'tool') {
|
|
102
|
+
const toolName = part.name || part.toolName || part.tool || 'tool';
|
|
103
|
+
const args = part.args || part.arguments || part.state?.input || {};
|
|
104
|
+
const argKeys = typeof args === 'object' ? Object.keys(args).join(', ') : '';
|
|
81
105
|
contentParts.push(`[tool-call: ${toolName}(${argKeys})]`);
|
|
82
|
-
} else if (
|
|
83
|
-
const preview = (
|
|
106
|
+
} else if (type === 'tool-result' || type === 'tool_result') {
|
|
107
|
+
const preview = (part.text || part.output || part.state?.output || '').substring(0, 500);
|
|
84
108
|
contentParts.push(`[tool-result] ${preview}`);
|
|
109
|
+
} else if (type === 'step-start' || type === 'step-finish') {
|
|
110
|
+
// Skip metadata parts
|
|
85
111
|
}
|
|
86
|
-
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// If no parts with content, check if message itself has content
|
|
115
|
+
if (contentParts.length === 0 && msg.role) {
|
|
116
|
+
contentParts.push(`[${msg.role}]`);
|
|
87
117
|
}
|
|
88
118
|
|
|
89
119
|
const content = contentParts.join('\n');
|
|
90
|
-
if (
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
if (content) {
|
|
121
|
+
// Extract model value - handle both string and object formats
|
|
122
|
+
let modelValue = null;
|
|
123
|
+
if (typeof msg.modelID === 'string') {
|
|
124
|
+
modelValue = msg.modelID;
|
|
125
|
+
} else if (msg.model && typeof msg.model === 'object' && msg.model.modelID) {
|
|
126
|
+
modelValue = msg.model.modelID;
|
|
127
|
+
} else if (typeof msg.model === 'string') {
|
|
128
|
+
modelValue = msg.model;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
messages.push({
|
|
132
|
+
role: msg.role || 'assistant',
|
|
133
|
+
content,
|
|
134
|
+
_model: modelValue,
|
|
135
|
+
_inputTokens: msg.tokens?.input,
|
|
136
|
+
_outputTokens: msg.tokens?.output,
|
|
137
|
+
_cacheRead: msg.tokens?.cache?.read,
|
|
138
|
+
_cacheWrite: msg.tokens?.cache?.write,
|
|
139
|
+
_finish: msg.finish,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
98
142
|
}
|
|
99
143
|
|
|
100
|
-
|
|
144
|
+
// Sort by creation time
|
|
145
|
+
return messages.sort((a, b) => {
|
|
146
|
+
const aTime = a.time?.created || 0;
|
|
147
|
+
const bTime = b.time?.created || 0;
|
|
148
|
+
return aTime - bTime;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ============================================================
|
|
153
|
+
// Adapter interface
|
|
154
|
+
// ============================================================
|
|
155
|
+
|
|
156
|
+
const name = 'opencode';
|
|
157
|
+
|
|
158
|
+
function getChats() {
|
|
159
|
+
const sessions = getAllSessions();
|
|
160
|
+
|
|
161
|
+
return sessions.map(s => ({
|
|
162
|
+
source: 'opencode',
|
|
163
|
+
composerId: s.id,
|
|
164
|
+
name: s.title || null,
|
|
165
|
+
createdAt: s.time?.created || null,
|
|
166
|
+
lastUpdatedAt: s.time?.updated || null,
|
|
167
|
+
mode: s.mode || 'opencode',
|
|
168
|
+
folder: s.directory || null,
|
|
169
|
+
encrypted: false,
|
|
170
|
+
bubbleCount: getMessageCount(s.id),
|
|
171
|
+
_agent: s.agent,
|
|
172
|
+
_model: s.modelID,
|
|
173
|
+
_provider: s.providerID,
|
|
174
|
+
_sessionData: s,
|
|
175
|
+
})).sort((a, b) => (b.lastUpdatedAt || 0) - (a.lastUpdatedAt || 0));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function getMessages(chat) {
|
|
179
|
+
return getMessagesForSession(chat.composerId);
|
|
101
180
|
}
|
|
102
181
|
|
|
103
182
|
const labels = { 'opencode': 'OpenCode' };
|
package/editors/vscode.js
CHANGED
|
@@ -315,6 +315,75 @@ function getMessages(chat) {
|
|
|
315
315
|
return messages;
|
|
316
316
|
}
|
|
317
317
|
|
|
318
|
+
// ============================================================
|
|
319
|
+
// Usage / quota data from GitHub Copilot internal API
|
|
320
|
+
// ============================================================
|
|
321
|
+
|
|
322
|
+
function getCopilotToken() {
|
|
323
|
+
const appsPath = path.join(os.homedir(), '.config', 'github-copilot', 'apps.json');
|
|
324
|
+
try {
|
|
325
|
+
if (!fs.existsSync(appsPath)) return null;
|
|
326
|
+
const data = JSON.parse(fs.readFileSync(appsPath, 'utf-8'));
|
|
327
|
+
// Pick the first available oauth_token
|
|
328
|
+
for (const entry of Object.values(data)) {
|
|
329
|
+
if (entry.oauth_token) return { token: entry.oauth_token, user: entry.user || null };
|
|
330
|
+
}
|
|
331
|
+
} catch {}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function fetchCopilotStatus(token) {
|
|
336
|
+
return new Promise((resolve) => {
|
|
337
|
+
const https = require('https');
|
|
338
|
+
const req = https.get('https://api.github.com/copilot_internal/v2/token', {
|
|
339
|
+
headers: {
|
|
340
|
+
'Authorization': `token ${token}`,
|
|
341
|
+
'Accept': 'application/json',
|
|
342
|
+
'User-Agent': 'agentlytics/1.0',
|
|
343
|
+
},
|
|
344
|
+
timeout: 10000,
|
|
345
|
+
}, (res) => {
|
|
346
|
+
let data = '';
|
|
347
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
348
|
+
res.on('end', () => {
|
|
349
|
+
try { resolve(JSON.parse(data)); } catch { resolve(null); }
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
req.on('error', () => resolve(null));
|
|
353
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function getUsage() {
|
|
358
|
+
const creds = getCopilotToken();
|
|
359
|
+
if (!creds) return null;
|
|
360
|
+
|
|
361
|
+
const status = await fetchCopilotStatus(creds.token);
|
|
362
|
+
if (!status || status.message) return null;
|
|
363
|
+
|
|
364
|
+
return {
|
|
365
|
+
source: 'vscode',
|
|
366
|
+
plan: {
|
|
367
|
+
name: status.sku || null,
|
|
368
|
+
individual: status.individual || false,
|
|
369
|
+
},
|
|
370
|
+
features: {
|
|
371
|
+
chat: status.chat_enabled || false,
|
|
372
|
+
codeReview: status.code_review_enabled || false,
|
|
373
|
+
agentMode: status.agent_mode_auto_approval || false,
|
|
374
|
+
xcode: status.xcode || false,
|
|
375
|
+
mcp: status.mcp || false,
|
|
376
|
+
},
|
|
377
|
+
limits: {
|
|
378
|
+
quotas: status.limited_user_quotas || null,
|
|
379
|
+
resetDate: status.limited_user_reset_date || null,
|
|
380
|
+
},
|
|
381
|
+
user: {
|
|
382
|
+
login: creds.user || null,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
318
387
|
const labels = { 'vscode': 'VS Code', 'vscode-insiders': 'VS Code Insiders' };
|
|
319
388
|
|
|
320
|
-
module.exports = { name, labels, getChats, getMessages };
|
|
389
|
+
module.exports = { name, labels, getChats, getMessages, getUsage };
|