agentlytics 0.0.8 → 0.0.10

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.
@@ -0,0 +1,159 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const os = require('os');
4
+
5
+ const COMMANDCODE_DIR = path.join(os.homedir(), '.commandcode');
6
+ const PROJECTS_DIR = path.join(COMMANDCODE_DIR, 'projects');
7
+
8
+ // ============================================================
9
+ // Adapter interface
10
+ // ============================================================
11
+
12
+ const name = 'commandcode';
13
+
14
+ function getChats() {
15
+ const chats = [];
16
+ if (!fs.existsSync(PROJECTS_DIR)) return chats;
17
+
18
+ let projDirs;
19
+ try { projDirs = fs.readdirSync(PROJECTS_DIR); } catch { return chats; }
20
+
21
+ for (const projDir of projDirs) {
22
+ const dir = path.join(PROJECTS_DIR, projDir);
23
+ try { if (!fs.statSync(dir).isDirectory()) continue; } catch { continue; }
24
+
25
+ // Decode folder path from dir name (e.g. users-fka-code-foo -> /users/fka/code/foo)
26
+ const decodedFolder = '/' + projDir.replace(/-/g, '/');
27
+
28
+ let files;
29
+ try { files = fs.readdirSync(dir).filter(f => f.endsWith('.jsonl') && !f.includes('.checkpoints.')); } catch { continue; }
30
+
31
+ for (const file of files) {
32
+ const sessionId = file.replace('.jsonl', '');
33
+ const fullPath = path.join(dir, file);
34
+ const metaPath = path.join(dir, `${sessionId}.meta.json`);
35
+
36
+ // Read meta.json for title
37
+ let title = null;
38
+ try {
39
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
40
+ title = meta.title || null;
41
+ } catch { /* no meta */ }
42
+
43
+ // Parse first and last lines for timestamps
44
+ try {
45
+ const stat = fs.statSync(fullPath);
46
+ const lines = fs.readFileSync(fullPath, 'utf-8').split('\n').filter(Boolean);
47
+ if (lines.length === 0) continue;
48
+
49
+ const first = JSON.parse(lines[0]);
50
+ const last = JSON.parse(lines[lines.length - 1]);
51
+
52
+ const firstPrompt = extractFirstPrompt(first);
53
+ const bubbleCount = lines.length;
54
+
55
+ chats.push({
56
+ source: 'commandcode',
57
+ composerId: sessionId,
58
+ name: title || cleanPrompt(firstPrompt),
59
+ createdAt: first.timestamp ? new Date(first.timestamp).getTime() : stat.birthtime.getTime(),
60
+ lastUpdatedAt: last.timestamp ? new Date(last.timestamp).getTime() : stat.mtime.getTime(),
61
+ mode: 'commandcode',
62
+ folder: decodedFolder,
63
+ encrypted: false,
64
+ bubbleCount,
65
+ _fullPath: fullPath,
66
+ _gitBranch: first.gitBranch || null,
67
+ });
68
+ } catch { /* skip */ }
69
+ }
70
+ }
71
+
72
+ return chats;
73
+ }
74
+
75
+ function extractFirstPrompt(obj) {
76
+ if (obj.role !== 'user') return null;
77
+ if (!obj.content) return null;
78
+ if (typeof obj.content === 'string') return obj.content;
79
+ if (Array.isArray(obj.content)) {
80
+ return obj.content
81
+ .filter(c => c.type === 'text')
82
+ .map(c => c.text)
83
+ .join(' ')
84
+ .substring(0, 200);
85
+ }
86
+ return null;
87
+ }
88
+
89
+ function cleanPrompt(text) {
90
+ if (!text) return null;
91
+ return text.replace(/\s+/g, ' ').trim().substring(0, 120) || null;
92
+ }
93
+
94
+ function getMessages(chat) {
95
+ const filePath = chat._fullPath;
96
+ if (!filePath || !fs.existsSync(filePath)) return [];
97
+
98
+ const messages = [];
99
+ const lines = fs.readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
100
+
101
+ for (const line of lines) {
102
+ let obj;
103
+ try { obj = JSON.parse(line); } catch { continue; }
104
+
105
+ if (obj.role === 'user') {
106
+ const content = extractContent(obj.content);
107
+ if (content) messages.push({ role: 'user', content });
108
+ } else if (obj.role === 'assistant') {
109
+ const { text, toolCalls } = extractAssistantContent(obj.content);
110
+ if (text) {
111
+ messages.push({
112
+ role: 'assistant',
113
+ content: text,
114
+ _toolCalls: toolCalls.length > 0 ? toolCalls : undefined,
115
+ });
116
+ }
117
+ } else if (obj.role === 'system') {
118
+ const text = extractContent(obj.content);
119
+ if (text) messages.push({ role: 'system', content: text });
120
+ }
121
+ }
122
+
123
+ return messages;
124
+ }
125
+
126
+ function extractContent(content) {
127
+ if (typeof content === 'string') return content;
128
+ if (!Array.isArray(content)) return '';
129
+ // Skip tool_result blocks for user messages (they are tool outputs)
130
+ return content
131
+ .filter(c => c.type === 'text')
132
+ .map(c => c.text)
133
+ .join('\n') || '';
134
+ }
135
+
136
+ function extractAssistantContent(content) {
137
+ if (typeof content === 'string') return { text: content, toolCalls: [] };
138
+ if (!Array.isArray(content)) return { text: '', toolCalls: [] };
139
+ const parts = [];
140
+ const toolCalls = [];
141
+ for (const block of content) {
142
+ if (block.type === 'thinking' && block.thinking) {
143
+ parts.push(`[thinking] ${block.thinking}`);
144
+ } else if (block.type === 'text' && block.text) {
145
+ parts.push(block.text);
146
+ } else if (block.type === 'tool_use') {
147
+ const args = block.input || {};
148
+ const argKeys = Object.keys(args).join(', ');
149
+ parts.push(`[tool-call: ${block.name || 'unknown'}(${argKeys})]`);
150
+ toolCalls.push({ name: block.name || 'unknown', args });
151
+ } else if (block.type === 'tool_result') {
152
+ const text = typeof block.content === 'string' ? block.content : '';
153
+ parts.push(`[tool-result: ${block.name || 'tool'}] ${text.substring(0, 500)}`);
154
+ }
155
+ }
156
+ return { text: parts.join('\n') || '', toolCalls };
157
+ }
158
+
159
+ module.exports = { name, getChats, getMessages };
package/editors/index.js CHANGED
@@ -4,11 +4,13 @@ const claude = require('./claude');
4
4
  const vscode = require('./vscode');
