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,255 @@
1
+ /**
2
+ * Project detail + project-level AI advice API.
3
+ *
4
+ * Routes
5
+ * ------
6
+ * GET /api/project/:key basic meta + sessions in window
7
+ * GET /api/project/:key/advice read cached project_advice
8
+ * POST /api/project/:key/advice body { force } trigger generation
9
+ *
10
+ * The `:key` path param is the URL-encoded project path (raw or canonical;
11
+ * the server canonicalises before any lookup). Windows-friendly via
12
+ * encodeURIComponent on the client side.
13
+ *
14
+ * Query params on the GET routes select the time window:
15
+ * scope=daily|weekly|all (default 'all')
16
+ * from=YYYY-MM-DD to=YYYY-MM-DD required when scope!=all
17
+ *
18
+ * Failure reasons from server/llm/project-advice.js are mapped to
19
+ * HTTP codes mirroring /api/advice's table.
20
+ *
21
+ * @author Felix
22
+ */
23
+
24
+ 'use strict';
25
+
26
+ const router = require('express').Router();
27
+
28
+ const {
29
+ generateProjectAdvice,
30
+ loadProjectAdvice,
31
+ listProjectAdviceCaches,
32
+ resolveProjectSessions,
33
+ attachSessionAdvice,
34
+ } = require('../llm/project-advice');
35
+ const { canonicalProject } = require('../utils/project');
36
+ const { queryOne } = require('../db/queries');
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Reason → HTTP
40
+ // ---------------------------------------------------------------------------
41
+
42
+ const REASON_TO_STATUS = {
43
+ 'no-project': 400,
44
+ 'no-window': 400,
45
+ 'no-sessions': 404,
46
+ 'no-session-advice': 409,
47
+ 'llm-disabled': 409,
48
+ 'no-cli': 409,
49
+ 'timeout': 504,
50
+ 'bad-json': 502,
51
+ 'spawn-error': 500,
52
+ 'exit-non-zero': 500,
53
+ 'internal': 500,
54
+ };
55
+
56
+ const REASON_TO_MESSAGE = {
57
+ 'no-project': '缺少 project',
58
+ 'no-window': '该 scope 需要 from/to',
59
+ 'no-sessions': '该时间段内项目没有任何会话',
60
+ 'no-session-advice': '该项目下没有任何 session 已经生成单会话 advice;请先在 session 详情页生成',
61
+ 'llm-disabled': 'LLM judging is disabled in settings',
62
+ 'no-cli': 'No opencode/claude CLI detected on PATH',
63
+ 'timeout': 'LLM call timed out',
64
+ 'bad-json': 'LLM returned non-JSON output',
65
+ 'spawn-error': 'Failed to spawn LLM CLI',
66
+ 'exit-non-zero': 'LLM CLI exited with a non-zero status',
67
+ 'internal': 'Internal project-advice generation error',
68
+ };
69
+
70
+ function failure(res, reason, extra) {
71
+ const status = REASON_TO_STATUS[reason] || 500;
72
+ const message = (extra && extra.error) || REASON_TO_MESSAGE[reason] || reason;
73
+ const body = {
74
+ ok: false,
75
+ error: { code: reason.toUpperCase().replace(/-/g, '_'), message },
76
+ };
77
+ if (extra && extra.meta) body.meta = extra.meta;
78
+ return res.status(status).json(body);
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------
82
+ // Window resolver — derives a concrete from/to from query string
83
+ // ---------------------------------------------------------------------------
84
+
85
+ function resolveWindow(query) {
86
+ const scope = (query.scope || 'all').toLowerCase();
87
+ if (scope === 'all') return { scope, windowFrom: '', windowTo: '' };
88
+ const from = query.from || '';
89
+ const to = query.to || '';
90
+ if (!from || !to) return null;
91
+ return { scope, windowFrom: from, windowTo: to };
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Routes
96
+ // ---------------------------------------------------------------------------
97
+
98
+ module.exports = function (db) {
99
+ // GET /api/project/:key — meta, stats, and session list under this project.
100
+ router.get('/:key', (req, res) => {
101
+ const project = canonicalProject(decodeURIComponent(req.params.key || ''));
102
+ if (!project) return failure(res, 'no-project');
103
+ const window = resolveWindow(req.query);
104
+ if (!window) return failure(res, 'no-window');
105
+
106
+ const raw = resolveProjectSessions(
107
+ db, project, window.windowFrom, window.windowTo
108
+ );
109
+ const enriched = attachSessionAdvice(db, raw);
110
+
111
+ // Stats are computed across the FULL set including subagents — the
112
+ // money / errors / advice-eligibility numbers should match what the
113
+ // user actually spent / generated. The session list rendered to the
114
+ // UI below filters subagents out.
115
+ let cost = 0, errors = 0, withAdvice = 0;
116
+ for (const s of enriched) {
117
+ cost += Number(s.cost) || 0;
118
+ errors += Number(s.errorCount) || 0;
119
+ if (s.advice && s.advice.categories) withAdvice++;
120
+ }
121
+ const topLevel = enriched.filter((s) => !s.parentSessionId);
122
+ const subagentCount = enriched.length - topLevel.length;
123
+
124
+ // Earliest/latest dates seen across all unified_session rows for this
125
+ // project (regardless of the requested window) — used by the UI to show
126
+ // "项目首次出现 / 最近活跃".
127
+ const span = queryOne(
128
+ db,
129
+ // We can't filter by canonical project in SQL cheaply, so range across
130
+ // anything LIKE-ish then ignore false positives via canonicalProject
131
+ // — but for the *summary numbers* it's fine to scope to the same
132
+ // resolveProjectSessions output.
133
+ `SELECT MIN(started_at) AS first_at, MAX(started_at) AS last_at
134
+ FROM unified_session
135
+ WHERE id IN (${raw.map(() => '?').join(',') || 'NULL'})`,
136
+ raw.map((r) => r.id)
137
+ );
138
+
139
+ return res.json({
140
+ ok: true,
141
+ data: {
142
+ project,
143
+ scope: window.scope,
144
+ windowFrom: window.windowFrom,
145
+ windowTo: window.windowTo,
146
+ // sessionCount = full count (incl. subagents) so stats match
147
+ // up; sessionsCountTopLevel = what the listed table shows.
148
+ sessionCount: enriched.length,
149
+ sessionsCountTopLevel: topLevel.length,
150
+ subagentCount,
151
+ sessionsWithAdvice: withAdvice,
152
+ missingAdvice: enriched.length - withAdvice,
153
+ cost: Math.round(cost * 10000) / 10000,
154
+ errors,
155
+ firstActivityAt: span ? span.first_at : null,
156
+ lastActivityAt: span ? span.last_at : null,
157
+ sessions: topLevel.map((s) => ({
158
+ id: s.id,
159
+ title: s.title,
160
+ model: s.model,
161
+ date: s.date,
162
+ cost: Math.round((s.cost || 0) * 10000) / 10000,
163
+ msgCount: s.msgCount,
164
+ errorCount: s.errorCount,
165
+ hasAdvice: !!(s.advice && s.advice.categories),
166
+ })),
167
+ },
168
+ meta: { generated_at: new Date().toISOString() },
169
+ });
170
+ });
171
+
172
+ // GET /api/project/:key/advice/index
173
+ // Compact list of every (scope, window_from, window_to) for which a
174
+ // project_advice cache exists. The UI uses it to pick a default
175
+ // window that actually has data (avoids "looks empty after reload"
176
+ // when the cached window doesn't match the default).
177
+ router.get('/:key/advice/index', (req, res) => {
178
+ const project = canonicalProject(decodeURIComponent(req.params.key || ''));
179
+ if (!project) return failure(res, 'no-project');
180
+ const caches = listProjectAdviceCaches(db, project);
181
+ res.json({
182
+ ok: true,
183
+ data: { project, caches },
184
+ meta: { generated_at: new Date().toISOString() },
185
+ });
186
+ });
187
+
188
+ // GET /api/project/:key/advice — read cached project_advice; never spawns CLI.
189
+ router.get('/:key/advice', (req, res) => {
190
+ const project = canonicalProject(decodeURIComponent(req.params.key || ''));
191
+ if (!project) return failure(res, 'no-project');
192
+ const window = resolveWindow(req.query);
193
+ if (!window) return failure(res, 'no-window');
194
+
195
+ const cached = loadProjectAdvice(
196
+ db, project, window.scope, window.windowFrom, window.windowTo
197
+ );
198
+ return res.json({
199
+ ok: true,
200
+ data: cached
201
+ ? {
202
+ project,
203
+ scope: window.scope,
204
+ windowFrom: window.windowFrom,
205
+ windowTo: window.windowTo,
206
+ advice: cached.payload,
207
+ sessionCount: cached.sessionCount,
208
+ cachedAt: cached.cachedAt,
209
+ cli: cached.cli,
210
+ fromCache: true,
211
+ }
212
+ : {
213
+ project,
214
+ scope: window.scope,
215
+ windowFrom: window.windowFrom,
216
+ windowTo: window.windowTo,
217
+ advice: null,
218
+ fromCache: false,
219
+ cachedAt: null,
220
+ cli: null,
221
+ },
222
+ meta: { generated_at: new Date().toISOString() },
223
+ });
224
+ });
225
+
226
+ // POST /api/project/:key/advice body { force?: boolean }
227
+ router.post('/:key/advice', async (req, res) => {
228
+ const project = canonicalProject(decodeURIComponent(req.params.key || ''));
229
+ if (!project) return failure(res, 'no-project');
230
+ const window = resolveWindow(req.query);
231
+ if (!window) return failure(res, 'no-window');
232
+ const force = req.body?.force === true || req.body?.force === '1';
233
+
234
+ try {
235
+ const result = await generateProjectAdvice(db, {
236
+ project,
237
+ scope: window.scope,
238
+ windowFrom: window.windowFrom,
239
+ windowTo: window.windowTo,
240
+ force,
241
+ });
242
+ if (!result.ok) return failure(res, result.reason, result);
243
+
244
+ return res.json({
245
+ ok: true,
246
+ data: result.data,
247
+ meta: { generated_at: new Date().toISOString() },
248
+ });
249
+ } catch (err) {
250
+ return failure(res, 'internal', { error: err && err.message });
251
+ }
252
+ });
253
+
254
+ return router;
255
+ };
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Report API routes for Agent Boss.
3
+ *
4
+ * Provides endpoints for yesterday, weekly, monthly, and session-detail
5
+ * reports built from analyzed boss.db data.
6
+ *
7
+ * @author Felix
8
+ */
9
+
10
+ const router = require('express').Router();
11
+
12
+ const {
13
+ buildYesterdayReport,
14
+ buildWeeklyReport,
15
+ buildSessionDetail,
16
+ } = require('../analysis/report-builder');
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /**
23
+ * Return the Monday (YYYY-MM-DD) of the current ISO week.
24
+ * @returns {string}
25
+ */
26
+ function currentWeekMonday() {
27
+ const d = new Date();
28
+ const day = d.getDay(); // 0=Sun … 6=Sat
29
+ const diff = day === 0 ? 6 : day - 1;
30
+ d.setDate(d.getDate() - diff);
31
+ const y = d.getFullYear();
32
+ const m = String(d.getMonth() + 1).padStart(2, '0');
33
+ const dd = String(d.getDate()).padStart(2, '0');
34
+ return `${y}-${m}-${dd}`;
35
+ }
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Routes
39
+ // ---------------------------------------------------------------------------
40
+
41
+ /**
42
+ * Create the reports router with database access.
43
+ *
44
+ * @param {object} db sql.js Database instance
45
+ * @returns {import('express').Router}
46
+ */
47
+ module.exports = function (db) {
48
+ // GET /api/reports/yesterday
49
+ router.get('/yesterday', (_req, res) => {
50
+ try {
51
+ const data = buildYesterdayReport(db);
52
+ res.json({
53
+ ok: true,
54
+ data,
55
+ meta: {
56
+ generated_at: new Date().toISOString(),
57
+ period: { from: data.date, to: data.date },
58
+ },
59
+ });
60
+ } catch (err) {
61
+ res.status(500).json({
62
+ ok: false,
63
+ error: { code: 'REPORT_ERROR', message: err.message },
64
+ });
65
+ }
66
+ });
67
+
68
+ // GET /api/reports/weekly?week=YYYY-MM-DD
69
+ router.get('/weekly', (req, res) => {
70
+ try {
71
+ const weekStart = req.query.week || currentWeekMonday();
72
+ const data = buildWeeklyReport(db, weekStart);
73
+ res.json({
74
+ ok: true,
75
+ data,
76
+ meta: {
77
+ generated_at: new Date().toISOString(),
78
+ period: { from: data.weekStart, to: data.weekEnd },
79
+ },
80
+ });
81
+ } catch (err) {
82
+ res.status(500).json({
83
+ ok: false,
84
+ error: { code: 'REPORT_ERROR', message: err.message },
85
+ });
86
+ }
87
+ });
88
+
89
+ // GET /api/reports/monthly?month=YYYY-MM
90
+ router.get('/monthly', (_req, res) => {
91
+ res.json({
92
+ ok: true,
93
+ data: null,
94
+ message: 'Coming soon',
95
+ });
96
+ });
97
+
98
+ // GET /api/reports/session/:id
99
+ router.get('/session/:id', (req, res) => {
100
+ try {
101
+ const data = buildSessionDetail(db, req.params.id);
102
+ if (!data) {
103
+ return res.status(404).json({
104
+ ok: false,
105
+ error: { code: 'NOT_FOUND', message: 'Session not found' },
106
+ });
107
+ }
108
+ res.json({
109
+ ok: true,
110
+ data,
111
+ meta: {
112
+ generated_at: new Date().toISOString(),
113
+ period: { from: data.date, to: data.date },
114
+ },
115
+ });
116
+ } catch (err) {
117
+ res.status(500).json({
118
+ ok: false,
119
+ error: { code: 'REPORT_ERROR', message: err.message },
120
+ });
121
+ }
122
+ });
123
+
124
+ return router;
125
+ };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Session API routes for Agent Boss.
3
+ *
4
+ * Provides endpoints for fetching individual session details, messages,
5
+ * and tool calls.
6
+ *
7
+ * @author Felix
8
+ */
9
+
10
+ const router = require('express').Router();
11
+
12
+ const {
13
+ getSessionById,
14
+ getAnalysisBySession,
15
+ getMessagesBySession,
16
+ getToolCallsBySession,
17
+ } = require('../db/queries');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Routes
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Create the sessions router with database access.
25
+ *
26
+ * @param {object} db sql.js Database instance
27
+ * @returns {import('express').Router}
28
+ */
29
+ module.exports = function (db) {
30
+ // GET /api/sessions/:id
31
+ router.get('/:id', (req, res) => {
32
+ try {
33
+ const session = getSessionById(db, req.params.id);
34
+ if (!session) {
35
+ return res.status(404).json({
36
+ ok: false,
37
+ error: { code: 'NOT_FOUND', message: 'Session not found' },
38
+ });
39
+ }
40
+
41
+ const analysis = getAnalysisBySession(db, req.params.id);
42
+
43
+ res.json({
44
+ ok: true,
45
+ data: { session, analysis },
46
+ meta: {
47
+ generated_at: new Date().toISOString(),
48
+ period: { from: session.date, to: session.date },
49
+ },
50
+ });
51
+ } catch (err) {
52
+ res.status(500).json({
53
+ ok: false,
54
+ error: { code: 'SESSION_ERROR', message: err.message },
55
+ });
56
+ }
57
+ });
58
+
59
+ // GET /api/sessions/:id/messages
60
+ router.get('/:id/messages', (req, res) => {
61
+ try {
62
+ const session = getSessionById(db, req.params.id);
63
+ if (!session) {
64
+ return res.status(404).json({
65
+ ok: false,
66
+ error: { code: 'NOT_FOUND', message: 'Session not found' },
67
+ });
68
+ }
69
+
70
+ const data = getMessagesBySession(db, req.params.id);
71
+
72
+ res.json({
73
+ ok: true,
74
+ data,
75
+ meta: {
76
+ generated_at: new Date().toISOString(),
77
+ period: { from: session.date, to: session.date },
78
+ },
79
+ });
80
+ } catch (err) {
81
+ res.status(500).json({
82
+ ok: false,
83
+ error: { code: 'SESSION_ERROR', message: err.message },
84
+ });
85
+ }
86
+ });
87
+
88
+ // GET /api/sessions/:id/tools
89
+ router.get('/:id/tools', (req, res) => {
90
+ try {
91
+ const session = getSessionById(db, req.params.id);
92
+ if (!session) {
93
+ return res.status(404).json({
94
+ ok: false,
95
+ error: { code: 'NOT_FOUND', message: 'Session not found' },
96
+ });
97
+ }
98
+
99
+ const data = getToolCallsBySession(db, req.params.id);
100
+
101
+ res.json({
102
+ ok: true,
103
+ data,
104
+ meta: {
105
+ generated_at: new Date().toISOString(),
106
+ period: { from: session.date, to: session.date },
107
+ },
108
+ });
109
+ } catch (err) {
110
+ res.status(500).json({
111
+ ok: false,
112
+ error: { code: 'SESSION_ERROR', message: err.message },
113
+ });
114
+ }
115
+ });
116
+
117
+ return router;
118
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Settings API routes for Agent Boss.
3
+ *
4
+ * Provides endpoints for reading/writing user settings and listing
5
+ * tool configurations (data source status).
6
+ *
7
+ * @author Felix
8
+ */
9
+
10
+ const router = require('express').Router();
11
+
12
+ const {
13
+ getAllSettings,
14
+ setSetting,
15
+ getAllToolConfigs,
16
+ } = require('../db/queries');
17
+ const { diagnose: diagnoseLlm, invalidateSettingsCache } = require('../llm/judge');
18
+
19
+ // ---------------------------------------------------------------------------
20
+ // Routes
21
+ // ---------------------------------------------------------------------------
22
+
23
+ /**
24
+ * Create the settings router with database access.
25
+ *
26
+ * @param {object} db sql.js Database instance
27
+ * @returns {import('express').Router}
28
+ */
29
+ module.exports = function (db) {
30
+ // GET /api/settings
31
+ router.get('/', (_req, res) => {
32
+ try {
33
+ const settings = getAllSettings(db);
34
+ const tools = getAllToolConfigs(db);
35
+
36
+ res.json({
37
+ ok: true,
38
+ data: { settings, tools },
39
+ meta: {
40
+ generated_at: new Date().toISOString(),
41
+ },
42
+ });
43
+ } catch (err) {
44
+ res.status(500).json({
45
+ ok: false,
46
+ error: { code: 'SETTINGS_ERROR', message: err.message },
47
+ });
48
+ }
49
+ });
50
+
51
+ // PUT /api/settings
52
+ router.put('/', (req, res) => {
53
+ try {
54
+ const body = req.body;
55
+ if (!body || typeof body !== 'object') {
56
+ return res.status(400).json({
57
+ ok: false,
58
+ error: { code: 'BAD_REQUEST', message: 'Request body must be a JSON object of {key: value} pairs' },
59
+ });
60
+ }
61
+
62
+ const keys = Object.keys(body);
63
+ for (const key of keys) {
64
+ setSetting(db, key, String(body[key]));
65
+ }
66
+
67
+ // v2: invalidate the LLM-judge settings cache so the toggle takes
68
+ // effect on the very next analysis pass.
69
+ if (keys.includes('enable_llm_judge')) invalidateSettingsCache();
70
+
71
+ const settings = getAllSettings(db);
72
+
73
+ res.json({
74
+ ok: true,
75
+ data: { settings, updated: keys },
76
+ meta: {
77
+ generated_at: new Date().toISOString(),
78
+ },
79
+ });
80
+ } catch (err) {
81
+ res.status(500).json({
82
+ ok: false,
83
+ error: { code: 'SETTINGS_ERROR', message: err.message },
84
+ });
85
+ }
86
+ });
87
+
88
+ // GET /api/settings/llm/diagnose — detect available CLI for LLM judge.
89
+ router.get('/llm/diagnose', async (_req, res) => {
90
+ try {
91
+ const info = await diagnoseLlm();
92
+ res.json({ ok: true, data: info, meta: { generated_at: new Date().toISOString() } });
93
+ } catch (err) {
94
+ res.status(500).json({ ok: false, error: { code: 'LLM_DIAGNOSE_ERROR', message: err.message } });
95
+ }
96
+ });
97
+
98
+ // GET /api/settings/tools
99
+ router.get('/tools', (_req, res) => {
100
+ try {
101
+ const data = getAllToolConfigs(db);
102
+
103
+ res.json({
104
+ ok: true,
105
+ data,
106
+ meta: {
107
+ generated_at: new Date().toISOString(),
108
+ },
109
+ });
110
+ } catch (err) {
111
+ res.status(500).json({
112
+ ok: false,
113
+ error: { code: 'SETTINGS_ERROR', message: err.message },
114
+ });
115
+ }
116
+ });
117
+
118
+ return router;
119
+ };