agentboss 0.1.0

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.
Files changed (53) hide show
  1. package/README.md +34 -0
  2. package/bin/aboss.js +288 -0
  3. package/client/dist/assets/index-C1wFD_Vo.css +1 -0
  4. package/client/dist/assets/index-DBj1Ujlx.js +137 -0
  5. package/client/dist/index.html +34 -0
  6. package/package.json +64 -0
  7. package/server/analysis/daily-aggregator.js +258 -0
  8. package/server/analysis/difficulty.js +129 -0
  9. package/server/analysis/dimensions/ai-knowledge.js +172 -0
  10. package/server/analysis/dimensions/ai-tools.js +161 -0
  11. package/server/analysis/dimensions/judgement.js +107 -0
  12. package/server/analysis/dimensions/llm-merge.js +57 -0
  13. package/server/analysis/dimensions/output-quality.js +167 -0
  14. package/server/analysis/dimensions/problem-definition.js +104 -0
  15. package/server/analysis/dimensions/system-thinking.js +225 -0
  16. package/server/analysis/evidence-builder.js +104 -0
  17. package/server/analysis/job.js +273 -0
  18. package/server/analysis/report-builder.js +581 -0
  19. package/server/analysis/scoring-v2.js +72 -0
  20. package/server/analysis/text-signals.js +179 -0
  21. package/server/analysis/thresholds-v2.js +358 -0
  22. package/server/api/advice.js +124 -0
  23. package/server/api/analysis.js +141 -0
  24. package/server/api/execution.js +330 -0
  25. package/server/api/metrics.js +277 -0
  26. package/server/api/overview.js +308 -0
  27. package/server/api/project.js +255 -0
  28. package/server/api/reports.js +125 -0
  29. package/server/api/sessions.js +118 -0
  30. package/server/api/settings.js +119 -0
  31. package/server/db/connection.js +175 -0
  32. package/server/db/queries.js +1051 -0
  33. package/server/db/schema.js +487 -0
  34. package/server/etl/active-time.js +150 -0
  35. package/server/etl/backfill-subagents.js +178 -0
  36. package/server/etl/claude-code.js +826 -0
  37. package/server/etl/detect.js +341 -0
  38. package/server/etl/judge-filter.js +117 -0
  39. package/server/etl/opencode.js +606 -0
  40. package/server/execution/job.js +662 -0
  41. package/server/execution/prompt.js +227 -0
  42. package/server/execution/runner.js +218 -0
  43. package/server/index.js +94 -0
  44. package/server/llm/advice-prompt.js +339 -0
  45. package/server/llm/advice.js +384 -0
  46. package/server/llm/analysis-prompt.js +162 -0
  47. package/server/llm/cli-runner.js +249 -0
  48. package/server/llm/judge-prompts.js +179 -0
  49. package/server/llm/judge.js +118 -0
  50. package/server/llm/project-advice-prompt.js +332 -0
  51. package/server/llm/project-advice.js +491 -0
  52. package/server/llm/session-analyzer.js +122 -0
  53. package/server/utils/project.js +80 -0
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Metrics API routes for Agent Boss.
3
+ *
4
+ * Provides endpoints for dimension scores, daily/hourly summaries, session
5
+ * listings with pagination, trend computation, and cross-tool comparisons.
6
+ *
7
+ * @author Felix
8
+ */
9
+
10
+ const router = require('express').Router();
11
+
12
+ const {
13
+ getDailySummaries,
14
+ getHourlyActivity,
15
+ getSessionsByDateRange,
16
+ countSessionsByDateRange,
17
+ } = require('../db/queries');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Date helpers
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Format a Date as YYYY-MM-DD.
25
+ * @param {Date} d
26
+ * @returns {string}
27
+ */
28
+ function fmt(d) {
29
+ const y = d.getFullYear();
30
+ const m = String(d.getMonth() + 1).padStart(2, '0');
31
+ const dd = String(d.getDate()).padStart(2, '0');
32
+ return `${y}-${m}-${dd}`;
33
+ }
34
+
35
+ /**
36
+ * Get the date N days ago as YYYY-MM-DD.
37
+ * @param {number} n
38
+ * @returns {string}
39
+ */
40
+ function daysAgo(n) {
41
+ const d = new Date();
42
+ d.setDate(d.getDate() - n);
43
+ return fmt(d);
44
+ }
45
+
46
+ /**
47
+ * Get yesterday as YYYY-MM-DD.
48
+ * @returns {string}
49
+ */
50
+ function yesterday() {
51
+ return daysAgo(1);
52
+ }
53
+
54
+ /**
55
+ * Get today as YYYY-MM-DD.
56
+ * @returns {string}
57
+ */
58
+ function today() {
59
+ return fmt(new Date());
60
+ }
61
+
62
+ /**
63
+ * Compute period boundaries for trend comparison.
64
+ * @param {string} period 'week' or 'month'
65
+ * @returns {{ current: {from: string, to: string}, previous: {from: string, to: string} }}
66
+ */
67
+ function trendPeriods(period) {
68
+ const now = new Date();
69
+ if (period === 'month') {
70
+ // Current: last 30 days, Previous: 30 days before that
71
+ return {
72
+ current: { from: daysAgo(30), to: fmt(now) },
73
+ previous: { from: daysAgo(60), to: daysAgo(31) },
74
+ };
75
+ }
76
+ // Default: week
77
+ return {
78
+ current: { from: daysAgo(7), to: fmt(now) },
79
+ previous: { from: daysAgo(14), to: daysAgo(8) },
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Sum key metrics from daily summary rows.
85
+ * @param {object[]} summaries
86
+ * @returns {object}
87
+ */
88
+ function sumSummaries(summaries) {
89
+ let sessions = 0;
90
+ let cost = 0;
91
+ let activeMinutes = 0;
92
+ let errors = 0;
93
+ let tokensInput = 0;
94
+ let tokensOutput = 0;
95
+
96
+ for (const s of summaries) {
97
+ sessions += s.session_count || 0;
98
+ cost += s.cost_usd || 0;
99
+ activeMinutes += s.active_minutes || 0;
100
+ errors += s.error_count || 0;
101
+ tokensInput += s.tokens_input || 0;
102
+ tokensOutput += s.tokens_output || 0;
103
+ }
104
+
105
+ return {
106
+ sessions,
107
+ cost: Math.round(cost * 100) / 100,
108
+ activeMinutes,
109
+ errors,
110
+ tokensInput,
111
+ tokensOutput,
112
+ };
113
+ }
114
+
115
+ // ---------------------------------------------------------------------------
116
+ // Routes
117
+ // ---------------------------------------------------------------------------
118
+
119
+ /**
120
+ * Create the metrics router with database access.
121
+ *
122
+ * @param {object} db sql.js Database instance
123
+ * @returns {import('express').Router}
124
+ */
125
+ module.exports = function (db) {
126
+ // GET /api/metrics/daily?from=&to=
127
+ router.get('/daily', (req, res) => {
128
+ try {
129
+ const from = req.query.from || daysAgo(7);
130
+ const to = req.query.to || today();
131
+ const data = getDailySummaries(db, from, to);
132
+ res.json({
133
+ ok: true,
134
+ data,
135
+ meta: {
136
+ generated_at: new Date().toISOString(),
137
+ period: { from, to },
138
+ },
139
+ });
140
+ } catch (err) {
141
+ res.status(500).json({
142
+ ok: false,
143
+ error: { code: 'METRICS_ERROR', message: err.message },
144
+ });
145
+ }
146
+ });
147
+
148
+ // GET /api/metrics/hourly?date=
149
+ router.get('/hourly', (req, res) => {
150
+ try {
151
+ const date = req.query.date || yesterday();
152
+ const data = getHourlyActivity(db, date);
153
+ res.json({
154
+ ok: true,
155
+ data,
156
+ meta: {
157
+ generated_at: new Date().toISOString(),
158
+ period: { from: date, to: date },
159
+ },
160
+ });
161
+ } catch (err) {
162
+ res.status(500).json({
163
+ ok: false,
164
+ error: { code: 'METRICS_ERROR', message: err.message },
165
+ });
166
+ }
167
+ });
168
+
169
+ // GET /api/metrics/sessions?from=&to=&source=&limit=50&offset=0
170
+ router.get('/sessions', (req, res) => {
171
+ try {
172
+ const from = req.query.from || daysAgo(7);
173
+ const to = req.query.to || today();
174
+ const source = req.query.source || undefined;
175
+ const limit = parseInt(req.query.limit, 10) || 50;
176
+ const offset = parseInt(req.query.offset, 10) || 0;
177
+
178
+ const data = getSessionsByDateRange(db, from, to, source, limit, offset);
179
+ const total = countSessionsByDateRange(db, from, to, source);
180
+
181
+ res.json({
182
+ ok: true,
183
+ data,
184
+ total,
185
+ meta: {
186
+ generated_at: new Date().toISOString(),
187
+ period: { from, to },
188
+ pagination: { limit, offset, total },
189
+ },
190
+ });
191
+ } catch (err) {
192
+ res.status(500).json({
193
+ ok: false,
194
+ error: { code: 'METRICS_ERROR', message: err.message },
195
+ });
196
+ }
197
+ });
198
+
199
+ // GET /api/metrics/trends?period=week|month
200
+ router.get('/trends', (req, res) => {
201
+ try {
202
+ const period = req.query.period === 'month' ? 'month' : 'week';
203
+ const { current, previous } = trendPeriods(period);
204
+
205
+ const currentSummaries = getDailySummaries(db, current.from, current.to);
206
+ const previousSummaries = getDailySummaries(db, previous.from, previous.to);
207
+
208
+ const cur = sumSummaries(currentSummaries);
209
+ const prev = sumSummaries(previousSummaries);
210
+
211
+ // Compute percentage change (null if previous is zero)
212
+ const pct = (c, p) => (p > 0 ? Math.round(((c - p) / p) * 1000) / 10 : null);
213
+
214
+ const data = {
215
+ period,
216
+ current: cur,
217
+ previous: prev,
218
+ change: {
219
+ sessions: pct(cur.sessions, prev.sessions),
220
+ cost: pct(cur.cost, prev.cost),
221
+ activeMinutes: pct(cur.activeMinutes, prev.activeMinutes),
222
+ errors: pct(cur.errors, prev.errors),
223
+ },
224
+ };
225
+
226
+ res.json({
227
+ ok: true,
228
+ data,
229
+ meta: {
230
+ generated_at: new Date().toISOString(),
231
+ period: { from: current.from, to: current.to },
232
+ },
233
+ });
234
+ } catch (err) {
235
+ res.status(500).json({
236
+ ok: false,
237
+ error: { code: 'METRICS_ERROR', message: err.message },
238
+ });
239
+ }
240
+ });
241
+
242
+ // GET /api/metrics/cross-tool
243
+ router.get('/cross-tool', (req, res) => {
244
+ try {
245
+ const from = req.query.from || daysAgo(30);
246
+ const to = req.query.to || today();
247
+
248
+ const opencodeSummaries = getDailySummaries(db, from, to, 'opencode');
249
+ const claudeCodeSummaries = getDailySummaries(db, from, to, 'claude-code');
250
+
251
+ const data = {
252
+ opencode: {
253
+ stats: sumSummaries(opencodeSummaries),
254
+ },
255
+ claudeCode: {
256
+ stats: sumSummaries(claudeCodeSummaries),
257
+ },
258
+ };
259
+
260
+ res.json({
261
+ ok: true,
262
+ data,
263
+ meta: {
264
+ generated_at: new Date().toISOString(),
265
+ period: { from, to },
266
+ },
267
+ });
268
+ } catch (err) {
269
+ res.status(500).json({
270
+ ok: false,
271
+ error: { code: 'METRICS_ERROR', message: err.message },
272
+ });
273
+ }
274
+ });
275
+
276
+ return router;
277
+ };
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Overview (home page) API for Agent Boss.
3
+ *
4
+ * Single endpoint `GET /api/overview?days=N` returns ETL-derived datasets
5
+ * — no dependency on the analysis layer, so the page renders the moment
6
+ * ETL sync completes:
7
+ *
8
+ * range — resolved window { from, to, days, label }
9
+ * trend — daily rows broken out by source (sessions / cost)
10
+ * topProjects — top 8 projects in window, by cost
11
+ * recentSessions — newest 10 sessions across all sources / dates
12
+ * cacheRate — per-day cache hit rate (%)
13
+ * errorRate — per-day error rate (%)
14
+ * codeNet — per-day additions / deletions / files
15
+ * topTools — top 10 tools by call count, with error rate
16
+ * capabilityRadar — v2 5-axis radar averaged across analysed sessions in
17
+ * the window (H1/H2/H3/ENV/O1), or null if analysis
18
+ * hasn't produced any rows yet
19
+ *
20
+ * `days` may be 7 / 14 / 30 / 90 or `all`. Unknown values fall back to 14.
21
+ *
22
+ * @author Felix
23
+ */
24
+
25
+ const router = require('express').Router();
26
+
27
+ const {
28
+ getOverviewTrend,
29
+ getOverviewTopProjects,
30
+ getOverviewRecentSessions,
31
+ getOverviewCacheRate,
32
+ getOverviewErrorRate,
33
+ getOverviewTopTools,
34
+ getEarliestSessionDate,
35
+ } = require('../db/queries');
36
+ const { getCurrentDimensionsV2 } = require('../analysis/report-builder');
37
+ const { canonicalProject, mapTopProjects } = require('../utils/project');
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // Date helpers
41
+ // ---------------------------------------------------------------------------
42
+
43
+ /** YYYY-MM-DD in local time. */
44
+ function fmt(d) {
45
+ const y = d.getFullYear();
46
+ const m = String(d.getMonth() + 1).padStart(2, '0');
47
+ const dd = String(d.getDate()).padStart(2, '0');
48
+ return `${y}-${m}-${dd}`;
49
+ }
50
+
51
+ function daysAgo(n) {
52
+ const d = new Date();
53
+ d.setDate(d.getDate() - n);
54
+ return fmt(d);
55
+ }
56
+
57
+ // ---------------------------------------------------------------------------
58
+ // Shape mappers
59
+ // ---------------------------------------------------------------------------
60
+
61
+ /**
62
+ * Reshape raw recent-session rows into the camelCase shape the home page
63
+ * table consumes.
64
+ *
65
+ * @param {Object[]} rows
66
+ * @returns {Object[]}
67
+ */
68
+ function mapRecentSessions(rows) {
69
+ return rows.map((r) => ({
70
+ id: r.id,
71
+ source: r.source,
72
+ startedAt: r.started_at,
73
+ project: r.project,
74
+ title: r.title,
75
+ model: r.model,
76
+ messageCount: r.message_count || 0,
77
+ cost: Math.round((r.cost_usd || 0) * 10000) / 10000,
78
+ errorCount: r.error_count || 0,
79
+ reverted: !!r.reverted,
80
+ }));
81
+ }
82
+
83
+ /**
84
+ * Pivot the (date, source) trend rows into one row per date with one column
85
+ * per source, so the recharts <TrendLine> can render overlaid lines.
86
+ *
87
+ * Output columns:
88
+ * date, opencode_cost, claude-code_cost, opencode_sessions, ...
89
+ * The active sources list is returned alongside so the UI can build the
90
+ * right Line definitions without re-scanning the rows.
91
+ *
92
+ * @param {Object[]} rows raw GROUP BY date, source rows
93
+ * @param {string} fromDate
94
+ * @param {string} toDate
95
+ * @returns {{ rows: Object[], sources: string[] }}
96
+ */
97
+ function pivotTrend(rows, fromDate, toDate) {
98
+ const byDate = new Map();
99
+ const sourceSet = new Set();
100
+
101
+ for (const r of rows) {
102
+ sourceSet.add(r.source);
103
+ if (!byDate.has(r.date)) {
104
+ byDate.set(r.date, { date: r.date });
105
+ }
106
+ const bucket = byDate.get(r.date);
107
+ bucket[`${r.source}_sessions`] = r.sessions || 0;
108
+ bucket[`${r.source}_cost`] = Math.round((r.cost || 0) * 100) / 100;
109
+ bucket[`${r.source}_activeMinutes`] = r.active_minutes || 0;
110
+ }
111
+
112
+ const sources = Array.from(sourceSet);
113
+
114
+ // For very large windows (e.g. "all") we skip the dense fill — emitting
115
+ // one row per day for 6 months gets noisy on the x-axis. Instead we
116
+ // just return the sparse rows ordered by date, with zeros injected for
117
+ // any source missing on a given day.
118
+ const start = new Date(fromDate + 'T00:00:00');
119
+ const end = new Date(toDate + 'T00:00:00');
120
+ const spanDays = Math.round((end - start) / 86400000) + 1;
121
+ const DENSE_FILL_MAX_DAYS = 120;
122
+
123
+ if (spanDays > DENSE_FILL_MAX_DAYS) {
124
+ const rows = Array.from(byDate.values()).sort((a, b) =>
125
+ a.date < b.date ? -1 : a.date > b.date ? 1 : 0
126
+ );
127
+ for (const row of rows) {
128
+ for (const s of sources) {
129
+ if (row[`${s}_sessions`] == null) row[`${s}_sessions`] = 0;
130
+ if (row[`${s}_cost`] == null) row[`${s}_cost`] = 0;
131
+ if (row[`${s}_activeMinutes`] == null) row[`${s}_activeMinutes`] = 0;
132
+ }
133
+ }
134
+ return { rows, sources };
135
+ }
136
+
137
+ // Fill the full date range so the x-axis is continuous (no gaps when a
138
+ // day had zero activity).
139
+ const filled = [];
140
+ for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {
141
+ const key = fmt(d);
142
+ const existing = byDate.get(key) || { date: key };
143
+ for (const s of sources) {
144
+ if (existing[`${s}_sessions`] == null) existing[`${s}_sessions`] = 0;
145
+ if (existing[`${s}_cost`] == null) existing[`${s}_cost`] = 0;
146
+ if (existing[`${s}_activeMinutes`] == null) existing[`${s}_activeMinutes`] = 0;
147
+ }
148
+ filled.push(existing);
149
+ }
150
+
151
+ return { rows: filled, sources };
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Router factory
156
+ // ---------------------------------------------------------------------------
157
+
158
+ /**
159
+ * @param {object} db sql.js Database instance
160
+ * @returns {import('express').Router}
161
+ */
162
+ /**
163
+ * Merge per-day rate rows into the pivoted trend rows in-place.
164
+ * This lets the front-end render any time-series metric (cost, sessions,
165
+ * tokens, hitRate, errorRate) from one flat array.
166
+ *
167
+ * Extra columns added per date row:
168
+ * hitRate, cacheRead, cacheWrite
169
+ * errorRate, errors, messages
170
+ *
171
+ * @param {Object[]} trendRows output of pivotTrend (one row per date)
172
+ * @param {{cache: Object[], error: Object[]}} extras
173
+ * @returns {Object[]} same trendRows array, mutated and returned for chaining
174
+ */
175
+ function mergeTimeseries(trendRows, extras) {
176
+ const byDate = new Map(trendRows.map((r) => [r.date, r]));
177
+
178
+ for (const c of extras.cache || []) {
179
+ const row = byDate.get(c.date);
180
+ if (!row) continue;
181
+ row.hitRate = c.hitRate;
182
+ row.cacheRead = c.cacheRead;
183
+ row.cacheWrite = c.cacheWrite;
184
+ }
185
+ for (const e of extras.error || []) {
186
+ const row = byDate.get(e.date);
187
+ if (!row) continue;
188
+ row.errorRate = e.rate;
189
+ row.errors = e.errors;
190
+ row.messages = e.messages;
191
+ }
192
+
193
+ // Ensure every column has a numeric default (so recharts doesn't drop
194
+ // points and tooltips don't render `undefined`).
195
+ for (const row of trendRows) {
196
+ if (row.hitRate == null) row.hitRate = 0;
197
+ if (row.errorRate == null) row.errorRate = 0;
198
+ }
199
+ return trendRows;
200
+ }
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Date-range resolution
204
+ // ---------------------------------------------------------------------------
205
+
206
+ const PRESETS = { '7': 7, '14': 14, '30': 30, '90': 90 };
207
+ const PRESET_LABEL = {
208
+ '7': '本周',
209
+ '14': '双周',
210
+ '30': '本月',
211
+ '90': '季度',
212
+ 'all': '全部',
213
+ };
214
+
215
+ /**
216
+ * Translate the `days` query parameter into a concrete window. Falls back
217
+ * to 14 days for unknown values. For `all`, the lower bound is the
218
+ * earliest session date in boss.db (or today if the DB is empty).
219
+ *
220
+ * @param {string|undefined} raw query string value
221
+ * @param {Object} db sql.js Database
222
+ * @returns {{ from: string, to: string, days: number|null, label: string }}
223
+ */
224
+ function resolveRange(raw, db) {
225
+ const to = fmt(new Date());
226
+ if (raw === 'all') {
227
+ const from = getEarliestSessionDate(db) || to;
228
+ return { from, to, days: null, label: PRESET_LABEL.all };
229
+ }
230
+ const key = String(raw || '14');
231
+ const n = PRESETS[key] != null ? PRESETS[key] : 14;
232
+ return {
233
+ from: daysAgo(n - 1),
234
+ to,
235
+ days: n,
236
+ label: PRESET_LABEL[String(n)] || `${n} 天`,
237
+ };
238
+ }
239
+
240
+ // ---------------------------------------------------------------------------
241
+ // Router
242
+ // ---------------------------------------------------------------------------
243
+
244
+ module.exports = function (db) {
245
+ // GET /api/overview?days=7|14|30|90|all
246
+ router.get('/', (req, res) => {
247
+ try {
248
+ const range = resolveRange(req.query.days, db);
249
+ const { from, to } = range;
250
+
251
+ const trendRows = getOverviewTrend(db, from, to);
252
+ // Pull a larger candidate pool than the final top-N so that
253
+ // collapsing duplicate paths (see canonicalProject) still leaves
254
+ // enough rows to fill the requested limit.
255
+ const TOP_N = 8;
256
+ const topProjectsRaw = getOverviewTopProjects(db, from, to, 40);
257
+ const recentSessions = getOverviewRecentSessions(db, 10);
258
+ const cacheRateRows = getOverviewCacheRate(db, from, to);
259
+ const errorRateRows = getOverviewErrorRate(db, from, to);
260
+ const topTools = getOverviewTopTools(db, from, to, 10);
261
+
262
+ // v2 capability radar — averaged across all analysed sessions in the
263
+ // window (H1/H2/E1/E2/O1) plus H3 from the rolling aggregator.
264
+ // Returns nulls when no sessions in the window have been analysed yet;
265
+ // the front-end shows an empty-state in that case.
266
+ let capabilityRadar = null;
267
+ try {
268
+ capabilityRadar = getCurrentDimensionsV2(db, from, to);
269
+ } catch (e) {
270
+ // Don't fail the whole overview if analysis tables are missing.
271
+ capabilityRadar = null;
272
+ }
273
+
274
+ const trend = pivotTrend(trendRows, from, to);
275
+ const timeseries = mergeTimeseries(trend.rows, {
276
+ cache: cacheRateRows,
277
+ error: errorRateRows,
278
+ });
279
+
280
+ res.json({
281
+ ok: true,
282
+ data: {
283
+ range,
284
+ trend: {
285
+ from,
286
+ to,
287
+ sources: trend.sources,
288
+ rows: timeseries,
289
+ },
290
+ topProjects: mapTopProjects(topProjectsRaw, TOP_N),
291
+ recentSessions: mapRecentSessions(recentSessions),
292
+ topTools,
293
+ capabilityRadar,
294
+ },
295
+ meta: {
296
+ generated_at: new Date().toISOString(),
297
+ },
298
+ });
299
+ } catch (err) {
300
+ res.status(500).json({
301
+ ok: false,
302
+ error: { code: 'OVERVIEW_ERROR', message: err.message },
303
+ });
304
+ }
305
+ });
306
+
307
+ return router;
308
+ };