5
5
  const zed = require('./zed');
6
6
  const opencode = require('./opencode');
7
+ const codex = require('./codex');
7
8
  const gemini = require('./gemini');
8
9
  const copilot = require('./copilot');
9
10
  const cursorAgent = require('./cursor-agent');
11
+ const commandcode = require('./commandcode');
10
12
 
11
- const editors = [cursor, windsurf, claude, vscode, zed, opencode, gemini, copilot, cursorAgent];
13
+ const editors = [cursor, windsurf, claude, vscode, zed, opencode, codex, gemini, copilot, cursorAgent, commandcode];
12
14
 
13
15
  /**
14
16
  * 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, Cursor Agent'));
97
+ console.log(chalk.dim(' Scanning editors: Cursor, Windsurf, Claude Code, VS Code, Zed, Antigravity, OpenCode, Codex, 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,7 +1,7 @@
1
1
  {
2
2
  "name": "agentlytics",
3
- "version": "0.0.8",
4
- "description": "Comprehensive analytics dashboard for AI coding agents — Cursor, Windsurf, Claude Code, VS Code Copilot, Zed, Antigravity, OpenCode",
3
+ "version": "0.0.10",
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": {
7
7
  "agentlytics": "./index.js"
@@ -34,6 +34,7 @@
34
34
  "zed",
35
35
  "antigravity",
36
36
  "opencode",
37
+ "codex",
37
38
  "analytics",
38
39
  "ai",
39
40
  "agent"
package/server.js CHANGED
@@ -11,9 +11,17 @@ app.use(express.static(path.join(__dirname, 'public')));
11
11
  // API endpoints — all reads from SQLite cache
12
12
  // ============================================================
13
13
 
14
+ // Helper: parse date query params into Unix ms timestamps
15
+ function parseDateOpts(query) {
16
+ const opts = {};
17
+ if (query.dateFrom) opts.dateFrom = parseInt(query.dateFrom) || null;
18
+ if (query.dateTo) opts.dateTo = parseInt(query.dateTo) || null;
19
+ return opts;
20
+ }
21
+
14
22
  app.get('/api/overview', (req, res) => {
15
23
  try {
16
- const opts = { editor: req.query.editor || null };
24
+ const opts = { editor: req.query.editor || null, ...parseDateOpts(req.query) };
17
25
  res.json(cache.getCachedOverview(opts));
18
26
  } catch (err) {
19
27
  res.status(500).json({ error: err.message });
@@ -22,7 +30,7 @@ app.get('/api/overview', (req, res) => {
22
30
 
23
31
  app.get('/api/daily-activity', (req, res) => {
24
32
  try {
25
- const opts = { editor: req.query.editor || null };
33
+ const opts = { editor: req.query.editor || null, ...parseDateOpts(req.query) };
26
34
  res.json(cache.getCachedDailyActivity(opts));
27
35
  } catch (err) {
28
36
  res.status(500).json({ error: err.message });
@@ -37,6 +45,7 @@ app.get('/api/chats', (req, res) => {
37
45
  named: req.query.named !== 'false',
38
46
  limit: req.query.limit ? parseInt(req.query.limit) : 200,
39
47
  offset: req.query.offset ? parseInt(req.query.offset) : 0,
48
+ ...parseDateOpts(req.query),
40
49
  };
41
50
  const total = cache.countCachedChats(opts);
42
51
  const rows = cache.getCachedChats(opts);
@@ -52,6 +61,7 @@ app.get('/api/chats', (req, res) => {
52
61
  lastUpdatedAt: c.last_updated_at,
53
62
  encrypted: !!c.encrypted,
54
63
  bubbleCount: c.bubble_count,
64
+ topModel: c.top_model || null,
55
65
  })),
56
66
  });
57
67
  } catch (err) {
@@ -116,7 +126,7 @@ app.get('/api/chats/:id/markdown', (req, res) => {
116
126
 
117
127
  app.get('/api/projects', (req, res) => {
118
128
  try {
119
- res.json(cache.getCachedProjects());
129
+ res.json(cache.getCachedProjects(parseDateOpts(req.query)));
120
130
  } catch (err) {
121
131
  res.status(500).json({ error: err.message });
122
132
  }
@@ -128,6 +138,7 @@ app.get('/api/deep-analytics', (req, res) => {
128
138
  editor: req.query.editor || null,
129
139
  folder: req.query.folder || null,
130
140
  limit: Math.min(parseInt(req.query.limit) || 500, 5000),
141
+ ...parseDateOpts(req.query),
131
142
  };
132
143
  res.json(cache.getCachedDeepAnalytics(opts));
133
144
  } catch (err) {
@@ -137,7 +148,7 @@ app.get('/api/deep-analytics', (req, res) => {
137
148
 
138
149
  app.get('/api/dashboard-stats', (req, res) => {
139
150
  try {
140
- const opts = { editor: req.query.editor || null };
151
+ const opts = { editor: req.query.editor || null, ...parseDateOpts(req.query) };
141
152
  res.json(cache.getCachedDashboardStats(opts));
142
153
  } catch (err) {
143
154
  res.status(500).json({ error: err.message });
package/ui/src/App.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import { useState, useEffect } from 'react'
1
+ import { useState, useEffect, useRef, useCallback } from 'react'
2
2
  import { Routes, Route, NavLink } from 'react-router-dom'
3
3
  import { Activity, BarChart3, GitCompare, MessageSquare, FolderOpen, Sun, Moon, RefreshCw, AlertTriangle, Github, Terminal, Database } from 'lucide-react'
4
4
  import { fetchOverview, refetchAgents } from './lib/api'
@@ -9,17 +9,37 @@ import DeepAnalysis from './pages/DeepAnalysis'
9
9
  import Compare from './pages/Compare'
10
10
  import ChatDetail from './pages/ChatDetail'
11
11
  import Projects from './pages/Projects'
12
+ import ProjectDetail from './pages/ProjectDetail'
12
13
  import SqlViewer from './pages/SqlViewer'
13
14
 
14
15
  export default function App() {
15
16
  const [overview, setOverview] = useState(null)
16
17
  const [refetchState, setRefetchState] = useState(null) // null | { scanned, total }
18
+ const [live, setLive] = useState(false)
19
+ const liveRef = useRef(null)
17
20
  const { dark, toggle } = useTheme()
18
21
 
19
- useEffect(() => {
22
+ const refreshOverview = useCallback(() => {
20
23
  fetchOverview().then(setOverview)
21
24
  }, [])
22
25
 
26
+ useEffect(() => {
27
+ refreshOverview()
28
+ }, [])
29
+
30
+ // Live mode: refetch overview every 60s
31
+ useEffect(() => {
32
+ if (live) {
33
+ liveRef.current = setInterval(() => {
34
+ refreshOverview()
35
+ }, 60000)
36
+ } else {
37
+ if (liveRef.current) clearInterval(liveRef.current)
38
+ liveRef.current = null
39
+ }
40
+ return () => { if (liveRef.current) clearInterval(liveRef.current) }
41
+ }, [live, refreshOverview])
42
+
23
43
  const handleRefetch = async () => {
24
44
  setRefetchState({ scanned: 0, total: 0 })
25
45
  try {
@@ -61,6 +81,22 @@ export default function App() {
61
81
  ))}
62
82
  </nav>
63
83
  <div className="ml-auto flex items-center gap-3">
84
+ <button
85
+ onClick={() => setLive(!live)}
86
+ className="flex items-center gap-1.5 px-2 py-0.5 text-[10px] transition"
87
+ style={{
88
+ color: live ? '#22c55e' : 'var(--c-text3)',
89
+ border: live ? '1px solid rgba(34,197,94,0.3)' : '1px solid var(--c-border)',
90
+ background: live ? 'rgba(34,197,94,0.08)' : 'transparent',
91
+ }}
92
+ title={live ? 'Disable live refresh' : 'Enable live refresh (every 60s)'}
93
+ >
94
+ <span
95
+ className={`inline-block w-1.5 h-1.5 rounded-full ${live ? 'pulse-dot' : ''}`}
96
+ style={{ background: live ? '#22c55e' : 'var(--c-text3)' }}
97
+ />
98
+ Live
99
+ </button>
64
100
  <button
65
101
  onClick={handleRefetch}
66
102
  disabled={!!refetchState}
@@ -98,8 +134,9 @@ export default function App() {
98
134
  <Routes>
99
135
  <Route path="/" element={<Dashboard overview={overview} />} />
100
136
  <Route path="/projects" element={<Projects overview={overview} />} />
137
+ <Route path="/projects/detail" element={<ProjectDetail />} />
101
138
  <Route path="/sessions" element={<Sessions overview={overview} />} />
102
- <Route path="/sessions/:id" element={<ChatDetail />} />
139
+ {/* ChatDetail is now a sidebar in Sessions */}
103
140
  <Route path="/analysis" element={<DeepAnalysis overview={overview} />} />
