agentlytics 0.0.5 → 0.0.7
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 +3 -2
- package/editors/cursor-agent.js +195 -0
- package/editors/index.js +2 -1
- package/index.js +1 -1
- package/package.json +1 -1
- package/share-image.js +36 -38
- package/ui/src/lib/constants.js +2 -0
package/README.md
CHANGED
|
@@ -6,12 +6,12 @@
|
|
|
6
6
|
|
|
7
7
|
<p align="center">
|
|
8
8
|
<strong>Unified analytics for your AI coding agents</strong><br>
|
|
9
|
-
<sub>Cursor · Windsurf · Claude Code · VS Code Copilot · Zed · Antigravity · OpenCode · Gemini CLI · Copilot CLI</sub>
|
|
9
|
+
<sub>Cursor · Windsurf · Claude Code · VS Code Copilot · Zed · Antigravity · OpenCode · Gemini CLI · Copilot CLI · Cursor Agent</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-12-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%A518-brightgreen" alt="node"></a>
|
|
17
17
|
</p>
|
|
@@ -56,6 +56,7 @@ Opens at **http://localhost:4637**. Requires Node.js ≥ 18, macOS.
|
|
|
56
56
|
| **OpenCode** | `opencode` | ✅ | ✅ | ✅ | ✅ |
|
|
57
57
|
| **Gemini CLI** | `gemini-cli` | ✅ | ✅ | ✅ | ✅ |
|
|
58
58
|
| **Copilot CLI** | `copilot-cli` | ✅ | ✅ | ✅ | ✅ |
|
|
59
|
+
| **Cursor Agent** | `cursor-agent` | ✅ | ❌ | ❌ | ❌ |
|
|
59
60
|
|
|
60
61
|
> Windsurf, Windsurf Next, and Antigravity must be running during scan.
|
|
61
62
|
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CURSOR_PROJECTS_DIR = path.join(os.homedir(), '.cursor', 'projects');
|
|
6
|
+
|
|
7
|
+
// ============================================================
|
|
8
|
+
// Adapter interface
|
|
9
|
+
// ============================================================
|
|
10
|
+
|
|
11
|
+
const name = 'cursor-agent';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Decode project directory name back to folder path.
|
|
15
|
+
* e.g. "Users-fka-Code-Wapple" → "/Users/fka/Code/Wapple"
|
|
16
|
+
*/
|
|
17
|
+
function decodeProjectDir(dirName) {
|
|
18
|
+
// The encoding replaces "/" with "-". We reconstruct by prepending "/"
|
|
19
|
+
// and replacing "-" back to "/". Handle ambiguity by checking if path exists.
|
|
20
|
+
const candidate = '/' + dirName.replace(/-/g, '/');
|
|
21
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
22
|
+
// Fallback: try common patterns (the first segment is usually "Users")
|
|
23
|
+
const parts = dirName.split('-');
|
|
24
|
+
for (let i = 2; i < parts.length; i++) {
|
|
25
|
+
const prefix = '/' + parts.slice(0, i).join('/');
|
|
26
|
+
const suffix = parts.slice(i).join('-');
|
|
27
|
+
const full = path.join(prefix, suffix);
|
|
28
|
+
if (fs.existsSync(full)) return full;
|
|
29
|
+
}
|
|
30
|
+
return candidate;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Find all agent transcript JSONL files across all projects.
|
|
35
|
+
* Two patterns:
|
|
36
|
+
* - <project>/agent-transcripts/<id>.jsonl (flat)
|
|
37
|
+
* - <project>/agent-transcripts/<id>/<id>.jsonl (nested)
|
|
38
|
+
*/
|
|
39
|
+
function findTranscripts() {
|
|
40
|
+
const results = [];
|
|
41
|
+
if (!fs.existsSync(CURSOR_PROJECTS_DIR)) return results;
|
|
42
|
+
|
|
43
|
+
let projectDirs;
|
|
44
|
+
try { projectDirs = fs.readdirSync(CURSOR_PROJECTS_DIR); } catch { return results; }
|
|
45
|
+
|
|
46
|
+
for (const projDir of projectDirs) {
|
|
47
|
+
const transcriptsDir = path.join(CURSOR_PROJECTS_DIR, projDir, 'agent-transcripts');
|
|
48
|
+
if (!fs.existsSync(transcriptsDir)) continue;
|
|
49
|
+
|
|
50
|
+
let entries;
|
|
51
|
+
try { entries = fs.readdirSync(transcriptsDir); } catch { continue; }
|
|
52
|
+
|
|
53
|
+
const folder = decodeProjectDir(projDir);
|
|
54
|
+
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const entryPath = path.join(transcriptsDir, entry);
|
|
57
|
+
|
|
58
|
+
// Flat pattern: <id>.jsonl
|
|
59
|
+
if (entry.endsWith('.jsonl')) {
|
|
60
|
+
const sessionId = entry.replace('.jsonl', '');
|
|
61
|
+
results.push({ sessionId, jsonlPath: entryPath, folder });
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Nested pattern: <id>/<id>.jsonl
|
|
66
|
+
try {
|
|
67
|
+
if (fs.statSync(entryPath).isDirectory()) {
|
|
68
|
+
const nestedJsonl = path.join(entryPath, entry + '.jsonl');
|
|
69
|
+
if (fs.existsSync(nestedJsonl)) {
|
|
70
|
+
results.push({ sessionId: entry, jsonlPath: nestedJsonl, folder });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} catch { /* skip */ }
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return results;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Parse a JSONL transcript file into an array of raw message objects.
|
|
81
|
+
* Each line: {"role":"user"|"assistant", "message":{"content":[{"type":"text","text":"..."}]}}
|
|
82
|
+
*/
|
|
83
|
+
function parseJsonl(jsonlPath) {
|
|
84
|
+
try {
|
|
85
|
+
const raw = fs.readFileSync(jsonlPath, 'utf-8');
|
|
86
|
+
return raw.split('\n').filter(Boolean).map(line => {
|
|
87
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
88
|
+
}).filter(Boolean);
|
|
89
|
+
} catch { return []; }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract the first user query text from a parsed transcript (for chat name).
|
|
94
|
+
*/
|
|
95
|
+
function extractFirstUserText(entries) {
|
|
96
|
+
for (const e of entries) {
|
|
97
|
+
if (e.role !== 'user') continue;
|
|
98
|
+
const parts = e.message?.content;
|
|
99
|
+
if (!Array.isArray(parts)) continue;
|
|
100
|
+
for (const p of parts) {
|
|
101
|
+
if (p.type === 'text' && p.text) {
|
|
102
|
+
// Strip <user_query> wrapper if present
|
|
103
|
+
let text = p.text.replace(/<\/?user_query>/g, '').trim();
|
|
104
|
+
// Strip <attached_files> blocks
|
|
105
|
+
text = text.replace(/<attached_files>[\s\S]*?<\/attached_files>/g, '').trim();
|
|
106
|
+
if (text) return text.substring(0, 120);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getChats() {
|
|
114
|
+
const chats = [];
|
|
115
|
+
const transcripts = findTranscripts();
|
|
116
|
+
|
|
117
|
+
for (const { sessionId, jsonlPath, folder } of transcripts) {
|
|
118
|
+
try {
|
|
119
|
+
const stat = fs.statSync(jsonlPath);
|
|
120
|
+
const entries = parseJsonl(jsonlPath);
|
|
121
|
+
if (entries.length === 0) continue;
|
|
122
|
+
|
|
123
|
+
// Count user/assistant messages
|
|
124
|
+
const userCount = entries.filter(e => e.role === 'user').length;
|
|
125
|
+
const assistantCount = entries.filter(e => e.role === 'assistant').length;
|
|
126
|
+
const bubbleCount = userCount + assistantCount;
|
|
127
|
+
if (bubbleCount === 0) continue;
|
|
128
|
+
|
|
129
|
+
chats.push({
|
|
130
|
+
source: 'cursor-agent',
|
|
131
|
+
composerId: sessionId,
|
|
132
|
+
name: extractFirstUserText(entries),
|
|
133
|
+
createdAt: stat.birthtimeMs || stat.mtimeMs,
|
|
134
|
+
lastUpdatedAt: stat.mtimeMs,
|
|
135
|
+
mode: 'agent',
|
|
136
|
+
folder,
|
|
137
|
+
bubbleCount,
|
|
138
|
+
_jsonlPath: jsonlPath,
|
|
139
|
+
});
|
|
140
|
+
} catch { /* skip */ }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return chats;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function getMessages(chat) {
|
|
147
|
+
const jsonlPath = chat._jsonlPath;
|
|
148
|
+
if (!jsonlPath || !fs.existsSync(jsonlPath)) return [];
|
|
149
|
+
|
|
150
|
+
const entries = parseJsonl(jsonlPath);
|
|
151
|
+
const result = [];
|
|
152
|
+
|
|
153
|
+
for (const entry of entries) {
|
|
154
|
+
const parts = entry.message?.content;
|
|
155
|
+
if (!Array.isArray(parts)) continue;
|
|
156
|
+
|
|
157
|
+
const textParts = [];
|
|
158
|
+
for (const p of parts) {
|
|
159
|
+
if (p.type === 'text' && p.text) {
|
|
160
|
+
let text = p.text;
|
|
161
|
+
// Clean up user_query wrappers
|
|
162
|
+
text = text.replace(/<\/?user_query>/g, '').trim();
|
|
163
|
+
// Clean attached_files blocks but note file references
|
|
164
|
+
const fileRefs = [];
|
|
165
|
+
text = text.replace(/<attached_files>([\s\S]*?)<\/attached_files>/g, (_, inner) => {
|
|
166
|
+
const paths = inner.match(/path="([^"]+)"/g);
|
|
167
|
+
if (paths) {
|
|
168
|
+
for (const pm of paths) {
|
|
169
|
+
const fp = pm.match(/path="([^"]+)"/);
|
|
170
|
+
if (fp) fileRefs.push(fp[1]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return '';
|
|
174
|
+
}).trim();
|
|
175
|
+
// Clean image_files blocks
|
|
176
|
+
text = text.replace(/<image_files>[\s\S]*?<\/image_files>/g, '[image]').trim();
|
|
177
|
+
if (text) textParts.push(text);
|
|
178
|
+
if (fileRefs.length > 0) {
|
|
179
|
+
textParts.push(fileRefs.map(f => `[file: ${f}]`).join('\n'));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (textParts.length > 0) {
|
|
185
|
+
result.push({
|
|
186
|
+
role: entry.role === 'user' ? 'user' : 'assistant',
|
|
187
|
+
content: textParts.join('\n'),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
module.exports = { name, getChats, getMessages };
|
package/editors/index.js
CHANGED
|
@@ -6,8 +6,9 @@ const zed = require('./zed');
|
|
|
6
6
|
const opencode = require('./opencode');
|
|
7
7
|
const gemini = require('./gemini');
|
|
8
8
|
const copilot = require('./copilot');
|
|
9
|
+
const cursorAgent = require('./cursor-agent');
|
|
9
10
|
|
|
10
|
-
const editors = [cursor, windsurf, claude, vscode, zed, opencode, gemini, copilot];
|
|
11
|
+
const editors = [cursor, windsurf, claude, vscode, zed, opencode, gemini, copilot, cursorAgent];
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Get all chats from all editor adapters, sorted by most recent first.
|
package/index.js
CHANGED
|
@@ -94,7 +94,7 @@ console.log(chalk.dim(' Initializing cache database...'));
|
|
|
94
94
|
cache.initDb();
|
|
95
95
|
|
|
96
96
|
// Scan all editors and populate cache
|
|
97
|
-
console.log(chalk.dim(' Scanning editors: Cursor, Windsurf, Claude Code, VS Code, Zed, Antigravity, OpenCode, Gemini CLI, Copilot CLI'));
|
|
97
|
+
console.log(chalk.dim(' Scanning editors: Cursor, Windsurf, Claude Code, VS Code, Zed, Antigravity, OpenCode, Gemini CLI, Copilot CLI, Cursor Agent'));
|
|
98
98
|
const startTime = Date.now();
|
|
99
99
|
const result = cache.scanAll((progress) => {
|
|
100
100
|
process.stdout.write(chalk.dim(`\r Scanning: ${progress.scanned}/${progress.total} chats (${progress.analyzed} analyzed, ${progress.skipped} cached)`));
|
package/package.json
CHANGED
package/share-image.js
CHANGED
|
@@ -22,6 +22,7 @@ const EDITOR_COLORS = {
|
|
|
22
22
|
'opencode': '#ec4899',
|
|
23
23
|
'gemini-cli': '#4285f4',
|
|
24
24
|
'copilot-cli': '#8957e5',
|
|
25
|
+
'cursor-agent': '#f59e0b',
|
|
25
26
|
};
|
|
26
27
|
|
|
27
28
|
const EDITOR_LABELS = {
|
|
@@ -37,6 +38,7 @@ const EDITOR_LABELS = {
|
|
|
37
38
|
'opencode': 'OpenCode',
|
|
38
39
|
'gemini-cli': 'Gemini CLI',
|
|
39
40
|
'copilot-cli': 'Copilot CLI',
|
|
41
|
+
'cursor-agent': 'Cursor Agent',
|
|
40
42
|
};
|
|
41
43
|
|
|
42
44
|
function generateShareSvg(overview, stats) {
|
|
@@ -55,9 +57,9 @@ function generateShareSvg(overview, stats) {
|
|
|
55
57
|
const label = (EDITOR_LABELS[e.id] || e.id).padEnd(12);
|
|
56
58
|
const y = 170 + i * 22;
|
|
57
59
|
return `
|
|
58
|
-
<text x="30" y="${y + 12}" fill="#
|
|
59
|
-
<rect x="140" y="${y + 1}" width="${barW}" height="14"
|
|
60
|
-
<text x="${146 + barW}" y="${y + 12}" fill="#
|
|
60
|
+
<text x="30" y="${y + 12}" fill="#888" font-size="10" font-family="${F}">${esc(label)}</text>
|
|
61
|
+
<rect x="140" y="${y + 1}" width="${barW}" height="14" fill="${color}" opacity="0.8"/>
|
|
62
|
+
<text x="${146 + barW}" y="${y + 12}" fill="#aaa" font-size="9" font-family="${F}">${e.count}</text>
|
|
61
63
|
`;
|
|
62
64
|
}).join('');
|
|
63
65
|
|
|
@@ -75,70 +77,66 @@ function generateShareSvg(overview, stats) {
|
|
|
75
77
|
const modelsList = topModels.map((m, i) => {
|
|
76
78
|
const y = 274 + i * 16;
|
|
77
79
|
const name = m.name.length > 24 ? m.name.substring(0, 24) : m.name;
|
|
78
|
-
return `<text x="590" y="${y}" fill="#
|
|
80
|
+
return `<text x="590" y="${y}" fill="#888" font-size="9" font-family="${F}">${esc(name)} <tspan fill="#555">${m.count}</tspan></text>`;
|
|
79
81
|
}).join('');
|
|
80
82
|
|
|
81
83
|
const dateStr = new Date().toISOString().split('T')[0];
|
|
82
84
|
|
|
83
85
|
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${W}" height="${H}" viewBox="0 0 ${W} ${H}">
|
|
84
86
|
<!-- Background -->
|
|
85
|
-
<rect width="${W}" height="${H}"
|
|
86
|
-
<rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}"
|
|
87
|
+
<rect width="${W}" height="${H}" fill="#000"/>
|
|
88
|
+
<rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" fill="none" stroke="#222" stroke-width="1"/>
|
|
87
89
|
|
|
88
90
|
<!-- Terminal title bar -->
|
|
89
|
-
<rect x="0" y="0" width="${W}" height="32"
|
|
90
|
-
<
|
|
91
|
-
<circle cx="18" cy="16" r="5" fill="#dc322f" opacity="0.8"/>
|
|
92
|
-
<circle cx="36" cy="16" r="5" fill="#b58900" opacity="0.8"/>
|
|
93
|
-
<circle cx="54" cy="16" r="5" fill="#859900" opacity="0.8"/>
|
|
94
|
-
<text x="${W / 2}" y="20" fill="#586e75" font-size="11" font-family="${F}" text-anchor="middle">agentlytics</text>
|
|
91
|
+
<rect x="0" y="0" width="${W}" height="32" fill="#111"/>
|
|
92
|
+
<text x="${W / 2}" y="20" fill="#555" font-size="11" font-family="${F}" text-anchor="middle">agentlytics</text>
|
|
95
93
|
|
|
96
94
|
<!-- Prompt line -->
|
|
97
|
-
<text x="24" y="58" fill="#
|
|
98
|
-
<text x="40" y="58" fill="#
|
|
95
|
+
<text x="24" y="58" fill="#666" font-size="12" font-family="${F}">$</text>
|
|
96
|
+
<text x="40" y="58" fill="#ccc" font-size="12" font-family="${F}">npx agentlytics</text>
|
|
99
97
|
|
|
100
98
|
<!-- Divider -->
|
|
101
|
-
<line x1="24" y1="68" x2="${W - 24}" y2="68" stroke="#
|
|
99
|
+
<line x1="24" y1="68" x2="${W - 24}" y2="68" stroke="#222" stroke-width="1"/>
|
|
102
100
|
|
|
103
101
|
<!-- KPI row -->
|
|
104
|
-
<rect x="24" y="78" width="175" height="58"
|
|
105
|
-
<text x="36" y="96" fill="#
|
|
106
|
-
<text x="36" y="122" fill="#
|
|
102
|
+
<rect x="24" y="78" width="175" height="58" fill="#111"/>
|
|
103
|
+
<text x="36" y="96" fill="#666" font-size="9" font-family="${F}">sessions</text>
|
|
104
|
+
<text x="36" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${fmt(overview.totalChats)}</text>
|
|
107
105
|
|
|
108
|
-
<rect x="210" y="78" width="175" height="58"
|
|
109
|
-
<text x="222" y="96" fill="#
|
|
110
|
-
<text x="222" y="122" fill="#
|
|
106
|
+
<rect x="210" y="78" width="175" height="58" fill="#111"/>
|
|
107
|
+
<text x="222" y="96" fill="#666" font-size="9" font-family="${F}">tokens</text>
|
|
108
|
+
<text x="222" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${fmt((tk.input || 0) + (tk.output || 0))}</text>
|
|
111
109
|
|
|
112
|
-
<rect x="396" y="78" width="175" height="58"
|
|
113
|
-
<text x="408" y="96" fill="#
|
|
114
|
-
<text x="408" y="122" fill="#
|
|
110
|
+
<rect x="396" y="78" width="175" height="58" fill="#111"/>
|
|
111
|
+
<text x="408" y="96" fill="#666" font-size="9" font-family="${F}">active_days</text>
|
|
112
|
+
<text x="408" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${streaks.totalDays || 0}</text>
|
|
115
113
|
|
|
116
|
-
<rect x="582" y="78" width="194" height="58"
|
|
117
|
-
<text x="594" y="96" fill="#
|
|
118
|
-
<text x="594" y="122" fill="#
|
|
114
|
+
<rect x="582" y="78" width="194" height="58" fill="#111"/>
|
|
115
|
+
<text x="594" y="96" fill="#666" font-size="9" font-family="${F}">streak <tspan fill="#555">longest:${streaks.longest || 0}</tspan></text>
|
|
116
|
+
<text x="594" y="122" fill="#fff" font-size="22" font-weight="bold" font-family="${F}">${streaks.current || 0} <tspan font-size="11" fill="#666">day${(streaks.current || 0) !== 1 ? 's' : ''}</tspan></text>
|
|
119
117
|
|
|
120
118
|
<!-- Editors section -->
|
|
121
|
-
<text x="24" y="160" fill="#
|
|
119
|
+
<text x="24" y="160" fill="#666" font-size="10" font-family="${F}"># editors</text>
|
|
122
120
|
${editorBars}
|
|
123
121
|
|
|
124
122
|
<!-- Right column: Peak Hours -->
|
|
125
|
-
<text x="590" y="160" fill="#
|
|
126
|
-
<polyline points="${sparkPoints}" fill="none" stroke="#
|
|
127
|
-
<text x="590" y="${180 + sparkH + 14}" fill="#
|
|
128
|
-
<text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#
|
|
123
|
+
<text x="590" y="160" fill="#666" font-size="10" font-family="${F}"># peak_hours</text>
|
|
124
|
+
<polyline points="${sparkPoints}" fill="none" stroke="#888" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
|
|
125
|
+
<text x="590" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">00:00</text>
|
|
126
|
+
<text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#555" font-size="8" font-family="${F}">23:00</text>
|
|
129
127
|
|
|
130
128
|
<!-- Top Models -->
|
|
131
|
-
<text x="590" y="258" fill="#
|
|
129
|
+
<text x="590" y="258" fill="#666" font-size="10" font-family="${F}"># models</text>
|
|
132
130
|
${modelsList}
|
|
133
131
|
|
|
134
132
|
<!-- Token breakdown -->
|
|
135
|
-
<line x1="24" y1="${H - 62}" x2="${W - 24}" y2="${H - 62}" stroke="#
|
|
136
|
-
<text x="24" y="${H - 44}" fill="#
|
|
133
|
+
<line x1="24" y1="${H - 62}" x2="${W - 24}" y2="${H - 62}" stroke="#222" stroke-width="1"/>
|
|
134
|
+
<text x="24" y="${H - 44}" fill="#666" font-size="9" font-family="${F}">in:${fmt(tk.input)} out:${fmt(tk.output)} cache:${fmt(tk.cacheRead)} tools:${fmt(stats.totalToolCalls || 0)} editors:${editors.length}</text>
|
|
137
135
|
|
|
138
136
|
<!-- Footer -->
|
|
139
|
-
<line x1="24" y1="${H - 28}" x2="${W - 24}" y2="${H - 28}" stroke="#
|
|
140
|
-
<text x="24" y="${H - 10}" fill="#
|
|
141
|
-
<text x="${W - 24}" y="${H - 10}" fill="#
|
|
137
|
+
<line x1="24" y1="${H - 28}" x2="${W - 24}" y2="${H - 28}" stroke="#222" stroke-width="1"/>
|
|
138
|
+
<text x="24" y="${H - 10}" fill="#555" font-size="9" font-family="${F}">github.com/f/agentlytics</text>
|
|
139
|
+
<text x="${W - 24}" y="${H - 10}" fill="#555" font-size="9" font-family="${F}" text-anchor="end">${esc(dateStr)}</text>
|
|
142
140
|
</svg>`;
|
|
143
141
|
|
|
144
142
|
return svg;
|
package/ui/src/lib/constants.js
CHANGED
|
@@ -11,6 +11,7 @@ export const EDITOR_COLORS = {
|
|
|
11
11
|
'opencode': '#ec4899',
|
|
12
12
|
'gemini-cli': '#4285f4',
|
|
13
13
|
'copilot-cli': '#8957e5',
|
|
14
|
+
'cursor-agent': '#f59e0b',
|
|
14
15
|
};
|
|
15
16
|
|
|
16
17
|
export const EDITOR_LABELS = {
|
|
@@ -26,6 +27,7 @@ export const EDITOR_LABELS = {
|
|
|
26
27
|
'opencode': 'OpenCode',
|
|
27
28
|
'gemini-cli': 'Gemini CLI',
|
|
28
29
|
'copilot-cli': 'Copilot CLI',
|
|
30
|
+
'cursor-agent': 'Cursor Agent',
|
|
29
31
|
};
|
|
30
32
|
|
|
31
33
|
export function editorColor(src) {
|