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.
Files changed (328) hide show
  1. package/dist/API/src/index.d.ts +10 -0
  2. package/dist/API/src/index.d.ts.map +1 -0
  3. package/dist/API/src/index.js +130 -0
  4. package/dist/API/src/index.js.map +1 -0
  5. package/dist/API/src/routes/activity.d.ts +7 -0
  6. package/dist/API/src/routes/activity.d.ts.map +1 -0
  7. package/dist/API/src/routes/activity.js +252 -0
  8. package/dist/API/src/routes/activity.js.map +1 -0
  9. package/dist/API/src/routes/config.d.ts +7 -0
  10. package/dist/API/src/routes/config.d.ts.map +1 -0
  11. package/dist/API/src/routes/config.js +521 -0
  12. package/dist/API/src/routes/config.js.map +1 -0
  13. package/dist/API/src/routes/health.d.ts +7 -0
  14. package/dist/API/src/routes/health.d.ts.map +1 -0
  15. package/dist/API/src/routes/health.js +172 -0
  16. package/dist/API/src/routes/health.js.map +1 -0
  17. package/dist/API/src/routes/incidents.d.ts +7 -0
  18. package/dist/API/src/routes/incidents.d.ts.map +1 -0
  19. package/dist/API/src/routes/incidents.js +117 -0
  20. package/dist/API/src/routes/incidents.js.map +1 -0
  21. package/dist/API/src/routes/projects.d.ts +7 -0
  22. package/dist/API/src/routes/projects.d.ts.map +1 -0
  23. package/dist/API/src/routes/projects.js +398 -0
  24. package/dist/API/src/routes/projects.js.map +1 -0
  25. package/dist/API/src/routes/runners.d.ts +7 -0
  26. package/dist/API/src/routes/runners.d.ts.map +1 -0
  27. package/dist/API/src/routes/runners.js +242 -0
  28. package/dist/API/src/routes/runners.js.map +1 -0
  29. package/dist/API/src/routes/tasks.d.ts +7 -0
  30. package/dist/API/src/routes/tasks.d.ts.map +1 -0
  31. package/dist/API/src/routes/tasks.js +1007 -0
  32. package/dist/API/src/routes/tasks.js.map +1 -0
  33. package/dist/API/src/utils/validation.d.ts +22 -0
  34. package/dist/API/src/utils/validation.d.ts.map +1 -0
  35. package/dist/API/src/utils/validation.js +50 -0
  36. package/dist/API/src/utils/validation.js.map +1 -0
  37. package/dist/index.d.ts +10 -0
  38. package/dist/index.d.ts.map +1 -0
  39. package/dist/index.js +184 -0
  40. package/dist/index.js.map +1 -0
  41. package/dist/routes/activity.d.ts +7 -0
  42. package/dist/routes/activity.d.ts.map +1 -0
  43. package/dist/routes/activity.js +252 -0
  44. package/dist/routes/activity.js.map +1 -0
  45. package/dist/routes/config.d.ts +7 -0
  46. package/dist/routes/config.d.ts.map +1 -0
  47. package/dist/routes/config.js +647 -0
  48. package/dist/routes/config.js.map +1 -0
  49. package/dist/routes/credit-alerts.d.ts +2 -0
  50. package/dist/routes/credit-alerts.d.ts.map +1 -0
  51. package/dist/routes/credit-alerts.js +97 -0
  52. package/dist/routes/credit-alerts.js.map +1 -0
  53. package/dist/routes/health.d.ts +7 -0
  54. package/dist/routes/health.d.ts.map +1 -0
  55. package/dist/routes/health.js +200 -0
  56. package/dist/routes/health.js.map +1 -0
  57. package/dist/routes/incidents.d.ts +7 -0
  58. package/dist/routes/incidents.d.ts.map +1 -0
  59. package/dist/routes/incidents.js +117 -0
  60. package/dist/routes/incidents.js.map +1 -0
  61. package/dist/routes/projects.d.ts +7 -0
  62. package/dist/routes/projects.d.ts.map +1 -0
  63. package/dist/routes/projects.js +643 -0
  64. package/dist/routes/projects.js.map +1 -0
  65. package/dist/routes/runners.d.ts +7 -0
  66. package/dist/routes/runners.d.ts.map +1 -0
  67. package/dist/routes/runners.js +299 -0
  68. package/dist/routes/runners.js.map +1 -0
  69. package/dist/routes/skills.d.ts +3 -0
  70. package/dist/routes/skills.d.ts.map +1 -0
  71. package/dist/routes/skills.js +109 -0
  72. package/dist/routes/skills.js.map +1 -0
  73. package/dist/routes/storage.d.ts +7 -0
  74. package/dist/routes/storage.d.ts.map +1 -0
  75. package/dist/routes/storage.js +93 -0
  76. package/dist/routes/storage.js.map +1 -0
  77. package/dist/routes/tasks.d.ts +7 -0
  78. package/dist/routes/tasks.d.ts.map +1 -0
  79. package/dist/routes/tasks.js +1145 -0
  80. package/dist/routes/tasks.js.map +1 -0
  81. package/dist/src/cleanup/invocation-logs.d.ts +30 -0
  82. package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
  83. package/dist/src/cleanup/invocation-logs.js +66 -0
  84. package/dist/src/cleanup/invocation-logs.js.map +1 -0
  85. package/dist/src/commands/loop-phases.d.ts +11 -0
  86. package/dist/src/commands/loop-phases.d.ts.map +1 -0
  87. package/dist/src/commands/loop-phases.js +304 -0
  88. package/dist/src/commands/loop-phases.js.map +1 -0
  89. package/dist/src/config/loader.d.ts +160 -0
  90. package/dist/src/config/loader.d.ts.map +1 -0
  91. package/dist/src/config/loader.js +276 -0
  92. package/dist/src/config/loader.js.map +1 -0
  93. package/dist/src/database/connection.d.ts +35 -0
  94. package/dist/src/database/connection.d.ts.map +1 -0
  95. package/dist/src/database/connection.js +197 -0
  96. package/dist/src/database/connection.js.map +1 -0
  97. package/dist/src/database/queries.d.ts +220 -0
  98. package/dist/src/database/queries.d.ts.map +1 -0
  99. package/dist/src/database/queries.js +589 -0
  100. package/dist/src/database/queries.js.map +1 -0
  101. package/dist/src/database/schema.d.ts +8 -0
  102. package/dist/src/database/schema.d.ts.map +1 -0
  103. package/dist/src/database/schema.js +184 -0
  104. package/dist/src/database/schema.js.map +1 -0
  105. package/dist/src/git/push.d.ts +26 -0
  106. package/dist/src/git/push.d.ts.map +1 -0
  107. package/dist/src/git/push.js +91 -0
  108. package/dist/src/git/push.js.map +1 -0
  109. package/dist/src/git/status.d.ts +83 -0
  110. package/dist/src/git/status.d.ts.map +1 -0
  111. package/dist/src/git/status.js +315 -0
  112. package/dist/src/git/status.js.map +1 -0
  113. package/dist/src/health/stuck-task-detector.d.ts +131 -0
  114. package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
  115. package/dist/src/health/stuck-task-detector.js +233 -0
  116. package/dist/src/health/stuck-task-detector.js.map +1 -0
  117. package/dist/src/health/stuck-task-recovery.d.ts +45 -0
  118. package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
  119. package/dist/src/health/stuck-task-recovery.js +309 -0
  120. package/dist/src/health/stuck-task-recovery.js.map +1 -0
  121. package/dist/src/index.d.ts +10 -0
  122. package/dist/src/index.d.ts.map +1 -0
  123. package/dist/src/index.js +130 -0
  124. package/dist/src/index.js.map +1 -0
  125. package/dist/src/locking/queries.d.ts +116 -0
  126. package/dist/src/locking/queries.d.ts.map +1 -0
  127. package/dist/src/locking/queries.js +232 -0
  128. package/dist/src/locking/queries.js.map +1 -0
  129. package/dist/src/locking/section-lock.d.ts +74 -0
  130. package/dist/src/locking/section-lock.d.ts.map +1 -0
  131. package/dist/src/locking/section-lock.js +196 -0
  132. package/dist/src/locking/section-lock.js.map +1 -0
  133. package/dist/src/locking/task-lock.d.ts +92 -0
  134. package/dist/src/locking/task-lock.d.ts.map +1 -0
  135. package/dist/src/locking/task-lock.js +233 -0
  136. package/dist/src/locking/task-lock.js.map +1 -0
  137. package/dist/src/migrations/index.d.ts +7 -0
  138. package/dist/src/migrations/index.d.ts.map +1 -0
  139. package/dist/src/migrations/index.js +9 -0
  140. package/dist/src/migrations/index.js.map +1 -0
  141. package/dist/src/migrations/manifest.d.ts +92 -0
  142. package/dist/src/migrations/manifest.d.ts.map +1 -0
  143. package/dist/src/migrations/manifest.js +255 -0
  144. package/dist/src/migrations/manifest.js.map +1 -0
  145. package/dist/src/migrations/runner.d.ts +84 -0
  146. package/dist/src/migrations/runner.d.ts.map +1 -0
  147. package/dist/src/migrations/runner.js +338 -0
  148. package/dist/src/migrations/runner.js.map +1 -0
  149. package/dist/src/orchestrator/coder.d.ts +32 -0
  150. package/dist/src/orchestrator/coder.d.ts.map +1 -0
  151. package/dist/src/orchestrator/coder.js +170 -0
  152. package/dist/src/orchestrator/coder.js.map +1 -0
  153. package/dist/src/orchestrator/coordinator.d.ts +28 -0
  154. package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
  155. package/dist/src/orchestrator/coordinator.js +252 -0
  156. package/dist/src/orchestrator/coordinator.js.map +1 -0
  157. package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
  158. package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
  159. package/dist/src/orchestrator/fallback-handler.js +280 -0
  160. package/dist/src/orchestrator/fallback-handler.js.map +1 -0
  161. package/dist/src/orchestrator/invoke.d.ts +14 -0
  162. package/dist/src/orchestrator/invoke.d.ts.map +1 -0
  163. package/dist/src/orchestrator/invoke.js +76 -0
  164. package/dist/src/orchestrator/invoke.js.map +1 -0
  165. package/dist/src/orchestrator/post-coder.d.ts +10 -0
  166. package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
  167. package/dist/src/orchestrator/post-coder.js +198 -0
  168. package/dist/src/orchestrator/post-coder.js.map +1 -0
  169. package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
  170. package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
  171. package/dist/src/orchestrator/post-reviewer.js +199 -0
  172. package/dist/src/orchestrator/post-reviewer.js.map +1 -0
  173. package/dist/src/orchestrator/reviewer.d.ts +35 -0
  174. package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
  175. package/dist/src/orchestrator/reviewer.js +237 -0
  176. package/dist/src/orchestrator/reviewer.js.map +1 -0
  177. package/dist/src/orchestrator/schemas.d.ts +10 -0
  178. package/dist/src/orchestrator/schemas.d.ts.map +1 -0
  179. package/dist/src/orchestrator/schemas.js +81 -0
  180. package/dist/src/orchestrator/schemas.js.map +1 -0
  181. package/dist/src/orchestrator/task-selector.d.ts +102 -0
  182. package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
  183. package/dist/src/orchestrator/task-selector.js +326 -0
  184. package/dist/src/orchestrator/task-selector.js.map +1 -0
  185. package/dist/src/orchestrator/types.d.ts +74 -0
  186. package/dist/src/orchestrator/types.d.ts.map +1 -0
  187. package/dist/src/orchestrator/types.js +5 -0
  188. package/dist/src/orchestrator/types.js.map +1 -0
  189. package/dist/src/prompts/coder.d.ts +36 -0
  190. package/dist/src/prompts/coder.d.ts.map +1 -0
  191. package/dist/src/prompts/coder.js +303 -0
  192. package/dist/src/prompts/coder.js.map +1 -0
  193. package/dist/src/prompts/prompt-helpers.d.ts +51 -0
  194. package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
  195. package/dist/src/prompts/prompt-helpers.js +299 -0
  196. package/dist/src/prompts/prompt-helpers.js.map +1 -0
  197. package/dist/src/prompts/reviewer.d.ts +40 -0
  198. package/dist/src/prompts/reviewer.d.ts.map +1 -0
  199. package/dist/src/prompts/reviewer.js +416 -0
  200. package/dist/src/prompts/reviewer.js.map +1 -0
  201. package/dist/src/providers/claude.d.ts +53 -0
  202. package/dist/src/providers/claude.d.ts.map +1 -0
  203. package/dist/src/providers/claude.js +227 -0
  204. package/dist/src/providers/claude.js.map +1 -0
  205. package/dist/src/providers/codex.d.ts +53 -0
  206. package/dist/src/providers/codex.d.ts.map +1 -0
  207. package/dist/src/providers/codex.js +253 -0
  208. package/dist/src/providers/codex.js.map +1 -0
  209. package/dist/src/providers/gemini.d.ts +58 -0
  210. package/dist/src/providers/gemini.d.ts.map +1 -0
  211. package/dist/src/providers/gemini.js +240 -0
  212. package/dist/src/providers/gemini.js.map +1 -0
  213. package/dist/src/providers/interface.d.ts +185 -0
  214. package/dist/src/providers/interface.d.ts.map +1 -0
  215. package/dist/src/providers/interface.js +92 -0
  216. package/dist/src/providers/interface.js.map +1 -0
  217. package/dist/src/providers/invocation-logger.d.ts +97 -0
  218. package/dist/src/providers/invocation-logger.d.ts.map +1 -0
  219. package/dist/src/providers/invocation-logger.js +378 -0
  220. package/dist/src/providers/invocation-logger.js.map +1 -0
  221. package/dist/src/providers/openai.d.ts +53 -0
  222. package/dist/src/providers/openai.d.ts.map +1 -0
  223. package/dist/src/providers/openai.js +230 -0
  224. package/dist/src/providers/openai.js.map +1 -0
  225. package/dist/src/providers/registry.d.ts +100 -0
  226. package/dist/src/providers/registry.d.ts.map +1 -0
  227. package/dist/src/providers/registry.js +170 -0
  228. package/dist/src/providers/registry.js.map +1 -0
  229. package/dist/src/routes/activity.d.ts +7 -0
  230. package/dist/src/routes/activity.d.ts.map +1 -0
  231. package/dist/src/routes/activity.js +252 -0
  232. package/dist/src/routes/activity.js.map +1 -0
  233. package/dist/src/routes/config.d.ts +7 -0
  234. package/dist/src/routes/config.d.ts.map +1 -0
  235. package/dist/src/routes/config.js +521 -0
  236. package/dist/src/routes/config.js.map +1 -0
  237. package/dist/src/routes/health.d.ts +7 -0
  238. package/dist/src/routes/health.d.ts.map +1 -0
  239. package/dist/src/routes/health.js +172 -0
  240. package/dist/src/routes/health.js.map +1 -0
  241. package/dist/src/routes/incidents.d.ts +7 -0
  242. package/dist/src/routes/incidents.d.ts.map +1 -0
  243. package/dist/src/routes/incidents.js +117 -0
  244. package/dist/src/routes/incidents.js.map +1 -0
  245. package/dist/src/routes/projects.d.ts +7 -0
  246. package/dist/src/routes/projects.d.ts.map +1 -0
  247. package/dist/src/routes/projects.js +398 -0
  248. package/dist/src/routes/projects.js.map +1 -0
  249. package/dist/src/routes/runners.d.ts +7 -0
  250. package/dist/src/routes/runners.d.ts.map +1 -0
  251. package/dist/src/routes/runners.js +242 -0
  252. package/dist/src/routes/runners.js.map +1 -0
  253. package/dist/src/routes/tasks.d.ts +7 -0
  254. package/dist/src/routes/tasks.d.ts.map +1 -0
  255. package/dist/src/routes/tasks.js +1007 -0
  256. package/dist/src/routes/tasks.js.map +1 -0
  257. package/dist/src/runners/activity-log.d.ts +65 -0
  258. package/dist/src/runners/activity-log.d.ts.map +1 -0
  259. package/dist/src/runners/activity-log.js +140 -0
  260. package/dist/src/runners/activity-log.js.map +1 -0
  261. package/dist/src/runners/cron.d.ts +30 -0
  262. package/dist/src/runners/cron.d.ts.map +1 -0
  263. package/dist/src/runners/cron.js +333 -0
  264. package/dist/src/runners/cron.js.map +1 -0
  265. package/dist/src/runners/daemon.d.ts +71 -0
  266. package/dist/src/runners/daemon.d.ts.map +1 -0
  267. package/dist/src/runners/daemon.js +233 -0
  268. package/dist/src/runners/daemon.js.map +1 -0
  269. package/dist/src/runners/global-db.d.ts +31 -0
  270. package/dist/src/runners/global-db.d.ts.map +1 -0
  271. package/dist/src/runners/global-db.js +220 -0
  272. package/dist/src/runners/global-db.js.map +1 -0
  273. package/dist/src/runners/hang-detector.d.ts +38 -0
  274. package/dist/src/runners/hang-detector.d.ts.map +1 -0
  275. package/dist/src/runners/hang-detector.js +130 -0
  276. package/dist/src/runners/hang-detector.js.map +1 -0
  277. package/dist/src/runners/heartbeat.d.ts +39 -0
  278. package/dist/src/runners/heartbeat.d.ts.map +1 -0
  279. package/dist/src/runners/heartbeat.js +71 -0
  280. package/dist/src/runners/heartbeat.js.map +1 -0
  281. package/dist/src/runners/lock.d.ts +47 -0
  282. package/dist/src/runners/lock.d.ts.map +1 -0
  283. package/dist/src/runners/lock.js +140 -0
  284. package/dist/src/runners/lock.js.map +1 -0
  285. package/dist/src/runners/orchestrator-loop.d.ts +20 -0
  286. package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
  287. package/dist/src/runners/orchestrator-loop.js +208 -0
  288. package/dist/src/runners/orchestrator-loop.js.map +1 -0
  289. package/dist/src/runners/projects.d.ts +96 -0
  290. package/dist/src/runners/projects.d.ts.map +1 -0
  291. package/dist/src/runners/projects.js +243 -0
  292. package/dist/src/runners/projects.js.map +1 -0
  293. package/dist/src/runners/wakeup.d.ts +37 -0
  294. package/dist/src/runners/wakeup.d.ts.map +1 -0
  295. package/dist/src/runners/wakeup.js +355 -0
  296. package/dist/src/runners/wakeup.js.map +1 -0
  297. package/dist/src/utils/validation.d.ts +22 -0
  298. package/dist/src/utils/validation.d.ts.map +1 -0
  299. package/dist/src/utils/validation.js +50 -0
  300. package/dist/src/utils/validation.js.map +1 -0
  301. package/dist/utils/sqlite.d.ts +17 -0
  302. package/dist/utils/sqlite.d.ts.map +1 -0
  303. package/dist/utils/sqlite.js +27 -0
  304. package/dist/utils/sqlite.js.map +1 -0
  305. package/dist/utils/storage-cache.d.ts +33 -0
  306. package/dist/utils/storage-cache.d.ts.map +1 -0
  307. package/dist/utils/storage-cache.js +81 -0
  308. package/dist/utils/storage-cache.js.map +1 -0
  309. package/dist/utils/validation.d.ts +22 -0
  310. package/dist/utils/validation.d.ts.map +1 -0
  311. package/dist/utils/validation.js +51 -0
  312. package/dist/utils/validation.js.map +1 -0
  313. package/package.json +39 -0
  314. package/src/index.ts +199 -0
  315. package/src/routes/activity.ts +302 -0
  316. package/src/routes/config.ts +723 -0
  317. package/src/routes/credit-alerts.ts +73 -0
  318. package/src/routes/health.ts +219 -0
  319. package/src/routes/incidents.ts +131 -0
  320. package/src/routes/projects.ts +854 -0
  321. package/src/routes/runners.ts +357 -0
  322. package/src/routes/skills.ts +127 -0
  323. package/src/routes/storage.ts +108 -0
  324. package/src/routes/tasks.ts +1372 -0
  325. package/src/utils/sqlite.ts +36 -0
  326. package/src/utils/storage-cache.ts +107 -0
  327. package/src/utils/validation.ts +61 -0
  328. package/tsconfig.json +20 -0
