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.
- package/bin/cli.ts +182 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/MermaidDiagram-By1J3Whj.js +2 -0
- package/dist/assets/_basePickBy-Dq6gnD-p.js +1 -0
- package/dist/assets/_baseUniq-SpgN6cGc.js +1 -0
- package/dist/assets/arc-D9SEA8dX.js +1 -0
- package/dist/assets/architectureDiagram-VXUJARFQ-CoTcwLeR.js +36 -0
- package/dist/assets/blockDiagram-VD42YOAC-Dvc63voV.js +122 -0
- package/dist/assets/c4Diagram-YG6GDRKO-tuDcG4_4.js +10 -0
- package/dist/assets/channel-CxhcHn7S.js +1 -0
- package/dist/assets/chunk-4BX2VUAB-x1hChYu9.js +1 -0
- package/dist/assets/chunk-55IACEB6-BX6HWnWP.js +1 -0
- package/dist/assets/chunk-B4BG7PRW-B4IlF03G.js +165 -0
- package/dist/assets/chunk-DI55MBZ5-TpYb5rLg.js +220 -0
- package/dist/assets/chunk-FMBD7UC4-DZX4LUPx.js +15 -0
- package/dist/assets/chunk-QN33PNHL-CuQs0NDY.js +1 -0
- package/dist/assets/chunk-QZHKN3VN-CygWwVx8.js +1 -0
- package/dist/assets/chunk-TZMSLE5B-CtI-IdKV.js +1 -0
- package/dist/assets/classDiagram-2ON5EDUG-BM0LptJF.js +1 -0
- package/dist/assets/classDiagram-v2-WZHVMYZB-BM0LptJF.js +1 -0
- package/dist/assets/clone-C6kt1rE7.js +1 -0
- package/dist/assets/cose-bilkent-S5V4N54A-PdySs991.js +1 -0
- package/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/dist/assets/dagre-6UL2VRFP-J0SxEiEu.js +4 -0
- package/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/dist/assets/diagram-PSM6KHXK-CW0HLXaH.js +24 -0
- package/dist/assets/diagram-QEK2KX5R-DADHF-k4.js +43 -0
- package/dist/assets/diagram-S2PKOQOG-9S0-PC3S.js +24 -0
- package/dist/assets/erDiagram-Q2GNP2WA-BEHAiY_q.js +60 -0
- package/dist/assets/flowDiagram-NV44I4VS-Cu6n9Xnd.js +162 -0
- package/dist/assets/ganttDiagram-JELNMOA3-Cr_BHryu.js +267 -0
- package/dist/assets/gitGraphDiagram-V2S2FVAM-D5pT35v5.js +65 -0
- package/dist/assets/graph-DKRbrxcs.js +1 -0
- package/dist/assets/index-Byjg9zgI.css +32 -0
- package/dist/assets/index-Cwj3m0hT.js +284 -0
- package/dist/assets/infoDiagram-HS3SLOUP-BaJlrgKV.js +2 -0
- package/dist/assets/init-Gi6I4Gst.js +1 -0
- package/dist/assets/journeyDiagram-XKPGCS4Q-CXC4-DJv.js +139 -0
- package/dist/assets/kanban-definition-3W4ZIXB7-BtHBu9cx.js +89 -0
- package/dist/assets/katex-BlHpptmG.js +261 -0
- package/dist/assets/layout-DnxSwLDJ.js +1 -0
- package/dist/assets/linear-CWbbIT8l.js +1 -0
- package/dist/assets/mermaid.core-DXn1cFAK.js +256 -0
- package/dist/assets/mindmap-definition-VGOIOE7T-BR8br1hs.js +68 -0
- package/dist/assets/ordinal-BENe2yWM.js +1 -0
- package/dist/assets/pieDiagram-ADFJNKIX-B7RmXV7I.js +30 -0
- package/dist/assets/quadrantDiagram-AYHSOK5B-DK0FhyV_.js +7 -0
- package/dist/assets/requirementDiagram-UZGBJVZJ-3zAGxSpL.js +64 -0
- package/dist/assets/sankeyDiagram-TZEHDZUN-DZppKtjc.js +10 -0
- package/dist/assets/sequenceDiagram-WL72ISMW-D1x3r3wV.js +145 -0
- package/dist/assets/stateDiagram-FKZM4ZOC-2ByD08Vq.js +1 -0
- package/dist/assets/stateDiagram-v2-4FDKWEC3-Vm1cYjgV.js +1 -0
- package/dist/assets/timeline-definition-IT6M3QCI-l5S35biR.js +61 -0
- package/dist/assets/treemap-GDKQZRPO-DIPadPiF.js +162 -0
- package/dist/assets/xychartDiagram-PRI3JC2R-9GUvwS2b.js +7 -0
- package/dist/favicon.svg +3 -0
- package/dist/fonts/Geist-Variable.woff2 +0 -0
- package/dist/fonts/GeistMono-Variable.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +33 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo.svg +3 -0
- package/dist/manifest.json +26 -0
- package/dist/og-image.png +0 -0
- package/dist/sw.js +130 -0
- package/package.json +92 -0
- package/server/ADAPTERS.md +121 -0
- package/server/adapter-registry-types.ts +28 -0
- package/server/adapter-registry.ts +293 -0
- package/server/adapter-types.ts +51 -0
- package/server/agent-executor.ts +249 -0
- package/server/agent-mcp-bridge.ts +148 -0
- package/server/agent-mcp-stdio.ts +171 -0
- package/server/agent-mcp-tools.ts +76 -0
- package/server/agent-store.ts +183 -0
- package/server/agent-types.ts +79 -0
- package/server/aider-adapter.ts +525 -0
- package/server/auth.ts +153 -0
- package/server/auto-namer.ts +132 -0
- package/server/capability-discovery.ts +431 -0
- package/server/claude-container-auth.ts +40 -0
- package/server/claude-stdio-adapter.ts +452 -0
- package/server/clawhub-export.ts +218 -0
- package/server/cli-launcher.ts +1613 -0
- package/server/codex-adapter.ts +1987 -0
- package/server/codex-container-auth.ts +40 -0
- package/server/codex-home.ts +26 -0
- package/server/collective-intelligence.ts +401 -0
- package/server/commands-discovery.ts +160 -0
- package/server/constants.ts +5 -0
- package/server/container-manager.ts +328 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.ts +149 -0
- package/server/cron-types.ts +63 -0
- package/server/deliberation-engine.ts +323 -0
- package/server/embedding.ts +89 -0
- package/server/env-manager.ts +146 -0
- package/server/environment-detector.ts +106 -0
- package/server/environment-rules.ts +106 -0
- package/server/gallery-store.ts +216 -0
- package/server/gallery-types.ts +52 -0
- package/server/gallery-votes.ts +101 -0
- package/server/git-utils.ts +384 -0
- package/server/github-pr.ts +379 -0
- package/server/goose-adapter.ts +929 -0
- package/server/image-pull-manager.ts +144 -0
- package/server/index.ts +363 -0
- package/server/linear-project-manager.ts +78 -0
- package/server/moltbook-client.ts +160 -0
- package/server/openclaw-adapter.ts +916 -0
- package/server/openclaw-channel/channel.ts +188 -0
- package/server/openclaw-channel/index.ts +68 -0
- package/server/openclaw-channel/package.json +19 -0
- package/server/openclaw-channel/types.ts +93 -0
- package/server/opencode-adapter.ts +951 -0
- package/server/openhands-adapter.ts +775 -0
- package/server/orchestrator-executor.ts +184 -0
- package/server/orchestrator-store.ts +106 -0
- package/server/orchestrator-types.ts +86 -0
- package/server/path-resolver.ts +168 -0
- package/server/pr-poller.ts +162 -0
- package/server/proactive-keepalive.ts +121 -0
- package/server/prompt-manager.ts +103 -0
- package/server/protocol-monitor.ts +197 -0
- package/server/race-controller.ts +295 -0
- package/server/race-store.ts +82 -0
- package/server/recorder.ts +359 -0
- package/server/recording-hub/compat-validator.ts +122 -0
- package/server/recording-hub/diagnostics.ts +289 -0
- package/server/recording-hub/hub-routes.ts +146 -0
- package/server/recording-hub/hub-store.ts +224 -0
- package/server/replay.ts +78 -0
- package/server/routes/adapter-routes.ts +31 -0
- package/server/routes/agent-mcp-routes.ts +35 -0
- package/server/routes/agent-routes.ts +185 -0
- package/server/routes/auth-routes.ts +101 -0
- package/server/routes/ci-routes.ts +165 -0
- package/server/routes/commands-routes.ts +94 -0
- package/server/routes/cron-routes.ts +100 -0
- package/server/routes/env-routes.ts +50 -0
- package/server/routes/folder-routes.ts +56 -0
- package/server/routes/fs-routes.ts +403 -0
- package/server/routes/gallery-routes.ts +410 -0
- package/server/routes/git-routes.ts +108 -0
- package/server/routes/index.ts +62 -0
- package/server/routes/linear-routes.ts +254 -0
- package/server/routes/monitor-routes.ts +11 -0
- package/server/routes/orchestrator-routes.ts +210 -0
- package/server/routes/prompt-routes.ts +47 -0
- package/server/routes/race-routes.ts +103 -0
- package/server/routes/recording-routes.ts +101 -0
- package/server/routes/route-deps.ts +32 -0
- package/server/routes/session-routes.ts +560 -0
- package/server/routes/settings-routes.ts +108 -0
- package/server/routes/skills-routes.ts +52 -0
- package/server/routes/system-routes.ts +243 -0
- package/server/routes/webhook-routes.ts +135 -0
- package/server/routes.ts +44 -0
- package/server/security-middleware.ts +100 -0
- package/server/semantic-memory.ts +389 -0
- package/server/service.ts +733 -0
- package/server/session-folders.ts +107 -0
- package/server/session-git-info.ts +89 -0
- package/server/session-linear-issues.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-store.ts +169 -0
- package/server/session-types.ts +415 -0
- package/server/settings-manager.ts +120 -0
- package/server/shared-context.ts +401 -0
- package/server/skills-manager.ts +250 -0
- package/server/sub-session-manager.ts +263 -0
- package/server/terminal-manager.ts +185 -0
- package/server/update-checker.ts +114 -0
- package/server/usage-limits.ts +192 -0
- package/server/webhook-manager.ts +228 -0
- package/server/webhook-store.ts +168 -0
- package/server/webhook-types.ts +66 -0
- package/server/worktree-tracker.ts +84 -0
- 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
|
+
}
|