steroids-api 0.2.7
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/dist/API/src/index.d.ts +10 -0
- package/dist/API/src/index.d.ts.map +1 -0
- package/dist/API/src/index.js +130 -0
- package/dist/API/src/index.js.map +1 -0
- package/dist/API/src/routes/activity.d.ts +7 -0
- package/dist/API/src/routes/activity.d.ts.map +1 -0
- package/dist/API/src/routes/activity.js +252 -0
- package/dist/API/src/routes/activity.js.map +1 -0
- package/dist/API/src/routes/config.d.ts +7 -0
- package/dist/API/src/routes/config.d.ts.map +1 -0
- package/dist/API/src/routes/config.js +521 -0
- package/dist/API/src/routes/config.js.map +1 -0
- package/dist/API/src/routes/health.d.ts +7 -0
- package/dist/API/src/routes/health.d.ts.map +1 -0
- package/dist/API/src/routes/health.js +172 -0
- package/dist/API/src/routes/health.js.map +1 -0
- package/dist/API/src/routes/incidents.d.ts +7 -0
- package/dist/API/src/routes/incidents.d.ts.map +1 -0
- package/dist/API/src/routes/incidents.js +117 -0
- package/dist/API/src/routes/incidents.js.map +1 -0
- package/dist/API/src/routes/projects.d.ts +7 -0
- package/dist/API/src/routes/projects.d.ts.map +1 -0
- package/dist/API/src/routes/projects.js +398 -0
- package/dist/API/src/routes/projects.js.map +1 -0
- package/dist/API/src/routes/runners.d.ts +7 -0
- package/dist/API/src/routes/runners.d.ts.map +1 -0
- package/dist/API/src/routes/runners.js +242 -0
- package/dist/API/src/routes/runners.js.map +1 -0
- package/dist/API/src/routes/tasks.d.ts +7 -0
- package/dist/API/src/routes/tasks.d.ts.map +1 -0
- package/dist/API/src/routes/tasks.js +1007 -0
- package/dist/API/src/routes/tasks.js.map +1 -0
- package/dist/API/src/utils/validation.d.ts +22 -0
- package/dist/API/src/utils/validation.d.ts.map +1 -0
- package/dist/API/src/utils/validation.js +50 -0
- package/dist/API/src/utils/validation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/activity.d.ts +7 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +252 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/config.d.ts +7 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +647 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/credit-alerts.d.ts +2 -0
- package/dist/routes/credit-alerts.d.ts.map +1 -0
- package/dist/routes/credit-alerts.js +97 -0
- package/dist/routes/credit-alerts.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +200 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/incidents.d.ts +7 -0
- package/dist/routes/incidents.d.ts.map +1 -0
- package/dist/routes/incidents.js +117 -0
- package/dist/routes/incidents.js.map +1 -0
- package/dist/routes/projects.d.ts +7 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +643 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/runners.d.ts +7 -0
- package/dist/routes/runners.d.ts.map +1 -0
- package/dist/routes/runners.js +299 -0
- package/dist/routes/runners.js.map +1 -0
- package/dist/routes/skills.d.ts +3 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +109 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/storage.d.ts +7 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +93 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/tasks.d.ts +7 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +1145 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/src/cleanup/invocation-logs.d.ts +30 -0
- package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
- package/dist/src/cleanup/invocation-logs.js +66 -0
- package/dist/src/cleanup/invocation-logs.js.map +1 -0
- package/dist/src/commands/loop-phases.d.ts +11 -0
- package/dist/src/commands/loop-phases.d.ts.map +1 -0
- package/dist/src/commands/loop-phases.js +304 -0
- package/dist/src/commands/loop-phases.js.map +1 -0
- package/dist/src/config/loader.d.ts +160 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +276 -0
- package/dist/src/config/loader.js.map +1 -0
- package/dist/src/database/connection.d.ts +35 -0
- package/dist/src/database/connection.d.ts.map +1 -0
- package/dist/src/database/connection.js +197 -0
- package/dist/src/database/connection.js.map +1 -0
- package/dist/src/database/queries.d.ts +220 -0
- package/dist/src/database/queries.d.ts.map +1 -0
- package/dist/src/database/queries.js +589 -0
- package/dist/src/database/queries.js.map +1 -0
- package/dist/src/database/schema.d.ts +8 -0
- package/dist/src/database/schema.d.ts.map +1 -0
- package/dist/src/database/schema.js +184 -0
- package/dist/src/database/schema.js.map +1 -0
- package/dist/src/git/push.d.ts +26 -0
- package/dist/src/git/push.d.ts.map +1 -0
- package/dist/src/git/push.js +91 -0
- package/dist/src/git/push.js.map +1 -0
- package/dist/src/git/status.d.ts +83 -0
- package/dist/src/git/status.d.ts.map +1 -0
- package/dist/src/git/status.js +315 -0
- package/dist/src/git/status.js.map +1 -0
- package/dist/src/health/stuck-task-detector.d.ts +131 -0
- package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
- package/dist/src/health/stuck-task-detector.js +233 -0
- package/dist/src/health/stuck-task-detector.js.map +1 -0
- package/dist/src/health/stuck-task-recovery.d.ts +45 -0
- package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
- package/dist/src/health/stuck-task-recovery.js +309 -0
- package/dist/src/health/stuck-task-recovery.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +130 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/locking/queries.d.ts +116 -0
- package/dist/src/locking/queries.d.ts.map +1 -0
- package/dist/src/locking/queries.js +232 -0
- package/dist/src/locking/queries.js.map +1 -0
- package/dist/src/locking/section-lock.d.ts +74 -0
- package/dist/src/locking/section-lock.d.ts.map +1 -0
- package/dist/src/locking/section-lock.js +196 -0
- package/dist/src/locking/section-lock.js.map +1 -0
- package/dist/src/locking/task-lock.d.ts +92 -0
- package/dist/src/locking/task-lock.d.ts.map +1 -0
- package/dist/src/locking/task-lock.js +233 -0
- package/dist/src/locking/task-lock.js.map +1 -0
- package/dist/src/migrations/index.d.ts +7 -0
- package/dist/src/migrations/index.d.ts.map +1 -0
- package/dist/src/migrations/index.js +9 -0
- package/dist/src/migrations/index.js.map +1 -0
- package/dist/src/migrations/manifest.d.ts +92 -0
- package/dist/src/migrations/manifest.d.ts.map +1 -0
- package/dist/src/migrations/manifest.js +255 -0
- package/dist/src/migrations/manifest.js.map +1 -0
- package/dist/src/migrations/runner.d.ts +84 -0
- package/dist/src/migrations/runner.d.ts.map +1 -0
- package/dist/src/migrations/runner.js +338 -0
- package/dist/src/migrations/runner.js.map +1 -0
- package/dist/src/orchestrator/coder.d.ts +32 -0
- package/dist/src/orchestrator/coder.d.ts.map +1 -0
- package/dist/src/orchestrator/coder.js +170 -0
- package/dist/src/orchestrator/coder.js.map +1 -0
- package/dist/src/orchestrator/coordinator.d.ts +28 -0
- package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
- package/dist/src/orchestrator/coordinator.js +252 -0
- package/dist/src/orchestrator/coordinator.js.map +1 -0
- package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
- package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
- package/dist/src/orchestrator/fallback-handler.js +280 -0
- package/dist/src/orchestrator/fallback-handler.js.map +1 -0
- package/dist/src/orchestrator/invoke.d.ts +14 -0
- package/dist/src/orchestrator/invoke.d.ts.map +1 -0
- package/dist/src/orchestrator/invoke.js +76 -0
- package/dist/src/orchestrator/invoke.js.map +1 -0
- package/dist/src/orchestrator/post-coder.d.ts +10 -0
- package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
- package/dist/src/orchestrator/post-coder.js +198 -0
- package/dist/src/orchestrator/post-coder.js.map +1 -0
- package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
- package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/post-reviewer.js +199 -0
- package/dist/src/orchestrator/post-reviewer.js.map +1 -0
- package/dist/src/orchestrator/reviewer.d.ts +35 -0
- package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/reviewer.js +237 -0
- package/dist/src/orchestrator/reviewer.js.map +1 -0
- package/dist/src/orchestrator/schemas.d.ts +10 -0
- package/dist/src/orchestrator/schemas.d.ts.map +1 -0
- package/dist/src/orchestrator/schemas.js +81 -0
- package/dist/src/orchestrator/schemas.js.map +1 -0
- package/dist/src/orchestrator/task-selector.d.ts +102 -0
- package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
- package/dist/src/orchestrator/task-selector.js +326 -0
- package/dist/src/orchestrator/task-selector.js.map +1 -0
- package/dist/src/orchestrator/types.d.ts +74 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +5 -0
- package/dist/src/orchestrator/types.js.map +1 -0
- package/dist/src/prompts/coder.d.ts +36 -0
- package/dist/src/prompts/coder.d.ts.map +1 -0
- package/dist/src/prompts/coder.js +303 -0
- package/dist/src/prompts/coder.js.map +1 -0
- package/dist/src/prompts/prompt-helpers.d.ts +51 -0
- package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
- package/dist/src/prompts/prompt-helpers.js +299 -0
- package/dist/src/prompts/prompt-helpers.js.map +1 -0
- package/dist/src/prompts/reviewer.d.ts +40 -0
- package/dist/src/prompts/reviewer.d.ts.map +1 -0
- package/dist/src/prompts/reviewer.js +416 -0
- package/dist/src/prompts/reviewer.js.map +1 -0
- package/dist/src/providers/claude.d.ts +53 -0
- package/dist/src/providers/claude.d.ts.map +1 -0
- package/dist/src/providers/claude.js +227 -0
- package/dist/src/providers/claude.js.map +1 -0
- package/dist/src/providers/codex.d.ts +53 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +253 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/gemini.d.ts +58 -0
- package/dist/src/providers/gemini.d.ts.map +1 -0
- package/dist/src/providers/gemini.js +240 -0
- package/dist/src/providers/gemini.js.map +1 -0
- package/dist/src/providers/interface.d.ts +185 -0
- package/dist/src/providers/interface.d.ts.map +1 -0
- package/dist/src/providers/interface.js +92 -0
- package/dist/src/providers/interface.js.map +1 -0
- package/dist/src/providers/invocation-logger.d.ts +97 -0
- package/dist/src/providers/invocation-logger.d.ts.map +1 -0
- package/dist/src/providers/invocation-logger.js +378 -0
- package/dist/src/providers/invocation-logger.js.map +1 -0
- package/dist/src/providers/openai.d.ts +53 -0
- package/dist/src/providers/openai.d.ts.map +1 -0
- package/dist/src/providers/openai.js +230 -0
- package/dist/src/providers/openai.js.map +1 -0
- package/dist/src/providers/registry.d.ts +100 -0
- package/dist/src/providers/registry.d.ts.map +1 -0
- package/dist/src/providers/registry.js +170 -0
- package/dist/src/providers/registry.js.map +1 -0
- package/dist/src/routes/activity.d.ts +7 -0
- package/dist/src/routes/activity.d.ts.map +1 -0
- package/dist/src/routes/activity.js +252 -0
- package/dist/src/routes/activity.js.map +1 -0
- package/dist/src/routes/config.d.ts +7 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +521 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/health.d.ts +7 -0
- package/dist/src/routes/health.d.ts.map +1 -0
- package/dist/src/routes/health.js +172 -0
- package/dist/src/routes/health.js.map +1 -0
- package/dist/src/routes/incidents.d.ts +7 -0
- package/dist/src/routes/incidents.d.ts.map +1 -0
- package/dist/src/routes/incidents.js +117 -0
- package/dist/src/routes/incidents.js.map +1 -0
- package/dist/src/routes/projects.d.ts +7 -0
- package/dist/src/routes/projects.d.ts.map +1 -0
- package/dist/src/routes/projects.js +398 -0
- package/dist/src/routes/projects.js.map +1 -0
- package/dist/src/routes/runners.d.ts +7 -0
- package/dist/src/routes/runners.d.ts.map +1 -0
- package/dist/src/routes/runners.js +242 -0
- package/dist/src/routes/runners.js.map +1 -0
- package/dist/src/routes/tasks.d.ts +7 -0
- package/dist/src/routes/tasks.d.ts.map +1 -0
- package/dist/src/routes/tasks.js +1007 -0
- package/dist/src/routes/tasks.js.map +1 -0
- package/dist/src/runners/activity-log.d.ts +65 -0
- package/dist/src/runners/activity-log.d.ts.map +1 -0
- package/dist/src/runners/activity-log.js +140 -0
- package/dist/src/runners/activity-log.js.map +1 -0
- package/dist/src/runners/cron.d.ts +30 -0
- package/dist/src/runners/cron.d.ts.map +1 -0
- package/dist/src/runners/cron.js +333 -0
- package/dist/src/runners/cron.js.map +1 -0
- package/dist/src/runners/daemon.d.ts +71 -0
- package/dist/src/runners/daemon.d.ts.map +1 -0
- package/dist/src/runners/daemon.js +233 -0
- package/dist/src/runners/daemon.js.map +1 -0
- package/dist/src/runners/global-db.d.ts +31 -0
- package/dist/src/runners/global-db.d.ts.map +1 -0
- package/dist/src/runners/global-db.js +220 -0
- package/dist/src/runners/global-db.js.map +1 -0
- package/dist/src/runners/hang-detector.d.ts +38 -0
- package/dist/src/runners/hang-detector.d.ts.map +1 -0
- package/dist/src/runners/hang-detector.js +130 -0
- package/dist/src/runners/hang-detector.js.map +1 -0
- package/dist/src/runners/heartbeat.d.ts +39 -0
- package/dist/src/runners/heartbeat.d.ts.map +1 -0
- package/dist/src/runners/heartbeat.js +71 -0
- package/dist/src/runners/heartbeat.js.map +1 -0
- package/dist/src/runners/lock.d.ts +47 -0
- package/dist/src/runners/lock.d.ts.map +1 -0
- package/dist/src/runners/lock.js +140 -0
- package/dist/src/runners/lock.js.map +1 -0
- package/dist/src/runners/orchestrator-loop.d.ts +20 -0
- package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
- package/dist/src/runners/orchestrator-loop.js +208 -0
- package/dist/src/runners/orchestrator-loop.js.map +1 -0
- package/dist/src/runners/projects.d.ts +96 -0
- package/dist/src/runners/projects.d.ts.map +1 -0
- package/dist/src/runners/projects.js +243 -0
- package/dist/src/runners/projects.js.map +1 -0
- package/dist/src/runners/wakeup.d.ts +37 -0
- package/dist/src/runners/wakeup.d.ts.map +1 -0
- package/dist/src/runners/wakeup.js +355 -0
- package/dist/src/runners/wakeup.js.map +1 -0
- package/dist/src/utils/validation.d.ts +22 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +50 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/dist/utils/sqlite.d.ts +17 -0
- package/dist/utils/sqlite.d.ts.map +1 -0
- package/dist/utils/sqlite.js +27 -0
- package/dist/utils/sqlite.js.map +1 -0
- package/dist/utils/storage-cache.d.ts +33 -0
- package/dist/utils/storage-cache.d.ts.map +1 -0
- package/dist/utils/storage-cache.js +81 -0
- package/dist/utils/storage-cache.js.map +1 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +51 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +199 -0
- package/src/routes/activity.ts +302 -0
- package/src/routes/config.ts +723 -0
- package/src/routes/credit-alerts.ts +73 -0
- package/src/routes/health.ts +219 -0
- package/src/routes/incidents.ts +131 -0
- package/src/routes/projects.ts +854 -0
- package/src/routes/runners.ts +357 -0
- package/src/routes/skills.ts +127 -0
- package/src/routes/storage.ts +108 -0
- package/src/routes/tasks.ts +1372 -0
- package/src/utils/sqlite.ts +36 -0
- package/src/utils/storage-cache.ts +107 -0
- package/src/utils/validation.ts +61 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path validation utilities for API security
|
|
3
|
+
*/
|
|
4
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
/**
|
|
7
|
+
* Validate that a path is safe and points to a valid Steroids project
|
|
8
|
+
*
|
|
9
|
+
* @param path - Path to validate
|
|
10
|
+
* @returns True if path is valid and safe
|
|
11
|
+
*/
|
|
12
|
+
export function isValidProjectPath(path) {
|
|
13
|
+
try {
|
|
14
|
+
const realPath = realpathSync(path);
|
|
15
|
+
// Must contain .steroids directory with database
|
|
16
|
+
const steroidsDb = join(realPath, '.steroids', 'steroids.db');
|
|
17
|
+
if (!existsSync(steroidsDb)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
// Must not be system directories
|
|
21
|
+
const forbidden = ['/etc', '/var', '/usr', '/bin', '/sbin'];
|
|
22
|
+
if (forbidden.some((f) => realPath.startsWith(f)) ||
|
|
23
|
+
(realPath.startsWith('/System') && !realPath.startsWith('/System/Volumes/Data'))) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate request body for path-based operations
|
|
34
|
+
*
|
|
35
|
+
* @param body - Request body
|
|
36
|
+
* @returns Validation result with error message if invalid
|
|
37
|
+
*/
|
|
38
|
+
export function validatePathRequest(body) {
|
|
39
|
+
if (!body || typeof body !== 'object') {
|
|
40
|
+
return { valid: false, error: 'Request body must be an object' };
|
|
41
|
+
}
|
|
42
|
+
const { path } = body;
|
|
43
|
+
if (!path || typeof path !== 'string') {
|
|
44
|
+
return { valid: false, error: 'Request body must contain a "path" string field' };
|
|
45
|
+
}
|
|
46
|
+
if (path.trim().length === 0) {
|
|
47
|
+
return { valid: false, error: 'Path cannot be empty' };
|
|
48
|
+
}
|
|
49
|
+
return { valid: true, path };
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validation.js","sourceRoot":"","sources":["../../src/utils/validation.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC;;;;;GAKG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAAY;IAC7C,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC;QAEpC,iDAAiD;QACjD,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,EAAE,WAAW,EAAE,aAAa,CAAC,CAAC;QAC9D,IAAI,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;YAC5B,OAAO,KAAK,CAAC;QACf,CAAC;QAED,iCAAiC;QACjC,MAAM,SAAS,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,CAAC;QAC5D,IACE,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;YAC7C,CAAC,QAAQ,CAAC,UAAU,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,sBAAsB,CAAC,CAAC,EAChF,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAAa;IAC/C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,gCAAgC,EAAE,CAAC;IACnE,CAAC;IAED,MAAM,EAAE,IAAI,EAAE,GAAG,IAA0B,CAAC;IAE5C,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;QACtC,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,iDAAiD,EAAE,CAAC;IACpF,CAAC;IAED,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC7B,OAAO,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,sBAAsB,EAAE,CAAC;IACzD,CAAC;IAED,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AAC/B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "steroids-api",
|
|
3
|
+
"version": "0.2.7",
|
|
4
|
+
"description": "REST API for Steroids multi-project monitoring",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"start": "node dist/API/src/index.js",
|
|
10
|
+
"dev": "tsx watch src/index.ts",
|
|
11
|
+
"test": "jest"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"api",
|
|
15
|
+
"rest",
|
|
16
|
+
"steroids",
|
|
17
|
+
"monitoring"
|
|
18
|
+
],
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@types/tail": "^2.2.3",
|
|
23
|
+
"better-sqlite3": "^11.0.0",
|
|
24
|
+
"cors": "^2.8.5",
|
|
25
|
+
"express": "^4.18.2",
|
|
26
|
+
"tail": "^2.2.6"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
30
|
+
"@types/cors": "^2.8.17",
|
|
31
|
+
"@types/express": "^4.17.21",
|
|
32
|
+
"@types/node": "^20.10.0",
|
|
33
|
+
"tsx": "^4.7.0",
|
|
34
|
+
"typescript": "^5.3.0"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steroids API Server
|
|
3
|
+
* REST API for multi-project monitoring and management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import cors from 'cors';
|
|
8
|
+
import { execSync } from 'node:child_process';
|
|
9
|
+
import { realpathSync, readFileSync, existsSync } from 'node:fs';
|
|
10
|
+
import { resolve, join, dirname } from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
// Load API keys from .zshrc if not already set in the environment.
|
|
14
|
+
// The API server is a daemon and may not inherit interactive shell env vars.
|
|
15
|
+
(function loadEnvFromZshrc() {
|
|
16
|
+
const vars = ['STEROIDS_ANTHROPIC', 'STEROIDS_GOOGLE', 'STEROIDS_MISTRAL', 'STEROIDS_OPENAI'];
|
|
17
|
+
const missing = vars.filter((v) => !process.env[v]);
|
|
18
|
+
if (missing.length === 0) return;
|
|
19
|
+
try {
|
|
20
|
+
const exports = missing.map((v) => `printf "${v}=%s\\n" "$${v}"`).join('; ');
|
|
21
|
+
const out = execSync(
|
|
22
|
+
`zsh -c 'source ~/.zshrc 2>/dev/null; ${exports}'`,
|
|
23
|
+
{ encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }
|
|
24
|
+
).trim();
|
|
25
|
+
for (const line of out.split('\n')) {
|
|
26
|
+
const eq = line.indexOf('=');
|
|
27
|
+
if (eq === -1) continue;
|
|
28
|
+
const key = line.slice(0, eq);
|
|
29
|
+
const val = line.slice(eq + 1).trim();
|
|
30
|
+
if (val && !process.env[key]) process.env[key] = val;
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
// .zshrc not accessible or vars not defined there — continue without them
|
|
34
|
+
}
|
|
35
|
+
}());
|
|
36
|
+
import projectsRouter from './routes/projects.js';
|
|
37
|
+
import storageRouter from './routes/storage.js';
|
|
38
|
+
import activityRouter from './routes/activity.js';
|
|
39
|
+
import runnersRouter from './routes/runners.js';
|
|
40
|
+
import tasksRouter from './routes/tasks.js';
|
|
41
|
+
import configRouter from './routes/config.js';
|
|
42
|
+
import healthRouter from './routes/health.js';
|
|
43
|
+
import incidentsRouter from './routes/incidents.js';
|
|
44
|
+
import skillsRouter from './routes/skills.js';
|
|
45
|
+
import { creditAlertRoutes } from './routes/credit-alerts.js';
|
|
46
|
+
|
|
47
|
+
const PORT = process.env.PORT || 3501;
|
|
48
|
+
const HOST = process.env.HOST || '0.0.0.0';
|
|
49
|
+
|
|
50
|
+
// Get version from package.json
|
|
51
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
52
|
+
const __dirname = dirname(__filename);
|
|
53
|
+
|
|
54
|
+
function getVersion(): string {
|
|
55
|
+
const paths = [
|
|
56
|
+
join(__dirname, '..', 'package.json'), // From src/
|
|
57
|
+
join(__dirname, '..', '..', 'package.json'), // From dist/
|
|
58
|
+
];
|
|
59
|
+
for (const p of paths) {
|
|
60
|
+
try {
|
|
61
|
+
if (existsSync(p)) {
|
|
62
|
+
return JSON.parse(readFileSync(p, 'utf-8')).version;
|
|
63
|
+
}
|
|
64
|
+
} catch { /* continue */ }
|
|
65
|
+
}
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const version = getVersion();
|
|
70
|
+
|
|
71
|
+
export function createApp(): express.Express {
|
|
72
|
+
const app = express();
|
|
73
|
+
|
|
74
|
+
// Middleware - CORS allowing all origins for local network access
|
|
75
|
+
app.use(cors({
|
|
76
|
+
origin: true, // Allow all origins
|
|
77
|
+
credentials: true,
|
|
78
|
+
}));
|
|
79
|
+
app.use(express.json());
|
|
80
|
+
|
|
81
|
+
// Request logging (keep test output clean)
|
|
82
|
+
if (process.env.NODE_ENV !== 'test') {
|
|
83
|
+
app.use((req, res, next) => {
|
|
84
|
+
console.log(`${new Date().toISOString()} ${req.method} ${req.path}`);
|
|
85
|
+
next();
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Routes
|
|
90
|
+
app.use('/api', projectsRouter);
|
|
91
|
+
app.use('/api', storageRouter);
|
|
92
|
+
app.use('/api', activityRouter);
|
|
93
|
+
app.use('/api', runnersRouter);
|
|
94
|
+
app.use('/api', tasksRouter);
|
|
95
|
+
app.use('/api', configRouter);
|
|
96
|
+
app.use('/api', healthRouter);
|
|
97
|
+
app.use('/api', incidentsRouter);
|
|
98
|
+
app.use('/api', skillsRouter);
|
|
99
|
+
app.use('/api/credit-alerts', creditAlertRoutes);
|
|
100
|
+
|
|
101
|
+
// Health check
|
|
102
|
+
app.get('/health', (req, res) => {
|
|
103
|
+
res.json({
|
|
104
|
+
status: 'ok',
|
|
105
|
+
timestamp: new Date().toISOString(),
|
|
106
|
+
version,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// Root endpoint
|
|
111
|
+
app.get('/', (req, res) => {
|
|
112
|
+
res.json({
|
|
113
|
+
name: 'Steroids API',
|
|
114
|
+
version,
|
|
115
|
+
endpoints: [
|
|
116
|
+
'GET /health',
|
|
117
|
+
'GET /api/projects',
|
|
118
|
+
'GET /api/projects/status?path=<path>',
|
|
119
|
+
'GET /api/projects/storage?path=<path>',
|
|
120
|
+
'GET /api/projects/<path>/tasks?status=<status>§ion=<id>&issue=<failed_retries|stale>&hours=<hours>',
|
|
121
|
+
'GET /api/projects/<path>/sections',
|
|
122
|
+
'POST /api/projects',
|
|
123
|
+
'POST /api/projects/remove',
|
|
124
|
+
'POST /api/projects/enable',
|
|
125
|
+
'POST /api/projects/disable',
|
|
126
|
+
'POST /api/projects/prune',
|
|
127
|
+
'GET /api/activity?hours=<hours>&project=<path>',
|
|
128
|
+
'GET /api/runners',
|
|
129
|
+
'GET /api/runners/active-tasks',
|
|
130
|
+
'GET /api/tasks/<taskId>?project=<path>',
|
|
131
|
+
'GET /api/tasks/<taskId>/logs?project=<path>',
|
|
132
|
+
'GET /api/config/schema',
|
|
133
|
+
'GET /api/config/schema/<category>',
|
|
134
|
+
'GET /api/config?scope=global|project|merged&project=<path>',
|
|
135
|
+
'PUT /api/config',
|
|
136
|
+
'GET /api/health?project=<path>',
|
|
137
|
+
'GET /api/incidents?project=<path>&limit=<n>&task=<prefix>&unresolved=<true|false>',
|
|
138
|
+
'GET /api/ai/providers',
|
|
139
|
+
'GET /api/ai/models/<provider>',
|
|
140
|
+
'GET /api/credit-alerts?project=<path>',
|
|
141
|
+
'POST /api/credit-alerts/<id>/dismiss',
|
|
142
|
+
'POST /api/credit-alerts/<id>/retry',
|
|
143
|
+
],
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// 404 handler
|
|
148
|
+
app.use((req, res) => {
|
|
149
|
+
res.status(404).json({
|
|
150
|
+
success: false,
|
|
151
|
+
error: 'Not found',
|
|
152
|
+
path: req.path,
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// Error handler
|
|
157
|
+
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
158
|
+
console.error('Unhandled error:', err);
|
|
159
|
+
res.status(500).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: 'Internal server error',
|
|
162
|
+
message: err.message,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return app;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const app = createApp();
|
|
170
|
+
|
|
171
|
+
export function startServer(): void {
|
|
172
|
+
// Start server on all interfaces
|
|
173
|
+
app.listen(Number(PORT), HOST, () => {
|
|
174
|
+
console.log(`Steroids API server listening on ${HOST}:${PORT}`);
|
|
175
|
+
console.log(`Health check: http://localhost:${PORT}/health`);
|
|
176
|
+
console.log(`API docs: http://localhost:${PORT}/`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizePath(p: string): string {
|
|
181
|
+
try {
|
|
182
|
+
return realpathSync(p);
|
|
183
|
+
} catch {
|
|
184
|
+
return resolve(p);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Only start listening when executed as the entrypoint (safe to import in tests).
|
|
189
|
+
// This works for `node dist/.../index.js` and `tsx watch src/index.ts` (tsx keeps the entry file in argv).
|
|
190
|
+
const shouldAutoStart = (() => {
|
|
191
|
+
if (process.env.NODE_ENV === 'test') return false;
|
|
192
|
+
const thisPath = normalizePath(fileURLToPath(import.meta.url));
|
|
193
|
+
const argvPaths = process.argv.slice(1).map((a) => normalizePath(a));
|
|
194
|
+
return argvPaths.includes(thisPath);
|
|
195
|
+
})();
|
|
196
|
+
|
|
197
|
+
if (shouldAutoStart) startServer();
|
|
198
|
+
|
|
199
|
+
export default app;
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity API routes
|
|
3
|
+
* Exposes activity log statistics for the dashboard
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import {
|
|
9
|
+
getActivityStatsByProject,
|
|
10
|
+
getActivityFiltered,
|
|
11
|
+
ProjectActivityStats,
|
|
12
|
+
ActivityStatus,
|
|
13
|
+
ActivityLogEntry,
|
|
14
|
+
} from '../../../dist/runners/activity-log.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get GitHub URL from git remote for a project
|
|
18
|
+
*/
|
|
19
|
+
function getGitHubUrl(projectPath: string): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const remoteUrl = execSync('git remote get-url origin', {
|
|
22
|
+
cwd: projectPath,
|
|
23
|
+
encoding: 'utf-8',
|
|
24
|
+
}).trim();
|
|
25
|
+
|
|
26
|
+
// Convert SSH or HTTPS URL to web URL
|
|
27
|
+
if (remoteUrl.startsWith('git@github.com:')) {
|
|
28
|
+
const path = remoteUrl.replace('git@github.com:', '').replace(/\.git$/, '');
|
|
29
|
+
return `https://github.com/${path}`;
|
|
30
|
+
} else if (remoteUrl.includes('github.com')) {
|
|
31
|
+
return remoteUrl.replace(/\.git$/, '');
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ActivityListEntry extends ActivityLogEntry {
|
|
40
|
+
github_url: string | null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const router = Router();
|
|
44
|
+
|
|
45
|
+
interface ActivityStatsResponse {
|
|
46
|
+
project_path: string;
|
|
47
|
+
project_name: string | null;
|
|
48
|
+
completed: number;
|
|
49
|
+
failed: number;
|
|
50
|
+
skipped: number;
|
|
51
|
+
partial: number;
|
|
52
|
+
disputed: number;
|
|
53
|
+
total: number;
|
|
54
|
+
first_activity: string | null;
|
|
55
|
+
last_activity: string | null;
|
|
56
|
+
tasks_per_hour: number;
|
|
57
|
+
success_rate: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* GET /api/activity
|
|
62
|
+
* Get activity statistics for a time range
|
|
63
|
+
* Query params:
|
|
64
|
+
* - hours: number (default: 24) - hours to look back
|
|
65
|
+
* - project: string (optional) - filter by project path
|
|
66
|
+
*/
|
|
67
|
+
router.get('/activity', (req: Request, res: Response) => {
|
|
68
|
+
try {
|
|
69
|
+
const hoursParam = req.query.hours;
|
|
70
|
+
const projectPath = req.query.project as string | undefined;
|
|
71
|
+
|
|
72
|
+
// Parse hours parameter (default: 24)
|
|
73
|
+
let hours = 24;
|
|
74
|
+
if (hoursParam !== undefined) {
|
|
75
|
+
const parsed = parseInt(hoursParam as string, 10);
|
|
76
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
77
|
+
res.status(400).json({
|
|
78
|
+
success: false,
|
|
79
|
+
error: 'Invalid hours parameter - must be a positive integer',
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
hours = parsed;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get stats from the activity log
|
|
87
|
+
const stats: ProjectActivityStats[] = getActivityStatsByProject(hours);
|
|
88
|
+
|
|
89
|
+
// Filter by project if specified
|
|
90
|
+
const filteredStats = projectPath
|
|
91
|
+
? stats.filter((s) => s.project_path === projectPath)
|
|
92
|
+
: stats;
|
|
93
|
+
|
|
94
|
+
// Calculate derived metrics for each project
|
|
95
|
+
const enrichedStats: ActivityStatsResponse[] = filteredStats.map((s) => {
|
|
96
|
+
// Calculate hours between first and last activity for rate
|
|
97
|
+
let tasksPerHour = 0;
|
|
98
|
+
if (s.first_activity && s.last_activity && s.total > 0) {
|
|
99
|
+
const firstTime = new Date(s.first_activity).getTime();
|
|
100
|
+
const lastTime = new Date(s.last_activity).getTime();
|
|
101
|
+
const hoursDiff = Math.max((lastTime - firstTime) / (1000 * 60 * 60), 1);
|
|
102
|
+
tasksPerHour = Math.round((s.total / hoursDiff) * 100) / 100;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Calculate success rate (completed / total)
|
|
106
|
+
const successRate =
|
|
107
|
+
s.total > 0 ? Math.round((s.completed / s.total) * 1000) / 10 : 0;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
project_path: s.project_path,
|
|
111
|
+
project_name: s.project_name,
|
|
112
|
+
completed: s.completed,
|
|
113
|
+
failed: s.failed,
|
|
114
|
+
skipped: s.skipped,
|
|
115
|
+
partial: s.partial,
|
|
116
|
+
disputed: s.disputed,
|
|
117
|
+
total: s.total,
|
|
118
|
+
first_activity: s.first_activity,
|
|
119
|
+
last_activity: s.last_activity,
|
|
120
|
+
tasks_per_hour: tasksPerHour,
|
|
121
|
+
success_rate: successRate,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// If filtering by a single project, return just that project's stats
|
|
126
|
+
// Otherwise return all projects
|
|
127
|
+
if (projectPath) {
|
|
128
|
+
const projectStats = enrichedStats[0] || {
|
|
129
|
+
project_path: projectPath,
|
|
130
|
+
project_name: null,
|
|
131
|
+
completed: 0,
|
|
132
|
+
failed: 0,
|
|
133
|
+
skipped: 0,
|
|
134
|
+
partial: 0,
|
|
135
|
+
disputed: 0,
|
|
136
|
+
total: 0,
|
|
137
|
+
first_activity: null,
|
|
138
|
+
last_activity: null,
|
|
139
|
+
tasks_per_hour: 0,
|
|
140
|
+
success_rate: 0,
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
res.json({
|
|
144
|
+
success: true,
|
|
145
|
+
hours,
|
|
146
|
+
stats: projectStats,
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
// Aggregate all projects for global stats
|
|
150
|
+
const totals = enrichedStats.reduce(
|
|
151
|
+
(acc, s) => ({
|
|
152
|
+
completed: acc.completed + s.completed,
|
|
153
|
+
failed: acc.failed + s.failed,
|
|
154
|
+
skipped: acc.skipped + s.skipped,
|
|
155
|
+
partial: acc.partial + s.partial,
|
|
156
|
+
disputed: acc.disputed + s.disputed,
|
|
157
|
+
total: acc.total + s.total,
|
|
158
|
+
}),
|
|
159
|
+
{ completed: 0, failed: 0, skipped: 0, partial: 0, disputed: 0, total: 0 }
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// Find the earliest first_activity and latest last_activity across all projects
|
|
163
|
+
let globalFirstActivity: string | null = null;
|
|
164
|
+
let globalLastActivity: string | null = null;
|
|
165
|
+
for (const s of enrichedStats) {
|
|
166
|
+
if (s.first_activity) {
|
|
167
|
+
if (!globalFirstActivity || s.first_activity < globalFirstActivity) {
|
|
168
|
+
globalFirstActivity = s.first_activity;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (s.last_activity) {
|
|
172
|
+
if (!globalLastActivity || s.last_activity > globalLastActivity) {
|
|
173
|
+
globalLastActivity = s.last_activity;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Calculate global metrics based on actual activity timespan
|
|
179
|
+
const globalSuccessRate =
|
|
180
|
+
totals.total > 0
|
|
181
|
+
? Math.round((totals.completed / totals.total) * 1000) / 10
|
|
182
|
+
: 0;
|
|
183
|
+
|
|
184
|
+
// Calculate tasks per hour based on actual time between first and last activity
|
|
185
|
+
let globalTasksPerHour = 0;
|
|
186
|
+
if (globalFirstActivity && globalLastActivity && totals.total > 0) {
|
|
187
|
+
const firstTime = new Date(globalFirstActivity).getTime();
|
|
188
|
+
const lastTime = new Date(globalLastActivity).getTime();
|
|
189
|
+
const hoursDiff = Math.max((lastTime - firstTime) / (1000 * 60 * 60), 1);
|
|
190
|
+
globalTasksPerHour = Math.round((totals.total / hoursDiff) * 100) / 100;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
res.json({
|
|
194
|
+
success: true,
|
|
195
|
+
hours,
|
|
196
|
+
stats: {
|
|
197
|
+
...totals,
|
|
198
|
+
first_activity: globalFirstActivity,
|
|
199
|
+
last_activity: globalLastActivity,
|
|
200
|
+
tasks_per_hour: globalTasksPerHour,
|
|
201
|
+
success_rate: globalSuccessRate,
|
|
202
|
+
},
|
|
203
|
+
by_project: enrichedStats,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error('Error getting activity stats:', error);
|
|
208
|
+
res.status(500).json({
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Failed to get activity stats',
|
|
211
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* GET /api/activity/list
|
|
218
|
+
* Get activity log entries with optional filters
|
|
219
|
+
* Query params:
|
|
220
|
+
* - hours: number (default: 24) - hours to look back
|
|
221
|
+
* - status: string (optional) - filter by status (completed, failed, skipped, partial, disputed)
|
|
222
|
+
* - project: string (optional) - filter by project path
|
|
223
|
+
* - limit: number (optional) - max entries to return
|
|
224
|
+
*/
|
|
225
|
+
router.get('/activity/list', (req: Request, res: Response) => {
|
|
226
|
+
try {
|
|
227
|
+
const hoursParam = req.query.hours;
|
|
228
|
+
const statusParam = req.query.status as string | undefined;
|
|
229
|
+
const projectPath = req.query.project as string | undefined;
|
|
230
|
+
const limitParam = req.query.limit;
|
|
231
|
+
|
|
232
|
+
// Parse hours parameter (default: 24)
|
|
233
|
+
let hours = 24;
|
|
234
|
+
if (hoursParam !== undefined) {
|
|
235
|
+
const parsed = parseInt(hoursParam as string, 10);
|
|
236
|
+
if (isNaN(parsed) || parsed <= 0) {
|
|
237
|
+
res.status(400).json({
|
|
238
|
+
success: false,
|
|
239
|
+
error: 'Invalid hours parameter - must be a positive integer',
|
|
240
|
+
});
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
hours = parsed;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Validate status if provided
|
|
247
|
+
const validStatuses: ActivityStatus[] = ['completed', 'failed', 'skipped', 'partial', 'disputed'];
|
|
248
|
+
if (statusParam && !validStatuses.includes(statusParam as ActivityStatus)) {
|
|
249
|
+
res.status(400).json({
|
|
250
|
+
success: false,
|
|
251
|
+
error: `Invalid status - must be one of: ${validStatuses.join(', ')}`,
|
|
252
|
+
});
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Parse limit
|
|
257
|
+
let limit: number | undefined;
|
|
258
|
+
if (limitParam !== undefined) {
|
|
259
|
+
const parsed = parseInt(limitParam as string, 10);
|
|
260
|
+
if (!isNaN(parsed) && parsed > 0) {
|
|
261
|
+
limit = parsed;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const rawEntries = getActivityFiltered({
|
|
266
|
+
hoursAgo: hours,
|
|
267
|
+
status: statusParam as ActivityStatus | undefined,
|
|
268
|
+
projectPath,
|
|
269
|
+
limit,
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Cache GitHub URLs by project path to avoid repeated git calls
|
|
273
|
+
const githubUrlCache = new Map<string, string | null>();
|
|
274
|
+
|
|
275
|
+
const entries: ActivityListEntry[] = rawEntries.map((entry) => {
|
|
276
|
+
if (!githubUrlCache.has(entry.project_path)) {
|
|
277
|
+
githubUrlCache.set(entry.project_path, getGitHubUrl(entry.project_path));
|
|
278
|
+
}
|
|
279
|
+
return {
|
|
280
|
+
...entry,
|
|
281
|
+
github_url: githubUrlCache.get(entry.project_path) ?? null,
|
|
282
|
+
};
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
res.json({
|
|
286
|
+
success: true,
|
|
287
|
+
hours,
|
|
288
|
+
status: statusParam || 'all',
|
|
289
|
+
entries,
|
|
290
|
+
count: entries.length,
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
console.error('Error getting activity list:', error);
|
|
294
|
+
res.status(500).json({
|
|
295
|
+
success: false,
|
|
296
|
+
error: 'Failed to get activity list',
|
|
297
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
export default router;
|