@hellcoder/companion 0.96.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (242) hide show
  1. package/bin/cli.ts +168 -0
  2. package/bin/ctl.ts +528 -0
  3. package/bin/generate-token.ts +28 -0
  4. package/dist/apple-touch-icon.png +0 -0
  5. package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
  6. package/dist/assets/CronManager-EGwLJONv.js +1 -0
  7. package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
  8. package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
  9. package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
  10. package/dist/assets/Playground-BV3k0RbV.js +109 -0
  11. package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
  12. package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
  13. package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
  14. package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
  15. package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
  16. package/dist/assets/index-BhUa1e6X.css +1 -0
  17. package/dist/assets/index-DkqeP-R9.js +134 -0
  18. package/dist/assets/sw-register-BibwRdvC.js +1 -0
  19. package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
  20. package/dist/favicon.svg +8 -0
  21. package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
  22. package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
  23. package/dist/icon-192.png +0 -0
  24. package/dist/icon-512.png +0 -0
  25. package/dist/index.html +20 -0
  26. package/dist/logo-codex.svg +14 -0
  27. package/dist/logo-docker.svg +4 -0
  28. package/dist/logo.svg +14 -0
  29. package/dist/manifest.json +24 -0
  30. package/dist/sw.js +2 -0
  31. package/package.json +104 -0
  32. package/server/agent-cron-migrator.test.ts +610 -0
  33. package/server/agent-cron-migrator.ts +85 -0
  34. package/server/agent-executor.test.ts +1108 -0
  35. package/server/agent-executor.ts +346 -0
  36. package/server/agent-store.test.ts +588 -0
  37. package/server/agent-store.ts +185 -0
  38. package/server/agent-types.ts +138 -0
  39. package/server/ai-validation-settings.test.ts +128 -0
  40. package/server/ai-validation-settings.ts +35 -0
  41. package/server/ai-validator.test.ts +387 -0
  42. package/server/ai-validator.ts +271 -0
  43. package/server/auth-manager.test.ts +83 -0
  44. package/server/auth-manager.ts +150 -0
  45. package/server/auto-namer.test.ts +252 -0
  46. package/server/auto-namer.ts +78 -0
  47. package/server/backend-adapter.test.ts +38 -0
  48. package/server/backend-adapter.ts +54 -0
  49. package/server/cache-headers.test.ts +98 -0
  50. package/server/cache-headers.ts +61 -0
  51. package/server/claude-adapter.test.ts +1363 -0
  52. package/server/claude-adapter.ts +889 -0
  53. package/server/claude-container-auth.test.ts +44 -0
  54. package/server/claude-container-auth.ts +30 -0
  55. package/server/claude-protocol-contract.test.ts +71 -0
  56. package/server/claude-protocol-drift.test.ts +78 -0
  57. package/server/claude-session-discovery.test.ts +132 -0
  58. package/server/claude-session-discovery.ts +157 -0
  59. package/server/claude-session-history.test.ts +158 -0
  60. package/server/claude-session-history.ts +410 -0
  61. package/server/cli-launcher.test.ts +1343 -0
  62. package/server/cli-launcher.ts +1298 -0
  63. package/server/cli.test.ts +16 -0
  64. package/server/codex-adapter.test.ts +5545 -0
  65. package/server/codex-adapter.ts +3062 -0
  66. package/server/codex-container-auth.test.ts +50 -0
  67. package/server/codex-container-auth.ts +24 -0
  68. package/server/codex-home.test.ts +61 -0
  69. package/server/codex-home.ts +26 -0
  70. package/server/codex-protocol-contract.test.ts +96 -0
  71. package/server/codex-protocol-drift.test.ts +123 -0
  72. package/server/codex-ws-proxy.cjs +226 -0
  73. package/server/commands-discovery.test.ts +179 -0
  74. package/server/commands-discovery.ts +81 -0
  75. package/server/constants.ts +7 -0
  76. package/server/container-manager.test.ts +1211 -0
  77. package/server/container-manager.ts +1053 -0
  78. package/server/cron-scheduler.test.ts +957 -0
  79. package/server/cron-scheduler.ts +243 -0
  80. package/server/cron-store.test.ts +422 -0
  81. package/server/cron-store.ts +148 -0
  82. package/server/cron-types.ts +63 -0
  83. package/server/env-manager.test.ts +268 -0
  84. package/server/env-manager.ts +161 -0
  85. package/server/event-bus-types.ts +64 -0
  86. package/server/event-bus.test.ts +244 -0
  87. package/server/event-bus.ts +124 -0
  88. package/server/execution-store.test.ts +307 -0
  89. package/server/execution-store.ts +170 -0
  90. package/server/fs-utils.ts +15 -0
  91. package/server/git-utils.test.ts +938 -0
  92. package/server/git-utils.ts +421 -0
  93. package/server/github-pr.test.ts +498 -0
  94. package/server/github-pr.ts +379 -0
  95. package/server/image-pull-manager.test.ts +303 -0
  96. package/server/image-pull-manager.ts +279 -0
  97. package/server/index.ts +396 -0
  98. package/server/linear-agent-bridge.test.ts +1157 -0
  99. package/server/linear-agent-bridge.ts +629 -0
  100. package/server/linear-agent.test.ts +473 -0
  101. package/server/linear-agent.ts +479 -0
  102. package/server/linear-cache.test.ts +136 -0
  103. package/server/linear-cache.ts +113 -0
  104. package/server/linear-connections.test.ts +350 -0
  105. package/server/linear-connections.ts +231 -0
  106. package/server/linear-credential-migration.test.ts +337 -0
  107. package/server/linear-credential-migration.ts +63 -0
  108. package/server/linear-oauth-connections-migration.test.ts +268 -0
  109. package/server/linear-oauth-connections.test.ts +365 -0
  110. package/server/linear-oauth-connections.ts +294 -0
  111. package/server/linear-project-manager.test.ts +162 -0
  112. package/server/linear-project-manager.ts +111 -0
  113. package/server/linear-prompt-builder.test.ts +74 -0
  114. package/server/linear-prompt-builder.ts +61 -0
  115. package/server/linear-staging.test.ts +276 -0
  116. package/server/linear-staging.ts +142 -0
  117. package/server/logger.test.ts +393 -0
  118. package/server/logger.ts +259 -0
  119. package/server/metrics-collector.test.ts +413 -0
  120. package/server/metrics-collector.ts +350 -0
  121. package/server/metrics-types.ts +108 -0
  122. package/server/middleware/managed-auth.test.ts +264 -0
  123. package/server/middleware/managed-auth.ts +195 -0
  124. package/server/novnc-proxy.test.ts +333 -0
  125. package/server/novnc-proxy.ts +99 -0
  126. package/server/path-resolver.test.ts +552 -0
  127. package/server/path-resolver.ts +186 -0
  128. package/server/paths.test.ts +31 -0
  129. package/server/paths.ts +11 -0
  130. package/server/pr-poller.test.ts +191 -0
  131. package/server/pr-poller.ts +162 -0
  132. package/server/prompt-manager.test.ts +211 -0
  133. package/server/prompt-manager.ts +211 -0
  134. package/server/protocol/claude-upstream/README.md +19 -0
  135. package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
  136. package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
  137. package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
  138. package/server/protocol/codex-upstream/README.md +18 -0
  139. package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
  140. package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
  141. package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
  142. package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
  143. package/server/protocol-monitor.ts +50 -0
  144. package/server/recorder.test.ts +454 -0
  145. package/server/recorder.ts +374 -0
  146. package/server/recording-hub/compat-validator.test.ts +150 -0
  147. package/server/recording-hub/compat-validator.ts +284 -0
  148. package/server/recording-hub/diagnostics.test.ts +140 -0
  149. package/server/recording-hub/diagnostics.ts +299 -0
  150. package/server/recording-hub/hub-config.test.ts +44 -0
  151. package/server/recording-hub/hub-config.ts +19 -0
  152. package/server/recording-hub/hub-routes.test.ts +417 -0
  153. package/server/recording-hub/hub-routes.ts +236 -0
  154. package/server/recording-hub/hub-store.test.ts +262 -0
  155. package/server/recording-hub/hub-store.ts +265 -0
  156. package/server/recording-hub/replay-adapter.test.ts +294 -0
  157. package/server/recording-hub/replay-adapter.ts +207 -0
  158. package/server/relay-client.test.ts +337 -0
  159. package/server/relay-client.ts +320 -0
  160. package/server/replay.test.ts +200 -0
  161. package/server/replay.ts +78 -0
  162. package/server/routes/agent-routes.test.ts +1400 -0
  163. package/server/routes/agent-routes.ts +409 -0
  164. package/server/routes/cron-routes.test.ts +881 -0
  165. package/server/routes/cron-routes.ts +103 -0
  166. package/server/routes/env-routes.test.ts +383 -0
  167. package/server/routes/env-routes.ts +95 -0
  168. package/server/routes/fs-routes.test.ts +1198 -0
  169. package/server/routes/fs-routes.ts +605 -0
  170. package/server/routes/git-routes.test.ts +813 -0
  171. package/server/routes/git-routes.ts +97 -0
  172. package/server/routes/linear-agent-routes.test.ts +721 -0
  173. package/server/routes/linear-agent-routes.ts +304 -0
  174. package/server/routes/linear-connection-routes.test.ts +927 -0
  175. package/server/routes/linear-connection-routes.ts +244 -0
  176. package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
  177. package/server/routes/linear-oauth-connection-routes.ts +129 -0
  178. package/server/routes/linear-routes.test.ts +1510 -0
  179. package/server/routes/linear-routes.ts +953 -0
  180. package/server/routes/metrics-routes.test.ts +103 -0
  181. package/server/routes/metrics-routes.ts +13 -0
  182. package/server/routes/prompt-routes.ts +67 -0
  183. package/server/routes/sandbox-routes.test.ts +513 -0
  184. package/server/routes/sandbox-routes.ts +127 -0
  185. package/server/routes/settings-routes.ts +270 -0
  186. package/server/routes/skills-routes.test.ts +690 -0
  187. package/server/routes/skills-routes.ts +100 -0
  188. package/server/routes/system-routes.test.ts +637 -0
  189. package/server/routes/system-routes.ts +228 -0
  190. package/server/routes/tailscale-routes.test.ts +176 -0
  191. package/server/routes/tailscale-routes.ts +22 -0
  192. package/server/routes.test.ts +4655 -0
  193. package/server/routes.ts +1277 -0
  194. package/server/sandbox-manager.test.ts +378 -0
  195. package/server/sandbox-manager.ts +168 -0
  196. package/server/service.test.ts +1419 -0
  197. package/server/service.ts +718 -0
  198. package/server/session-creation-service.test.ts +661 -0
  199. package/server/session-creation-service.ts +473 -0
  200. package/server/session-git-info.ts +104 -0
  201. package/server/session-linear-issues.test.ts +118 -0
  202. package/server/session-linear-issues.ts +88 -0
  203. package/server/session-names.test.ts +94 -0
  204. package/server/session-names.ts +67 -0
  205. package/server/session-orchestrator.test.ts +1784 -0
  206. package/server/session-orchestrator.ts +973 -0
  207. package/server/session-state-machine.test.ts +606 -0
  208. package/server/session-state-machine.ts +207 -0
  209. package/server/session-store.test.ts +290 -0
  210. package/server/session-store.ts +146 -0
  211. package/server/session-types.ts +509 -0
  212. package/server/settings-manager.test.ts +275 -0
  213. package/server/settings-manager.ts +173 -0
  214. package/server/tailscale-manager.test.ts +553 -0
  215. package/server/tailscale-manager.ts +451 -0
  216. package/server/terminal-manager.ts +240 -0
  217. package/server/update-checker.test.ts +306 -0
  218. package/server/update-checker.ts +197 -0
  219. package/server/usage-limits.test.ts +536 -0
  220. package/server/usage-limits.ts +225 -0
  221. package/server/worktree-tracker.test.ts +243 -0
  222. package/server/worktree-tracker.ts +84 -0
  223. package/server/ws-auth.test.ts +59 -0
  224. package/server/ws-auth.ts +41 -0
  225. package/server/ws-bridge-browser-ingest.test.ts +272 -0
  226. package/server/ws-bridge-browser-ingest.ts +72 -0
  227. package/server/ws-bridge-browser.ts +112 -0
  228. package/server/ws-bridge-cli-ingest.test.ts +302 -0
  229. package/server/ws-bridge-cli-ingest.ts +81 -0
  230. package/server/ws-bridge-codex.test.ts +1837 -0
  231. package/server/ws-bridge-codex.ts +266 -0
  232. package/server/ws-bridge-controls.test.ts +124 -0
  233. package/server/ws-bridge-controls.ts +20 -0
  234. package/server/ws-bridge-persist.test.ts +296 -0
  235. package/server/ws-bridge-persist.ts +66 -0
  236. package/server/ws-bridge-publish.test.ts +234 -0
  237. package/server/ws-bridge-publish.ts +79 -0
  238. package/server/ws-bridge-replay.test.ts +44 -0
  239. package/server/ws-bridge-replay.ts +61 -0
  240. package/server/ws-bridge-types.ts +106 -0
  241. package/server/ws-bridge.test.ts +4777 -0
  242. package/server/ws-bridge.ts +1279 -0