104
141
  <Route path="/compare" element={<Compare overview={overview} />} />
105
142
  <Route path="/sql" element={<SqlViewer />} />
@@ -2,6 +2,7 @@ import { useState, useMemo, useRef } from 'react'
2
2
  import { Chart as ChartJS, CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend } from 'chart.js'
3
3
  import { Line } from 'react-chartjs-2'
4
4
  import { editorColor, editorLabel } from '../lib/constants'
5
+ import { useTheme } from '../lib/theme'
5
6
 
6
7
  ChartJS.register(CategoryScale, LinearScale, PointElement, LineElement, Tooltip, Legend)
7
8
 
@@ -24,6 +25,7 @@ const INTENSITY_COLORS_DARK = ['rgba(255,255,255,0.03)', '#0e4429', '#006d32', '
24
25
  const INTENSITY_COLORS_LIGHT = ['rgba(0,0,0,0.04)', '#9be9a8', '#40c463', '#30a14e', '#216e39']
25
26
 
26
27
  export default function ActivityHeatmap({ dailyData }) {
28
+ const { dark } = useTheme()
27
29
  const [selectedDay, setSelectedDay] = useState(null)
28
30
  const containerRef = useRef(null)
29
31
 
@@ -96,8 +98,10 @@ export default function ActivityHeatmap({ dailyData }) {
96
98
 
97
99
  if (!grid.weeks.length) return null
98
100
 
99
- const isDark = !document.documentElement.classList.contains('light')
100
- const COLORS = isDark ? INTENSITY_COLORS_DARK : INTENSITY_COLORS_LIGHT
101
+ const COLORS = dark ? INTENSITY_COLORS_DARK : INTENSITY_COLORS_LIGHT
102
+ const txtDim = dark ? '#555' : '#999'
103
+ const gridColor = dark ? 'rgba(255,255,255,0.03)' : 'rgba(0,0,0,0.06)'
104
+ const legendColor = dark ? '#888' : '#555'
101
105
  const svgWidth = WEEK_COLS * (CELL_SIZE + CELL_GAP) + 28
102
106
  const svgHeight = 7 * (CELL_SIZE + CELL_GAP) + 20
103
107
 
@@ -172,19 +176,19 @@ export default function ActivityHeatmap({ dailyData }) {
172
176
  interaction: { mode: 'index', intersect: false },
173
177
  scales: {
174
178
  x: {
175
- grid: { color: 'rgba(255,255,255,0.03)' },
176
- ticks: { color: '#555', font: { size: 9, family: 'JetBrains Mono, monospace' }, maxRotation: 0 },
179
+ grid: { color: gridColor },
180
+ ticks: { color: txtDim, font: { size: 9, family: 'JetBrains Mono, monospace' }, maxRotation: 0 },
177
181
  },
178
182
  y: {
179
183
  beginAtZero: true,
180
- grid: { color: 'rgba(255,255,255,0.03)' },
181
- ticks: { color: '#555', stepSize: 1, font: { size: 9, family: 'JetBrains Mono, monospace' } },
184
+ grid: { color: gridColor },
185
+ ticks: { color: txtDim, stepSize: 1, font: { size: 9, family: 'JetBrains Mono, monospace' } },
182
186
  },
183
187
  },
184
188
  plugins: {
185
189
  legend: {
186
190
  position: 'top',
187
- labels: { color: '#888', font: { size: 9, family: 'JetBrains Mono, monospace' }, usePointStyle: true, pointStyle: 'circle', padding: 8 },
191
+ labels: { color: legendColor, font: { size: 9, family: 'JetBrains Mono, monospace' }, usePointStyle: true, pointStyle: 'circle', padding: 8 },
188
192
  },
189
193
  tooltip: {
190
194
  bodyFont: { family: 'JetBrains Mono, monospace', size: 10 },