ai-control-center 1.15.2

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 (154) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +584 -0
  3. package/bin/aicc.js +772 -0
  4. package/lib/actions/approve.js +71 -0
  5. package/lib/actions/assign-project.js +132 -0
  6. package/lib/actions/browser-test.js +64 -0
  7. package/lib/actions/cleanup.js +174 -0
  8. package/lib/actions/debug.js +298 -0
  9. package/lib/actions/deploy.js +1229 -0
  10. package/lib/actions/fix-bug.js +134 -0
  11. package/lib/actions/new-feature.js +255 -0
  12. package/lib/actions/reject.js +307 -0
  13. package/lib/actions/review.js +706 -0
  14. package/lib/actions/status.js +47 -0
  15. package/lib/agents/browser-qa-agent.js +611 -0
  16. package/lib/agents/payment-agent.js +116 -0
  17. package/lib/agents/suggestion-agent.js +88 -0
  18. package/lib/cli.js +303 -0
  19. package/lib/config.js +243 -0
  20. package/lib/hub/hub-server.js +440 -0
  21. package/lib/hub/project-poller.js +75 -0
  22. package/lib/hub/skill-registry.js +89 -0
  23. package/lib/hub/state-aggregator.js +204 -0
  24. package/lib/index.js +471 -0
  25. package/lib/init/doctor.js +523 -0
  26. package/lib/init/presets.js +222 -0
  27. package/lib/init/skill-fetcher.js +77 -0
  28. package/lib/init/wizard.js +973 -0
  29. package/lib/integrations/codex-runner.js +128 -0
  30. package/lib/integrations/github-actions.js +248 -0
  31. package/lib/integrations/github-reporter.js +229 -0
  32. package/lib/integrations/screenshot-store.js +102 -0
  33. package/lib/openclaw/bridge.js +650 -0
  34. package/lib/openclaw/generate-skill.js +235 -0
  35. package/lib/openclaw/openclaw.json +64 -0
  36. package/lib/orchestrator/autonomous-loop.js +429 -0
  37. package/lib/orchestrator/thread-triggers.js +63 -0
  38. package/lib/roleplay/agent-messenger.js +75 -0
  39. package/lib/roleplay/discussion-threads.js +303 -0
  40. package/lib/roleplay/health-monitor.js +121 -0
  41. package/lib/roleplay/pm-agent.js +513 -0
  42. package/lib/roleplay/roleplay-config.js +25 -0
  43. package/lib/roleplay/room.js +164 -0
  44. package/lib/shared/action-runner.js +2330 -0
  45. package/lib/shared/event-bus.js +185 -0
  46. package/lib/slack/bot.js +378 -0
  47. package/lib/telegram/bot.js +416 -0
  48. package/lib/telegram/commands.js +1267 -0
  49. package/lib/telegram/keyboards.js +113 -0
  50. package/lib/telegram/notifications.js +247 -0
  51. package/lib/twitch/bot.js +354 -0
  52. package/lib/twitch/commands.js +302 -0
  53. package/lib/twitch/notifications.js +63 -0
  54. package/lib/utils/achievements.js +191 -0
  55. package/lib/utils/activity-log.js +182 -0
  56. package/lib/utils/agent-leaderboard.js +119 -0
  57. package/lib/utils/audit-logger.js +232 -0
  58. package/lib/utils/codebase-context.js +288 -0
  59. package/lib/utils/codebase-indexer.js +381 -0
  60. package/lib/utils/config-schema.js +230 -0
  61. package/lib/utils/context-compressor.js +172 -0
  62. package/lib/utils/correlation.js +63 -0
  63. package/lib/utils/cost-tracker.js +423 -0
  64. package/lib/utils/cron-scheduler.js +53 -0
  65. package/lib/utils/db-adapter.js +293 -0
  66. package/lib/utils/display.js +272 -0
  67. package/lib/utils/errors.js +116 -0
  68. package/lib/utils/format.js +134 -0
  69. package/lib/utils/intent-engine.js +464 -0
  70. package/lib/utils/mcp-client.js +238 -0
  71. package/lib/utils/model-ab-test.js +164 -0
  72. package/lib/utils/notify.js +122 -0
  73. package/lib/utils/persona-loader.js +80 -0
  74. package/lib/utils/pipeline-lock.js +73 -0
  75. package/lib/utils/pipeline.js +214 -0
  76. package/lib/utils/plugin-runner.js +234 -0
  77. package/lib/utils/rate-limiter.js +84 -0
  78. package/lib/utils/rbac.js +74 -0
  79. package/lib/utils/runner.js +1809 -0
  80. package/lib/utils/security.js +191 -0
  81. package/lib/utils/self-healer.js +144 -0
  82. package/lib/utils/skill-loader.js +255 -0
  83. package/lib/utils/spinner.js +132 -0
  84. package/lib/utils/stage-queue.js +50 -0
  85. package/lib/utils/state-machine.js +89 -0
  86. package/lib/utils/status-bar.js +327 -0
  87. package/lib/utils/token-estimator.js +101 -0
  88. package/lib/utils/ux-analyzer.js +101 -0
  89. package/lib/utils/webhook-emitter.js +83 -0
  90. package/lib/web/public/css/styles.css +417 -0
  91. package/lib/web/public/dark-mode.js +44 -0
  92. package/lib/web/public/hub/kanban.html +206 -0
  93. package/lib/web/public/index.html +45 -0
  94. package/lib/web/public/js/app.js +71 -0
  95. package/lib/web/public/js/ask.js +110 -0
  96. package/lib/web/public/js/dashboard.js +165 -0
  97. package/lib/web/public/js/deploy.js +72 -0
  98. package/lib/web/public/js/feature.js +79 -0
  99. package/lib/web/public/js/health.js +65 -0
  100. package/lib/web/public/js/logs.js +93 -0
  101. package/lib/web/public/js/review.js +123 -0
  102. package/lib/web/public/js/ws-client.js +82 -0
  103. package/lib/web/public/office/css/office.css +678 -0
  104. package/lib/web/public/office/index.html +148 -0
  105. package/lib/web/public/office/js/achievements-ui.js +117 -0
  106. package/lib/web/public/office/js/character.js +1056 -0
  107. package/lib/web/public/office/js/chat-bubbles.js +177 -0
  108. package/lib/web/public/office/js/cost-overlay.js +123 -0
  109. package/lib/web/public/office/js/day-night.js +68 -0
  110. package/lib/web/public/office/js/effects.js +632 -0
  111. package/lib/web/public/office/js/engine.js +146 -0
  112. package/lib/web/public/office/js/feature-ticket.js +216 -0
  113. package/lib/web/public/office/js/hub-client.js +60 -0
  114. package/lib/web/public/office/js/main.js +1757 -0
  115. package/lib/web/public/office/js/office-layout.js +1524 -0
  116. package/lib/web/public/office/js/pathfinding.js +144 -0
  117. package/lib/web/public/office/js/pixel-sprites.js +1454 -0
  118. package/lib/web/public/office/js/progress-bars.js +117 -0
  119. package/lib/web/public/office/js/replay.js +191 -0
  120. package/lib/web/public/office/js/sound-effects.js +91 -0
  121. package/lib/web/public/office/js/sprite-renderer.js +211 -0
  122. package/lib/web/public/office/js/stamina-system.js +89 -0
  123. package/lib/web/public/office/js/ui.js +107 -0
  124. package/lib/web/public/onboarding/index.html +243 -0
  125. package/lib/web/public/timeline/index.html +195 -0
  126. package/lib/web/routes/api.js +499 -0
  127. package/lib/web/routes/logs.js +20 -0
  128. package/lib/web/routes/metrics.js +99 -0
  129. package/lib/web/server.js +183 -0
  130. package/lib/web/ws/handler.js +65 -0
  131. package/package.json +67 -0
  132. package/templates/agent-architect.md +69 -0
  133. package/templates/agent-gemini-pm.md +49 -0
  134. package/templates/agent-gemini-reviewer.md +52 -0
  135. package/templates/copilot-instructions.md +36 -0
  136. package/templates/pipelines/mobile.json +27 -0
  137. package/templates/pipelines/nodejs-api.json +27 -0
  138. package/templates/pipelines/python.json +27 -0
  139. package/templates/pipelines/react.json +27 -0
  140. package/templates/pipelines/salesforce.json +27 -0
  141. package/templates/role-gemini.md +97 -0
  142. package/templates/skill-architect.md +114 -0
  143. package/templates/skill-browser-qa.md +50 -0
  144. package/templates/skill-bug-from-qa.md +58 -0
  145. package/templates/skill-chatbot.md +93 -0
  146. package/templates/skill-implement.md +78 -0
  147. package/templates/skill-openclaw.md +174 -0
  148. package/templates/skill-payment.md +110 -0
  149. package/templates/skill-pm-spec.md +77 -0
  150. package/templates/skill-requirement-capture.md +97 -0
  151. package/templates/skill-review.md +108 -0
  152. package/templates/skill-reviewer-qa.md +44 -0
  153. package/templates/skill-suggestion.md +45 -0
  154. package/templates/skill-template.md +142 -0
