agentlytics 0.0.6 → 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.6",
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) {
@@ -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) {