the-campfire 0.1.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 (180) hide show
  1. package/bin/cli.ts +182 -0
  2. package/dist/apple-touch-icon.png +0 -0
  3. package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
  4. package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
  5. package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
  6. package/dist/assets/arc-D9SEA8dX.js +1 -0
  7. package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
  8. package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
  9. package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
  10. package/dist/assets/channel-CxhcHn7S.js +1 -0
  11. package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
  12. package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
  13. package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
  14. package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
  15. package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
  16. package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
  17. package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
  18. package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
  19. package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
  20. package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
  21. package/dist/assets/clone-C6kt1rE7.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
  23. package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
  24. package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
  25. package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  26. package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
  27. package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
  28. package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
  29. package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
  30. package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
  31. package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
  32. package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
  33. package/dist/assets/graph-DKRbrxcs.js +1 -0
  34. package/dist/assets/index-Byjg9zgI.css +32 -0
  35. package/dist/assets/index-Cwj3m0hT.js +284 -0
  36. package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
  37. package/dist/assets/init-Gi6I4Gst.js +1 -0
  38. package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
  39. package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
  40. package/dist/assets/katex-BlHpptmG.js +261 -0
  41. package/dist/assets/layout-DnxSwLDJ.js +1 -0
  42. package/dist/assets/linear-CWbbIT8l.js +1 -0
  43. package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
  44. package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
  45. package/dist/assets/ordinal-BENe2yWM.js +1 -0
  46. package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
  47. package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
  48. package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
  49. package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
  50. package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
  51. package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
  52. package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
  53. package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
  54. package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
  55. package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
  56. package/dist/favicon.svg +3 -0
  57. package/dist/fonts/Geist-Variable.woff2 +0 -0
  58. package/dist/fonts/GeistMono-Variable.woff2 +0 -0
  59. package/dist/icon-192.png +0 -0
  60. package/dist/icon-512.png +0 -0
  61. package/dist/index.html +33 -0
  62. package/dist/logo-codex.svg +14 -0
  63. package/dist/logo.svg +3 -0
  64. package/dist/manifest.json +26 -0
  65. package/dist/og-image.png +0 -0
  66. package/dist/sw.js +130 -0
  67. package/package.json +92 -0
  68. package/server/ADAPTERS.md +121 -0
  69. package/server/adapter-registry-types.ts +28 -0
  70. package/server/adapter-registry.ts +293 -0
  71. package/server/adapter-types.ts +51 -0
  72. package/server/agent-executor.ts +249 -0
  73. package/server/agent-mcp-bridge.ts +148 -0
  74. package/server/agent-mcp-stdio.ts +171 -0
  75. package/server/agent-mcp-tools.ts +76 -0
  76. package/server/agent-store.ts +183 -0
  77. package/server/agent-types.ts +79 -0
  78. package/server/aider-adapter.ts +525 -0
  79. package/server/auth.ts +153 -0
  80. package/server/auto-namer.ts +132 -0
  81. package/server/capability-discovery.ts +431 -0
  82. package/server/claude-container-auth.ts +40 -0
  83. package/server/claude-stdio-adapter.ts +452 -0
  84. package/server/clawhub-export.ts +218 -0
  85. package/server/cli-launcher.ts +1613 -0
  86. package/server/codex-adapter.ts +1987 -0
  87. package/server/codex-container-auth.ts +40 -0
  88. package/server/codex-home.ts +26 -0
  89. package/server/collective-intelligence.ts +401 -0
  90. package/server/commands-discovery.ts +160 -0
  91. package/server/constants.ts +5 -0
  92. package/server/container-manager.ts +328 -0
  93. package/server/cron-scheduler.ts +243 -0
  94. package/server/cron-store.ts +149 -0
  95. package/server/cron-types.ts +63 -0
  96. package/server/deliberation-engine.ts +323 -0
  97. package/server/embedding.ts +89 -0
  98. package/server/env-manager.ts +146 -0
  99. package/server/environment-detector.ts +106 -0
  100. package/server/environment-rules.ts +106 -0
  101. package/server/gallery-store.ts +216 -0
  102. package/server/gallery-types.ts +52 -0
  103. package/server/gallery-votes.ts +101 -0
  104. package/server/git-utils.ts +384 -0
  105. package/server/github-pr.ts +379 -0
  106. package/server/goose-adapter.ts +929 -0
  107. package/server/image-pull-manager.ts +144 -0
  108. package/server/index.ts +363 -0
  109. package/server/linear-project-manager.ts +78 -0
  110. package/server/moltbook-client.ts +160 -0
  111. package/server/openclaw-adapter.ts +916 -0
  112. package/server/openclaw-channel/channel.ts +188 -0
  113. package/server/openclaw-channel/index.ts +68 -0
  114. package/server/openclaw-channel/package.json +19 -0
  115. package/server/openclaw-channel/types.ts +93 -0
  116. package/server/opencode-adapter.ts +951 -0
  117. package/server/openhands-adapter.ts +775 -0
  118. package/server/orchestrator-executor.ts +184 -0
  119. package/server/orchestrator-store.ts +106 -0
  120. package/server/orchestrator-types.ts +86 -0
  121. package/server/path-resolver.ts +168 -0
  122. package/server/pr-poller.ts +162 -0
  123. package/server/proactive-keepalive.ts +121 -0
  124. package/server/prompt-manager.ts +103 -0
  125. package/server/protocol-monitor.ts +197 -0
  126. package/server/race-controller.ts +295 -0
  127. package/server/race-store.ts +82 -0
  128. package/server/recorder.ts +359 -0
  129. package/server/recording-hub/compat-validator.ts +122 -0
  130. package/server/recording-hub/diagnostics.ts +289 -0
  131. package/server/recording-hub/hub-routes.ts +146 -0
  132. package/server/recording-hub/hub-store.ts +224 -0
  133. package/server/replay.ts +78 -0
  134. package/server/routes/adapter-routes.ts +31 -0
  135. package/server/routes/agent-mcp-routes.ts +35 -0
  136. package/server/routes/agent-routes.ts +185 -0
  137. package/server/routes/auth-routes.ts +101 -0
  138. package/server/routes/ci-routes.ts +165 -0
  139. package/server/routes/commands-routes.ts +94 -0
  140. package/server/routes/cron-routes.ts +100 -0
  141. package/server/routes/env-routes.ts +50 -0
  142. package/server/routes/folder-routes.ts +56 -0
  143. package/server/routes/fs-routes.ts +403 -0
  144. package/server/routes/gallery-routes.ts +410 -0
  145. package/server/routes/git-routes.ts +108 -0
  146. package/server/routes/index.ts +62 -0
  147. package/server/routes/linear-routes.ts +254 -0
  148. package/server/routes/monitor-routes.ts +11 -0
  149. package/server/routes/orchestrator-routes.ts +210 -0
  150. package/server/routes/prompt-routes.ts +47 -0
  151. package/server/routes/race-routes.ts +103 -0
  152. package/server/routes/recording-routes.ts +101 -0
  153. package/server/routes/route-deps.ts +32 -0
  154. package/server/routes/session-routes.ts +560 -0
  155. package/server/routes/settings-routes.ts +108 -0
  156. package/server/routes/skills-routes.ts +52 -0
  157. package/server/routes/system-routes.ts +243 -0
  158. package/server/routes/webhook-routes.ts +135 -0
  159. package/server/routes.ts +44 -0
  160. package/server/security-middleware.ts +100 -0
  161. package/server/semantic-memory.ts +389 -0
  162. package/server/service.ts +733 -0
  163. package/server/session-folders.ts +107 -0
  164. package/server/session-git-info.ts +89 -0
  165. package/server/session-linear-issues.ts +94 -0
  166. package/server/session-names.ts +67 -0
  167. package/server/session-store.ts +169 -0
  168. package/server/session-types.ts +415 -0
  169. package/server/settings-manager.ts +120 -0
  170. package/server/shared-context.ts +401 -0
  171. package/server/skills-manager.ts +250 -0
  172. package/server/sub-session-manager.ts +263 -0
  173. package/server/terminal-manager.ts +185 -0
  174. package/server/update-checker.ts +114 -0
  175. package/server/usage-limits.ts +192 -0
  176. package/server/webhook-manager.ts +228 -0
  177. package/server/webhook-store.ts +168 -0
  178. package/server/webhook-types.ts +66 -0
  179. package/server/worktree-tracker.ts +84 -0
  180. package/server/ws-bridge.ts +2322 -0
