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,723 @@
1
+ /**
2
+ * Configuration API routes
3
+ * Provides schema and config value endpoints
4
+ */
5
+
6
+ import { Router, Request, Response } from 'express';
7
+ import { execSync } from 'node:child_process';
8
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
9
+ import { dirname, join } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+ import { homedir } from 'node:os';
12
+ import { parse, stringify } from 'yaml';
13
+
14
+ // Types for API model responses
15
+ interface APIModel {
16
+ id: string;
17
+ name: string;
18
+ description?: string;
19
+ contextWindow?: number;
20
+ }
21
+
22
+ // ES module equivalent of __dirname
23
+ const __filename = fileURLToPath(import.meta.url);
24
+ const __dirname = dirname(__filename);
25
+
26
+ const router = Router();
27
+
28
+ const STEROIDS_DIR = '.steroids';
29
+ const CONFIG_FILE = 'config.yaml';
30
+
31
+ /**
32
+ * Get config schema by running CLI command
33
+ * Uses the globally installed steroids command
34
+ */
35
+ function getSchema(category?: string): object | null {
36
+ try {
37
+ const cmd = category
38
+ ? `steroids config schema ${category} --json`
39
+ : `steroids config schema --json`;
40
+ const output = execSync(cmd, { encoding: 'utf-8', timeout: 5000 });
41
+ return JSON.parse(output);
42
+ } catch (error) {
43
+ console.error('Failed to get schema:', error);
44
+ return null;
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get global config path
50
+ */
51
+ function getGlobalConfigPath(): string {
52
+ return join(homedir(), STEROIDS_DIR, CONFIG_FILE);
53
+ }
54
+
55
+ /**
56
+ * Get project config path
57
+ */
58
+ function getProjectConfigPath(projectPath: string): string {
59
+ return join(projectPath, STEROIDS_DIR, CONFIG_FILE);
60
+ }
61
+
62
+ /**
63
+ * Load config from file
64
+ */
65
+ function loadConfigFile(filePath: string): Record<string, unknown> {
66
+ if (!existsSync(filePath)) {
67
+ return {};
68
+ }
69
+ try {
70
+ const content = readFileSync(filePath, 'utf-8');
71
+ return parse(content) ?? {};
72
+ } catch (error) {
73
+ console.error(`Failed to load config from ${filePath}:`, error);
74
+ return {};
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Save config to file
80
+ */
81
+ function saveConfigFile(filePath: string, config: Record<string, unknown>): void {
82
+ const dir = dirname(filePath);
83
+ if (!existsSync(dir)) {
84
+ mkdirSync(dir, { recursive: true });
85
+ }
86
+ const content = stringify(config, { indent: 2 });
87
+ writeFileSync(filePath, content, 'utf-8');
88
+ }
89
+
90
+ /**
91
+ * Deep merge two objects
92
+ */
93
+ function mergeConfigs(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
94
+ const result: Record<string, unknown> = { ...base };
95
+
96
+ for (const key of Object.keys(override)) {
97
+ const baseValue = base[key];
98
+ const overrideValue = override[key];
99
+
100
+ if (
101
+ baseValue !== null &&
102
+ typeof baseValue === 'object' &&
103
+ !Array.isArray(baseValue) &&
104
+ overrideValue !== null &&
105
+ typeof overrideValue === 'object' &&
106
+ !Array.isArray(overrideValue)
107
+ ) {
108
+ result[key] = mergeConfigs(
109
+ baseValue as Record<string, unknown>,
110
+ overrideValue as Record<string, unknown>
111
+ );
112
+ } else {
113
+ result[key] = overrideValue;
114
+ }
115
+ }
116
+
117
+ return result;
118
+ }
119
+
120
+ /**
121
+ * Set a nested value in config object
122
+ */
123
+ function setConfigValue(config: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> {
124
+ const result = JSON.parse(JSON.stringify(config));
125
+ const parts = path.split('.');
126
+ let current: Record<string, unknown> = result;
127
+
128
+ for (let i = 0; i < parts.length - 1; i++) {
129
+ if (!(parts[i] in current) || typeof current[parts[i]] !== 'object') {
130
+ current[parts[i]] = {};
131
+ }
132
+ current = current[parts[i]] as Record<string, unknown>;
133
+ }
134
+
135
+ current[parts[parts.length - 1]] = value;
136
+ return result;
137
+ }
138
+
139
+ // GET /api/config/schema - Get full configuration schema
140
+ router.get('/config/schema', (req: Request, res: Response) => {
141
+ const schema = getSchema();
142
+ if (!schema) {
143
+ return res.status(500).json({
144
+ success: false,
145
+ error: 'Failed to load schema',
146
+ });
147
+ }
148
+ res.json({
149
+ success: true,
150
+ data: schema,
151
+ });
152
+ });
153
+
154
+ // GET /api/config/schema/:category - Get schema for a specific category
155
+ router.get('/config/schema/:category', (req: Request, res: Response) => {
156
+ const { category } = req.params;
157
+ const schema = getSchema(category);
158
+ if (!schema) {
159
+ return res.status(404).json({
160
+ success: false,
161
+ error: `Category not found: ${category}`,
162
+ });
163
+ }
164
+ res.json({
165
+ success: true,
166
+ data: schema,
167
+ });
168
+ });
169
+
170
+ // GET /api/config - Get configuration values
171
+ router.get('/config', (req: Request, res: Response) => {
172
+ const scope = req.query.scope as string || 'merged';
173
+ const projectPath = req.query.project as string;
174
+
175
+ try {
176
+ let config: Record<string, unknown>;
177
+
178
+ if (scope === 'global') {
179
+ config = loadConfigFile(getGlobalConfigPath());
180
+ } else if (scope === 'project') {
181
+ if (!projectPath) {
182
+ return res.status(400).json({
183
+ success: false,
184
+ error: 'Project path required for project scope',
185
+ });
186
+ }
187
+ config = loadConfigFile(getProjectConfigPath(projectPath));
188
+ } else {
189
+ // Merged: global + project
190
+ const globalConfig = loadConfigFile(getGlobalConfigPath());
191
+ if (projectPath) {
192
+ const projectConfig = loadConfigFile(getProjectConfigPath(projectPath));
193
+ config = mergeConfigs(globalConfig, projectConfig);
194
+ } else {
195
+ config = globalConfig;
196
+ }
197
+ }
198
+
199
+ res.json({
200
+ success: true,
201
+ data: {
202
+ scope,
203
+ project: projectPath || null,
204
+ config,
205
+ },
206
+ });
207
+ } catch (error) {
208
+ res.status(500).json({
209
+ success: false,
210
+ error: 'Failed to load config',
211
+ message: error instanceof Error ? error.message : 'Unknown error',
212
+ });
213
+ }
214
+ });
215
+
216
+ // PUT /api/config - Update configuration values
217
+ router.put('/config', (req: Request, res: Response) => {
218
+ const { scope, project, updates } = req.body as {
219
+ scope?: 'global' | 'project';
220
+ project?: string;
221
+ updates: Record<string, unknown>;
222
+ };
223
+
224
+ if (!updates || typeof updates !== 'object') {
225
+ return res.status(400).json({
226
+ success: false,
227
+ error: 'Updates object required',
228
+ });
229
+ }
230
+
231
+ const targetScope = scope || 'global';
232
+
233
+ try {
234
+ let configPath: string;
235
+
236
+ if (targetScope === 'project') {
237
+ if (!project) {
238
+ return res.status(400).json({
239
+ success: false,
240
+ error: 'Project path required for project scope',
241
+ });
242
+ }
243
+ configPath = getProjectConfigPath(project);
244
+ } else {
245
+ configPath = getGlobalConfigPath();
246
+ }
247
+
248
+ // Load existing config
249
+ let config = loadConfigFile(configPath);
250
+
251
+ // Apply updates
252
+ for (const [path, value] of Object.entries(updates)) {
253
+ config = setConfigValue(config, path, value);
254
+ }
255
+
256
+ // Save config
257
+ saveConfigFile(configPath, config);
258
+
259
+ res.json({
260
+ success: true,
261
+ data: {
262
+ scope: targetScope,
263
+ project: project || null,
264
+ path: configPath,
265
+ updates,
266
+ },
267
+ });
268
+ } catch (error) {
269
+ res.status(500).json({
270
+ success: false,
271
+ error: 'Failed to save config',
272
+ message: error instanceof Error ? error.message : 'Unknown error',
273
+ });
274
+ }
275
+ });
276
+
277
+ // Claude CLI alias models — always valid regardless of API key availability
278
+ const CLAUDE_ALIAS_MODELS: APIModel[] = [
279
+ { id: 'opus', name: 'Claude Opus (latest)' },
280
+ { id: 'sonnet', name: 'Claude Sonnet (latest)' },
281
+ { id: 'haiku', name: 'Claude Haiku (latest)' },
282
+ ];
283
+
284
+ // Static model lists for each provider (fallback when API key present but API call fails)
285
+ const FALLBACK_MODELS: Record<string, APIModel[]> = {
286
+ claude: [
287
+ { id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
288
+ { id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
289
+ { id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' },
290
+ { id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' },
291
+ { id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
292
+ ],
293
+ gemini: [
294
+ { id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
295
+ { id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
296
+ { id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite' },
297
+ { id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (Preview)' },
298
+ { id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Preview)' },
299
+ ],
300
+ codex: [
301
+ { id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' },
302
+ { id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark' },
303
+ { id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex' },
304
+ { id: 'gpt-5.2', name: 'GPT-5.2' },
305
+ { id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' },
306
+ { id: 'gpt-5.1', name: 'GPT-5.1' },
307
+ ],
308
+ mistral: [
309
+ { id: 'pixtral-large-latest', name: 'Pixtral Large' },
310
+ { id: 'devstral-2', name: 'Devstral (Le Chat)' },
311
+ { id: 'devstral-small', name: 'Devstral Small (Le Chat)' },
312
+ { id: 'mistral-large-latest', name: 'Mistral Large (latest)' },
313
+ { id: 'mistral-medium-latest', name: 'Mistral Medium (latest)' },
314
+ { id: 'mistral-small-latest', name: 'Mistral Small (latest)' },
315
+ { id: 'ministral-8b-latest', name: 'Ministral 8B (latest)' },
316
+ ],
317
+ };
318
+
319
+ // CLI config file paths
320
+ const CLI_CONFIG_PATHS = {
321
+ claude: join(homedir(), '.claude', '.credentials.json'),
322
+ gemini: join(homedir(), '.gemini', 'oauth_creds.json'),
323
+ codex: join(homedir(), '.codex', 'auth.json'),
324
+ codexModelsCache: join(homedir(), '.codex', 'models_cache.json'),
325
+ };
326
+
327
+ /**
328
+ * Get Claude API key
329
+ * Note: CLI OAuth tokens (sk-ant-oat01-*) don't work with /v1/models endpoint
330
+ * Only real API keys (sk-ant-api-*) work, so we check env vars
331
+ */
332
+ function getClaudeApiKey(): string | null {
333
+ // Check environment variable first (real API key)
334
+ const envKey = process.env.STEROIDS_ANTHROPIC;
335
+ if (envKey && envKey.startsWith('sk-ant-api')) {
336
+ return envKey;
337
+ }
338
+ return null;
339
+ }
340
+
341
+ /**
342
+ * Get Gemini API key
343
+ * Note: CLI OAuth tokens don't have the right scope for generativelanguage API
344
+ * Only API keys work, so we check env vars
345
+ */
346
+ function getGeminiApiKey(): string | null {
347
+ // Check environment variables (real API key)
348
+ return process.env.STEROIDS_GOOGLE || null;
349
+ }
350
+
351
+ /**
352
+ * Get Mistral API key
353
+ */
354
+ function getMistralApiKey(): string | null {
355
+ return process.env.STEROIDS_MISTRAL || null;
356
+ }
357
+
358
+ /**
359
+ * Read Codex CLI OAuth token
360
+ */
361
+ function getCodexToken(): string | null {
362
+ try {
363
+ if (!existsSync(CLI_CONFIG_PATHS.codex)) return null;
364
+ const data = JSON.parse(readFileSync(CLI_CONFIG_PATHS.codex, 'utf-8'));
365
+ return data?.tokens?.access_token || null;
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Read Codex models from local cache file
373
+ */
374
+ function getCodexModelsFromCache(): APIModel[] | null {
375
+ try {
376
+ if (!existsSync(CLI_CONFIG_PATHS.codexModelsCache)) return null;
377
+ const data = JSON.parse(readFileSync(CLI_CONFIG_PATHS.codexModelsCache, 'utf-8'));
378
+ if (!data?.models?.length) return null;
379
+ return data.models
380
+ .filter((m: { visibility?: string }) => m.visibility === 'list')
381
+ .map((m: { slug: string; display_name?: string; description?: string }) => ({
382
+ id: m.slug,
383
+ name: m.display_name || m.slug,
384
+ description: m.description,
385
+ }));
386
+ } catch {
387
+ return null;
388
+ }
389
+ }
390
+
391
+ /**
392
+ * Fetch Claude models from Anthropic API
393
+ * Note: Only works with real API keys, not CLI OAuth tokens.
394
+ * Without an API key we return only the 3 CLI aliases (opus/sonnet/haiku) because
395
+ * those are always valid regardless of the installed Claude CLI version.
396
+ */
397
+ async function fetchClaudeModels(): Promise<{ models: APIModel[]; source: string }> {
398
+ const apiKey = getClaudeApiKey();
399
+ if (!apiKey) {
400
+ return { models: CLAUDE_ALIAS_MODELS, source: 'aliases' };
401
+ }
402
+
403
+ try {
404
+ const response = await fetch('https://api.anthropic.com/v1/models', {
405
+ method: 'GET',
406
+ headers: {
407
+ 'x-api-key': apiKey,
408
+ 'anthropic-version': '2023-06-01',
409
+ },
410
+ });
411
+
412
+ if (!response.ok) {
413
+ return { models: FALLBACK_MODELS.claude, source: 'fallback' };
414
+ }
415
+
416
+ const data = await response.json() as { data: Array<{ id: string; display_name?: string }> };
417
+ const models: APIModel[] = data.data.map((m) => ({
418
+ id: m.id,
419
+ name: m.display_name || m.id,
420
+ }));
421
+
422
+ // Sort: opus first, then sonnet, then haiku
423
+ models.sort((a, b) => {
424
+ const getScore = (id: string) => {
425
+ if (id.includes('opus')) return 0;
426
+ if (id.includes('sonnet')) return 1;
427
+ if (id.includes('haiku')) return 2;
428
+ return 3;
429
+ };
430
+ return getScore(a.id) - getScore(b.id);
431
+ });
432
+
433
+ return { models, source: 'api' };
434
+ } catch {
435
+ return { models: FALLBACK_MODELS.claude, source: 'fallback' };
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Fetch Gemini models from Google API
441
+ * Note: Only works with API keys, not CLI OAuth tokens
442
+ */
443
+ async function fetchGeminiModels(): Promise<{ models: APIModel[]; source: string }> {
444
+ const apiKey = getGeminiApiKey();
445
+ if (!apiKey) {
446
+ return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
447
+ }
448
+
449
+ try {
450
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
451
+ method: 'GET',
452
+ });
453
+
454
+ if (!response.ok) {
455
+ return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
456
+ }
457
+
458
+ const data = await response.json() as {
459
+ models: Array<{
460
+ name: string;
461
+ displayName?: string;
462
+ description?: string;
463
+ supportedGenerationMethods?: string[];
464
+ }>;
465
+ };
466
+
467
+ const models: APIModel[] = data.models
468
+ .filter((m) => m.supportedGenerationMethods?.includes('generateContent') && m.name.includes('gemini'))
469
+ .map((m) => ({
470
+ id: m.name.replace('models/', ''),
471
+ name: m.displayName || m.name.replace('models/', ''),
472
+ description: m.description,
473
+ }));
474
+
475
+ // Sort by version (newer first)
476
+ models.sort((a, b) => {
477
+ const getScore = (id: string) => {
478
+ if (id.includes('3')) return 0;
479
+ if (id.includes('2.5')) return 1;
480
+ if (id.includes('2.0')) return 2;
481
+ if (id.includes('1.5')) return 3;
482
+ return 4;
483
+ };
484
+ return getScore(a.id) - getScore(b.id);
485
+ });
486
+
487
+ return { models, source: 'api' };
488
+ } catch {
489
+ return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
490
+ }
491
+ }
492
+
493
+ /**
494
+ * Fetch Mistral models from Mistral API
495
+ */
496
+ async function fetchMistralModels(): Promise<{ models: APIModel[]; source: string }> {
497
+ const apiKey = getMistralApiKey();
498
+ if (!apiKey) {
499
+ return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
500
+ }
501
+
502
+ try {
503
+ const response = await fetch('https://api.mistral.ai/v1/models', {
504
+ method: 'GET',
505
+ headers: {
506
+ 'Authorization': `Bearer ${apiKey}`,
507
+ },
508
+ });
509
+
510
+ if (!response.ok) {
511
+ return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
512
+ }
513
+
514
+ const data = await response.json() as
515
+ | { data?: Array<{ id: string; name?: string; description?: string; max_context_length?: number }> }
516
+ | Array<{ id: string; name?: string; description?: string; max_context_length?: number }>;
517
+
518
+ const rawModels = Array.isArray(data) ? data : (data.data ?? []);
519
+ const modelById = new Map<string, APIModel>();
520
+ for (const m of rawModels) {
521
+ const mapped: APIModel = {
522
+ id: m.id,
523
+ name: m.name || formatMistralModelName(m.id),
524
+ description: m.description,
525
+ contextWindow: m.max_context_length,
526
+ };
527
+
528
+ if (!modelById.has(mapped.id)) {
529
+ modelById.set(mapped.id, mapped);
530
+ }
531
+ }
532
+
533
+ const models = dedupeMistralModels([...modelById.values()]);
534
+
535
+ models.sort((a, b) => {
536
+ const aScore = getMistralModelScore(a.id);
537
+ const bScore = getMistralModelScore(b.id);
538
+ if (aScore !== bScore) return aScore - bScore;
539
+ return a.id.localeCompare(b.id);
540
+ });
541
+
542
+ return { models, source: 'api' };
543
+ } catch {
544
+ return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
545
+ }
546
+ }
547
+
548
+ function formatMistralModelName(id: string): string {
549
+ return id
550
+ .split('-')
551
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
552
+ .join(' ');
553
+ }
554
+
555
+ function getMistralModelScore(id: string): number {
556
+ if (id.includes('codestral')) return 0;
557
+ if (id.includes('mistral-large')) return 1;
558
+ if (id.includes('mistral-medium')) return 2;
559
+ if (id.includes('mistral-small')) return 3;
560
+ if (id.includes('ministral')) return 4;
561
+ return 5;
562
+ }
563
+
564
+ function dedupeMistralModels(models: APIModel[]): APIModel[] {
565
+ const byName = new Map<string, APIModel>();
566
+
567
+ for (const model of models) {
568
+ const key = (model.name || model.id).trim().toLowerCase();
569
+ const existing = byName.get(key);
570
+
571
+ if (!existing || preferMistralModel(model, existing)) {
572
+ byName.set(key, model);
573
+ }
574
+ }
575
+
576
+ return [...byName.values()];
577
+ }
578
+
579
+ function preferMistralModel(candidate: APIModel, existing: APIModel): boolean {
580
+ const score = (model: APIModel): number => {
581
+ let value = 0;
582
+ if (model.id.includes('latest')) value += 10;
583
+ if (model.id.endsWith('-latest')) value += 5;
584
+ if (model.name?.toLowerCase().includes('latest')) value += 1;
585
+ return value;
586
+ };
587
+
588
+ const candidateScore = score(candidate);
589
+ const existingScore = score(existing);
590
+
591
+ if (candidateScore !== existingScore) {
592
+ return candidateScore > existingScore;
593
+ }
594
+
595
+ return candidate.id.localeCompare(existing.id) < 0;
596
+ }
597
+
598
+ /**
599
+ * Fetch Codex models - uses local cache or OpenAI API
600
+ */
601
+ async function fetchCodexModels(): Promise<{ models: APIModel[]; source: string }> {
602
+ // First try local cache (faster, always up to date from CLI)
603
+ const cachedModels = getCodexModelsFromCache();
604
+ if (cachedModels?.length) {
605
+ return { models: cachedModels, source: 'cache' };
606
+ }
607
+
608
+ // Fall back to API if no cache
609
+ const token = getCodexToken();
610
+ if (!token) {
611
+ return { models: FALLBACK_MODELS.codex, source: 'fallback' };
612
+ }
613
+
614
+ try {
615
+ const response = await fetch('https://api.openai.com/v1/models', {
616
+ method: 'GET',
617
+ headers: {
618
+ 'Authorization': `Bearer ${token}`,
619
+ },
620
+ });
621
+
622
+ if (!response.ok) {
623
+ return { models: FALLBACK_MODELS.codex, source: 'fallback' };
624
+ }
625
+
626
+ const data = await response.json() as { data: Array<{ id: string }> };
627
+ const models: APIModel[] = data.data
628
+ .filter((m) => m.id.includes('gpt') || m.id.startsWith('o'))
629
+ .map((m) => ({
630
+ id: m.id,
631
+ name: m.id,
632
+ }));
633
+
634
+ return { models, source: 'api' };
635
+ } catch {
636
+ return { models: FALLBACK_MODELS.codex, source: 'fallback' };
637
+ }
638
+ }
639
+
640
+ // GET /api/ai/models/:provider - Get models for a provider
641
+ // Tries CLI config credentials first, falls back to static list
642
+ router.get('/ai/models/:provider', async (req: Request, res: Response) => {
643
+ const { provider } = req.params;
644
+
645
+ if (!FALLBACK_MODELS[provider]) {
646
+ return res.status(400).json({
647
+ success: false,
648
+ error: `Unknown provider: ${provider}`,
649
+ });
650
+ }
651
+
652
+ let result: { models: APIModel[]; source: string };
653
+
654
+ switch (provider) {
655
+ case 'claude':
656
+ result = await fetchClaudeModels();
657
+ break;
658
+ case 'gemini':
659
+ result = await fetchGeminiModels();
660
+ break;
661
+ case 'mistral':
662
+ result = await fetchMistralModels();
663
+ break;
664
+ case 'codex':
665
+ result = await fetchCodexModels();
666
+ break;
667
+ default:
668
+ result = { models: FALLBACK_MODELS[provider], source: 'fallback' };
669
+ }
670
+
671
+ res.json({
672
+ success: true,
673
+ provider,
674
+ source: result.source,
675
+ models: result.models,
676
+ });
677
+ });
678
+
679
+ /**
680
+ * Check if a CLI tool is installed
681
+ */
682
+ function isCliInstalled(command: string): boolean {
683
+ try {
684
+ execSync(`which ${command}`, { encoding: 'utf-8', stdio: 'pipe' });
685
+ return true;
686
+ } catch {
687
+ return false;
688
+ }
689
+ }
690
+
691
+ // GET /api/ai/providers - Get list of available providers
692
+ router.get('/ai/providers', (req: Request, res: Response) => {
693
+ // Codex and Mistral depend on local CLIs
694
+ const providers = [
695
+ {
696
+ id: 'claude',
697
+ name: 'Anthropic (claude)',
698
+ installed: isCliInstalled('claude'),
699
+ },
700
+ {
701
+ id: 'gemini',
702
+ name: 'Google (gemini)',
703
+ installed: isCliInstalled('gemini'),
704
+ },
705
+ {
706
+ id: 'mistral',
707
+ name: 'Mistral (vibe)',
708
+ installed: isCliInstalled('vibe'),
709
+ },
710
+ {
711
+ id: 'codex',
712
+ name: 'OpenAI (codex)',
713
+ installed: isCliInstalled('codex'),
714
+ },
715
+ ];
716
+
717
+ res.json({
718
+ success: true,
719
+ providers,
720
+ });
721
+ });
722
+
723
+ export default router;