agentlytics 0.1.18 → 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/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
- module.exports = { name, labels, getChats, getMessages, getUsage };
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,
@@ -240,4 +240,14 @@ async function getUsage() {
240
240
 
241
241
  const labels = { 'copilot-cli': 'Copilot CLI' };
242
242
 
243
- module.exports = { name, labels, getChats, getMessages, getUsage };
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
- module.exports = { name, labels, getChats, getMessages, getUsage };
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
- module.exports = { name, labels, getChats, getMessages };
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 CLI
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 raw = execSync(
18
- `sqlite3 -json ${JSON.stringify(DB_PATH)} ${JSON.stringify(sql)}`,
19
- { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024, stdio: ['pipe', 'pipe', 'pipe'] }
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
- module.exports = { name, labels, getChats, getMessages, resetCache };
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
- module.exports = { getAllChats, getMessages, editors, editorLabels, resetCaches, getAllUsage };
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
- module.exports = { name, labels, getChats, getMessages };
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 };
@@ -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 different locations depending on the platform
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
- const home = os.homedir();
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 (older/primary store)
16
+ // OpenCode also stores data in a SQLite database
27
17
  function getOpenCodeDbPath() {
28
- const home = os.homedir();
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
- module.exports = { name, labels, getChats, getMessages, getUsage };
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 };
@@ -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
- // wmic provides CSV-formatted process data
22
- const output = execSync('wmic process get CommandLine,ProcessId /format:csv', {
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, split by comma
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 < 2) return null;
31
- const commandLine = parts.slice(0, -1).join(',').trim().replace(/^"|"$/g, '');
32
- const pid = parts[parts.length - 1].trim();
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
- // netstat -ano shows PID in the last column
53
- const output = execSync(`netstat -ano | findstr ${pid}`, {
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
- module.exports = { name, sources, labels, getChats, getMessages, resetCache, getUsage };
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
- execSync(`zstd -d -f -q ${JSON.stringify(tmpIn)} -o ${JSON.stringify(tmpOut)}`, { stdio: ['pipe', 'pipe', 'pipe'] });
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
- // In collect-only mode, exit after cache is built
273
- if (collectOnly) {
274
- const cacheDbPath = path.join(os.homedir(), '.agentlytics', 'cache.db');
275
- console.log(chalk.dim(` Cache file: ${cacheDbPath}`));
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
- // Start server
281
- const app = require('./server');
282
- app.listen(PORT, () => {
283
- const url = `http://localhost:${PORT}`;
284
- console.log(chalk.green(` ✓ Dashboard ready at ${chalk.bold.white(url)}`));
285
- console.log('');
286
- console.log(chalk.dim(' 💡 Share sessions with your team:'));
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
- // Auto-open browser
293
- const open = require('open');
294
- open(url).catch(() => {});
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.18",
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": {