@@ -0,0 +1,409 @@
1
+ import crypto from "node:crypto";
2
+ import type { Hono } from "hono";
3
+ import * as agentStore from "../agent-store.js";
4
+ import type { AgentExecutor } from "../agent-executor.js";
5
+ import type { AgentConfig, AgentConfigCreateInput, AgentConfigExport } from "../agent-types.js";
6
+ import { getSettings, updateSettings } from "../settings-manager.js";
7
+ import * as staging from "../linear-staging.js";
8
+ import { getOAuthConnection, createOAuthConnection } from "../linear-oauth-connections.js";
9
+
10
+ /** Fields the user can set when creating/updating an agent */
11
+ const EDITABLE_FIELDS = [
12
+ "name", "description", "icon", "version",
13
+ "backendType", "model", "permissionMode", "cwd",
14
+ "envSlug", "env", "allowedTools", "codexInternetAccess",
15
+ "prompt", "mcpServers", "skills",
16
+ "container", "branch", "createBranch", "useWorktree",
17
+ "triggers", "enabled",
18
+ ] as const;
19
+
20
+ function pickEditable(body: Record<string, unknown>): Partial<AgentConfig> {
21
+ const result: Record<string, unknown> = {};
22
+ for (const key of EDITABLE_FIELDS) {
23
+ if (key in body) result[key] = body[key];
24
+ }
25
+ return result as Partial<AgentConfig>;
26
+ }
27
+
28
+ function buildCreateInput(
29
+ body: Record<string, unknown>,
30
+ overrides?: Partial<Pick<AgentConfigCreateInput, "enabled" | "version">>,
31
+ ): AgentConfigCreateInput {
32
+ return {
33
+ version: overrides?.version ?? 1,
34
+ name: (body.name as string | undefined) || "",
35
+ description: (body.description as string | undefined) || "",
36
+ icon: body.icon as string | undefined,
37
+ backendType: (body.backendType as AgentConfig["backendType"] | undefined) || "claude",
38
+ model: (body.model as string | undefined) || "",
39
+ permissionMode: (body.permissionMode as string | undefined) || "bypassPermissions",
40
+ cwd: (body.cwd as string | undefined) || "",
41
+ envSlug: body.envSlug as string | undefined,
42
+ env: body.env as Record<string, string> | undefined,
43
+ allowedTools: body.allowedTools as string[] | undefined,
44
+ codexInternetAccess: body.codexInternetAccess as boolean | undefined,
45
+ prompt: (body.prompt as string | undefined) || "",
46
+ mcpServers: body.mcpServers as AgentConfig["mcpServers"] | undefined,
47
+ skills: body.skills as string[] | undefined,
48
+ container: body.container as AgentConfig["container"] | undefined,
49
+ branch: body.branch as string | undefined,
50
+ createBranch: body.createBranch as boolean | undefined,
51
+ useWorktree: body.useWorktree as boolean | undefined,
52
+ triggers: body.triggers as AgentConfig["triggers"] | undefined,
53
+ enabled: overrides?.enabled ?? ((body.enabled as boolean | undefined) ?? true),
54
+ };
55
+ }
56
+
57
+ /** Strip sensitive Linear OAuth credentials before sending to the browser */
58
+ function sanitizeAgent(agent: AgentConfig & { nextRunAt?: number | null }): Record<string, unknown> {
59
+ if (!agent.triggers?.linear) return agent as unknown as Record<string, unknown>;
60
+ const { oauthClientSecret, webhookSecret, accessToken, refreshToken, ...safeLinear } = agent.triggers.linear;
61
+
62
+ // Resolve connection info for display and flag derivation
63
+ const conn = safeLinear.oauthConnectionId
64
+ ? getOAuthConnection(safeLinear.oauthConnectionId)
65
+ : null;
66
+ const oauthConnectionName = conn?.name;
67
+ const oauthConnectionStatus = conn?.status;
68
+
69
+ return {
70
+ ...agent,
71
+ triggers: {
72
+ ...agent.triggers,
73
+ linear: {
74
+ ...safeLinear,
75
+ hasAccessToken: !!(accessToken || oauthConnectionStatus === "connected"),
76
+ hasClientSecret: !!(oauthClientSecret || conn?.oauthClientSecret),
77
+ hasWebhookSecret: !!(webhookSecret || conn?.webhookSecret),
78
+ oauthConnectionName,
79
+ oauthConnectionStatus,
80
+ },
81
+ },
82
+ } as unknown as Record<string, unknown>;
83
+ }
84
+
85
+ /** Strip internal tracking fields to produce a portable export */
86
+ function toExport(agent: AgentConfig): AgentConfigExport {
87
+ const {
88
+ id: _id,
89
+ createdAt: _ca,
90
+ updatedAt: _ua,
91
+ totalRuns: _tr,
92
+ consecutiveFailures: _cf,
93
+ lastRunAt: _lr,
94
+ lastSessionId: _ls,
95
+ enabled: _en,
96
+ ...exportable
97
+ } = agent;
98
+ // Strip Linear OAuth credentials from export (keep oauthConnectionId for reference)
99
+ if (exportable.triggers?.linear) {
100
+ const { oauthClientId, oauthClientSecret, webhookSecret, accessToken, refreshToken, ...safeLinear } = exportable.triggers.linear;
101
+ exportable.triggers = { ...exportable.triggers, linear: safeLinear };
102
+ }
103
+ return exportable;
104
+ }
105
+
106
+ export function registerAgentRoutes(
107
+ api: Hono,
108
+ agentExecutor?: AgentExecutor,
109
+ ): void {
110
+ // ── CRUD ────────────────────────────────────────────────────────────────
111
+
112
+ api.get("/agents", (c) => {
113
+ const agents = agentStore.listAgents();
114
+ const enriched = agents.map((a) => sanitizeAgent({
115
+ ...a,
116
+ nextRunAt: agentExecutor?.getNextRunTime(a.id)?.getTime() ?? null,
117
+ }));
118
+ return c.json(enriched);
119
+ });
120
+
121
+ api.get("/agents/:id", (c) => {
122
+ const agent = agentStore.getAgent(c.req.param("id"));
123
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
124
+ return c.json(sanitizeAgent({
125
+ ...agent,
126
+ nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null,
127
+ }));
128
+ });
129
+
130
+ api.post("/agents", async (c) => {
131
+ const body = await c.req.json().catch(() => ({}));
132
+ try {
133
+ const agent = agentStore.createAgent(buildCreateInput(body));
134
+
135
+ // If this is a Linear agent, resolve credentials:
136
+ // New model: oauthConnectionId already set in triggers.linear
137
+ // Legacy model: resolve from staging/clone/global
138
+ if (agent.triggers?.linear?.enabled) {
139
+ // New model: oauthConnectionId passed directly — nothing more to do
140
+ if (agent.triggers.linear.oauthConnectionId) {
141
+ // Already stored via triggers, just proceed
142
+ } else if (!agent.triggers.linear.oauthClientId) {
143
+ // Legacy model: resolve credentials from staging/clone/global
144
+ let linearCreds: {
145
+ oauthClientId: string;
146
+ oauthClientSecret: string;
147
+ webhookSecret: string;
148
+ accessToken: string;
149
+ refreshToken: string;
150
+ } | null = null;
151
+
152
+ // Priority 1: staging slot → create OAuth connection from it
153
+ if (body.stagingId) {
154
+ const slot = staging.consumeSlot(body.stagingId);
155
+ if (slot?.clientId) {
156
+ // Create a new OAuth connection from the staging slot
157
+ const conn = createOAuthConnection({
158
+ name: `${agent.name} OAuth App`,
159
+ oauthClientId: slot.clientId,
160
+ oauthClientSecret: slot.clientSecret,
161
+ webhookSecret: slot.webhookSecret,
162
+ accessToken: slot.accessToken,
163
+ refreshToken: slot.refreshToken,
164
+ });
165
+ const updated = agentStore.updateAgent(agent.id, {
166
+ triggers: {
167
+ ...agent.triggers,
168
+ linear: {
169
+ ...agent.triggers.linear,
170
+ oauthConnectionId: conn.id,
171
+ },
172
+ },
173
+ });
174
+ if (updated) {
175
+ if (updated.enabled && updated.triggers?.schedule?.enabled) {
176
+ agentExecutor?.scheduleAgent(updated);
177
+ }
178
+ return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
179
+ }
180
+ }
181
+ }
182
+
183
+ // Priority 2: clone from existing agent
184
+ if (!linearCreds && body.cloneFromAgentId) {
185
+ const source = agentStore.getAgent(body.cloneFromAgentId);
186
+ // Prefer cloning the oauthConnectionId reference
187
+ if (source?.triggers?.linear?.oauthConnectionId) {
188
+ const updated = agentStore.updateAgent(agent.id, {
189
+ triggers: {
190
+ ...agent.triggers,
191
+ linear: {
192
+ ...agent.triggers.linear,
193
+ oauthConnectionId: source.triggers.linear.oauthConnectionId,
194
+ },
195
+ },
196
+ });
197
+ if (updated) {
198
+ if (updated.enabled && updated.triggers?.schedule?.enabled) {
199
+ agentExecutor?.scheduleAgent(updated);
200
+ }
201
+ return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
202
+ }
203
+ } else if (source?.triggers?.linear?.oauthClientId) {
204
+ linearCreds = {
205
+ oauthClientId: source.triggers.linear.oauthClientId,
206
+ oauthClientSecret: source.triggers.linear.oauthClientSecret || "",
207
+ webhookSecret: source.triggers.linear.webhookSecret || "",
208
+ accessToken: source.triggers.linear.accessToken || "",
209
+ refreshToken: source.triggers.linear.refreshToken || "",
210
+ };
211
+ }
212
+ }
213
+
214
+ // Priority 3: global staging (backward compat)
215
+ if (!linearCreds) {
216
+ const settings = getSettings();
217
+ if (settings.linearOAuthClientId) {
218
+ linearCreds = {
219
+ oauthClientId: settings.linearOAuthClientId,
220
+ oauthClientSecret: settings.linearOAuthClientSecret,
221
+ webhookSecret: settings.linearOAuthWebhookSecret,
222
+ accessToken: settings.linearOAuthAccessToken,
223
+ refreshToken: settings.linearOAuthRefreshToken,
224
+ };
225
+ }
226
+ }
227
+
228
+ if (linearCreds) {
229
+ const updated = agentStore.updateAgent(agent.id, {
230
+ triggers: {
231
+ ...agent.triggers,
232
+ linear: {
233
+ ...agent.triggers.linear,
234
+ ...linearCreds,
235
+ },
236
+ },
237
+ });
238
+ if (updated) {
239
+ // Clear global staging if we used it (no stagingId and no clone source)
240
+ if (!body.stagingId && !body.cloneFromAgentId) {
241
+ updateSettings({
242
+ linearOAuthClientId: "",
243
+ linearOAuthClientSecret: "",
244
+ linearOAuthWebhookSecret: "",
245
+ linearOAuthAccessToken: "",
246
+ linearOAuthRefreshToken: "",
247
+ });
248
+ }
249
+ if (updated.enabled && updated.triggers?.schedule?.enabled) {
250
+ agentExecutor?.scheduleAgent(updated);
251
+ }
252
+ return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
253
+ }
254
+ }
255
+ }
256
+ }
257
+
258
+ if (agent.enabled && agent.triggers?.schedule?.enabled) {
259
+ agentExecutor?.scheduleAgent(agent);
260
+ }
261
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
262
+ } catch (e: unknown) {
263
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
264
+ }
265
+ });
266
+
267
+ api.put("/agents/:id", async (c) => {
268
+ const id = c.req.param("id");
269
+ const body = await c.req.json().catch(() => ({}));
270
+ try {
271
+ const allowed = pickEditable(body);
272
+ const agent = agentStore.updateAgent(id, allowed);
273
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
274
+ // Stop old timer (id may differ after a rename)
275
+ if (agent.id !== id) {
276
+ agentExecutor?.stopAgent(id);
277
+ }
278
+ // Reschedule if enabled
279
+ if (agent.enabled && agent.triggers?.schedule?.enabled) {
280
+ agentExecutor?.scheduleAgent(agent);
281
+ } else {
282
+ agentExecutor?.stopAgent(agent.id);
283
+ }
284
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
285
+ } catch (e: unknown) {
286
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
287
+ }
288
+ });
289
+
290
+ api.delete("/agents/:id", (c) => {
291
+ const id = c.req.param("id");
292
+ agentExecutor?.stopAgent(id);
293
+ const deleted = agentStore.deleteAgent(id);
294
+ if (!deleted) return c.json({ error: "Agent not found" }, 404);
295
+ return c.json({ ok: true });
296
+ });
297
+
298
+ // ── Toggle ──────────────────────────────────────────────────────────────
299
+
300
+ api.post("/agents/:id/toggle", (c) => {
301
+ const id = c.req.param("id");
302
+ const agent = agentStore.getAgent(id);
303
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
304
+ const updated = agentStore.updateAgent(id, { enabled: !agent.enabled });
305
+ if (updated?.enabled && updated.triggers?.schedule?.enabled) {
306
+ agentExecutor?.scheduleAgent(updated);
307
+ } else if (updated) {
308
+ agentExecutor?.stopAgent(updated.id);
309
+ }
310
+ return c.json(updated ? sanitizeAgent({ ...updated, nextRunAt: agentExecutor?.getNextRunTime(updated.id)?.getTime() ?? null }) : updated);
311
+ });
312
+
313
+ // ── Run (manual trigger) ───────────────────────────────────────────────
314
+
315
+ api.post("/agents/:id/run", async (c) => {
316
+ const id = c.req.param("id");
317
+ const agent = agentStore.getAgent(id);
318
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
319
+ const body = await c.req.json().catch(() => ({}));
320
+ const input = typeof body.input === "string" ? body.input : undefined;
321
+ agentExecutor?.executeAgentManually(id, input);
322
+ return c.json({ ok: true, message: "Agent triggered" });
323
+ });
324
+
325
+ // ── Executions ─────────────────────────────────────────────────────────
326
+
327
+ api.get("/agents/:id/executions", (c) => {
328
+ const id = c.req.param("id");
329
+ return c.json(agentExecutor?.getExecutions(id) ?? []);
330
+ });
331
+
332
+ /** List executions across all agents with filtering and pagination (for Runs view). */
333
+ api.get("/executions", (c) => {
334
+ const agentId = c.req.query("agentId");
335
+ const triggerType = c.req.query("triggerType");
336
+ const rawStatus = c.req.query("status");
337
+ const status = (rawStatus === "running" || rawStatus === "success" || rawStatus === "error")
338
+ ? rawStatus : undefined;
339
+ const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 500);
340
+ const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
341
+ return c.json(agentExecutor?.listAllExecutions({ agentId, triggerType, status, limit, offset }) ?? { executions: [], total: 0 });
342
+ });
343
+
344
+ // ── Import / Export ────────────────────────────────────────────────────
345
+
346
+ api.post("/agents/import", async (c) => {
347
+ const body = await c.req.json().catch(() => ({}));
348
+ try {
349
+ // Accept an exported agent JSON and create a new agent from it
350
+ const agent = agentStore.createAgent(buildCreateInput(body, {
351
+ version: (body.version as AgentConfigCreateInput["version"] | undefined) || 1,
352
+ enabled: false, // Imported agents start disabled for safety
353
+ }));
354
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
355
+ } catch (e: unknown) {
356
+ return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
357
+ }
358
+ });
359
+
360
+ api.get("/agents/:id/export", (c) => {
361
+ const agent = agentStore.getAgent(c.req.param("id"));
362
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
363
+ return c.json(toExport(agent));
364
+ });
365
+
366
+ // ── Webhook Secret ─────────────────────────────────────────────────────
367
+
368
+ api.post("/agents/:id/regenerate-secret", (c) => {
369
+ const id = c.req.param("id");
370
+ const agent = agentStore.regenerateWebhookSecret(id);
371
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
372
+ return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
373
+ });
374
+
375
+ // ── Webhook Trigger ────────────────────────────────────────────────────
376
+
377
+ api.post("/agents/:id/webhook/:secret", async (c) => {
378
+ const id = c.req.param("id");
379
+ const secret = c.req.param("secret");
380
+
381
+ const agent = agentStore.getAgent(id);
382
+ if (!agent) return c.json({ error: "Agent not found" }, 404);
383
+
384
+ // Validate webhook is enabled and secret matches
385
+ if (!agent.triggers?.webhook?.enabled) {
386
+ return c.json({ error: "Webhook not enabled for this agent" }, 403);
387
+ }
388
+ // Use constant-time comparison to prevent timing attacks
389
+ const expected = Buffer.from(agent.triggers.webhook.secret);
390
+ const received = Buffer.from(secret);
391
+ if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
392
+ return c.json({ error: "Invalid webhook secret" }, 401);
393
+ }
394
+
395
+ // Extract input from body — accept JSON { input: "..." } or plain text
396
+ let input: string | undefined;
397
+ const contentType = c.req.header("content-type") || "";
398
+ if (contentType.includes("application/json")) {
399
+ const body = await c.req.json().catch(() => ({}));
400
+ input = typeof body.input === "string" ? body.input : undefined;
401
+ } else {
402
+ const text = await c.req.text().catch(() => "");
403
+ if (text.trim()) input = text.trim();
404
+ }
405
+
406
+ agentExecutor?.executeAgentManually(id, input);
407
+ return c.json({ ok: true, message: "Agent triggered via webhook" });
408
+ });
409
+ }