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 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
+ });