@@ -0,0 +1,499 @@
1
+ /**
2
+ * REST API routes for the AI Control Center web dashboard.
3
+ */
4
+ import { Router } from 'express';
5
+ import { existsSync, readFileSync, readdirSync } from 'fs';
6
+ import { join } from 'path';
7
+ import * as actions from '../../shared/action-runner.js';
8
+ import { searchSkills, filterByStage, getRegistry } from '../../hub/skill-registry.js';
9
+ import { requirePermission, checkApiKey } from '../../utils/rbac.js';
10
+ import { metricsRouter } from './metrics.js';
11
+ import { createRateLimiter } from '../../utils/rate-limiter.js';
12
+
13
+ export const apiRouter = Router();
14
+
15
+ // RBAC middleware (opt-in — won't break existing unless configured)
16
+ apiRouter.use((req, res, next) => {
17
+ try {
18
+ const apiKey = req.headers['x-api-key'];
19
+ if (apiKey) {
20
+ const role = checkApiKey(apiKey);
21
+ req.userRole = role;
22
+ }
23
+ } catch (e) { /* RBAC is optional */ }
24
+ next();
25
+ });
26
+
27
+ // ─── Project Info (name, color, icon for office visualizer) ──────────────────
28
+
29
+ apiRouter.get('/info', async (req, res) => {
30
+ try {
31
+ const { getConfig } = await import('../../config.js');
32
+ const cfg = getConfig();
33
+ res.json({
34
+ name: cfg.name || 'Unknown Project',
35
+ color: cfg.color || '#4A90D9',
36
+ icon: cfg.icon || '',
37
+ description: cfg.description || '',
38
+ });
39
+ } catch {
40
+ res.json({ name: 'Unknown Project', color: '#4A90D9', icon: '' });
41
+ }
42
+ });
43
+
44
+ // ─── Status ───────────────────────────────────────────────────────────────────
45
+
46
+ apiRouter.get('/status', (req, res) => {
47
+ res.json(actions.getStatusData());
48
+ });
49
+
50
+ // ─── Health Check ─────────────────────────────────────────────────────────────
51
+
52
+ apiRouter.get('/health', async (req, res) => {
53
+ const data = await actions.getHealthData();
54
+ res.json(data);
55
+ });
56
+
57
+ // ─── Files ────────────────────────────────────────────────────────────────────
58
+
59
+ apiRouter.get('/files/:subdir', (req, res) => {
60
+ const files = actions.listFiles(req.params.subdir);
61
+ res.json(files);
62
+ });
63
+
64
+ apiRouter.get('/files/:subdir/:name', (req, res) => {
65
+ const content = actions.readFile(req.params.subdir, req.params.name);
66
+ if (content === null) return res.status(404).json({ error: 'File not found' });
67
+ res.json({ name: req.params.name, content });
68
+ });
69
+
70
+ // ─── Review Data ──────────────────────────────────────────────────────────────
71
+
72
+ apiRouter.get('/review/latest', (req, res) => {
73
+ const review = actions.getLatestReview();
74
+ if (!review) return res.json({ found: false });
75
+ res.json({ found: true, ...review });
76
+ });
77
+
78
+ // ─── Actions ──────────────────────────────────────────────────────────────────
79
+
80
+ apiRouter.post('/feature', async (req, res) => {
81
+ const { description, mode = 'manual', type = 'feature' } = req.body;
82
+ const result = await actions.runNewFeature(description, mode, type);
83
+ res.json(result);
84
+ });
85
+
86
+ apiRouter.post('/approve', async (req, res) => {
87
+ const result = await actions.runApprove();
88
+ res.json(result);
89
+ });
90
+
91
+ apiRouter.post('/reject', async (req, res) => {
92
+ const { reason } = req.body;
93
+ const result = await actions.runReject(reason);
94
+ res.json(result);
95
+ });
96
+
97
+ apiRouter.post('/deploy', async (req, res) => {
98
+ const { testLevel = 'NoTestRun' } = req.body;
99
+ const result = await actions.runDeploy(testLevel);
100
+ res.json(result);
101
+ });
102
+
103
+ // ─── Cleanup ──────────────────────────────────────────────────────────────────
104
+
105
+ apiRouter.post('/cleanup', async (req, res) => {
106
+ const result = await actions.runCleanup();
107
+ res.json(result);
108
+ });
109
+
110
+ // ─── Toggle Auto-Pilot ───────────────────────────────────────────────────────
111
+
112
+ apiRouter.post('/autopilot', (req, res) => {
113
+ const result = actions.toggleAutoPilot();
114
+ res.json(result);
115
+ });
116
+
117
+ // ─── Reset / Abandon ─────────────────────────────────────────────────────────
118
+
119
+ apiRouter.post('/reset', (req, res) => {
120
+ const result = actions.runReset();
121
+ res.json(result);
122
+ });
123
+
124
+ // ─── Ollama / Local AI ──────────────────────────────────────────────────────
125
+
126
+ apiRouter.post('/ask', async (req, res) => {
127
+ const { question, context } = req.body;
128
+ if (!question) return res.status(400).json({ error: 'Question is required.' });
129
+ const result = await actions.askAI(question, context);
130
+ if (typeof result === 'string') {
131
+ res.json({ success: true, answer: result });
132
+ } else {
133
+ res.json({ success: false, error: result?.error || 'No response' });
134
+ }
135
+ });
136
+
137
+ apiRouter.get('/ai/status', async (req, res) => {
138
+ const status = await actions.getAIProviderStatus();
139
+ res.json(status);
140
+ });
141
+
142
+ // ─── Custom Queries ──────────────────────────────────────────────────────────
143
+
144
+ apiRouter.get('/query/:name', async (req, res) => {
145
+ const result = await actions.runCustomQuery(req.params.name);
146
+ res.json(result);
147
+ });
148
+
149
+ // ─── Review (trigger) ────────────────────────────────────────────────────────
150
+
151
+ apiRouter.post('/review', async (req, res) => {
152
+ const result = await actions.runReview();
153
+ res.json(result);
154
+ });
155
+
156
+ // ─── Fix (Copilot fix for review blockers) ──────────────────────────────────
157
+
158
+ apiRouter.post('/fix', async (req, res) => {
159
+ const result = await actions.runFix();
160
+ res.json(result);
161
+ });
162
+
163
+ // ─── Implement (start/resume Copilot implementation) ────────────────────────
164
+
165
+ apiRouter.post('/implement', async (req, res) => {
166
+ const result = await actions.runImplementation();
167
+ res.json(result);
168
+ });
169
+
170
+ // ─── P0: Cost Tracking ───────────────────────────────────────────────────────
171
+
172
+ apiRouter.get('/costs', (req, res) => {
173
+ const data = actions.getCostsData();
174
+ res.json(data);
175
+ });
176
+
177
+ apiRouter.get('/costs/:featureId', (req, res) => {
178
+ const data = actions.getCostsData(req.params.featureId);
179
+ res.json(data);
180
+ });
181
+
182
+ // ─── P0: Audit Log ──────────────────────────────────────────────────────────
183
+
184
+ apiRouter.get('/audit', (req, res) => {
185
+ const { event, limit } = req.query;
186
+ const filters = {};
187
+ if (event) filters.event = event;
188
+ if (limit) filters.limit = parseInt(limit);
189
+ const data = actions.getAuditData(filters);
190
+ res.json(data);
191
+ });
192
+
193
+ // ─── P0: Plugins ────────────────────────────────────────────────────────────
194
+
195
+ apiRouter.get('/plugins', (req, res) => {
196
+ const data = actions.getPluginsData();
197
+ res.json(data);
198
+ });
199
+
200
+ // ─── Retry from Checkpoint ──────────────────────────────────────────────────
201
+
202
+ apiRouter.post('/retry', async (req, res) => {
203
+ const { fresh = false } = req.body || {};
204
+ const result = await actions.retryFromCheckpoint(fresh);
205
+ res.json(result);
206
+ });
207
+
208
+ // ─── Dry Run ────────────────────────────────────────────────────────────────
209
+
210
+ apiRouter.post('/dry-run', async (req, res) => {
211
+ const { description = 'Test feature (dry run)' } = req.body || {};
212
+ const result = await actions.runDryRun(description);
213
+ res.json(result);
214
+ });
215
+
216
+ // ─── Skill Registry ─────────────────────────────────────────────────────────
217
+
218
+ apiRouter.get('/skills', (req, res) => {
219
+ try {
220
+ const { q, stage } = req.query;
221
+ let skills;
222
+ if (q) {
223
+ skills = searchSkills(q);
224
+ } else if (stage) {
225
+ skills = filterByStage(stage);
226
+ } else {
227
+ const registry = getRegistry();
228
+ skills = registry.skills || [];
229
+ }
230
+ res.json({ skills });
231
+ } catch (e) {
232
+ res.json({ skills: [], error: e.message });
233
+ }
234
+ });
235
+
236
+ // ─── Pipeline Replay ─────────────────────────────────────────────────────────
237
+
238
+ apiRouter.get('/replay/:featureId', (req, res) => {
239
+ try {
240
+ const { featureId } = req.params;
241
+ const logsDir = join(process.cwd(), '.ai-workflow', 'logs');
242
+ const events = [];
243
+
244
+ if (existsSync(logsDir)) {
245
+ const files = readdirSync(logsDir)
246
+ .filter(f => f.endsWith('.log') || f.endsWith('.jsonl'))
247
+ .sort();
248
+
249
+ for (const file of files) {
250
+ try {
251
+ const content = readFileSync(join(logsDir, file), 'utf8');
252
+ const lines = content.split('\n').filter(Boolean);
253
+ for (const line of lines) {
254
+ try {
255
+ const entry = JSON.parse(line);
256
+ if (!featureId || entry.featureId === featureId || entry.feature === featureId) {
257
+ events.push({
258
+ timestamp: new Date(entry.ts || entry.timestamp || 0).getTime(),
259
+ stage: entry.stage || entry.type || '',
260
+ agent: entry.agent || entry.ai || '',
261
+ message: entry.message || entry.msg || '',
262
+ type: entry.type || 'info',
263
+ progress: entry.progress,
264
+ });
265
+ }
266
+ } catch {
267
+ const match = line.match(/^(\S+)\s+(\S+)\s+[·✓✗]\s+(.+)$/);
268
+ if (match) {
269
+ events.push({
270
+ timestamp: new Date(match[1]).getTime() || Date.now(),
271
+ stage: '',
272
+ agent: match[2].toLowerCase(),
273
+ message: match[3],
274
+ type: 'info',
275
+ });
276
+ }
277
+ }
278
+ }
279
+ } catch { /* skip unreadable files */ }
280
+ }
281
+ }
282
+
283
+ const activityLog = join(process.cwd(), '.ai-workflow', 'activity.jsonl');
284
+ if (existsSync(activityLog)) {
285
+ try {
286
+ const content = readFileSync(activityLog, 'utf8');
287
+ for (const line of content.split('\n').filter(Boolean)) {
288
+ try {
289
+ const entry = JSON.parse(line);
290
+ if (!featureId || entry.featureId === featureId) {
291
+ events.push({
292
+ timestamp: new Date(entry.ts || 0).getTime(),
293
+ stage: entry.stage || '',
294
+ agent: entry.ai || entry.agent || '',
295
+ message: entry.message || '',
296
+ type: entry.type || 'info',
297
+ });
298
+ }
299
+ } catch { /* skip */ }
300
+ }
301
+ } catch { /* skip */ }
302
+ }
303
+
304
+ events.sort((a, b) => a.timestamp - b.timestamp);
305
+ res.json({ featureId, events, total: events.length });
306
+ } catch (e) {
307
+ res.json({ featureId: req.params.featureId, events: [], error: e.message });
308
+ }
309
+ });
310
+
311
+ // ─── Leaderboard ─────────────────────────────────────────────────────────────
312
+
313
+ apiRouter.get('/leaderboard', async (req, res) => {
314
+ try {
315
+ const { getLeaderboard } = await import('../../utils/agent-leaderboard.js');
316
+ const entries = getLeaderboard();
317
+ res.json({ entries });
318
+ } catch (e) {
319
+ res.json({ entries: [], error: e.message });
320
+ }
321
+ });
322
+
323
+ // ─── Discussion Threads (V2) ─────────────────────────────────────────────────
324
+
325
+ apiRouter.get('/threads', async (req, res) => {
326
+ try {
327
+ const { listThreads } = await import('../../roleplay/discussion-threads.js');
328
+ const { status, type, limit } = req.query;
329
+ res.json(listThreads({ status, type, limit: parseInt(limit) || 20 }));
330
+ } catch (e) {
331
+ res.json([]);
332
+ }
333
+ });
334
+
335
+ apiRouter.get('/threads/:id', async (req, res) => {
336
+ try {
337
+ const { getThread } = await import('../../roleplay/discussion-threads.js');
338
+ const thread = getThread(req.params.id);
339
+ if (!thread) return res.status(404).json({ error: 'Thread not found' });
340
+ res.json(thread);
341
+ } catch (e) {
342
+ res.status(404).json({ error: e.message });
343
+ }
344
+ });
345
+
346
+ apiRouter.post('/threads/:id/reply', async (req, res) => {
347
+ try {
348
+ const { postToThread } = await import('../../roleplay/discussion-threads.js');
349
+ const { from, content } = req.body;
350
+ if (!from || !content) return res.status(400).json({ error: 'from and content required' });
351
+ const thread = postToThread(req.params.id, { from, content });
352
+ res.json(thread);
353
+ } catch (e) {
354
+ res.status(400).json({ error: e.message });
355
+ }
356
+ });
357
+
358
+ apiRouter.post('/threads/:id/resolve', async (req, res) => {
359
+ try {
360
+ const { resolveThread } = await import('../../roleplay/discussion-threads.js');
361
+ const { by, resolution } = req.body;
362
+ const thread = resolveThread(req.params.id, { by: by || 'CEO', resolution });
363
+ res.json(thread);
364
+ } catch (e) {
365
+ res.status(400).json({ error: e.message });
366
+ }
367
+ });
368
+
369
+ // ─── Browser QA ──────────────────────────────────────────────────────────────
370
+
371
+ apiRouter.post('/qa', async (req, res) => {
372
+ try {
373
+ const result = await actions.runBrowserQA();
374
+ res.json(result);
375
+ } catch (e) {
376
+ res.status(500).json({ error: e.message });
377
+ }
378
+ });
379
+
380
+ // ─── Suggestions ──────────────────────────────────────────────────────────────
381
+
382
+ apiRouter.post('/suggest', async (req, res) => {
383
+ try {
384
+ const { runSuggestionAgent } = await import('../../agents/suggestion-agent.js');
385
+ const result = await runSuggestionAgent();
386
+ res.json(result);
387
+ } catch (e) {
388
+ res.status(500).json({ error: e.message });
389
+ }
390
+ });
391
+
392
+ // ─── Project Assignment ──────────────────────────────────────────────────────
393
+
394
+ apiRouter.post('/assign', async (req, res) => {
395
+ try {
396
+ const { url, goal, credentials } = req.body;
397
+ const { assignProjectAction } = await import('../../actions/assign-project.js');
398
+ const result = await assignProjectAction({ url, goal, credentials });
399
+ res.json(result);
400
+ } catch (e) {
401
+ res.status(500).json({ error: e.message });
402
+ }
403
+ });
404
+
405
+ // ─── Suggestions (V2) ───────────────────────────────────────────────────────
406
+
407
+ apiRouter.get('/suggestions', async (req, res) => {
408
+ try {
409
+ const { existsSync, readdirSync, readFileSync } = await import('fs');
410
+ const { resolve } = await import('path');
411
+ const { getWorkflowDir } = await import('../../utils/pipeline.js');
412
+
413
+ const dir = resolve(getWorkflowDir(), 'feature-suggestions');
414
+ if (!existsSync(dir)) return res.json([]);
415
+
416
+ const files = readdirSync(dir)
417
+ .filter(f => f.startsWith('SUGGEST-') && f.endsWith('.json'))
418
+ .sort()
419
+ .reverse()
420
+ .slice(0, 10);
421
+
422
+ const suggestions = files.map(f => {
423
+ try { return JSON.parse(readFileSync(resolve(dir, f), 'utf8')); }
424
+ catch { return null; }
425
+ }).filter(Boolean);
426
+
427
+ res.json(suggestions);
428
+ } catch (e) {
429
+ res.json([]);
430
+ }
431
+ });
432
+
433
+ apiRouter.post('/suggestions/:index/accept', async (req, res) => {
434
+ try {
435
+ const { index } = req.params;
436
+ const { existsSync, readdirSync, readFileSync } = await import('fs');
437
+ const { resolve } = await import('path');
438
+ const { getWorkflowDir } = await import('../../utils/pipeline.js');
439
+
440
+ const dir = resolve(getWorkflowDir(), 'feature-suggestions');
441
+ if (!existsSync(dir)) return res.status(404).json({ error: 'No suggestions found' });
442
+
443
+ const files = readdirSync(dir).filter(f => f.startsWith('SUGGEST-')).sort().reverse();
444
+ if (files.length === 0) return res.status(404).json({ error: 'No suggestions found' });
445
+
446
+ const data = JSON.parse(readFileSync(resolve(dir, files[0]), 'utf8'));
447
+ const suggestion = data.suggestions?.[parseInt(index)];
448
+ if (!suggestion) return res.status(404).json({ error: 'Suggestion index out of range' });
449
+
450
+ const result = await actions.runNewFeature(
451
+ `${suggestion.title}: ${suggestion.description || suggestion.why}`,
452
+ 'auto',
453
+ 'feature',
454
+ );
455
+
456
+ res.json({ success: true, featureId: result.featureId, suggestion });
457
+ } catch (e) {
458
+ res.status(500).json({ error: e.message });
459
+ }
460
+ });
461
+
462
+ // ─── V2 Health Check (enhanced) ─────────────────────────────────────────────
463
+
464
+ apiRouter.get('/v2/health', async (req, res) => {
465
+ try {
466
+ const { getStatus } = await import('../../utils/pipeline.js');
467
+ const { isLocked } = await import('../../utils/pipeline-lock.js');
468
+ const { getCurrentRunCost } = await import('../../utils/security.js');
469
+
470
+ const status = getStatus();
471
+ const lock = isLocked();
472
+ const cost = getCurrentRunCost();
473
+
474
+ res.json({
475
+ status: 'running',
476
+ pipeline: {
477
+ stage: status.stage || 'idle',
478
+ featureId: status.featureId,
479
+ lastTransition: status.transitionedAt,
480
+ error: status.error,
481
+ },
482
+ lock: {
483
+ locked: lock.locked,
484
+ holder: lock.holder,
485
+ },
486
+ cost: {
487
+ currentRun: cost,
488
+ },
489
+ uptime: process.uptime(),
490
+ timestamp: new Date().toISOString(),
491
+ });
492
+ } catch (e) {
493
+ res.status(500).json({ error: e.message });
494
+ }
495
+ });
496
+
497
+ // ─── Metrics ────────────────────────────────────────────────────────────────
498
+
499
+ apiRouter.use('/metrics', metricsRouter);
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Log file API routes.
3
+ */
4
+ import { Router } from 'express';
5
+ import * as actions from '../../shared/action-runner.js';
6
+
7
+ export const logsRouter = Router();
8
+
9
+ // GET /api/logs — latest session log tail
10
+ logsRouter.get('/', (req, res) => {
11
+ const lines = parseInt(req.query.lines) || 100;
12
+ const data = actions.getLogsData(lines);
13
+ res.json(data);
14
+ });
15
+
16
+ // GET /api/logs/sessions — list all session log files
17
+ logsRouter.get('/sessions', (req, res) => {
18
+ const data = actions.getLogsData(0);
19
+ res.json({ sessions: data.sessions });
20
+ });
@@ -0,0 +1,99 @@
1
+ import { Router } from 'express';
2
+ import { getStatus } from '../../utils/pipeline.js';
3
+ import { getCostSummary } from '../../utils/cost-tracker.js';
4
+
5
+ const METRIC_TYPES = {
6
+ aicc_pipeline_runs_total: { type: 'counter', help: 'Total pipeline runs' },
7
+ aicc_stage_duration_seconds: { type: 'gauge', help: 'Last observed stage duration in seconds' },
8
+ aicc_ai_calls_total: { type: 'counter', help: 'Total AI API calls' },
9
+ aicc_ai_errors_total: { type: 'counter', help: 'Total AI errors' },
10
+ aicc_tokens_used_total: { type: 'counter', help: 'Total tokens consumed' },
11
+ aicc_cost_dollars_total: { type: 'gauge', help: 'Total cost in dollars' },
12
+ aicc_circuit_breaker_state: { type: 'gauge', help: 'Circuit breaker state (0=closed, 1=open, 0.5=half_open)' },
13
+ aicc_pipeline_current_stage: { type: 'gauge', help: 'Current pipeline stage info' },
14
+ };
15
+
16
+ /** @type {Map<string, number>} key = "metric_name{label=value,...}" */
17
+ const metrics = new Map();
18
+
19
+ function labelsToString(labels) {
20
+ if (!labels || Object.keys(labels).length === 0) return '';
21
+ const parts = Object.entries(labels)
22
+ .map(([k, v]) => `${k}="${v}"`)
23
+ .join(',');
24
+ return `{${parts}}`;
25
+ }
26
+
27
+ function metricKey(name, labels) {
28
+ return `${name}${labelsToString(labels)}`;
29
+ }
30
+
31
+ export function recordMetric(name, labels, value) {
32
+ metrics.set(metricKey(name, labels), value);
33
+ }
34
+
35
+ export function incrementCounter(name, labels) {
36
+ const key = metricKey(name, labels);
37
+ metrics.set(key, (metrics.get(key) || 0) + 1);
38
+ }
39
+
40
+ export function resetMetrics() {
41
+ metrics.clear();
42
+ }
43
+
44
+ function injectLiveMetrics() {
45
+ try {
46
+ const status = getStatus();
47
+ if (status.stage) {
48
+ recordMetric('aicc_pipeline_current_stage', { stage: status.stage }, 1);
49
+ }
50
+ } catch { /* config may not be loaded */ }
51
+
52
+ try {
53
+ const costs = getCostSummary();
54
+ if (costs.byProvider) {
55
+ for (const [provider, data] of Object.entries(costs.byProvider)) {
56
+ recordMetric('aicc_cost_dollars_total', { provider }, data.cost || 0);
57
+ recordMetric('aicc_tokens_used_total', { provider, stage: 'all' }, (data.inputTokens || 0) + (data.outputTokens || 0));
58
+ }
59
+ }
60
+ } catch { /* cost tracker may not be available */ }
61
+ }
62
+
63
+ function formatMetrics() {
64
+ const lines = [];
65
+ const grouped = new Map();
66
+
67
+ for (const [key, value] of metrics) {
68
+ const name = key.includes('{') ? key.slice(0, key.indexOf('{')) : key;
69
+ if (!grouped.has(name)) grouped.set(name, []);
70
+ grouped.get(name).push({ key, value });
71
+ }
72
+
73
+ for (const [name, entries] of grouped) {
74
+ const meta = METRIC_TYPES[name];
75
+ if (meta) {
76
+ lines.push(`# HELP ${name} ${meta.help}`);
77
+ lines.push(`# TYPE ${name} ${meta.type}`);
78
+ }
79
+ for (const { key, value } of entries) {
80
+ lines.push(`${key} ${value}`);
81
+ }
82
+ lines.push('');
83
+ }
84
+
85
+ return lines.join('\n');
86
+ }
87
+
88
+ export const metricsRouter = Router();
89
+
90
+ metricsRouter.get('/', (_req, res) => {
91
+ try {
92
+ injectLiveMetrics();
93
+ const body = formatMetrics();
94
+ res.set('Content-Type', 'text/plain; version=0.0.4');
95
+ res.send(body);
96
+ } catch (err) {
97
+ res.status(500).send(`# error generating metrics\n# ${err.message}\n`);
98
+ }
99
+ });