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 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-11-818cf8" alt="editors"></a>
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.0.5",
3
+ "version": "0.0.7",
4
4
  "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode",
5
5
  "main": "index.js",
6
6
  "bin": {
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="#586e75" font-size="10" font-family="${F}">${esc(label)}</text>
59
- <rect x="140" y="${y + 1}" width="${barW}" height="14" rx="2" fill="${color}" opacity="0.8"/>
60
- <text x="${146 + barW}" y="${y + 12}" fill="#839496" font-size="9" font-family="${F}">${e.count}</text>
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="#586e75" font-size="9" font-family="${F}">${esc(name)} <tspan fill="#475569">${m.count}</tspan></text>`;
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}" rx="12" fill="#002b36"/>
86
- <rect x="0.5" y="0.5" width="${W - 1}" height="${H - 1}" rx="12" fill="none" stroke="#073642" stroke-width="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" rx="12" fill="#073642"/>
90
- <rect x="0" y="16" width="${W}" height="16" fill="#073642"/>
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="#859900" font-size="12" font-family="${F}">$</text>
98
- <text x="40" y="58" fill="#93a1a1" font-size="12" font-family="${F}">npx agentlytics</text>
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="#073642" stroke-width="1"/>
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" rx="6" fill="#073642"/>
105
- <text x="36" y="96" fill="#586e75" font-size="9" font-family="${F}">sessions</text>
106
- <text x="36" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${fmt(overview.totalChats)}</text>
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" rx="6" fill="#073642"/>
109
- <text x="222" y="96" fill="#586e75" font-size="9" font-family="${F}">tokens</text>
110
- <text x="222" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${fmt((tk.input || 0) + (tk.output || 0))}</text>
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" rx="6" fill="#073642"/>
113
- <text x="408" y="96" fill="#586e75" font-size="9" font-family="${F}">active_days</text>
114
- <text x="408" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${streaks.totalDays || 0}</text>
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" rx="6" fill="#073642"/>
117
- <text x="594" y="96" fill="#586e75" font-size="9" font-family="${F}">streak <tspan fill="#475569">longest:${streaks.longest || 0}</tspan></text>
118
- <text x="594" y="122" fill="#93a1a1" font-size="22" font-weight="bold" font-family="${F}">${streaks.current || 0} <tspan font-size="11" fill="#586e75">day${(streaks.current || 0) !== 1 ? 's' : ''}</tspan></text>
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="#859900" font-size="10" font-family="${F}"># editors</text>
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="#859900" font-size="10" font-family="${F}"># peak_hours</text>
126
- <polyline points="${sparkPoints}" fill="none" stroke="#268bd2" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round"/>
127
- <text x="590" y="${180 + sparkH + 14}" fill="#475569" font-size="8" font-family="${F}">00:00</text>
128
- <text x="${590 + sparkW - 28}" y="${180 + sparkH + 14}" fill="#475569" font-size="8" font-family="${F}">23:00</text>
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="#859900" font-size="10" font-family="${F}"># models</text>
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="#073642" stroke-width="1"/>
136
- <text x="24" y="${H - 44}" fill="#586e75" 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>
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="#073642" stroke-width="1"/>
140
- <text x="24" y="${H - 10}" fill="#475569" font-size="9" font-family="${F}">github.com/f/agentlytics</text>
141
- <text x="${W - 24}" y="${H - 10}" fill="#475569" font-size="9" font-family="${F}" text-anchor="end">${esc(dateStr)}</text>
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;
@@ -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) {