agentboss 0.1.0 → 0.1.1
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/bin/aboss.js +288 -288
- package/client/dist/assets/{index-DBj1Ujlx.js → index-CsVml4AS.js} +49 -49
- package/client/dist/index.html +1 -1
- package/package.json +1 -1
- package/server/analysis/job.js +54 -8
- package/server/analysis/report-builder.js +574 -581
- package/server/analysis/scoring-v2.js +122 -72
- package/server/analysis/thresholds-v2.js +364 -358
- package/server/llm/analysis-prompt.js +173 -162
- package/server/llm/cli-runner.js +18 -2
- package/server/llm/judge.js +6 -1
- package/server/llm/session-analyzer.js +10 -1
package/bin/aboss.js
CHANGED
|
@@ -1,288 +1,288 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Agent Boss CLI entry point.
|
|
4
|
-
* Usage: aboss [--port PORT] [--no-open]
|
|
5
|
-
*
|
|
6
|
-
* Startup sequence (design doc §12.3):
|
|
7
|
-
* 1. Parse CLI args
|
|
8
|
-
* 2. Print banner
|
|
9
|
-
* 3. Initialise database
|
|
10
|
-
* 4. Detect data sources
|
|
11
|
-
* 5. Run ETL sync (incremental)
|
|
12
|
-
* 6. Calculate active times
|
|
13
|
-
* 7. Start analysis job (background)
|
|
14
|
-
* 8. Start Express server (with port auto-increment)
|
|
15
|
-
* 9. Open browser (unless --no-open)
|
|
16
|
-
* 10. Handle graceful shutdown
|
|
17
|
-
*
|
|
18
|
-
* @author Felix
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
'use strict';
|
|
22
|
-
|
|
23
|
-
const { getDb, closeDb } = require('../server/db/connection');
|
|
24
|
-
const { detectSources } = require('../server/etl/detect');
|
|
25
|
-
const { backfillSubagents } = require('../server/etl/backfill-subagents');
|
|
26
|
-
const { collectOpenCode } = require('../server/etl/opencode');
|
|
27
|
-
const { collectClaudeCode } = require('../server/etl/claude-code');
|
|
28
|
-
const { calculateActiveTime } = require('../server/etl/active-time');
|
|
29
|
-
const { runAnalysisJob } = require('../server/analysis/job');
|
|
30
|
-
const { startServer } = require('../server/index');
|
|
31
|
-
|
|
32
|
-
// ---------------------------------------------------------------------------
|
|
33
|
-
// CLI arg parsing
|
|
34
|
-
// ---------------------------------------------------------------------------
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* Parse process.argv into { port, noOpen }.
|
|
38
|
-
* @returns {{ port: number, noOpen: boolean }}
|
|
39
|
-
*/
|
|
40
|
-
function parseArgs() {
|
|
41
|
-
const args = process.argv.slice(2);
|
|
42
|
-
let port = 3141;
|
|
43
|
-
let noOpen = false;
|
|
44
|
-
|
|
45
|
-
for (let i = 0; i < args.length; i++) {
|
|
46
|
-
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
47
|
-
const parsed = Number(args[i + 1]);
|
|
48
|
-
if (!Number.isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
49
|
-
port = parsed;
|
|
50
|
-
}
|
|
51
|
-
i++; // skip next arg (the port value)
|
|
52
|
-
} else if (args[i] === '--no-open') {
|
|
53
|
-
noOpen = true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
return { port, noOpen };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// ---------------------------------------------------------------------------
|
|
61
|
-
// Banner
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
|
|
64
|
-
function printBanner() {
|
|
65
|
-
console.log('');
|
|
66
|
-
console.log(' ___ _ ____');
|
|
67
|
-
console.log(' / _ \\ __ _ ___ _ __ | |_ | __ ) ___ ___ ___');
|
|
68
|
-
console.log("| |_| |/ _` |/ _ \\ '_ \\| __| | _ \\ / _ \\/ __/ __|");
|
|
69
|
-
console.log('| ___ | (_| | __/ | | | |_ | |_) | (_) \\__ \\__ \\');
|
|
70
|
-
console.log('|_| |_|\\__, |\\___|_| |_|\\__| |____/ \\___/|___/___/');
|
|
71
|
-
console.log(' |___/');
|
|
72
|
-
console.log('');
|
|
73
|
-
console.log(' Agent Boss v0.1.0');
|
|
74
|
-
console.log(' Be your AI agent\'s boss, not its babysitter.');
|
|
75
|
-
console.log('');
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// ---------------------------------------------------------------------------
|
|
79
|
-
// Port auto-increment helper
|
|
80
|
-
// ---------------------------------------------------------------------------
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Try starting the server on `startPort`, auto-incrementing on EADDRINUSE.
|
|
84
|
-
* @param {object} db
|
|
85
|
-
* @param {number} startPort
|
|
86
|
-
* @param {number} maxAttempts
|
|
87
|
-
* @returns {Promise<number>} the actual port used
|
|
88
|
-
*/
|
|
89
|
-
async function startWithPortRetry(db, startPort, maxAttempts = 10) {
|
|
90
|
-
for (let i = 0; i < maxAttempts; i++) {
|
|
91
|
-
const port = startPort + i;
|
|
92
|
-
try {
|
|
93
|
-
await startServer(db, port);
|
|
94
|
-
if (port !== startPort) {
|
|
95
|
-
console.log(`[server] Port ${startPort} is busy, using ${port} instead`);
|
|
96
|
-
}
|
|
97
|
-
return port;
|
|
98
|
-
} catch (err) {
|
|
99
|
-
if (err.code === 'EADDRINUSE' && i < maxAttempts - 1) {
|
|
100
|
-
continue; // try next port
|
|
101
|
-
}
|
|
102
|
-
throw err;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ---------------------------------------------------------------------------
|
|
108
|
-
// Main
|
|
109
|
-
// ---------------------------------------------------------------------------
|
|
110
|
-
|
|
111
|
-
async function main() {
|
|
112
|
-
const { port: requestedPort, noOpen } = parseArgs();
|
|
113
|
-
|
|
114
|
-
// 1. Banner
|
|
115
|
-
printBanner();
|
|
116
|
-
|
|
117
|
-
// 2. Initialise database
|
|
118
|
-
let db;
|
|
119
|
-
try {
|
|
120
|
-
db = await getDb();
|
|
121
|
-
console.log('[db] Database initialised');
|
|
122
|
-
} catch (err) {
|
|
123
|
-
console.error('[db] Failed to initialise database:', err.message);
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// 3. Detect data sources
|
|
128
|
-
let sources;
|
|
129
|
-
try {
|
|
130
|
-
sources = await detectSources(db);
|
|
131
|
-
} catch (err) {
|
|
132
|
-
console.error('[detect] Source detection failed:', err.message);
|
|
133
|
-
sources = {
|
|
134
|
-
opencode: { status: 'not_found', path: '' },
|
|
135
|
-
claudeCode: { status: 'not_found', path: '' },
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const ocAvailable = sources.opencode.status === 'available';
|
|
140
|
-
const ccAvailable = sources.claudeCode.status === 'available';
|
|
141
|
-
|
|
142
|
-
console.log(
|
|
143
|
-
`[detect] OpenCode: ${ocAvailable ? 'available' : 'not found'}` +
|
|
144
|
-
(ocAvailable ? ` (${sources.opencode.path})` : '')
|
|
145
|
-
);
|
|
146
|
-
console.log(
|
|
147
|
-
`[detect] Claude Code: ${ccAvailable ? 'available' : 'not found'}` +
|
|
148
|
-
(ccAvailable ? ` (${sources.claudeCode.path})` : '')
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
// 4. Run ETL sync (incremental)
|
|
152
|
-
if (ocAvailable) {
|
|
153
|
-
try {
|
|
154
|
-
console.log('[etl] Starting OpenCode ETL sync...');
|
|
155
|
-
const stats = await collectOpenCode(db, sources.opencode.path, {
|
|
156
|
-
onProgress: (msg) => console.log(`[etl] ${msg}`),
|
|
157
|
-
});
|
|
158
|
-
console.log(
|
|
159
|
-
`[etl] OpenCode ETL done: ${stats.sessionCount} sessions, ` +
|
|
160
|
-
`${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
|
|
161
|
-
(stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
|
|
162
|
-
);
|
|
163
|
-
} catch (err) {
|
|
164
|
-
console.error('[etl] OpenCode ETL sync failed:', err.message);
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
if (ccAvailable) {
|
|
169
|
-
try {
|
|
170
|
-
console.log('[etl] Starting Claude Code ETL sync...');
|
|
171
|
-
const stats = await collectClaudeCode(db, sources.claudeCode.path, {
|
|
172
|
-
onProgress: (msg) => console.log(`[etl] ${msg}`),
|
|
173
|
-
});
|
|
174
|
-
console.log(
|
|
175
|
-
`[etl] Claude Code ETL done: ${stats.sessionCount} sessions, ` +
|
|
176
|
-
`${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
|
|
177
|
-
(stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
|
|
178
|
-
);
|
|
179
|
-
} catch (err) {
|
|
180
|
-
console.error('[etl] Claude Code ETL sync failed:', err.message);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
if (!ocAvailable && !ccAvailable) {
|
|
185
|
-
console.log('[etl] Skipping ETL (no data sources available)');
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// 5. Calculate active times
|
|
189
|
-
try {
|
|
190
|
-
const updated = calculateActiveTime(db);
|
|
191
|
-
if (updated > 0) {
|
|
192
|
-
console.log(`[active] Updated active_minutes for ${updated} session(s)`);
|
|
193
|
-
}
|
|
194
|
-
} catch (err) {
|
|
195
|
-
console.error('[active] Active-time calculation failed:', err.message);
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// 5.5 Backfill parent_session_id / agent_type for legacy rows imported
|
|
199
|
-
// before the subagent linkage columns existed. Idempotent + cheap
|
|
200
|
-
// after the first run. Failures are non-fatal.
|
|
201
|
-
if (ocAvailable) {
|
|
202
|
-
try {
|
|
203
|
-
const r = await backfillSubagents(db);
|
|
204
|
-
if (r.updated > 0) {
|
|
205
|
-
console.log(
|
|
206
|
-
`[backfill] Marked ${r.updated} subagent session(s) ` +
|
|
207
|
-
`(scanned ${r.scanned})`
|
|
208
|
-
);
|
|
209
|
-
} else if (r.reason) {
|
|
210
|
-
console.log(`[backfill] Skipped: ${r.reason}`);
|
|
211
|
-
}
|
|
212
|
-
} catch (err) {
|
|
213
|
-
console.error('[backfill] Subagent backfill failed:', err.message);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 6. Start analysis job in background (don't await)
|
|
218
|
-
runAnalysisJob(db, {
|
|
219
|
-
onProgress: (p) => {
|
|
220
|
-
if (p.error) {
|
|
221
|
-
console.log(`[analysis] Error on ${p.date} session ${p.sessionId}: ${p.error}`);
|
|
222
|
-
} else if (p.aggregationError) {
|
|
223
|
-
console.log(`[analysis] Aggregation error for ${p.date}: ${p.aggregationError}`);
|
|
224
|
-
} else {
|
|
225
|
-
console.log(`[analysis] ${p.analyzed}/${p.total} sessions (${p.date})`);
|
|
226
|
-
}
|
|
227
|
-
},
|
|
228
|
-
})
|
|
229
|
-
.then((result) => {
|
|
230
|
-
console.log(
|
|
231
|
-
`[analysis] Background job complete: ${result.analyzed || 0} sessions analyzed`
|
|
232
|
-
);
|
|
233
|
-
})
|
|
234
|
-
.catch((err) => {
|
|
235
|
-
console.error('[analysis] Background job failed:', err.message);
|
|
236
|
-
});
|
|
237
|
-
console.log('[analysis] Analysis job started in background...');
|
|
238
|
-
|
|
239
|
-
// 7. Start Express server (with port auto-increment on EADDRINUSE)
|
|
240
|
-
let actualPort;
|
|
241
|
-
try {
|
|
242
|
-
actualPort = await startWithPortRetry(db, requestedPort);
|
|
243
|
-
} catch (err) {
|
|
244
|
-
console.error('[server] Failed to start server:', err.message);
|
|
245
|
-
process.exit(1);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// 8. Open browser (unless --no-open)
|
|
249
|
-
if (!noOpen) {
|
|
250
|
-
const url = `http://localhost:${actualPort}`;
|
|
251
|
-
import('open')
|
|
252
|
-
.then((m) => m.default(url))
|
|
253
|
-
.catch(() => {
|
|
254
|
-
// 'open' is optional — if it fails, just print the URL
|
|
255
|
-
console.log(`[browser] Could not open browser. Visit: ${url}`);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// 9. Graceful shutdown
|
|
260
|
-
process.on('SIGINT', async () => {
|
|
261
|
-
console.log('\n[shutdown] Shutting down gracefully...');
|
|
262
|
-
try {
|
|
263
|
-
await closeDb();
|
|
264
|
-
} catch (_) {
|
|
265
|
-
// best-effort
|
|
266
|
-
}
|
|
267
|
-
process.exit(0);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
process.on('SIGTERM', async () => {
|
|
271
|
-
console.log('\n[shutdown] Received SIGTERM, shutting down...');
|
|
272
|
-
try {
|
|
273
|
-
await closeDb();
|
|
274
|
-
} catch (_) {
|
|
275
|
-
// best-effort
|
|
276
|
-
}
|
|
277
|
-
process.exit(0);
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
// ---------------------------------------------------------------------------
|
|
282
|
-
// Run
|
|
283
|
-
// ---------------------------------------------------------------------------
|
|
284
|
-
|
|
285
|
-
main().catch((err) => {
|
|
286
|
-
console.error('Fatal error:', err);
|
|
287
|
-
process.exit(1);
|
|
288
|
-
});
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Agent Boss CLI entry point.
|
|
4
|
+
* Usage: aboss [--port PORT] [--no-open]
|
|
5
|
+
*
|
|
6
|
+
* Startup sequence (design doc §12.3):
|
|
7
|
+
* 1. Parse CLI args
|
|
8
|
+
* 2. Print banner
|
|
9
|
+
* 3. Initialise database
|
|
10
|
+
* 4. Detect data sources
|
|
11
|
+
* 5. Run ETL sync (incremental)
|
|
12
|
+
* 6. Calculate active times
|
|
13
|
+
* 7. Start analysis job (background)
|
|
14
|
+
* 8. Start Express server (with port auto-increment)
|
|
15
|
+
* 9. Open browser (unless --no-open)
|
|
16
|
+
* 10. Handle graceful shutdown
|
|
17
|
+
*
|
|
18
|
+
* @author Felix
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const { getDb, closeDb } = require('../server/db/connection');
|
|
24
|
+
const { detectSources } = require('../server/etl/detect');
|
|
25
|
+
const { backfillSubagents } = require('../server/etl/backfill-subagents');
|
|
26
|
+
const { collectOpenCode } = require('../server/etl/opencode');
|
|
27
|
+
const { collectClaudeCode } = require('../server/etl/claude-code');
|
|
28
|
+
const { calculateActiveTime } = require('../server/etl/active-time');
|
|
29
|
+
const { runAnalysisJob } = require('../server/analysis/job');
|
|
30
|
+
const { startServer } = require('../server/index');
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// CLI arg parsing
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parse process.argv into { port, noOpen }.
|
|
38
|
+
* @returns {{ port: number, noOpen: boolean }}
|
|
39
|
+
*/
|
|
40
|
+
function parseArgs() {
|
|
41
|
+
const args = process.argv.slice(2);
|
|
42
|
+
let port = 3141;
|
|
43
|
+
let noOpen = false;
|
|
44
|
+
|
|
45
|
+
for (let i = 0; i < args.length; i++) {
|
|
46
|
+
if ((args[i] === '--port' || args[i] === '-p') && args[i + 1]) {
|
|
47
|
+
const parsed = Number(args[i + 1]);
|
|
48
|
+
if (!Number.isNaN(parsed) && parsed > 0 && parsed < 65536) {
|
|
49
|
+
port = parsed;
|
|
50
|
+
}
|
|
51
|
+
i++; // skip next arg (the port value)
|
|
52
|
+
} else if (args[i] === '--no-open') {
|
|
53
|
+
noOpen = true;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return { port, noOpen };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Banner
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
function printBanner() {
|
|
65
|
+
console.log('');
|
|
66
|
+
console.log(' ___ _ ____');
|
|
67
|
+
console.log(' / _ \\ __ _ ___ _ __ | |_ | __ ) ___ ___ ___');
|
|
68
|
+
console.log("| |_| |/ _` |/ _ \\ '_ \\| __| | _ \\ / _ \\/ __/ __|");
|
|
69
|
+
console.log('| ___ | (_| | __/ | | | |_ | |_) | (_) \\__ \\__ \\');
|
|
70
|
+
console.log('|_| |_|\\__, |\\___|_| |_|\\__| |____/ \\___/|___/___/');
|
|
71
|
+
console.log(' |___/');
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(' Agent Boss v0.1.0');
|
|
74
|
+
console.log(' Be your AI agent\'s boss, not its babysitter.');
|
|
75
|
+
console.log('');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// Port auto-increment helper
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Try starting the server on `startPort`, auto-incrementing on EADDRINUSE.
|
|
84
|
+
* @param {object} db
|
|
85
|
+
* @param {number} startPort
|
|
86
|
+
* @param {number} maxAttempts
|
|
87
|
+
* @returns {Promise<number>} the actual port used
|
|
88
|
+
*/
|
|
89
|
+
async function startWithPortRetry(db, startPort, maxAttempts = 10) {
|
|
90
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
91
|
+
const port = startPort + i;
|
|
92
|
+
try {
|
|
93
|
+
await startServer(db, port);
|
|
94
|
+
if (port !== startPort) {
|
|
95
|
+
console.log(`[server] Port ${startPort} is busy, using ${port} instead`);
|
|
96
|
+
}
|
|
97
|
+
return port;
|
|
98
|
+
} catch (err) {
|
|
99
|
+
if (err.code === 'EADDRINUSE' && i < maxAttempts - 1) {
|
|
100
|
+
continue; // try next port
|
|
101
|
+
}
|
|
102
|
+
throw err;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Main
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
async function main() {
|
|
112
|
+
const { port: requestedPort, noOpen } = parseArgs();
|
|
113
|
+
|
|
114
|
+
// 1. Banner
|
|
115
|
+
printBanner();
|
|
116
|
+
|
|
117
|
+
// 2. Initialise database
|
|
118
|
+
let db;
|
|
119
|
+
try {
|
|
120
|
+
db = await getDb();
|
|
121
|
+
console.log('[db] Database initialised');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.error('[db] Failed to initialise database:', err.message);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Detect data sources
|
|
128
|
+
let sources;
|
|
129
|
+
try {
|
|
130
|
+
sources = await detectSources(db);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
console.error('[detect] Source detection failed:', err.message);
|
|
133
|
+
sources = {
|
|
134
|
+
opencode: { status: 'not_found', path: '' },
|
|
135
|
+
claudeCode: { status: 'not_found', path: '' },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const ocAvailable = sources.opencode.status === 'available';
|
|
140
|
+
const ccAvailable = sources.claudeCode.status === 'available';
|
|
141
|
+
|
|
142
|
+
console.log(
|
|
143
|
+
`[detect] OpenCode: ${ocAvailable ? 'available' : 'not found'}` +
|
|
144
|
+
(ocAvailable ? ` (${sources.opencode.path})` : '')
|
|
145
|
+
);
|
|
146
|
+
console.log(
|
|
147
|
+
`[detect] Claude Code: ${ccAvailable ? 'available' : 'not found'}` +
|
|
148
|
+
(ccAvailable ? ` (${sources.claudeCode.path})` : '')
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
// 4. Run ETL sync (incremental)
|
|
152
|
+
if (ocAvailable) {
|
|
153
|
+
try {
|
|
154
|
+
console.log('[etl] Starting OpenCode ETL sync...');
|
|
155
|
+
const stats = await collectOpenCode(db, sources.opencode.path, {
|
|
156
|
+
onProgress: (msg) => console.log(`[etl] ${msg}`),
|
|
157
|
+
});
|
|
158
|
+
console.log(
|
|
159
|
+
`[etl] OpenCode ETL done: ${stats.sessionCount} sessions, ` +
|
|
160
|
+
`${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
|
|
161
|
+
(stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
|
|
162
|
+
);
|
|
163
|
+
} catch (err) {
|
|
164
|
+
console.error('[etl] OpenCode ETL sync failed:', err.message);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (ccAvailable) {
|
|
169
|
+
try {
|
|
170
|
+
console.log('[etl] Starting Claude Code ETL sync...');
|
|
171
|
+
const stats = await collectClaudeCode(db, sources.claudeCode.path, {
|
|
172
|
+
onProgress: (msg) => console.log(`[etl] ${msg}`),
|
|
173
|
+
});
|
|
174
|
+
console.log(
|
|
175
|
+
`[etl] Claude Code ETL done: ${stats.sessionCount} sessions, ` +
|
|
176
|
+
`${stats.messageCount} messages, ${stats.toolCallCount} tool calls` +
|
|
177
|
+
(stats.errorSessionCount ? `, ${stats.errorSessionCount} failed` : '')
|
|
178
|
+
);
|
|
179
|
+
} catch (err) {
|
|
180
|
+
console.error('[etl] Claude Code ETL sync failed:', err.message);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!ocAvailable && !ccAvailable) {
|
|
185
|
+
console.log('[etl] Skipping ETL (no data sources available)');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// 5. Calculate active times
|
|
189
|
+
try {
|
|
190
|
+
const updated = calculateActiveTime(db);
|
|
191
|
+
if (updated > 0) {
|
|
192
|
+
console.log(`[active] Updated active_minutes for ${updated} session(s)`);
|
|
193
|
+
}
|
|
194
|
+
} catch (err) {
|
|
195
|
+
console.error('[active] Active-time calculation failed:', err.message);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 5.5 Backfill parent_session_id / agent_type for legacy rows imported
|
|
199
|
+
// before the subagent linkage columns existed. Idempotent + cheap
|
|
200
|
+
// after the first run. Failures are non-fatal.
|
|
201
|
+
if (ocAvailable) {
|
|
202
|
+
try {
|
|
203
|
+
const r = await backfillSubagents(db);
|
|
204
|
+
if (r.updated > 0) {
|
|
205
|
+
console.log(
|
|
206
|
+
`[backfill] Marked ${r.updated} subagent session(s) ` +
|
|
207
|
+
`(scanned ${r.scanned})`
|
|
208
|
+
);
|
|
209
|
+
} else if (r.reason) {
|
|
210
|
+
console.log(`[backfill] Skipped: ${r.reason}`);
|
|
211
|
+
}
|
|
212
|
+
} catch (err) {
|
|
213
|
+
console.error('[backfill] Subagent backfill failed:', err.message);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// 6. Start analysis job in background (don't await)
|
|
218
|
+
runAnalysisJob(db, {
|
|
219
|
+
onProgress: (p) => {
|
|
220
|
+
if (p.error) {
|
|
221
|
+
console.log(`[analysis] Error on ${p.date} session ${p.sessionId}: ${p.error}`);
|
|
222
|
+
} else if (p.aggregationError) {
|
|
223
|
+
console.log(`[analysis] Aggregation error for ${p.date}: ${p.aggregationError}`);
|
|
224
|
+
} else {
|
|
225
|
+
console.log(`[analysis] ${p.analyzed}/${p.total} sessions (${p.date})`);
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
})
|
|
229
|
+
.then((result) => {
|
|
230
|
+
console.log(
|
|
231
|
+
`[analysis] Background job complete: ${result.analyzed || 0} sessions analyzed`
|
|
232
|
+
);
|
|
233
|
+
})
|
|
234
|
+
.catch((err) => {
|
|
235
|
+
console.error('[analysis] Background job failed:', err.message);
|
|
236
|
+
});
|
|
237
|
+
console.log('[analysis] Analysis job started in background...');
|
|
238
|
+
|
|
239
|
+
// 7. Start Express server (with port auto-increment on EADDRINUSE)
|
|
240
|
+
let actualPort;
|
|
241
|
+
try {
|
|
242
|
+
actualPort = await startWithPortRetry(db, requestedPort);
|
|
243
|
+
} catch (err) {
|
|
244
|
+
console.error('[server] Failed to start server:', err.message);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// 8. Open browser (unless --no-open)
|
|
249
|
+
if (!noOpen) {
|
|
250
|
+
const url = `http://localhost:${actualPort}`;
|
|
251
|
+
import('open')
|
|
252
|
+
.then((m) => m.default(url))
|
|
253
|
+
.catch(() => {
|
|
254
|
+
// 'open' is optional — if it fails, just print the URL
|
|
255
|
+
console.log(`[browser] Could not open browser. Visit: ${url}`);
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// 9. Graceful shutdown
|
|
260
|
+
process.on('SIGINT', async () => {
|
|
261
|
+
console.log('\n[shutdown] Shutting down gracefully...');
|
|
262
|
+
try {
|
|
263
|
+
await closeDb();
|
|
264
|
+
} catch (_) {
|
|
265
|
+
// best-effort
|
|
266
|
+
}
|
|
267
|
+
process.exit(0);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
process.on('SIGTERM', async () => {
|
|
271
|
+
console.log('\n[shutdown] Received SIGTERM, shutting down...');
|
|
272
|
+
try {
|
|
273
|
+
await closeDb();
|
|
274
|
+
} catch (_) {
|
|
275
|
+
// best-effort
|
|
276
|
+
}
|
|
277
|
+
process.exit(0);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ---------------------------------------------------------------------------
|
|
282
|
+
// Run
|
|
283
|
+
// ---------------------------------------------------------------------------
|
|
284
|
+
|
|
285
|
+
main().catch((err) => {
|
|
286
|
+
console.error('Fatal error:', err);
|
|
287
|
+
process.exit(1);
|
|
288
|
+
});
|