@@ -0,0 +1,254 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+ import { getSettings } from "../settings-manager.js";
4
+ import * as linearProjectManager from "../linear-project-manager.js";
5
+ import * as sessionLinearIssues from "../session-linear-issues.js";
6
+
7
+ export function registerLinearRoutes(api: Hono, _deps: RouteDeps): void {
8
+ api.get("/linear/connection", async (c) => {
9
+ const { linearApiKey } = getSettings();
10
+ if (!linearApiKey?.trim()) {
11
+ return c.json({ connected: false });
12
+ }
13
+ try {
14
+ const res = await fetch("https://api.linear.app/graphql", {
15
+ method: "POST",
16
+ headers: {
17
+ "Content-Type": "application/json",
18
+ "Authorization": linearApiKey,
19
+ },
20
+ body: JSON.stringify({
21
+ query: `query { viewer { id name email } teams { nodes { id key name } } }`,
22
+ }),
23
+ });
24
+ const data = await res.json() as { data?: { viewer?: { name: string; email: string }; teams?: { nodes: Array<{ id: string; key: string; name: string }> } }; errors?: unknown[] };
25
+ if (data.errors || !data.data?.viewer) {
26
+ return c.json({ connected: false });
27
+ }
28
+ return c.json({
29
+ connected: true,
30
+ viewer: data.data.viewer,
31
+ teams: data.data.teams?.nodes ?? [],
32
+ });
33
+ } catch {
34
+ return c.json({ connected: false });
35
+ }
36
+ });
37
+
38
+ api.get("/linear/issues", async (c) => {
39
+ const { linearApiKey } = getSettings();
40
+ if (!linearApiKey?.trim()) {
41
+ return c.json({ error: "Linear API key not configured" }, 401);
42
+ }
43
+ const query = c.req.query("query") ?? "";
44
+ const limit = Math.min(parseInt(c.req.query("limit") ?? "10", 10), 50);
45
+ try {
46
+ const res = await fetch("https://api.linear.app/graphql", {
47
+ method: "POST",
48
+ headers: {
49
+ "Content-Type": "application/json",
50
+ "Authorization": linearApiKey,
51
+ },
52
+ body: JSON.stringify({
53
+ query: `
54
+ query SearchIssues($query: String!, $first: Int!) {
55
+ issueSearch(query: $query, first: $first) {
56
+ nodes {
57
+ id
58
+ identifier
59
+ title
60
+ url
61
+ state { name }
62
+ team { id key name }
63
+ }
64
+ }
65
+ }
66
+ `,
67
+ variables: { query, first: limit },
68
+ }),
69
+ });
70
+ const data = await res.json() as { data?: { issueSearch?: { nodes: unknown[] } }; errors?: unknown[] };
71
+ if (data.errors) {
72
+ return c.json({ error: "Linear API error" }, 502);
73
+ }
74
+ return c.json({ issues: data.data?.issueSearch?.nodes ?? [] });
75
+ } catch (e) {
76
+ return c.json({ error: "Failed to reach Linear API" }, 502);
77
+ }
78
+ });
79
+
80
+ api.get("/linear/teams", async (c) => {
81
+ const { linearApiKey } = getSettings();
82
+ if (!linearApiKey?.trim()) {
83
+ return c.json({ error: "Linear API key not configured" }, 401);
84
+ }
85
+ try {
86
+ const res = await fetch("https://api.linear.app/graphql", {
87
+ method: "POST",
88
+ headers: {
89
+ "Content-Type": "application/json",
90
+ "Authorization": linearApiKey,
91
+ },
92
+ body: JSON.stringify({
93
+ query: `query { teams { nodes { id key name } } }`,
94
+ }),
95
+ });
96
+ const data = await res.json() as { data?: { teams?: { nodes: unknown[] } }; errors?: unknown[] };
97
+ if (data.errors) {
98
+ return c.json({ error: "Linear API error" }, 502);
99
+ }
100
+ return c.json({ teams: data.data?.teams?.nodes ?? [] });
101
+ } catch {
102
+ return c.json({ error: "Failed to reach Linear API" }, 502);
103
+ }
104
+ });
105
+
106
+ api.get("/linear/team/:id/states", async (c) => {
107
+ const { linearApiKey } = getSettings();
108
+ if (!linearApiKey?.trim()) {
109
+ return c.json({ error: "Linear API key not configured" }, 401);
110
+ }
111
+ const teamId = c.req.param("id");
112
+ try {
113
+ const res = await fetch("https://api.linear.app/graphql", {
114
+ method: "POST",
115
+ headers: {
116
+ "Content-Type": "application/json",
117
+ "Authorization": linearApiKey,
118
+ },
119
+ body: JSON.stringify({
120
+ query: `
121
+ query TeamStates($teamId: String!) {
122
+ team(id: $teamId) {
123
+ states { nodes { id name type position } }
124
+ }
125
+ }
126
+ `,
127
+ variables: { teamId },
128
+ }),
129
+ });
130
+ const data = await res.json() as { data?: { team?: { states?: { nodes: unknown[] } } }; errors?: unknown[] };
131
+ if (data.errors) {
132
+ return c.json({ error: "Linear API error" }, 502);
133
+ }
134
+ return c.json({ states: data.data?.team?.states?.nodes ?? [] });
135
+ } catch {
136
+ return c.json({ error: "Failed to reach Linear API" }, 502);
137
+ }
138
+ });
139
+
140
+ api.get("/linear/projects", async (c) => {
141
+ const { linearApiKey } = getSettings();
142
+ if (!linearApiKey?.trim()) {
143
+ return c.json({ error: "Linear API key not configured" }, 401);
144
+ }
145
+ try {
146
+ const res = await fetch("https://api.linear.app/graphql", {
147
+ method: "POST",
148
+ headers: {
149
+ "Content-Type": "application/json",
150
+ "Authorization": linearApiKey,
151
+ },
152
+ body: JSON.stringify({
153
+ query: `query { projects { nodes { id name state } } }`,
154
+ }),
155
+ });
156
+ const data = await res.json() as { data?: { projects?: { nodes: unknown[] } }; errors?: unknown[] };
157
+ if (data.errors) {
158
+ return c.json({ error: "Linear API error" }, 502);
159
+ }
160
+ return c.json({ projects: data.data?.projects?.nodes ?? [] });
161
+ } catch {
162
+ return c.json({ error: "Failed to reach Linear API" }, 502);
163
+ }
164
+ });
165
+
166
+ api.post("/linear/issues/:id/transition", async (c) => {
167
+ const { linearApiKey } = getSettings();
168
+ if (!linearApiKey?.trim()) {
169
+ return c.json({ error: "Linear API key not configured" }, 401);
170
+ }
171
+ const issueId = c.req.param("id");
172
+ const body = await c.req.json().catch(() => ({}));
173
+ const { stateId } = body;
174
+ if (!stateId) return c.json({ error: "stateId is required" }, 400);
175
+ try {
176
+ const res = await fetch("https://api.linear.app/graphql", {
177
+ method: "POST",
178
+ headers: {
179
+ "Content-Type": "application/json",
180
+ "Authorization": linearApiKey,
181
+ },
182
+ body: JSON.stringify({
183
+ query: `
184
+ mutation TransitionIssue($issueId: String!, $stateId: String!) {
185
+ issueUpdate(id: $issueId, input: { stateId: $stateId }) {
186
+ success
187
+ issue { id identifier state { name } }
188
+ }
189
+ }
190
+ `,
191
+ variables: { issueId, stateId },
192
+ }),
193
+ });
194
+ const data = await res.json() as { data?: { issueUpdate?: { success: boolean; issue?: unknown } }; errors?: unknown[] };
195
+ if (data.errors) {
196
+ return c.json({ error: "Linear API error" }, 502);
197
+ }
198
+ return c.json({ ok: data.data?.issueUpdate?.success ?? false, issue: data.data?.issueUpdate?.issue });
199
+ } catch {
200
+ return c.json({ error: "Failed to reach Linear API" }, 502);
201
+ }
202
+ });
203
+
204
+ // ─── Project-repo mapping ──────────────────────────────────────────
205
+ api.get("/linear/project-mapping", (c) => {
206
+ const repoRoot = c.req.query("repoRoot");
207
+ if (repoRoot) {
208
+ const mapping = linearProjectManager.getProjectForRepo(repoRoot);
209
+ return c.json({ mapping });
210
+ }
211
+ return c.json({ mappings: linearProjectManager.listMappings() });
212
+ });
213
+
214
+ api.post("/linear/project-mapping", async (c) => {
215
+ const body = await c.req.json().catch(() => ({}));
216
+ const { repoRoot, teamId, teamKey, teamName, projectId, projectName } = body;
217
+ if (!repoRoot || !teamId || !teamKey || !teamName) {
218
+ return c.json({ error: "repoRoot, teamId, teamKey, and teamName are required" }, 400);
219
+ }
220
+ const mapping = linearProjectManager.setProjectForRepo(repoRoot, {
221
+ teamId, teamKey, teamName, projectId, projectName,
222
+ });
223
+ return c.json({ mapping });
224
+ });
225
+
226
+ api.delete("/linear/project-mapping", async (c) => {
227
+ const body = await c.req.json().catch(() => ({}));
228
+ const { repoRoot } = body;
229
+ if (!repoRoot) return c.json({ error: "repoRoot is required" }, 400);
230
+ const removed = linearProjectManager.removeProjectMapping(repoRoot);
231
+ if (!removed) return c.json({ error: "No mapping found" }, 404);
232
+ return c.json({ ok: true });
233
+ });
234
+
235
+ // ─── Session-issue linking ──────────────────────────────────────────
236
+ api.post("/linear/session/:id/link-issue", async (c) => {
237
+ const sessionId = c.req.param("id");
238
+ const body = await c.req.json().catch(() => ({}));
239
+ const { issueId, identifier, title, url, state, teamKey } = body;
240
+ if (!issueId || !identifier || !title) {
241
+ return c.json({ error: "issueId, identifier, and title are required" }, 400);
242
+ }
243
+ const linked = sessionLinearIssues.linkIssueToSession(sessionId, {
244
+ issueId, identifier, title, url: url || "", state: state || "", teamKey: teamKey || "",
245
+ });
246
+ return c.json({ linked });
247
+ });
248
+
249
+ api.get("/linear/session/:id/issue", (c) => {
250
+ const sessionId = c.req.param("id");
251
+ const issue = sessionLinearIssues.getLinkedIssue(sessionId);
252
+ return c.json({ issue });
253
+ });
254
+ }
@@ -0,0 +1,11 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+
4
+ export function registerMonitorRoutes(api: Hono, deps: RouteDeps): void {
5
+ const { protocolMonitor } = deps;
6
+
7
+ api.get("/monitor/stats", (c) => {
8
+ if (!protocolMonitor) return c.json({ error: "Monitor not available" }, 500);
9
+ return c.json(protocolMonitor.getSnapshot());
10
+ });
11
+ }
@@ -0,0 +1,210 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { Hono } from "hono";
3
+ import type { RouteDeps } from "./route-deps.js";
4
+ import type { Pipeline, PipelineStage } from "../orchestrator-types.js";
5
+ import {
6
+ listPipelines, getPipeline, savePipeline, deletePipeline,
7
+ listRuns, getRun, deleteRun,
8
+ } from "../orchestrator-store.js";
9
+ import { executePipeline, cancelRun, isRunActive } from "../orchestrator-executor.js";
10
+
11
+ export function registerOrchestratorRoutes(api: Hono, deps: RouteDeps): void {
12
+ // ─── Pipelines CRUD ──────────────────────────────────────────────────
13
+
14
+ api.get("/orchestrator/pipelines", (c) => {
15
+ return c.json(listPipelines());
16
+ });
17
+
18
+ api.get("/orchestrator/pipelines/:id", (c) => {
19
+ const pipeline = getPipeline(c.req.param("id"));
20
+ if (!pipeline) return c.json({ error: "Pipeline not found" }, 404);
21
+ return c.json(pipeline);
22
+ });
23
+
24
+ api.post("/orchestrator/pipelines", async (c) => {
25
+ const body = await c.req.json<{ name: string; description?: string; cwd: string; stages: Omit<PipelineStage, "id">[] }>();
26
+ if (!body.name || !body.cwd || !body.stages?.length) {
27
+ return c.json({ error: "name, cwd, and stages are required" }, 400);
28
+ }
29
+
30
+ const pipeline: Pipeline = {
31
+ id: randomUUID(),
32
+ name: body.name,
33
+ description: body.description,
34
+ cwd: body.cwd,
35
+ stages: body.stages.map((s) => ({ ...s, id: randomUUID() })),
36
+ createdAt: Date.now(),
37
+ updatedAt: Date.now(),
38
+ };
39
+
40
+ savePipeline(pipeline);
41
+ return c.json(pipeline, 201);
42
+ });
43
+
44
+ api.put("/orchestrator/pipelines/:id", async (c) => {
45
+ const id = c.req.param("id");
46
+ const existing = getPipeline(id);
47
+ if (!existing) return c.json({ error: "Pipeline not found" }, 404);
48
+
49
+ const body = await c.req.json<Partial<Pipeline>>();
50
+ const updated: Pipeline = {
51
+ ...existing,
52
+ name: body.name || existing.name,
53
+ description: body.description ?? existing.description,
54
+ cwd: body.cwd || existing.cwd,
55
+ stages: body.stages || existing.stages,
56
+ updatedAt: Date.now(),
57
+ };
58
+
59
+ savePipeline(updated);
60
+ return c.json(updated);
61
+ });
62
+
63
+ api.delete("/orchestrator/pipelines/:id", (c) => {
64
+ const ok = deletePipeline(c.req.param("id"));
65
+ if (!ok) return c.json({ error: "Pipeline not found" }, 404);
66
+ return c.json({ ok: true });
67
+ });
68
+
69
+ // ─── Runs ────────────────────────────────────────────────────────────
70
+
71
+ api.get("/orchestrator/runs", (c) => {
72
+ const pipelineId = c.req.query("pipeline_id");
73
+ return c.json(listRuns(pipelineId || undefined));
74
+ });
75
+
76
+ api.get("/orchestrator/runs/:id", (c) => {
77
+ const run = getRun(c.req.param("id"));
78
+ if (!run) return c.json({ error: "Run not found" }, 404);
79
+ return c.json(run);
80
+ });
81
+
82
+ api.delete("/orchestrator/runs/:id", (c) => {
83
+ const ok = deleteRun(c.req.param("id"));
84
+ if (!ok) return c.json({ error: "Run not found" }, 404);
85
+ return c.json({ ok: true });
86
+ });
87
+
88
+ // ─── Execute a pipeline ──────────────────────────────────────────────
89
+
90
+ api.post("/orchestrator/pipelines/:id/run", async (c) => {
91
+ const pipeline = getPipeline(c.req.param("id"));
92
+ if (!pipeline) return c.json({ error: "Pipeline not found" }, 404);
93
+
94
+ // Create session helper that uses the existing launcher
95
+ const createSession = async (opts: {
96
+ cwd: string;
97
+ backend: string;
98
+ model?: string;
99
+ permissionMode?: string;
100
+ prompt?: string;
101
+ }) => {
102
+ try {
103
+ const info = deps.launcher.launch({
104
+ cwd: opts.cwd,
105
+ backendType: (opts.backend || "claude") as "claude" | "codex",
106
+ model: opts.model,
107
+ permissionMode: opts.permissionMode,
108
+ });
109
+ const sessionId = info.sessionId;
110
+
111
+ // Wait for CLI to connect (max 15s)
112
+ for (let i = 0; i < 30; i++) {
113
+ await new Promise((r) => setTimeout(r, 500));
114
+ const session = deps.wsBridge.getSession(sessionId);
115
+ if (session?.cliSocket || session?.adapter) {
116
+ // Send the prompt
117
+ if (opts.prompt) {
118
+ deps.wsBridge.injectUserMessage(sessionId, opts.prompt);
119
+ }
120
+ return { sessionId };
121
+ }
122
+ }
123
+ return null;
124
+ } catch {
125
+ return null;
126
+ }
127
+ };
128
+
129
+ // Session status helper — checks launcher state + session history for completion
130
+ const getSessionStatus = (sessionId: string) => {
131
+ const launchInfo = deps.launcher.getSession(sessionId);
132
+ const session = deps.wsBridge.getSession(sessionId);
133
+ if (!launchInfo && !session) return null;
134
+
135
+ // Determine status from launcher state
136
+ const launcherState = launchInfo?.state || "starting";
137
+ let status = "running";
138
+ if (launcherState === "exited") {
139
+ status = "idle";
140
+ } else if (session) {
141
+ // Check if there's a recent result message (indicates turn completed)
142
+ const history = session.messageHistory;
143
+ const lastEntry = history[history.length - 1] as Record<string, unknown> | undefined;
144
+ if (lastEntry?.type === "result") {
145
+ status = "idle";
146
+ }
147
+ }
148
+
149
+ // Extract last assistant message text
150
+ let lastMessage = "";
151
+ if (session) {
152
+ const assistantMsgs = session.messageHistory.filter(
153
+ (m: Record<string, unknown>) => (m as { type?: string }).type === "assistant",
154
+ );
155
+ const lastMsg = assistantMsgs[assistantMsgs.length - 1] as
156
+ | { message?: { content?: unknown } }
157
+ | undefined;
158
+ if (lastMsg?.message?.content) {
159
+ const content = lastMsg.message.content;
160
+ if (typeof content === "string") {
161
+ lastMessage = content;
162
+ } else if (Array.isArray(content)) {
163
+ lastMessage = (content as Array<{ type: string; text?: string }>)
164
+ .filter((b) => b.type === "text" && b.text)
165
+ .map((b) => b.text!)
166
+ .join("\n");
167
+ }
168
+ }
169
+ }
170
+
171
+ return {
172
+ status,
173
+ cost: session?.state.total_cost_usd || 0,
174
+ lastMessage,
175
+ };
176
+ };
177
+
178
+ // Execute asynchronously — return the run ID immediately
179
+ const runPromise = executePipeline(pipeline, createSession, getSessionStatus);
180
+
181
+ // Return early with the run (in pending state initially, then running)
182
+ const earlyRun = await Promise.race([
183
+ runPromise,
184
+ new Promise<null>((resolve) => setTimeout(() => resolve(null), 500)),
185
+ ]);
186
+
187
+ if (earlyRun) {
188
+ return c.json(earlyRun, 201);
189
+ }
190
+
191
+ // Run started but not yet complete — get the ID from the store
192
+ const runs = listRuns(pipeline.id);
193
+ const latest = runs[0];
194
+ if (latest) {
195
+ return c.json(latest, 202);
196
+ }
197
+
198
+ return c.json({ error: "Failed to start pipeline run" }, 500);
199
+ });
200
+
201
+ // Cancel a running pipeline
202
+ api.post("/orchestrator/runs/:id/cancel", (c) => {
203
+ const runId = c.req.param("id");
204
+ if (!isRunActive(runId)) {
205
+ return c.json({ error: "Run is not active" }, 400);
206
+ }
207
+ const ok = cancelRun(runId);
208
+ return c.json({ ok });
209
+ });
210
+ }
@@ -0,0 +1,47 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+
4
+ export function registerPromptRoutes(api: Hono, _deps: RouteDeps): void {
5
+ api.get("/prompts", async (c) => {
6
+ const { listPrompts } = await import("../prompt-manager.js");
7
+ const cwd = c.req.query("cwd");
8
+ return c.json(listPrompts(cwd ? { cwd } : undefined));
9
+ });
10
+
11
+ api.post("/prompts", async (c) => {
12
+ const { createPrompt } = await import("../prompt-manager.js");
13
+ const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
14
+ if (typeof body.name !== "string" || !body.name.trim()) {
15
+ return c.json({ error: "name is required" }, 400);
16
+ }
17
+ if (typeof body.content !== "string" || !body.content.trim()) {
18
+ return c.json({ error: "content is required" }, 400);
19
+ }
20
+ const scope = body.scope === "project" ? "project" : "global";
21
+ const projectPath = scope === "project" && typeof body.projectPath === "string" ? body.projectPath : undefined;
22
+ const prompt = createPrompt(body.name.trim(), body.content.trim(), scope, projectPath);
23
+ return c.json(prompt, 201);
24
+ });
25
+
26
+ api.put("/prompts/:id", async (c) => {
27
+ const { updatePrompt } = await import("../prompt-manager.js");
28
+ const id = c.req.param("id");
29
+ const body = await c.req.json().catch(() => ({})) as Record<string, unknown>;
30
+ const updates: Record<string, unknown> = {};
31
+ if (typeof body.name === "string") updates.name = body.name.trim();
32
+ if (typeof body.content === "string") updates.content = body.content.trim();
33
+ if (body.scope === "global" || body.scope === "project") updates.scope = body.scope;
34
+ if (typeof body.projectPath === "string") updates.projectPath = body.projectPath;
35
+ const updated = updatePrompt(id, updates as Parameters<typeof updatePrompt>[1]);
36
+ if (!updated) return c.json({ error: "Prompt not found" }, 404);
37
+ return c.json(updated);
38
+ });
39
+
40
+ api.delete("/prompts/:id", async (c) => {
41
+ const { deletePrompt } = await import("../prompt-manager.js");
42
+ const id = c.req.param("id");
43
+ const deleted = deletePrompt(id);
44
+ if (!deleted) return c.json({ error: "Prompt not found" }, 404);
45
+ return c.json({ ok: true });
46
+ });
47
+ }
@@ -0,0 +1,103 @@
1
+ import type { Hono } from "hono";
2
+ import { execFileSync } from "node:child_process";
3
+ import type { RouteDeps } from "./route-deps.js";
4
+ import type { BackendType } from "../session-types.js";
5
+ import { RaceController } from "../race-controller.js";
6
+ import { deleteRace, getRace, listRaces } from "../race-store.js";
7
+
8
+ const VALID_BACKENDS = new Set<BackendType>(["claude", "codex", "goose", "aider", "openhands", "openclaw", "opencode"]);
9
+
10
+ export function registerRaceRoutes(api: Hono, deps: RouteDeps): void {
11
+ const controller = new RaceController(deps.launcher, deps.wsBridge);
12
+
13
+ api.post("/races", async (c) => {
14
+ const body = await c.req.json<{
15
+ prompt?: string;
16
+ backends?: string[];
17
+ repoRoot?: string;
18
+ baseBranch?: string;
19
+ modelByBackend?: Partial<Record<BackendType, string>>;
20
+ envSlug?: string;
21
+ env?: Record<string, string>;
22
+ }>().catch(() => ({} as {
23
+ prompt?: string;
24
+ backends?: string[];
25
+ repoRoot?: string;
26
+ baseBranch?: string;
27
+ modelByBackend?: Partial<Record<BackendType, string>>;
28
+ envSlug?: string;
29
+ env?: Record<string, string>;
30
+ }));
31
+
32
+ const prompt = body.prompt?.trim() || "";
33
+ const backends = (body.backends ?? []).filter((backend: string): backend is BackendType => VALID_BACKENDS.has(backend as BackendType));
34
+ if (!prompt) return c.json({ error: "prompt is required" }, 400);
35
+ if (backends.length < 2) return c.json({ error: "Select at least two valid backends" }, 400);
36
+ if (!body.repoRoot) return c.json({ error: "repoRoot is required" }, 400);
37
+
38
+ try {
39
+ const race = controller.startRace({
40
+ prompt,
41
+ backends,
42
+ repoRoot: body.repoRoot,
43
+ baseBranch: body.baseBranch,
44
+ modelByBackend: body.modelByBackend,
45
+ envSlug: body.envSlug,
46
+ env: body.env,
47
+ });
48
+ return c.json(race, 201);
49
+ } catch (err) {
50
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
51
+ }
52
+ });
53
+
54
+ api.get("/races", (c) => c.json(listRaces()));
55
+
56
+ api.get("/races/:id", (c) => {
57
+ const race = getRace(c.req.param("id"));
58
+ if (!race) return c.json({ error: "Race not found" }, 404);
59
+ return c.json(race);
60
+ });
61
+
62
+ api.get("/races/:id/entries/:entryId/diff", (c) => {
63
+ const race = getRace(c.req.param("id"));
64
+ if (!race) return c.json({ error: "Race not found" }, 404);
65
+ const entry = race.entries.find((item) => item.id === c.req.param("entryId"));
66
+ if (!entry) return c.json({ error: "Race entry not found" }, 404);
67
+ try {
68
+ const diff = execFileSync("git", ["diff", "--", "."], {
69
+ cwd: entry.worktreePath,
70
+ encoding: "utf-8",
71
+ timeout: 20_000,
72
+ stdio: ["ignore", "pipe", "pipe"],
73
+ });
74
+ return c.json({ diff, files: entry.filesChanged ?? [] });
75
+ } catch (err) {
76
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
77
+ }
78
+ });
79
+
80
+ api.post("/races/:id/pick", async (c) => {
81
+ const body = await c.req.json<{ sessionId?: string }>().catch(() => ({} as { sessionId?: string }));
82
+ if (!body.sessionId) return c.json({ error: "sessionId is required" }, 400);
83
+ try {
84
+ const race = controller.pickWinner(c.req.param("id"), body.sessionId);
85
+ if (!race) return c.json({ error: "Race not found" }, 404);
86
+ return c.json(race);
87
+ } catch (err) {
88
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 500);
89
+ }
90
+ });
91
+
92
+ api.post("/races/:id/cancel", async (c) => {
93
+ const race = await controller.cancelRace(c.req.param("id"));
94
+ if (!race) return c.json({ error: "Race not found" }, 404);
95
+ return c.json(race);
96
+ });
97
+
98
+ api.delete("/races/:id", (c) => {
99
+ const ok = deleteRace(c.req.param("id"));
100
+ if (!ok) return c.json({ error: "Race not found" }, 404);
101
+ return c.json({ ok: true });
102
+ });
103
+ }