agentlytics 0.1.19 → 0.1.20
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/cache.js +3 -2
- package/editors/antigravity.js +504 -31
- package/editors/base.js +87 -0
- package/editors/claude.js +11 -1
- package/editors/codex.js +11 -0
- package/editors/copilot.js +11 -1
- package/editors/cursor.js +11 -1
- package/editors/gemini.js +11 -1
- package/editors/goose.js +30 -8
- package/editors/index.js +40 -1
- package/editors/kiro.js +11 -1
- package/editors/opencode.js +4 -22
- package/editors/vscode.js +11 -1
- package/editors/windsurf.js +21 -10
- package/editors/zed.js +23 -3
- package/index.js +40 -38
- package/package.json +1 -1
- package/server.js +81 -1
- package/ui/src/App.jsx +75 -16
- package/ui/src/components/AiAuditCard.jsx +4 -5
- package/ui/src/components/AnimatedLoader.jsx +14 -0
- package/ui/src/lib/api.js +13 -0
- package/ui/src/pages/Artifacts.jsx +600 -0
- package/ui/src/pages/CostAnalysis.jsx +2 -1
- package/ui/src/pages/Dashboard.jsx +2 -1
- package/ui/src/pages/ProjectDetail.jsx +3 -1
- package/ui/src/pages/Projects.jsx +2 -1
- package/ui/src/pages/RelayDashboard.jsx +2 -1
- package/ui/src/pages/Settings.jsx +2 -1
- package/ui/src/pages/Subscriptions.jsx +2 -1
package/editors/base.js
CHANGED
|
@@ -32,6 +32,93 @@ function getAppDataPath(appName) {
|
|
|
32
32
|
* { role: 'user'|'assistant'|'system'|'tool', content: string|Array }
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Scan a project folder for artifact files.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} folder - Absolute path to the project folder
|
|
39
|
+
* @param {Object} opts
|
|
40
|
+
* @param {string} opts.editor - Editor identifier (e.g. 'cursor', 'claude-code')
|
|
41
|
+
* @param {string} opts.label - Display label (e.g. 'Cursor', 'Claude Code')
|
|
42
|
+
* @param {string[]} [opts.files] - Relative file paths to check (e.g. ['CLAUDE.md'])
|
|
43
|
+
* @param {string[]} [opts.dirs] - Relative directories to scan for .md/.yaml/.yml/.json files
|
|
44
|
+
* @returns {Array} Array of artifact objects
|
|
45
|
+
*/
|
|
46
|
+
function scanArtifacts(folder, { editor, label, files = [], dirs = [] }) {
|
|
47
|
+
const fs = require('fs');
|
|
48
|
+
const artifacts = [];
|
|
49
|
+
if (!folder || !fs.existsSync(folder)) return artifacts;
|
|
50
|
+
|
|
51
|
+
for (const relPath of files) {
|
|
52
|
+
const filePath = path.join(folder, relPath);
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.statSync(filePath);
|
|
55
|
+
if (!stat.isFile()) continue;
|
|
56
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
57
|
+
artifacts.push({
|
|
58
|
+
editor,
|
|
59
|
+
editorLabel: label,
|
|
60
|
+
name: relPath,
|
|
61
|
+
path: filePath,
|
|
62
|
+
relativePath: relPath,
|
|
63
|
+
size: stat.size,
|
|
64
|
+
modifiedAt: stat.mtime.getTime(),
|
|
65
|
+
preview: content.substring(0, 500),
|
|
66
|
+
lines: content.split('\n').length,
|
|
67
|
+
});
|
|
68
|
+
} catch { /* skip */ }
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const isArtifactFile = (f) =>
|
|
72
|
+
f.endsWith('.md') || f.endsWith('.mdc') || f.endsWith('.yaml') || f.endsWith('.yml') || f.endsWith('.json');
|
|
73
|
+
|
|
74
|
+
const addFile = (filePath, relPath, fileName) => {
|
|
75
|
+
try {
|
|
76
|
+
const fstat = fs.statSync(filePath);
|
|
77
|
+
if (!fstat.isFile()) return;
|
|
78
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
79
|
+
artifacts.push({
|
|
80
|
+
editor,
|
|
81
|
+
editorLabel: label,
|
|
82
|
+
name: fileName,
|
|
83
|
+
path: filePath,
|
|
84
|
+
relativePath: relPath,
|
|
85
|
+
size: fstat.size,
|
|
86
|
+
modifiedAt: fstat.mtime.getTime(),
|
|
87
|
+
preview: content.substring(0, 500),
|
|
88
|
+
lines: content.split('\n').length,
|
|
89
|
+
});
|
|
90
|
+
} catch { /* skip */ }
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
for (const dir of dirs) {
|
|
94
|
+
const dirPath = path.join(folder, dir);
|
|
95
|
+
try {
|
|
96
|
+
const stat = fs.statSync(dirPath);
|
|
97
|
+
if (!stat.isDirectory()) continue;
|
|
98
|
+
const entries = fs.readdirSync(dirPath);
|
|
99
|
+
for (const entry of entries) {
|
|
100
|
+
const entryPath = path.join(dirPath, entry);
|
|
101
|
+
if (isArtifactFile(entry)) {
|
|
102
|
+
addFile(entryPath, path.join(dir, entry), entry);
|
|
103
|
+
} else {
|
|
104
|
+
// Recurse one level into subdirectories (e.g. .kiro/specs/<name>/, .windsurf/skills/<name>/)
|
|
105
|
+
try {
|
|
106
|
+
const eStat = fs.statSync(entryPath);
|
|
107
|
+
if (!eStat.isDirectory()) continue;
|
|
108
|
+
const subEntries = fs.readdirSync(entryPath).filter(isArtifactFile);
|
|
109
|
+
for (const subFile of subEntries) {
|
|
110
|
+
addFile(path.join(entryPath, subFile), path.join(dir, entry, subFile), subFile);
|
|
111
|
+
}
|
|
112
|
+
} catch { /* skip */ }
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
} catch { /* skip */ }
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return artifacts;
|
|
119
|
+
}
|
|
120
|
+
|
|
35
121
|
module.exports = {
|
|
36
122
|
getAppDataPath,
|
|
123
|
+
scanArtifacts,
|
|
37
124
|
};
|
package/editors/claude.js
CHANGED
|
@@ -301,4 +301,14 @@ async function getUsage() {
|
|
|
301
301
|
|
|
302
302
|
const labels = { 'claude-code': 'Claude Code' };
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
function getArtifacts(folder) {
|
|
305
|
+
const { scanArtifacts } = require('./base');
|
|
306
|
+
return scanArtifacts(folder, {
|
|
307
|
+
editor: 'claude-code',
|
|
308
|
+
label: 'Claude Code',
|
|
309
|
+
files: ['CLAUDE.md', '.claude/settings.json', '.claude/settings.local.json', '.mcp.json'],
|
|
310
|
+
dirs: ['.claude/commands'],
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
|
package/editors/codex.js
CHANGED
|
@@ -499,9 +499,20 @@ async function getUsage() {
|
|
|
499
499
|
|
|
500
500
|
const labels = { 'codex': 'Codex' };
|
|
501
501
|
|
|
502
|
+
function getArtifacts(folder) {
|
|
503
|
+
const { scanArtifacts } = require('./base');
|
|
504
|
+
return scanArtifacts(folder, {
|
|
505
|
+
editor: 'codex',
|
|
506
|
+
label: 'Codex',
|
|
507
|
+
files: ['AGENTS.md', 'codex.md'],
|
|
508
|
+
dirs: [],
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
|
|
502
512
|
module.exports = {
|
|
503
513
|
name,
|
|
504
514
|
labels,
|
|
515
|
+
getArtifacts,
|
|
505
516
|
getChats,
|
|
506
517
|
getMessages,
|
|
507
518
|
getUsage,
|
package/editors/copilot.js
CHANGED
|
@@ -240,4 +240,14 @@ async function getUsage() {
|
|
|
240
240
|
|
|
241
241
|
const labels = { 'copilot-cli': 'Copilot CLI' };
|
|
242
242
|
|
|
243
|
-
|
|
243
|
+
function getArtifacts(folder) {
|
|
244
|
+
const { scanArtifacts } = require('./base');
|
|
245
|
+
return scanArtifacts(folder, {
|
|
246
|
+
editor: 'copilot-cli',
|
|
247
|
+
label: 'Copilot',
|
|
248
|
+
files: ['.github/copilot-instructions.md'],
|
|
249
|
+
dirs: [],
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
|
package/editors/cursor.js
CHANGED
|
@@ -415,4 +415,14 @@ async function getUsage() {
|
|
|
415
415
|
|
|
416
416
|
const labels = { 'cursor': 'Cursor' };
|
|
417
417
|
|
|
418
|
-
|
|
418
|
+
function getArtifacts(folder) {
|
|
419
|
+
const { scanArtifacts } = require('./base');
|
|
420
|
+
return scanArtifacts(folder, {
|
|
421
|
+
editor: 'cursor',
|
|
422
|
+
label: 'Cursor',
|
|
423
|
+
files: ['.cursorrules', 'AGENTS.md'],
|
|
424
|
+
dirs: ['.cursor/rules', '.cursor/plans'],
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
|
package/editors/gemini.js
CHANGED
|
@@ -173,4 +173,14 @@ function getMessages(chat) {
|
|
|
173
173
|
|
|
174
174
|
const labels = { 'gemini-cli': 'Gemini CLI' };
|
|
175
175
|
|
|
176
|
-
|
|
176
|
+
function getArtifacts(folder) {
|
|
177
|
+
const { scanArtifacts } = require('./base');
|
|
178
|
+
return scanArtifacts(folder, {
|
|
179
|
+
editor: 'gemini-cli',
|
|
180
|
+
label: 'Gemini CLI',
|
|
181
|
+
files: ['GEMINI.md'],
|
|
182
|
+
dirs: [],
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts };
|
package/editors/goose.js
CHANGED
|
@@ -1,24 +1,36 @@
|
|
|
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
|
const GOOSE_DIR = path.join(os.homedir(), '.local', 'share', 'goose', 'sessions');
|
|
7
6
|
const DB_PATH = path.join(GOOSE_DIR, 'sessions.db');
|
|
8
7
|
const CONFIG_PATH = path.join(os.homedir(), '.config', 'goose', 'config.yaml');
|
|
9
8
|
|
|
10
9
|
// ============================================================
|
|
11
|
-
// Query SQLite via
|
|
10
|
+
// Query SQLite via better-sqlite3 (cross-platform)
|
|
12
11
|
// ============================================================
|
|
13
12
|
|
|
13
|
+
let Database;
|
|
14
|
+
function getDatabase() {
|
|
15
|
+
if (!Database) {
|
|
16
|
+
try {
|
|
17
|
+
Database = require('better-sqlite3');
|
|
18
|
+
} catch {
|
|
19
|
+
// better-sqlite3 not available
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return Database;
|
|
23
|
+
}
|
|
24
|
+
|
|
14
25
|
function queryDb(sql) {
|
|
15
26
|
if (!fs.existsSync(DB_PATH)) return [];
|
|
27
|
+
const Db = getDatabase();
|
|
28
|
+
if (!Db) return []; // Fallback if better-sqlite3 not available
|
|
16
29
|
try {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return JSON.parse(raw);
|
|
30
|
+
const db = new Db(DB_PATH, { readonly: true });
|
|
31
|
+
const rows = db.prepare(sql).all();
|
|
32
|
+
db.close();
|
|
33
|
+
return rows;
|
|
22
34
|
} catch { return []; }
|
|
23
35
|
}
|
|
24
36
|
|
|
@@ -284,4 +296,14 @@ function resetCache() {
|
|
|
284
296
|
|
|
285
297
|
const labels = { 'goose': 'Goose' };
|
|
286
298
|
|
|
287
|
-
|
|
299
|
+
function getArtifacts(folder) {
|
|
300
|
+
const { scanArtifacts } = require('./base');
|
|
301
|
+
return scanArtifacts(folder, {
|
|
302
|
+
editor: 'goose',
|
|
303
|
+
label: 'Goose',
|
|
304
|
+
files: ['.goosehints'],
|
|
305
|
+
dirs: [],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
module.exports = { name, labels, getChats, getMessages, resetCache, getArtifacts };
|
package/editors/index.js
CHANGED
|
@@ -80,4 +80,43 @@ async function getAllUsage() {
|
|
|
80
80
|
return results;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Get all artifacts for a given project folder from all editors.
|
|
85
|
+
* Also scans for general/shared artifact files (plan.md, etc.).
|
|
86
|
+
*/
|
|
87
|
+
function getAllArtifacts(folder) {
|
|
88
|
+
const { scanArtifacts } = require('./base');
|
|
89
|
+
const artifacts = [];
|
|
90
|
+
|
|
91
|
+
// Collect from each editor that implements getArtifacts
|
|
92
|
+
for (const editor of editors) {
|
|
93
|
+
if (typeof editor.getArtifacts !== 'function') continue;
|
|
94
|
+
try {
|
|
95
|
+
artifacts.push(...editor.getArtifacts(folder));
|
|
96
|
+
} catch { /* skip broken adapters */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// General / shared artifact files (not tied to any specific editor)
|
|
100
|
+
if (folder) {
|
|
101
|
+
try {
|
|
102
|
+
artifacts.push(...scanArtifacts(folder, {
|
|
103
|
+
editor: '_general',
|
|
104
|
+
label: 'General',
|
|
105
|
+
files: ['AGENTS.md', '.mcp.json', 'plan.md', 'progress.md', 'TODO.md', 'CONVENTIONS.md', 'ARCHITECTURE.md', 'PLANNING.md'],
|
|
106
|
+
dirs: [],
|
|
107
|
+
}));
|
|
108
|
+
} catch { /* skip */ }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Deduplicate by path — editor-specific entries take priority over general
|
|
112
|
+
const seen = new Map();
|
|
113
|
+
for (const a of artifacts) {
|
|
114
|
+
const existing = seen.get(a.path);
|
|
115
|
+
if (!existing || (existing.editor === '_general' && a.editor !== '_general')) {
|
|
116
|
+
seen.set(a.path, a);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return Array.from(seen.values());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage, getAllArtifacts };
|
package/editors/kiro.js
CHANGED
|
@@ -293,4 +293,14 @@ function getFileMtime(filePath) {
|
|
|
293
293
|
|
|
294
294
|
const labels = { 'kiro': 'Kiro' };
|
|
295
295
|
|
|
296
|
-
|
|
296
|
+
function getArtifacts(folder) {
|
|
297
|
+
const { scanArtifacts } = require('./base');
|
|
298
|
+
return scanArtifacts(folder, {
|
|
299
|
+
editor: 'kiro',
|
|
300
|
+
label: 'Kiro',
|
|
301
|
+
files: ['AGENTS.md'],
|
|
302
|
+
dirs: ['.kiro/specs', '.kiro/steering'],
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
module.exports = { name, labels, getChats, getMessages, getArtifacts };
|
package/editors/opencode.js
CHANGED
|
@@ -3,19 +3,9 @@ const fs = require('fs');
|
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const Database = require('better-sqlite3');
|
|
5
5
|
|
|
6
|
-
// OpenCode stores data in
|
|
7
|
-
// - Windows: %LOCALAPPDATA%\opencode\storage (not Roaming)
|
|
8
|
-
// - macOS/Linux: ~/.local/share/opencode/storage (XDG path)
|
|
6
|
+
// OpenCode stores data in XDG-style paths across all platforms
|
|
9
7
|
function getOpenCodeStoragePath() {
|
|
10
|
-
|
|
11
|
-
switch (process.platform) {
|
|
12
|
-
case 'win32':
|
|
13
|
-
return path.join(home, 'AppData', 'Local', 'opencode', 'storage');
|
|
14
|
-
case 'darwin':
|
|
15
|
-
case 'linux':
|
|
16
|
-
default:
|
|
17
|
-
return path.join(home, '.local', 'share', 'opencode', 'storage');
|
|
18
|
-
}
|
|
8
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'storage');
|
|
19
9
|
}
|
|
20
10
|
|
|
21
11
|
const STORAGE_DIR = getOpenCodeStoragePath();
|
|
@@ -23,17 +13,9 @@ const SESSION_DIR = path.join(STORAGE_DIR, 'session');
|
|
|
23
13
|
const MESSAGE_DIR = path.join(STORAGE_DIR, 'message');
|
|
24
14
|
const PART_DIR = path.join(STORAGE_DIR, 'part');
|
|
25
15
|
|
|
26
|
-
// OpenCode also stores data in a SQLite database
|
|
16
|
+
// OpenCode also stores data in a SQLite database
|
|
27
17
|
function getOpenCodeDbPath() {
|
|
28
|
-
|
|
29
|
-
switch (process.platform) {
|
|
30
|
-
case 'win32':
|
|
31
|
-
return path.join(home, 'AppData', 'Local', 'opencode', 'opencode.db');
|
|
32
|
-
case 'darwin':
|
|
33
|
-
case 'linux':
|
|
34
|
-
default:
|
|
35
|
-
return path.join(home, '.local', 'share', 'opencode', 'opencode.db');
|
|
36
|
-
}
|
|
18
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
37
19
|
}
|
|
38
20
|
|
|
39
21
|
const DB_PATH = getOpenCodeDbPath();
|
package/editors/vscode.js
CHANGED
|
@@ -386,4 +386,14 @@ async function getUsage() {
|
|
|
386
386
|
|
|
387
387
|
const labels = { 'vscode': 'VS Code', 'vscode-insiders': 'VS Code Insiders' };
|
|
388
388
|
|
|
389
|
-
|
|
389
|
+
function getArtifacts(folder) {
|
|
390
|
+
const { scanArtifacts } = require('./base');
|
|
391
|
+
return scanArtifacts(folder, {
|
|
392
|
+
editor: 'vscode',
|
|
393
|
+
label: 'VS Code',
|
|
394
|
+
files: ['.github/copilot-instructions.md'],
|
|
395
|
+
dirs: ['.vscode'],
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
module.exports = { name, labels, getChats, getMessages, getUsage, getArtifacts };
|
package/editors/windsurf.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { execSync } = require('child_process');
|
|
1
|
+
const { execSync, execFileSync } = require('child_process');
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const fs = require('fs');
|
|
@@ -18,18 +18,19 @@ const IS_WINDOWS = process.platform === 'win32';
|
|
|
18
18
|
function getProcessList() {
|
|
19
19
|
try {
|
|
20
20
|
if (IS_WINDOWS) {
|
|
21
|
-
//
|
|
22
|
-
const output =
|
|
21
|
+
// Use PowerShell Get-Process (WMIC is deprecated in Windows 10/11)
|
|
22
|
+
const output = execFileSync('powershell', ['-Command', 'Get-Process | Select-Object Id, Path, CommandLine | ConvertTo-Csv -NoTypeInformation'], {
|
|
23
23
|
encoding: 'utf-8',
|
|
24
24
|
maxBuffer: 10 * 1024 * 1024,
|
|
25
25
|
});
|
|
26
|
-
// Parse CSV: skip header
|
|
26
|
+
// Parse CSV: skip header
|
|
27
27
|
const lines = output.split('\n').slice(1);
|
|
28
28
|
return lines.map(line => {
|
|
29
29
|
const parts = line.split(',');
|
|
30
|
-
if (parts.length <
|
|
31
|
-
const
|
|
32
|
-
const
|
|
30
|
+
if (parts.length < 3) return null;
|
|
31
|
+
const pid = parts[0].trim().replace(/^"|"$/g, '');
|
|
32
|
+
const commandLine = parts[2].trim().replace(/^"|"$/g, '');
|
|
33
|
+
if (!pid || !commandLine) return null;
|
|
33
34
|
return { commandLine, pid };
|
|
34
35
|
}).filter(Boolean);
|
|
35
36
|
} else {
|
|
@@ -49,8 +50,8 @@ function getProcessList() {
|
|
|
49
50
|
function getListeningPorts(pid) {
|
|
50
51
|
try {
|
|
51
52
|
if (IS_WINDOWS) {
|
|
52
|
-
//
|
|
53
|
-
const output =
|
|
53
|
+
// Use PowerShell to get netstat output and filter by PID
|
|
54
|
+
const output = execFileSync('powershell', ['-Command', `netstat -ano | Select-String "${pid}$"`], {
|
|
54
55
|
encoding: 'utf-8',
|
|
55
56
|
maxBuffer: 10 * 1024 * 1024,
|
|
56
57
|
});
|
|
@@ -577,4 +578,14 @@ function resetCache() { _lsCache = null; }
|
|
|
577
578
|
|
|
578
579
|
const labels = { 'windsurf': 'Windsurf', 'windsurf-next': 'Windsurf Next' };
|
|
579
580
|
|
|
580
|
-
|
|
581
|
+
function getArtifacts(folder) {
|
|
582
|
+
const { scanArtifacts } = require('./base');
|
|
583
|
+
return scanArtifacts(folder, {
|
|
584
|
+
editor: 'windsurf',
|
|
585
|
+
label: 'Windsurf',
|
|
586
|
+
files: ['.windsurfrules'],
|
|
587
|
+
dirs: ['.windsurf/workflows', '.windsurf/rules', '.windsurf/plans', '.windsurf/skills'],
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage, getArtifacts };
|
package/editors/zed.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
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
|
const Database = require('better-sqlite3');
|
|
7
6
|
|
|
@@ -24,7 +23,7 @@ function getZedDataPath() {
|
|
|
24
23
|
const THREADS_DB = path.join(getZedDataPath(), 'threads', 'threads.db');
|
|
25
24
|
|
|
26
25
|
// ============================================================
|
|
27
|
-
// Decompress zstd blob via CLI
|
|
26
|
+
// Decompress zstd blob via CLI (with cross-platform support)
|
|
28
27
|
// ============================================================
|
|
29
28
|
|
|
30
29
|
function decompressZstd(buf) {
|
|
@@ -32,7 +31,28 @@ function decompressZstd(buf) {
|
|
|
32
31
|
const tmpOut = tmpIn.replace('.zst', '.json');
|
|
33
32
|
try {
|
|
34
33
|
fs.writeFileSync(tmpIn, buf);
|
|
35
|
-
|
|
34
|
+
|
|
35
|
+
// Try zstd CLI first
|
|
36
|
+
try {
|
|
37
|
+
const { execFileSync } = require('child_process');
|
|
38
|
+
const zstdCmd = process.platform === 'win32' ? 'zstd.exe' : 'zstd';
|
|
39
|
+
execFileSync(zstdCmd, ['-d', '-f', '-q', tmpIn, '-o', tmpOut], { stdio: ['pipe', 'pipe', 'pipe'] });
|
|
40
|
+
} catch {
|
|
41
|
+
// Fallback: try using Node.js zstd library if available
|
|
42
|
+
try {
|
|
43
|
+
const zlib = require('zlib');
|
|
44
|
+
// Check if Node version supports zstd natively (v22+)
|
|
45
|
+
if (zlib.createZstdDecompress) {
|
|
46
|
+
const decompressed = zlib.zstdDecompressSync(buf);
|
|
47
|
+
fs.writeFileSync(tmpOut, decompressed);
|
|
48
|
+
} else {
|
|
49
|
+
throw new Error('zstd not available');
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
throw new Error('zstd decompression not available on this system');
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
36
56
|
const data = fs.readFileSync(tmpOut, 'utf-8');
|
|
37
57
|
return data;
|
|
38
58
|
} finally {
|
package/index.js
CHANGED
|
@@ -244,7 +244,7 @@ for (const [src, label, count] of displayList) {
|
|
|
244
244
|
}
|
|
245
245
|
console.log('');
|
|
246
246
|
|
|
247
|
-
// ── Analyze sessions with robot animation
|
|
247
|
+
// ── Analyze sessions with robot animation (async to allow Ctrl+C) ──
|
|
248
248
|
const logUpdate = require('log-update');
|
|
249
249
|
const BOT_STYLES = [
|
|
250
250
|
{ l: '(', r: ')', color: '#818cf8' },
|
|
@@ -252,44 +252,46 @@ const BOT_STYLES = [
|
|
|
252
252
|
{ l: '{', r: '}', color: '#34d399' },
|
|
253
253
|
{ l: '<', r: '>', color: '#fbbf24' },
|
|
254
254
|
];
|
|
255
|
-
let tick = 0;
|
|
256
|
-
|
|
257
|
-
const startTime = Date.now();
|
|
258
|
-
const result = cache.scanAll((p) => {
|
|
259
|
-
tick++;
|
|
260
|
-
if (tick % 5 !== 0) return;
|
|
261
|
-
const frame = Math.floor(tick / 40);
|
|
262
|
-
const b = BOT_STYLES[frame % 4];
|
|
263
|
-
const dots = '.'.repeat((Math.floor(tick / 10) % 3) + 1).padEnd(3);
|
|
264
|
-
logUpdate(` ${chalk.hex(b.color)(`${b.l}● ●${b.r}`)} ${chalk.dim(`Analyzing${dots} ${p.scanned}/${p.total}`)}`);
|
|
265
|
-
}, { chats: allChats });
|
|
266
|
-
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
267
|
-
const allFaces = BOT_STYLES.map(b => chalk.hex(b.color)(`${b.l}● ●${b.r}`)).join(' ');
|
|
268
|
-
logUpdate(` ${allFaces} ${chalk.green(`✓ ${result.analyzed} analyzed, ${result.skipped} cached (${elapsed}s)`)}`);
|
|
269
|
-
logUpdate.done();
|
|
270
|
-
console.log('');
|
|
271
255
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
256
|
+
(async () => {
|
|
257
|
+
let tick = 0;
|
|
258
|
+
const startTime = Date.now();
|
|
259
|
+
const result = await cache.scanAllAsync((p) => {
|
|
260
|
+
tick++;
|
|
261
|
+
if (tick % 5 !== 0) return;
|
|
262
|
+
const frame = Math.floor(tick / 40);
|
|
263
|
+
const b = BOT_STYLES[frame % 4];
|
|
264
|
+
const dots = '.'.repeat((Math.floor(tick / 10) % 3) + 1).padEnd(3);
|
|
265
|
+
logUpdate(` ${chalk.hex(b.color)(`${b.l}● ●${b.r}`)} ${chalk.dim(`Analyzing${dots} ${p.scanned}/${p.total}`)}`);
|
|
266
|
+
}, { chats: allChats });
|
|
267
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
268
|
+
const allFaces = BOT_STYLES.map(b => chalk.hex(b.color)(`${b.l}● ●${b.r}`)).join(' ');
|
|
269
|
+
logUpdate(` ${allFaces} ${chalk.green(`✓ ${result.analyzed} analyzed, ${result.skipped} cached (${elapsed}s)`)}`);
|
|
270
|
+
logUpdate.done();
|
|
276
271
|
console.log('');
|
|
277
|
-
process.exit(0);
|
|
278
|
-
}
|
|
279
272
|
|
|
280
|
-
//
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
console.log(chalk.dim(` npx agentlytics --relay Start a relay server`));
|
|
288
|
-
console.log(chalk.dim(` npx agentlytics --join <host:port> --username Join a relay server`));
|
|
289
|
-
console.log('');
|
|
290
|
-
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
273
|
+
// In collect-only mode, exit after cache is built
|
|
274
|
+
if (collectOnly) {
|
|
275
|
+
const cacheDbPath = path.join(os.homedir(), '.agentlytics', 'cache.db');
|
|
276
|
+
console.log(chalk.dim(` Cache file: ${cacheDbPath}`));
|
|
277
|
+
console.log('');
|
|
278
|
+
process.exit(0);
|
|
279
|
+
}
|
|
291
280
|
|
|
292
|
-
//
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
}
|
|
281
|
+
// Start server
|
|
282
|
+
const app = require('./server');
|
|
283
|
+
app.listen(PORT, () => {
|
|
284
|
+
const url = `http://localhost:${PORT}`;
|
|
285
|
+
console.log(chalk.green(` ✓ Dashboard ready at ${chalk.bold.white(url)}`));
|
|
286
|
+
console.log('');
|
|
287
|
+
console.log(chalk.dim(' 💡 Share sessions with your team:'));
|
|
288
|
+
console.log(chalk.dim(` npx agentlytics --relay Start a relay server`));
|
|
289
|
+
console.log(chalk.dim(` npx agentlytics --join <host:port> --username Join a relay server`));
|
|
290
|
+
console.log('');
|
|
291
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
292
|
+
|
|
293
|
+
// Auto-open browser
|
|
294
|
+
const open = require('open');
|
|
295
|
+
open(url).catch(() => {});
|
|
296
|
+
});
|
|
297
|
+
})();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlytics",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.20",
|
|
4
4
|
"description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode, Command Code",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/server.js
CHANGED
|
@@ -338,8 +338,15 @@ app.get('/api/check-ai', async (req, res) => {
|
|
|
338
338
|
if (!folder) return res.status(400).json({ error: 'folder query param required' });
|
|
339
339
|
try {
|
|
340
340
|
const { execFile } = require('child_process');
|
|
341
|
+
const isWindows = process.platform === 'win32';
|
|
342
|
+
// On Windows, use npx.cmd with shell; on Unix, use npx directly
|
|
343
|
+
const cmd = isWindows ? 'npx.cmd' : 'npx';
|
|
341
344
|
const result = await new Promise((resolve, reject) => {
|
|
342
|
-
execFile(
|
|
345
|
+
execFile(cmd, ['-y', 'check-ai', '--json', folder], {
|
|
346
|
+
timeout: 60000,
|
|
347
|
+
maxBuffer: 1024 * 1024,
|
|
348
|
+
shell: isWindows
|
|
349
|
+
}, (err, stdout) => {
|
|
343
350
|
try {
|
|
344
351
|
const json = JSON.parse(stdout);
|
|
345
352
|
resolve(json);
|
|
@@ -354,6 +361,79 @@ app.get('/api/check-ai', async (req, res) => {
|
|
|
354
361
|
}
|
|
355
362
|
});
|
|
356
363
|
|
|
364
|
+
// ============================================================
|
|
365
|
+
// Artifacts — delegates to editors/index.js getAllArtifacts
|
|
366
|
+
// ============================================================
|
|
367
|
+
|
|
368
|
+
app.get('/api/artifacts', (req, res) => {
|
|
369
|
+
try {
|
|
370
|
+
const { getAllArtifacts } = require('./editors');
|
|
371
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
372
|
+
const result = [];
|
|
373
|
+
|
|
374
|
+
for (const project of projects) {
|
|
375
|
+
const folder = project.folder;
|
|
376
|
+
if (!folder) continue;
|
|
377
|
+
|
|
378
|
+
const artifacts = getAllArtifacts(folder);
|
|
379
|
+
if (artifacts.length === 0) continue;
|
|
380
|
+
|
|
381
|
+
// Group by editor
|
|
382
|
+
const byEditor = {};
|
|
383
|
+
for (const a of artifacts) {
|
|
384
|
+
if (!byEditor[a.editor]) byEditor[a.editor] = { editor: a.editor, label: a.editorLabel, files: [] };
|
|
385
|
+
byEditor[a.editor].files.push(a);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
result.push({
|
|
389
|
+
folder,
|
|
390
|
+
name: project.name || path.basename(folder),
|
|
391
|
+
totalArtifacts: artifacts.length,
|
|
392
|
+
editors: Object.values(byEditor),
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Sort by total artifacts descending
|
|
397
|
+
result.sort((a, b) => b.totalArtifacts - a.totalArtifacts);
|
|
398
|
+
res.json(result);
|
|
399
|
+
} catch (err) {
|
|
400
|
+
res.status(500).json({ error: err.message });
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
app.get('/api/artifact-content', (req, res) => {
|
|
405
|
+
try {
|
|
406
|
+
const filePath = req.query.path;
|
|
407
|
+
if (!filePath) return res.status(400).json({ error: 'path query param required' });
|
|
408
|
+
|
|
409
|
+
// Security: validate file exists in known artifact results for at least one project
|
|
410
|
+
const { getAllArtifacts } = require('./editors');
|
|
411
|
+
const projects = cache.getCachedProjects({ hiddenFolders: getHiddenFolders() });
|
|
412
|
+
let allowed = false;
|
|
413
|
+
for (const project of projects) {
|
|
414
|
+
if (!project.folder) continue;
|
|
415
|
+
const artifacts = getAllArtifacts(project.folder);
|
|
416
|
+
if (artifacts.some(a => a.path === filePath)) { allowed = true; break; }
|
|
417
|
+
}
|
|
418
|
+
// Also check global/editor-level artifacts not tied to any project (e.g. brain files)
|
|
419
|
+
if (!allowed) {
|
|
420
|
+
try {
|
|
421
|
+
const artifacts = getAllArtifacts(null);
|
|
422
|
+
if (artifacts.some(a => a.path === filePath)) allowed = true;
|
|
423
|
+
} catch { /* skip */ }
|
|
424
|
+
}
|
|
425
|
+
if (!allowed) return res.status(403).json({ error: 'Not an artifact file' });
|
|
426
|
+
|
|
427
|
+
if (!fs.existsSync(filePath)) return res.status(404).json({ error: 'File not found' });
|
|
428
|
+
|
|
429
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
430
|
+
const stat = fs.statSync(filePath);
|
|
431
|
+
res.json({ path: filePath, name: path.basename(filePath), content, size: stat.size, modifiedAt: stat.mtime.getTime() });
|
|
432
|
+
} catch (err) {
|
|
433
|
+
res.status(500).json({ error: err.message });
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
|
|
357
437
|
app.get('/api/all-projects', (req, res) => {
|
|
358
438
|
try {
|
|
359
439
|
res.json(cache.getCachedProjects({ ...parseDateOpts(req.query), includeHidden: true }));
|