@@ -0,0 +1,854 @@
1
+ /**
2
+ * Projects API routes
3
+ * Manages global project registry
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import { execSync } from 'node:child_process';
8
+ import { existsSync, readdirSync, statSync, realpathSync } from 'node:fs';
9
+ import Database from 'better-sqlite3';
10
+ import { join, relative, resolve, sep } from 'node:path';
11
+ import {
12
+ getRegisteredProjects,
13
+ registerProject,
14
+ unregisterProject,
15
+ enableProject,
16
+ disableProject,
17
+ pruneProjects,
18
+ getRegisteredProject,
19
+ } from '../../../dist/runners/projects.js';
20
+ import { openGlobalDatabase } from '../../../dist/runners/global-db.js';
21
+ import { hasActiveParallelSessionForProjectDb } from '../../../dist/runners/parallel-session-state.js';
22
+ import { isValidProjectPath, validatePathRequest } from '../utils/validation.js';
23
+ import { openSqliteForRead } from '../utils/sqlite.js';
24
+ import { getCachedListStorage } from '../utils/storage-cache.js';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ const router = Router();
28
+
29
+ interface ProjectLiveData {
30
+ stats: {
31
+ pending: number;
32
+ in_progress: number;
33
+ review: number;
34
+ completed: number;
35
+ failed: number;
36
+ disputed: number;
37
+ skipped: number;
38
+ };
39
+ last_task_added_at: string | null;
40
+ isBlocked: boolean;
41
+ isUnreachable: boolean;
42
+ }
43
+
44
+ /**
45
+ * Query live task stats and last task added from a project's local database
46
+ */
47
+ function getProjectLiveData(projectPath: string): ProjectLiveData {
48
+ const empty: ProjectLiveData = {
49
+ stats: { pending: 0, in_progress: 0, review: 0, completed: 0, failed: 0, disputed: 0, skipped: 0 },
50
+ last_task_added_at: null,
51
+ isBlocked: false,
52
+ isUnreachable: true,
53
+ };
54
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
55
+ if (!existsSync(dbPath)) return empty;
56
+
57
+ try {
58
+ const projectDb = openSqliteForRead(dbPath, { timeoutMs: 500 });
59
+ try {
60
+ const row = projectDb
61
+ .prepare(
62
+ `SELECT
63
+ COALESCE(SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END), 0) as pending,
64
+ COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
65
+ COALESCE(SUM(CASE WHEN status = 'review' THEN 1 ELSE 0 END), 0) as review,
66
+ COALESCE(SUM(CASE WHEN status = 'completed' THEN 1 ELSE 0 END), 0) as completed,
67
+ COALESCE(SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END), 0) as failed,
68
+ COALESCE(SUM(CASE WHEN status = 'skipped' THEN 1 ELSE 0 END), 0) as skipped,
69
+ COALESCE(SUM(CASE WHEN status = 'disputed' THEN 1 ELSE 0 END), 0) as disputed,
70
+ COALESCE(SUM(CASE WHEN failure_count >= 3 THEN 1 ELSE 0 END), 0) as high_failures,
71
+ MAX(created_at) as last_task_added_at
72
+ FROM tasks`
73
+ )
74
+ .get() as {
75
+ pending: number;
76
+ in_progress: number;
77
+ review: number;
78
+ completed: number;
79
+ failed: number;
80
+ disputed: number;
81
+ skipped: number;
82
+ high_failures: number;
83
+ last_task_added_at: string | null;
84
+ } | undefined;
85
+
86
+ const failedCount = row?.failed ?? 0;
87
+ const disputedCount = row?.disputed ?? 0;
88
+ const skippedCount = row?.skipped ?? 0;
89
+ const highFailuresCount = row?.high_failures ?? 0;
90
+
91
+ return {
92
+ stats: {
93
+ pending: row?.pending ?? 0,
94
+ in_progress: row?.in_progress ?? 0,
95
+ review: row?.review ?? 0,
96
+ completed: row?.completed ?? 0,
97
+ failed: failedCount,
98
+ disputed: disputedCount,
99
+ skipped: skippedCount,
100
+ },
101
+ last_task_added_at: row?.last_task_added_at ?? null,
102
+ isBlocked: failedCount > 0 || disputedCount > 0 || skippedCount > 0 || highFailuresCount > 0,
103
+ isUnreachable: false,
104
+ };
105
+ } finally {
106
+ projectDb.close();
107
+ }
108
+ } catch {
109
+ return empty;
110
+ }
111
+ }
112
+
113
+ interface ProjectResponse {
114
+ path: string;
115
+ name: string | null;
116
+ enabled: boolean;
117
+ registered_at: string;
118
+ last_seen_at: string;
119
+ last_activity_at: string | null; // Runner heartbeat or null if no active runner
120
+ last_task_added_at: string | null; // Most recent task created_at
121
+ isBlocked: boolean;
122
+ isUnreachable: boolean;
123
+ stats?: {
124
+ pending: number;
125
+ in_progress: number;
126
+ review: number;
127
+ completed: number;
128
+ failed: number;
129
+ disputed: number;
130
+ skipped: number;
131
+ };
132
+ runner?: {
133
+ id: string;
134
+ status: string;
135
+ pid: number | null;
136
+ current_task_id: string | null;
137
+ heartbeat_at: string | null;
138
+ } | null;
139
+ storage_bytes: number | null;
140
+ storage_human: string | null;
141
+ storage_warning: 'orange' | 'red' | null;
142
+ orphaned_in_progress: number;
143
+ }
144
+
145
+ /**
146
+ * GET /api/projects
147
+ * List all registered projects with stats and runner info
148
+ */
149
+ router.get('/projects', (req: Request, res: Response) => {
150
+ try {
151
+ const includeDisabled = req.query.include_disabled === 'true';
152
+ const projects = getRegisteredProjects(includeDisabled);
153
+
154
+ // Get runner info and stats for each project
155
+ const { db, close } = openGlobalDatabase();
156
+ try {
157
+ const projectsWithData: ProjectResponse[] = projects.map((project) => {
158
+ // Get runner info (including heartbeat)
159
+ const runner = db
160
+ .prepare('SELECT id, status, pid, current_task_id, heartbeat_at FROM runners WHERE project_path = ?')
161
+ .get(project.path) as {
162
+ id: string;
163
+ status: string;
164
+ pid: number | null;
165
+ current_task_id: string | null;
166
+ heartbeat_at: string | null;
167
+ } | undefined;
168
+
169
+ // Get live stats + last task added from project-local database
170
+ const liveData = getProjectLiveData(project.path);
171
+
172
+ // Lightweight storage info (non-blocking, 5-min cache)
173
+ const storageInfo = getCachedListStorage(project.path);
174
+
175
+ // Inline SQL against the already-open db — no extra DB connections
176
+ const hasStandaloneRunner = db.prepare(
177
+ `SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
178
+ AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
179
+ ).get(project.path) !== undefined;
180
+ // Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
181
+ const hasParallelSession = hasActiveParallelSessionForProjectDb(db as never, project.path);
182
+ const orphanedInProgress = (hasStandaloneRunner || hasParallelSession)
183
+ ? 0
184
+ : (liveData.stats.in_progress ?? 0);
185
+
186
+ const response: ProjectResponse = {
187
+ path: project.path,
188
+ name: project.name,
189
+ enabled: project.enabled,
190
+ registered_at: project.registered_at,
191
+ last_seen_at: project.last_seen_at,
192
+ last_activity_at: runner?.heartbeat_at || null,
193
+ last_task_added_at: liveData.last_task_added_at,
194
+ isBlocked: liveData.isBlocked,
195
+ isUnreachable: liveData.isUnreachable,
196
+ stats: liveData.stats,
197
+ runner: runner
198
+ ? {
199
+ id: runner.id,
200
+ status: runner.status,
201
+ pid: runner.pid,
202
+ current_task_id: runner.current_task_id,
203
+ heartbeat_at: runner.heartbeat_at,
204
+ }
205
+ : null,
206
+ storage_bytes: storageInfo?.storage_bytes ?? null,
207
+ storage_human: storageInfo?.storage_human ?? null,
208
+ storage_warning: storageInfo?.storage_warning ?? null,
209
+ orphaned_in_progress: orphanedInProgress,
210
+ };
211
+
212
+ return response;
213
+ });
214
+
215
+ // Sort by most recently modified: last task added or project enabled, most recent first
216
+ projectsWithData.sort((a, b) => {
217
+ const aTime = a.last_task_added_at || a.last_seen_at || a.registered_at;
218
+ const bTime = b.last_task_added_at || b.last_seen_at || b.registered_at;
219
+ return new Date(bTime).getTime() - new Date(aTime).getTime();
220
+ });
221
+
222
+ res.json({
223
+ success: true,
224
+ projects: projectsWithData,
225
+ count: projectsWithData.length,
226
+ });
227
+ } finally {
228
+ close();
229
+ }
230
+ } catch (error) {
231
+ console.error('Error listing projects:', error);
232
+ res.status(500).json({
233
+ success: false,
234
+ error: 'Failed to list projects',
235
+ message: error instanceof Error ? error.message : 'Unknown error',
236
+ });
237
+ }
238
+ });
239
+
240
+ /**
241
+ * POST /api/projects
242
+ * Register a new project
243
+ * Body: { path: string, name?: string }
244
+ */
245
+ router.post('/projects', (req: Request, res: Response) => {
246
+ try {
247
+ const validation = validatePathRequest(req.body);
248
+ if (!validation.valid) {
249
+ res.status(400).json({
250
+ success: false,
251
+ error: validation.error,
252
+ });
253
+ return;
254
+ }
255
+
256
+ const { path } = validation;
257
+ const { name } = req.body as { name?: string };
258
+
259
+ // Validate path is a valid project
260
+ if (!isValidProjectPath(path!)) {
261
+ res.status(400).json({
262
+ success: false,
263
+ error: 'Invalid project path - must contain .steroids/steroids.db and not be a system directory',
264
+ });
265
+ return;
266
+ }
267
+
268
+ registerProject(path!, name);
269
+
270
+ // Fetch the registered project to return
271
+ const project = getRegisteredProject(path!);
272
+
273
+ res.status(201).json({
274
+ success: true,
275
+ message: 'Project registered successfully',
276
+ project: { ...project, orphaned_in_progress: 0 },
277
+ });
278
+ } catch (error) {
279
+ console.error('Error registering project:', error);
280
+ res.status(500).json({
281
+ success: false,
282
+ error: 'Failed to register project',
283
+ message: error instanceof Error ? error.message : 'Unknown error',
284
+ });
285
+ }
286
+ });
287
+
288
+ /**
289
+ * POST /api/projects/remove
290
+ * Unregister a project
291
+ * Body: { path: string }
292
+ */
293
+ router.post('/projects/remove', (req: Request, res: Response) => {
294
+ try {
295
+ const validation = validatePathRequest(req.body);
296
+ if (!validation.valid) {
297
+ res.status(400).json({
298
+ success: false,
299
+ error: validation.error,
300
+ });
301
+ return;
302
+ }
303
+
304
+ const { path } = validation;
305
+
306
+ // Check if project exists
307
+ const project = getRegisteredProject(path!);
308
+ if (!project) {
309
+ res.status(404).json({
310
+ success: false,
311
+ error: 'Project not found in registry',
312
+ });
313
+ return;
314
+ }
315
+
316
+ unregisterProject(path!);
317
+
318
+ res.json({
319
+ success: true,
320
+ message: 'Project unregistered successfully',
321
+ });
322
+ } catch (error) {
323
+ console.error('Error unregistering project:', error);
324
+ res.status(500).json({
325
+ success: false,
326
+ error: 'Failed to unregister project',
327
+ message: error instanceof Error ? error.message : 'Unknown error',
328
+ });
329
+ }
330
+ });
331
+
332
+ /**
333
+ * POST /api/projects/enable
334
+ * Enable a project for wakeup
335
+ * Body: { path: string }
336
+ */
337
+ router.post('/projects/enable', (req: Request, res: Response) => {
338
+ try {
339
+ const validation = validatePathRequest(req.body);
340
+ if (!validation.valid) {
341
+ res.status(400).json({
342
+ success: false,
343
+ error: validation.error,
344
+ });
345
+ return;
346
+ }
347
+
348
+ const { path } = validation;
349
+
350
+ // Check if project exists
351
+ const project = getRegisteredProject(path!);
352
+ if (!project) {
353
+ res.status(404).json({
354
+ success: false,
355
+ error: 'Project not found in registry',
356
+ });
357
+ return;
358
+ }
359
+
360
+ enableProject(path!);
361
+
362
+ res.json({
363
+ success: true,
364
+ message: 'Project enabled successfully',
365
+ });
366
+ } catch (error) {
367
+ console.error('Error enabling project:', error);
368
+ res.status(500).json({
369
+ success: false,
370
+ error: 'Failed to enable project',
371
+ message: error instanceof Error ? error.message : 'Unknown error',
372
+ });
373
+ }
374
+ });
375
+
376
+ /**
377
+ * POST /api/projects/reset
378
+ * Reset failed, skipped, and disputed tasks for a project, and re-enable it.
379
+ * Body: { path: string }
380
+ */
381
+ router.post('/projects/reset', (req: Request, res: Response) => {
382
+ try {
383
+ const validation = validatePathRequest(req.body);
384
+ if (!validation.valid) {
385
+ res.status(400).json({
386
+ success: false,
387
+ error: 'Invalid request',
388
+ message: validation.error,
389
+ });
390
+ return;
391
+ }
392
+
393
+ const projectPath = validation.path!;
394
+
395
+ if (!isValidProjectPath(projectPath)) {
396
+ res.status(403).json({
397
+ success: false,
398
+ error: 'Access denied',
399
+ message: 'Invalid project path',
400
+ });
401
+ return;
402
+ }
403
+
404
+ // Re-enable project first
405
+ enableProject(projectPath);
406
+
407
+ // Run the CLI reset command
408
+ const cliBin = fileURLToPath(new URL('../../../dist/index.js', import.meta.url));
409
+ execSync(`node "${cliBin}" tasks reset --all`, { cwd: projectPath, stdio: 'pipe' });
410
+
411
+ // Reset orphaned in_progress tasks — only when no active runner exists
412
+ // Uses inline SQL against the already-open globalDb — same pattern as detection
413
+ const { db: globalDb, close: closeGlobalDb } = openGlobalDatabase();
414
+ try {
415
+ const hasStandaloneRunner = globalDb.prepare(
416
+ `SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
417
+ AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
418
+ ).get(projectPath) !== undefined;
419
+ // Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
420
+ const hasParallelSession = hasActiveParallelSessionForProjectDb(globalDb as never, projectPath);
421
+
422
+ if (!hasStandaloneRunner && !hasParallelSession) {
423
+ const dbPath = join(projectPath, '.steroids', 'steroids.db');
424
+ if (existsSync(dbPath)) {
425
+ // Declare before try so finally can safely reference it (even if constructor throws)
426
+ let projectDb: Database.Database | undefined;
427
+ try {
428
+ projectDb = new Database(dbPath, { fileMustExist: true });
429
+ projectDb.transaction(() => {
430
+ // Clear locks first — 60-min TTL would block new runner pickup otherwise
431
+ projectDb!
432
+ .prepare(`DELETE FROM task_locks WHERE task_id IN (SELECT id FROM tasks WHERE status = 'in_progress')`)
433
+ .run();
434
+ projectDb!
435
+ .prepare(`UPDATE tasks SET status = 'pending', updated_at = datetime('now') WHERE status = 'in_progress'`)
436
+ .run();
437
+ })();
438
+ } finally {
439
+ projectDb?.close();
440
+ }
441
+ }
442
+ }
443
+ // No wakeup() call — its blast radius covers ALL projects, not just this one.
444
+ // The cron daemon picks up newly-pending tasks on its next cycle.
445
+ // Users wanting immediate pickup can hit "Start Daemon."
446
+ } finally {
447
+ closeGlobalDb();
448
+ }
449
+
450
+ res.json({
451
+ success: true,
452
+ message: 'Project tasks reset and project enabled successfully'
453
+ });
454
+ } catch (error) {
455
+ console.error('Error resetting project:', error);
456
+ res.status(500).json({
457
+ success: false,
458
+ error: 'Failed to reset project',
459
+ message: error instanceof Error ? error.message : 'Unknown error',
460
+ });
461
+ }
462
+ });
463
+
464
+ /**
465
+ * POST /api/projects/disable
466
+ * Disable a project (skip in wakeup)
467
+ * Body: { path: string }
468
+ */
469
+ router.post('/projects/disable', (req: Request, res: Response) => {
470
+ try {
471
+ const validation = validatePathRequest(req.body);
472
+ if (!validation.valid) {
473
+ res.status(400).json({
474
+ success: false,
475
+ error: validation.error,
476
+ });
477
+ return;
478
+ }
479
+
480
+ const { path } = validation;
481
+
482
+ // Check if project exists
483
+ const project = getRegisteredProject(path!);
484
+ if (!project) {
485
+ res.status(404).json({
486
+ success: false,
487
+ error: 'Project not found in registry',
488
+ });
489
+ return;
490
+ }
491
+
492
+ disableProject(path!);
493
+
494
+ res.json({
495
+ success: true,
496
+ message: 'Project disabled successfully',
497
+ });
498
+ } catch (error) {
499
+ console.error('Error disabling project:', error);
500
+ res.status(500).json({
501
+ success: false,
502
+ error: 'Failed to disable project',
503
+ message: error instanceof Error ? error.message : 'Unknown error',
504
+ });
505
+ }
506
+ });
507
+
508
+ /**
509
+ * POST /api/projects/prune
510
+ * Remove stale projects (directories that no longer exist)
511
+ */
512
+ router.post('/projects/prune', (req: Request, res: Response) => {
513
+ try {
514
+ const removedCount = pruneProjects();
515
+
516
+ res.json({
517
+ success: true,
518
+ message: `Pruned ${removedCount} stale project(s)`,
519
+ removed_count: removedCount,
520
+ });
521
+ } catch (error) {
522
+ console.error('Error pruning projects:', error);
523
+ res.status(500).json({
524
+ success: false,
525
+ error: 'Failed to prune projects',
526
+ message: error instanceof Error ? error.message : 'Unknown error',
527
+ });
528
+ }
529
+ });
530
+
531
+ /**
532
+ * GET /api/projects/status
533
+ * Get a single project's status by path (query param)
534
+ */
535
+ router.get('/projects/status', (req: Request, res: Response) => {
536
+ try {
537
+ const path = req.query.path as string;
538
+
539
+ if (!path) {
540
+ res.status(400).json({
541
+ success: false,
542
+ error: 'Path query parameter is required',
543
+ });
544
+ return;
545
+ }
546
+
547
+ const project = getRegisteredProject(path);
548
+ if (!project) {
549
+ res.status(404).json({
550
+ success: false,
551
+ error: 'Project not found in registry',
552
+ });
553
+ return;
554
+ }
555
+
556
+ // Get runner info
557
+ const { db, close } = openGlobalDatabase();
558
+ try {
559
+ const runner = db
560
+ .prepare('SELECT id, status, pid, current_task_id, heartbeat_at FROM runners WHERE project_path = ?')
561
+ .get(path) as {
562
+ id: string;
563
+ status: string;
564
+ pid: number | null;
565
+ current_task_id: string | null;
566
+ heartbeat_at: string | null;
567
+ } | undefined;
568
+
569
+ const storageInfo = getCachedListStorage(project.path);
570
+ const liveData = getProjectLiveData(project.path);
571
+
572
+ // Inline SQL against the already-open db — no extra DB connections
573
+ const hasStandaloneRunner = db.prepare(
574
+ `SELECT 1 FROM runners WHERE project_path = ? AND status != 'stopped'
575
+ AND heartbeat_at > datetime('now', '-5 minutes') AND parallel_session_id IS NULL`
576
+ ).get(path) !== undefined;
577
+ // Cast: DbLike's run signature uses unknown[] but better-sqlite3 uses {} — runtime-compatible
578
+ const hasParallelSession = hasActiveParallelSessionForProjectDb(db as never, path);
579
+ const orphanedInProgress = (hasStandaloneRunner || hasParallelSession)
580
+ ? 0
581
+ : (liveData.stats.in_progress ?? 0);
582
+
583
+ const response: ProjectResponse = {
584
+ path: project.path,
585
+ name: project.name,
586
+ enabled: project.enabled,
587
+ registered_at: project.registered_at,
588
+ last_seen_at: project.last_seen_at,
589
+ last_activity_at: runner?.heartbeat_at || null,
590
+ last_task_added_at: liveData.last_task_added_at,
591
+ isBlocked: liveData.isBlocked,
592
+ isUnreachable: liveData.isUnreachable,
593
+ stats: liveData.stats,
594
+ runner: runner
595
+ ? {
596
+ id: runner.id,
597
+ status: runner.status,
598
+ pid: runner.pid,
599
+ current_task_id: runner.current_task_id,
600
+ heartbeat_at: runner.heartbeat_at,
601
+ }
602
+ : null,
603
+ storage_bytes: storageInfo?.storage_bytes ?? null,
604
+ storage_human: storageInfo?.storage_human ?? null,
605
+ storage_warning: storageInfo?.storage_warning ?? null,
606
+ orphaned_in_progress: orphanedInProgress,
607
+ };
608
+
609
+ res.json({
610
+ success: true,
611
+ project: response,
612
+ });
613
+ } finally {
614
+ close();
615
+ }
616
+ } catch (error) {
617
+ console.error('Error getting project status:', error);
618
+ res.status(500).json({
619
+ success: false,
620
+ error: 'Failed to get project status',
621
+ message: error instanceof Error ? error.message : 'Unknown error',
622
+ });
623
+ }
624
+ });
625
+
626
+ /** POST /api/projects/open - Open project folder in Finder */
627
+ router.post('/projects/open', (req: Request, res: Response) => {
628
+ try {
629
+ const validation = validatePathRequest(req.body);
630
+ if (!validation.valid) {
631
+ res.status(400).json({ success: false, error: validation.error });
632
+ return;
633
+ }
634
+ const { path } = validation;
635
+ if (!existsSync(path!)) {
636
+ res.status(404).json({ success: false, error: 'Path does not exist' });
637
+ return;
638
+ }
639
+ execSync(`open "${path}"`, { encoding: 'utf-8' });
640
+ res.json({ success: true, message: 'Folder opened in Finder' });
641
+ } catch (error) {
642
+ console.error('Error opening project folder:', error);
643
+ res.status(500).json({
644
+ success: false, error: 'Failed to open project folder',
645
+ message: error instanceof Error ? error.message : 'Unknown error',
646
+ });
647
+ }
648
+ });
649
+
650
+ /**
651
+ * GET /api/projects/logs
652
+ * List all available log files for a project
653
+ */
654
+ router.get('/projects/logs', (req: Request, res: Response) => {
655
+ try {
656
+ const projectPath = req.query.path as string;
657
+ if (!projectPath) {
658
+ res.status(400).json({ success: false, error: 'Path query parameter is required' });
659
+ return;
660
+ }
661
+
662
+ if (!isValidProjectPath(projectPath)) {
663
+ res.status(403).json({ success: false, error: 'Invalid project path' });
664
+ return;
665
+ }
666
+
667
+ const logsDir = join(projectPath, '.steroids', 'logs');
668
+ const invocationsDir = join(projectPath, '.steroids', 'invocations');
669
+
670
+ const logs: { name: string; path: string; size: number; mtime: Date; type: 'log' | 'invocation' }[] = [];
671
+
672
+ [ { dir: logsDir, type: 'log' as const }, { dir: invocationsDir, type: 'invocation' as const } ].forEach(({ dir, type }) => {
673
+ if (existsSync(dir)) {
674
+ const files = readdirSync(dir);
675
+ for (const file of files) {
676
+ if (file.endsWith('.log') || file.endsWith('.jsonl') || file.endsWith('.txt')) {
677
+ const filePath = join(dir, file);
678
+ const stats = statSync(filePath);
679
+ if (stats.isFile()) {
680
+ logs.push({
681
+ name: file,
682
+ path: relative(projectPath, filePath),
683
+ size: stats.size,
684
+ mtime: stats.mtime,
685
+ type
686
+ });
687
+ }
688
+ }
689
+ }
690
+ }
691
+ });
692
+
693
+ // Sort by modified time, newest first
694
+ logs.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
695
+
696
+ res.json({ success: true, logs });
697
+ } catch (error) {
698
+ console.error('Error listing project logs:', error);
699
+ res.status(500).json({ success: false, error: 'Failed to list project logs' });
700
+ }
701
+ });
702
+
703
+ /**
704
+ * GET /api/projects/logs/content
705
+ * Get the content of a specific log file
706
+ */
707
+ router.get('/projects/logs/content', (req: Request, res: Response) => {
708
+ try {
709
+ const projectPath = req.query.path as string;
710
+ const logFile = req.query.file as string;
711
+
712
+ if (!projectPath || !logFile) {
713
+ res.status(400).json({ success: false, error: 'Path and file query parameters are required' });
714
+ return;
715
+ }
716
+
717
+ if (!isValidProjectPath(projectPath)) {
718
+ res.status(403).json({ success: false, error: 'Invalid project path' });
719
+ return;
720
+ }
721
+
722
+ const realProjectPath = realpathSync(projectPath);
723
+ let fullLogPath = resolve(realProjectPath, logFile);
724
+
725
+ if (!existsSync(fullLogPath)) {
726
+ res.status(404).json({ success: false, error: 'Log file not found' });
727
+ return;
728
+ }
729
+
730
+ fullLogPath = realpathSync(fullLogPath);
731
+
732
+ // Security (Path Traversal Guard): Strict canonicalization and root path verification
733
+ if (!fullLogPath.startsWith(realProjectPath + sep)) {
734
+ res.status(403).json({ success: false, error: 'Access denied: Path traversal detected' });
735
+ return;
736
+ }
737
+
738
+ // Only allow access to .steroids/logs and .steroids/invocations
739
+ const allowedLogsDir = join(realProjectPath, '.steroids', 'logs') + sep;
740
+ const allowedInvocationsDir = join(realProjectPath, '.steroids', 'invocations') + sep;
741
+
742
+ if (!fullLogPath.startsWith(allowedLogsDir) && !fullLogPath.startsWith(allowedInvocationsDir)) {
743
+ res.status(403).json({ success: false, error: 'Access denied: Only log directories are allowed' });
744
+ return;
745
+ }
746
+
747
+ // Since files can be large, we use sendFile
748
+ res.sendFile(fullLogPath);
749
+ } catch (error) {
750
+ console.error('Error reading log file:', error);
751
+ res.status(500).json({ success: false, error: 'Failed to read log file' });
752
+ }
753
+ });
754
+
755
+ /**
756
+ * GET /api/projects/instructions?path=<projectPath>
757
+ * Returns instruction files (AGENTS.md, CLAUDE.md, GEMINI.md) with existence, enabled state, and content.
758
+ * Also returns customInstructions string.
759
+ */
760
+ router.get('/projects/instructions', (req: Request, res: Response) => {
761
+ try {
762
+ const projectPath = req.query.path as string;
763
+ if (!projectPath) {
764
+ res.status(400).json({ success: false, error: 'Path query parameter is required' });
765
+ return;
766
+ }
767
+ if (!isValidProjectPath(projectPath)) {
768
+ res.status(403).json({ success: false, error: 'Invalid project path' });
769
+ return;
770
+ }
771
+
772
+ // Dynamically import from compiled dist to avoid circular build issues
773
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
774
+ const { getInstructionFilesList, readInstructionOverrides } = require('../../../dist/prompts/instruction-files.js');
775
+ const files = getInstructionFilesList(projectPath);
776
+ const overrides = readInstructionOverrides(projectPath);
777
+
778
+ res.json({
779
+ success: true,
780
+ files,
781
+ customInstructions: overrides.customInstructions ?? '',
782
+ });
783
+ } catch (error) {
784
+ console.error('Error getting project instructions:', error);
785
+ res.status(500).json({
786
+ success: false,
787
+ error: 'Failed to get project instructions',
788
+ message: error instanceof Error ? error.message : 'Unknown error',
789
+ });
790
+ }
791
+ });
792
+
793
+ /**
794
+ * POST /api/projects/instructions
795
+ * Toggle a specific instruction file on/off, or save custom instructions.
796
+ * Body (file toggle): { path: string, key: "agentsMd" | "claudeMd" | "geminiMd", enabled: boolean }
797
+ * Body (custom instructions): { path: string, customInstructions: string }
798
+ */
799
+ router.post('/projects/instructions', (req: Request, res: Response) => {
800
+ try {
801
+ const { path: projectPath, key, enabled, customInstructions } = req.body as {
802
+ path: string;
803
+ key?: string;
804
+ enabled?: boolean;
805
+ customInstructions?: string;
806
+ };
807
+
808
+ if (!projectPath) {
809
+ res.status(400).json({ success: false, error: 'path is required' });
810
+ return;
811
+ }
812
+ if (!isValidProjectPath(projectPath)) {
813
+ res.status(403).json({ success: false, error: 'Invalid project path' });
814
+ return;
815
+ }
816
+
817
+ const validKeys = ['agentsMd', 'claudeMd', 'geminiMd'];
818
+ const updatingFile = key !== undefined;
819
+ const updatingCustom = customInstructions !== undefined;
820
+
821
+ if (updatingFile && !validKeys.includes(key!)) {
822
+ res.status(400).json({ success: false, error: `key must be one of: ${validKeys.join(', ')}` });
823
+ return;
824
+ }
825
+ if (!updatingFile && !updatingCustom) {
826
+ res.status(400).json({ success: false, error: 'Provide either key+enabled or customInstructions' });
827
+ return;
828
+ }
829
+
830
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
831
+ const { readInstructionOverrides, writeInstructionOverrides } = require('../../../dist/prompts/instruction-files.js');
832
+ const overrides = readInstructionOverrides(projectPath);
833
+
834
+ if (updatingFile) {
835
+ overrides[key!] = enabled;
836
+ }
837
+ if (updatingCustom) {
838
+ overrides.customInstructions = customInstructions;
839
+ }
840
+
841
+ writeInstructionOverrides(projectPath, overrides);
842
+
843
+ res.json({ success: true, message: 'Instructions updated' });
844
+ } catch (error) {
845
+ console.error('Error updating project instructions:', error);
846
+ res.status(500).json({
847
+ success: false,
848
+ error: 'Failed to update project instructions',
849
+ message: error instanceof Error ? error.message : 'Unknown error',
850
+ });
851
+ }
852
+ });
853
+
854
+ export default router;