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,51 @@
1
+ /**
2
+ * AgentAdapter — the formal interface that all backend adapters must implement.
3
+ *
4
+ * CodexAdapter and GooseAdapter both implement this interface so that
5
+ * WsBridge can treat all stdio-based backends uniformly.
6
+ */
7
+
8
+ import type {
9
+ BrowserIncomingMessage,
10
+ BrowserOutgoingMessage,
11
+ } from "./session-types.js";
12
+
13
+ /** Metadata emitted by adapters during initialization. */
14
+ export interface AdapterSessionMeta {
15
+ cliSessionId?: string;
16
+ model?: string;
17
+ cwd?: string;
18
+ }
19
+
20
+ /**
21
+ * Common interface for all agent backend adapters.
22
+ *
23
+ * Each adapter translates between a backend-specific protocol (JSON-RPC, etc.)
24
+ * and the Campfire's BrowserIncomingMessage/BrowserOutgoingMessage types,
25
+ * making the browser completely unaware of which backend is running.
26
+ */
27
+ export interface AgentAdapter {
28
+ /** Route a message from the browser to the backend. Returns false if the message was rejected. */
29
+ sendBrowserMessage(msg: BrowserOutgoingMessage): boolean;
30
+
31
+ /** Register callback for translated messages heading to the browser. */
32
+ onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void;
33
+
34
+ /** Register callback for session metadata updates (cliSessionId, model, cwd). */
35
+ onSessionMeta(cb: (meta: AdapterSessionMeta) => void): void;
36
+
37
+ /** Register callback for when the backend process disconnects. */
38
+ onDisconnect(cb: () => void): void;
39
+
40
+ /** Register callback for initialization failures. */
41
+ onInitError(cb: (error: string) => void): void;
42
+
43
+ /** Whether the adapter has completed initialization and the backend is connected. */
44
+ isConnected(): boolean;
45
+
46
+ /** Gracefully disconnect from the backend process. */
47
+ disconnect(): Promise<void>;
48
+
49
+ /** Get the backend's internal session/thread ID, or null if not yet initialized. */
50
+ getBackendSessionId(): string | null;
51
+ }
@@ -0,0 +1,249 @@
1
+ import { Cron } from "croner";
2
+ import { randomUUID } from "node:crypto";
3
+ import type { AgentProfile, AgentExecution } from "./agent-types.js";
4
+ import type { CliLauncher } from "./cli-launcher.js";
5
+ import type { WsBridge } from "./ws-bridge.js";
6
+ import * as agentStore from "./agent-store.js";
7
+ import * as envManager from "./env-manager.js";
8
+ import * as sessionNames from "./session-names.js";
9
+ import { getSettings } from "./settings-manager.js";
10
+
11
+ function resolveEnvVars(agent: AgentProfile): Record<string, string> | undefined {
12
+ let envVars: Record<string, string> | undefined;
13
+ if (agent.envSlug) {
14
+ const env = envManager.getEnv(agent.envSlug);
15
+ if (env) envVars = env.variables;
16
+ }
17
+ if (agent.env) {
18
+ envVars = { ...envVars, ...agent.env };
19
+ }
20
+ // Auto-inject provider tokens from global settings (if not already set)
21
+ const settings = getSettings();
22
+ if (agent.backendType === "claude" && settings.claudeOAuthToken && !envVars?.["CLAUDE_CODE_OAUTH_TOKEN"]) {
23
+ envVars = { ...envVars, CLAUDE_CODE_OAUTH_TOKEN: settings.claudeOAuthToken };
24
+ }
25
+ if (agent.backendType === "codex" && settings.openaiApiKey && !envVars?.["OPENAI_API_KEY"]) {
26
+ envVars = { ...envVars, OPENAI_API_KEY: settings.openaiApiKey };
27
+ }
28
+ if (settings.anthropicApiKey && !envVars?.["ANTHROPIC_API_KEY"]) {
29
+ envVars = { ...envVars, ANTHROPIC_API_KEY: settings.anthropicApiKey };
30
+ }
31
+ return envVars;
32
+ }
33
+
34
+ function resolvePrompt(template: string, input?: string): string {
35
+ if (!input) return template;
36
+ if (template.includes("{{input}}")) return template.replaceAll("{{input}}", input);
37
+ return template + "\n\n" + input;
38
+ }
39
+
40
+ function resolveCodexSandbox(agent: AgentProfile): "danger-full-access" | "workspace-write" | undefined {
41
+ if (agent.backendType !== "codex") return undefined;
42
+ return agent.permissionMode === "bypassPermissions" ? "danger-full-access" : "workspace-write";
43
+ }
44
+
45
+ const MAX_CONSECUTIVE_FAILURES = 5;
46
+ const CLI_CONNECT_TIMEOUT_MS = 30_000;
47
+ const CLI_CONNECT_POLL_MS = 500;
48
+
49
+ export class AgentExecutor {
50
+ private readonly timers = new Map<string, Cron>();
51
+ private readonly launcher: CliLauncher;
52
+ private readonly wsBridge: WsBridge;
53
+
54
+ constructor(launcher: CliLauncher, wsBridge: WsBridge) {
55
+ this.launcher = launcher;
56
+ this.wsBridge = wsBridge;
57
+ }
58
+
59
+ /** Start all enabled scheduled agents. Called once at server startup. */
60
+ startAll(): void {
61
+ const agents = agentStore.listAgents();
62
+ let scheduled = 0;
63
+ for (const agent of agents) {
64
+ if (agent.enabled && agent.triggers?.schedule?.enabled) {
65
+ this.scheduleAgent(agent);
66
+ scheduled++;
67
+ }
68
+ }
69
+ if (scheduled > 0) {
70
+ console.log(`[agent-executor] Scheduled ${scheduled} agent(s)`);
71
+ }
72
+ }
73
+
74
+ /** Schedule or reschedule a single agent's cron trigger. */
75
+ scheduleAgent(agent: AgentProfile): void {
76
+ this.unscheduleAgent(agent.id);
77
+
78
+ const schedule = agent.triggers?.schedule;
79
+ if (!schedule?.enabled || !schedule.expression) return;
80
+
81
+ try {
82
+ if (schedule.recurring) {
83
+ const timer = new Cron(schedule.expression, () => {
84
+ this.executeAgent(agent.id, { trigger: "schedule" }).catch((err) => {
85
+ console.error(`[agent-executor] Scheduled run of "${agent.name}" failed:`, err);
86
+ });
87
+ });
88
+ this.timers.set(agent.id, timer);
89
+ } else {
90
+ const runAt = new Date(schedule.expression);
91
+ if (runAt.getTime() <= Date.now()) return;
92
+ const timer = new Cron(runAt, () => {
93
+ this.executeAgent(agent.id, { trigger: "schedule" }).catch((err) => {
94
+ console.error(`[agent-executor] One-shot agent "${agent.name}" failed:`, err);
95
+ });
96
+ agentStore.updateAgent(agent.id, {
97
+ triggers: {
98
+ ...agent.triggers,
99
+ schedule: { ...schedule, enabled: false },
100
+ },
101
+ });
102
+ this.timers.delete(agent.id);
103
+ });
104
+ this.timers.set(agent.id, timer);
105
+ }
106
+ } catch (err) {
107
+ console.error(`[agent-executor] Invalid schedule for "${agent.name}":`, err);
108
+ }
109
+ }
110
+
111
+ /** Stop cron timer for an agent. */
112
+ unscheduleAgent(agentId: string): void {
113
+ const timer = this.timers.get(agentId);
114
+ if (timer) {
115
+ timer.stop();
116
+ this.timers.delete(agentId);
117
+ }
118
+ }
119
+
120
+ /** Execute an agent: create a session, inject the prompt, track execution. */
121
+ async executeAgent(
122
+ agentId: string,
123
+ opts: { input?: string; trigger?: "manual" | "webhook" | "schedule"; force?: boolean } = {},
124
+ ): Promise<AgentExecution> {
125
+ const agent = agentStore.getAgent(agentId);
126
+ if (!agent) throw new Error(`Agent "${agentId}" not found`);
127
+ if (!agent.enabled && !opts.force) throw new Error(`Agent "${agent.name}" is disabled`);
128
+
129
+ // Overlap prevention
130
+ if (agent.lastSessionId && this.launcher.isAlive(agent.lastSessionId)) {
131
+ throw new Error(`Agent "${agent.name}" is already running (${agent.lastSessionId})`);
132
+ }
133
+
134
+ console.log(`[agent-executor] Executing agent "${agent.name}" (${agentId}) [${opts.trigger || "manual"}]`);
135
+
136
+ const execution: AgentExecution = {
137
+ executionId: randomUUID(),
138
+ agentId,
139
+ sessionId: "",
140
+ input: opts.input,
141
+ trigger: opts.trigger || "manual",
142
+ startedAt: Date.now(),
143
+ };
144
+
145
+ try {
146
+ const envVars = resolveEnvVars(agent);
147
+
148
+ // Launch session via CliLauncher (works for ALL backends)
149
+ const sessionInfo = this.launcher.launch({
150
+ model: agent.model,
151
+ permissionMode: agent.permissionMode,
152
+ cwd: agent.cwd,
153
+ env: envVars,
154
+ backendType: agent.backendType,
155
+ codexInternetAccess: agent.backendType === "codex" ? (agent.codexInternetAccess ?? true) : undefined,
156
+ codexSandbox: resolveCodexSandbox(agent),
157
+ });
158
+
159
+ execution.sessionId = sessionInfo.sessionId;
160
+
161
+ // Set session name with agent icon
162
+ const label = `${agent.icon || "\u{1F916}"} ${agent.name}`;
163
+ sessionNames.setName(sessionInfo.sessionId, label);
164
+
165
+ // Wait for CLI to connect
166
+ await this.waitForCLIConnection(sessionInfo.sessionId);
167
+
168
+ // Resolve prompt and inject with agent prefix for traceability
169
+ const resolvedPrompt = resolvePrompt(agent.prompt, opts.input);
170
+ const fullPrompt = `[agent:${agent.id} ${agent.name}]\n\n${resolvedPrompt}`;
171
+ this.wsBridge.injectUserMessage(sessionInfo.sessionId, fullPrompt);
172
+
173
+ // Update agent tracking
174
+ agentStore.updateAgent(agentId, {
175
+ lastRunAt: Date.now(),
176
+ lastSessionId: sessionInfo.sessionId,
177
+ totalRuns: agent.totalRuns + 1,
178
+ consecutiveFailures: 0,
179
+ });
180
+
181
+ execution.success = true;
182
+ agentStore.appendExecution(execution);
183
+
184
+ return execution;
185
+ } catch (err) {
186
+ console.error(`[agent-executor] Agent "${agent.name}" failed:`, err);
187
+ execution.error = err instanceof Error ? err.message : String(err);
188
+ execution.completedAt = Date.now();
189
+ agentStore.appendExecution(execution);
190
+
191
+ const failures = agent.consecutiveFailures + 1;
192
+ const updates: Partial<AgentProfile> = {
193
+ consecutiveFailures: failures,
194
+ lastRunAt: Date.now(),
195
+ };
196
+
197
+ if (failures >= MAX_CONSECUTIVE_FAILURES) {
198
+ updates.enabled = false;
199
+ this.unscheduleAgent(agentId);
200
+ console.warn(`[agent-executor] Agent "${agent.name}" disabled after ${failures} consecutive failures`);
201
+ }
202
+
203
+ agentStore.updateAgent(agentId, updates);
204
+ throw err;
205
+ }
206
+ }
207
+
208
+ /** Manual trigger (bypasses enabled check). */
209
+ executeAgentManually(agentId: string, input?: string): void {
210
+ this.executeAgent(agentId, { input, trigger: "manual", force: true }).catch((err) => {
211
+ console.error(`[agent-executor] Manual execution of agent "${agentId}" failed:`, err);
212
+ });
213
+ }
214
+
215
+ /** Get next scheduled run time for an agent. */
216
+ getNextRunTime(agentId: string): Date | null {
217
+ const timer = this.timers.get(agentId);
218
+ if (!timer) return null;
219
+ return timer.nextRun() ?? null;
220
+ }
221
+
222
+ /** Check if an agent's last session is still alive. */
223
+ isRunning(agentId: string): boolean {
224
+ const agent = agentStore.getAgent(agentId);
225
+ if (!agent?.lastSessionId) return false;
226
+ return this.launcher.isAlive(agent.lastSessionId);
227
+ }
228
+
229
+ /** Graceful shutdown: stop all timers. */
230
+ destroy(): void {
231
+ for (const [, timer] of this.timers) {
232
+ timer.stop();
233
+ }
234
+ this.timers.clear();
235
+ }
236
+
237
+ private async waitForCLIConnection(sessionId: string): Promise<void> {
238
+ const start = Date.now();
239
+ while (Date.now() - start < CLI_CONNECT_TIMEOUT_MS) {
240
+ const info = this.launcher.getSession(sessionId);
241
+ if (info && (info.state === "connected" || info.state === "running")) return;
242
+ if (info?.state === "exited") {
243
+ throw new Error(`CLI process exited before connecting (exit code: ${info.exitCode})`);
244
+ }
245
+ await new Promise((r) => setTimeout(r, CLI_CONNECT_POLL_MS));
246
+ }
247
+ throw new Error(`CLI process did not connect within ${CLI_CONNECT_TIMEOUT_MS / 1000}s`);
248
+ }
249
+ }
@@ -0,0 +1,148 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import type { BackendType, BrowserOutgoingMessage, CLIControlRequestMessage, PermissionRequest } from "./session-types.js";
3
+ import type { WsBridge } from "./ws-bridge.js";
4
+ import { backendFromAskTool, createAgentMcpServerConfig } from "./agent-mcp-tools.js";
5
+ import type { SubSessionManager, SubSessionResult } from "./sub-session-manager.js";
6
+
7
+ export interface AgentMcpBridgeOptions {
8
+ port: number;
9
+ packageRoot: string;
10
+ token?: string;
11
+ backends?: BackendType[];
12
+ }
13
+
14
+ type PermissionResponseMessage = Extract<BrowserOutgoingMessage, { type: "permission_response" }>;
15
+ type PermissionResponder = (msg: PermissionResponseMessage) => void;
16
+
17
+ function backendFromCampfireMcpTool(toolName: string): BackendType | null {
18
+ const parts = toolName.split(":");
19
+ if (parts.length !== 3 || parts[0] !== "mcp") return null;
20
+ const [, serverName, mcpToolName] = parts;
21
+ if (serverName !== "campfire_agents" && serverName !== "campfire-agents") return null;
22
+ return backendFromAskTool(mcpToolName);
23
+ }
24
+
25
+ export class AgentMcpBridge {
26
+ readonly token: string;
27
+ private readonly injectedSessions = new Set<string>();
28
+ private readonly backends: BackendType[];
29
+
30
+ constructor(
31
+ private readonly wsBridge: WsBridge,
32
+ private readonly subSessionManager: SubSessionManager,
33
+ private readonly options: AgentMcpBridgeOptions,
34
+ ) {
35
+ this.token = options.token ?? randomUUID();
36
+ this.backends = options.backends ?? ["codex", "goose", "aider", "openhands", "claude"];
37
+ process.env.CAMPFIRE_INTERNAL_AGENT_MCP_TOKEN = this.token;
38
+ }
39
+
40
+ onSessionReady(sessionId: string, backendType: BackendType, cwd: string): void {
41
+ if (!cwd || this.injectedSessions.has(sessionId)) return;
42
+ const session = this.wsBridge.getSession(sessionId);
43
+ if (session?.state.parent_session_id || session?.state.orchestration_role === "subagent" || session?.state.orchestration_role === "race_entry") return;
44
+ if (!this.supportsRuntimeMcp(backendType)) return;
45
+ if (process.env.CAMPFIRE_ENABLE_AGENT_MCP === "0") return;
46
+
47
+ const config = createAgentMcpServerConfig({
48
+ port: this.options.port,
49
+ token: this.token,
50
+ packageRoot: this.options.packageRoot,
51
+ parentSessionId: sessionId,
52
+ backends: this.backends.filter((backend) => backend !== backendType),
53
+ });
54
+ this.injectedSessions.add(sessionId);
55
+ this.wsBridge.setMcpServers(sessionId, { campfire_agents: config });
56
+ }
57
+
58
+ handlePermissionRequest(sessionId: string, msg: CLIControlRequestMessage): boolean {
59
+ const backend = backendFromAskTool(msg.request.tool_name);
60
+ if (!backend) return false;
61
+
62
+ const session = this.wsBridge.getSession(sessionId);
63
+ if (!session) return false;
64
+ session.pendingPermissions.delete(msg.request_id);
65
+ const response = {
66
+ type: "control_response",
67
+ response: {
68
+ subtype: "success",
69
+ request_id: msg.request_id,
70
+ response: {
71
+ behavior: "allow",
72
+ updatedInput: msg.request.input,
73
+ },
74
+ },
75
+ };
76
+ (this.wsBridge as unknown as { sendToCLI?: (s: unknown, ndjson: string) => void }).sendToCLI?.(session, JSON.stringify(response));
77
+ return true;
78
+ }
79
+
80
+ handleAdapterPermissionRequest(_sessionId: string, request: PermissionRequest, respond: PermissionResponder): boolean {
81
+ const backend = backendFromCampfireMcpTool(request.tool_name);
82
+ if (!backend) return false;
83
+
84
+ respond({
85
+ type: "permission_response",
86
+ request_id: request.request_id,
87
+ behavior: "allow",
88
+ updated_input: request.input,
89
+ });
90
+ return true;
91
+ }
92
+
93
+ async callTool(parentSessionId: string, toolName: string, input: Record<string, unknown>): Promise<SubSessionResult> {
94
+ const backendType = backendFromAskTool(toolName);
95
+ if (!backendType) {
96
+ return {
97
+ sessionId: "",
98
+ backendType: "claude",
99
+ text: "",
100
+ filesChanged: [],
101
+ costUsd: 0,
102
+ durationMs: 0,
103
+ error: `Unknown agent tool: ${toolName}`,
104
+ };
105
+ }
106
+
107
+ const parent = this.wsBridge.getSession(parentSessionId);
108
+ if (!parent?.state.cwd) {
109
+ return {
110
+ sessionId: "",
111
+ backendType,
112
+ text: "",
113
+ filesChanged: [],
114
+ costUsd: 0,
115
+ durationMs: 0,
116
+ error: `Parent session ${parentSessionId} is not available.`,
117
+ };
118
+ }
119
+
120
+ const prompt = typeof input.prompt === "string" ? input.prompt.trim() : "";
121
+ if (!prompt) {
122
+ return {
123
+ sessionId: "",
124
+ backendType,
125
+ text: "",
126
+ filesChanged: [],
127
+ costUsd: 0,
128
+ durationMs: 0,
129
+ error: "prompt is required.",
130
+ };
131
+ }
132
+
133
+ const timeoutSeconds = typeof input.timeout_seconds === "number" && Number.isFinite(input.timeout_seconds)
134
+ ? Math.max(1, Math.min(3600, input.timeout_seconds))
135
+ : 300;
136
+
137
+ return this.subSessionManager.spawnSubSession(parentSessionId, backendType, prompt, parent.state.cwd, {
138
+ timeoutMs: timeoutSeconds * 1000,
139
+ toolUseId: `ask-${backendType}-${randomUUID().slice(0, 8)}`,
140
+ name: `Ask ${backendType}`,
141
+ description: prompt.slice(0, 160),
142
+ });
143
+ }
144
+
145
+ private supportsRuntimeMcp(backendType: BackendType): boolean {
146
+ return backendType === "claude" || backendType === "codex";
147
+ }
148
+ }
@@ -0,0 +1,171 @@
1
+ #!/usr/bin/env bun
2
+ import { generateAgentToolDefinitions } from "./agent-mcp-tools.js";
3
+ import type { AgentToolDefinition } from "./agent-mcp-tools.js";
4
+
5
+ interface JsonRpcRequest {
6
+ jsonrpc?: "2.0";
7
+ id?: string | number | null;
8
+ method?: string;
9
+ params?: Record<string, unknown>;
10
+ }
11
+
12
+ type FramingMode = "content-length" | "ndjson";
13
+
14
+ const url = process.env.CAMPFIRE_AGENT_MCP_URL || "http://127.0.0.1:4567/api/internal/agent-mcp";
15
+ const token = process.env.CAMPFIRE_AGENT_MCP_TOKEN || "";
16
+ const parentSessionId = process.env.CAMPFIRE_PARENT_SESSION_ID || "";
17
+ const backends = (process.env.CAMPFIRE_AGENT_MCP_BACKENDS || "")
18
+ .split(",")
19
+ .map((s) => s.trim())
20
+ .filter(Boolean) as Array<AgentToolDefinition["backendType"]>;
21
+
22
+ let framingMode: FramingMode | null = null;
23
+
24
+ function send(payload: unknown): void {
25
+ const json = JSON.stringify(payload);
26
+ if (framingMode === "content-length") {
27
+ process.stdout.write(`Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n${json}`);
28
+ return;
29
+ }
30
+ process.stdout.write(`${json}\n`);
31
+ }
32
+
33
+ function respond(id: JsonRpcRequest["id"], result: unknown): void {
34
+ if (id === undefined || id === null) return;
35
+ send({ jsonrpc: "2.0", id, result });
36
+ }
37
+
38
+ function respondError(id: JsonRpcRequest["id"], code: number, message: string): void {
39
+ if (id === undefined || id === null) return;
40
+ send({ jsonrpc: "2.0", id, error: { code, message } });
41
+ }
42
+
43
+ function mcpTools() {
44
+ return generateAgentToolDefinitions(backends.length ? backends : undefined).map((tool) => ({
45
+ name: tool.name,
46
+ description: tool.description,
47
+ inputSchema: tool.input_schema,
48
+ }));
49
+ }
50
+
51
+ async function callTool(name: string, input: Record<string, unknown>): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
52
+ const res = await fetch(`${url}/call`, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ Authorization: `Bearer ${token}`,
57
+ },
58
+ body: JSON.stringify({ parentSessionId, toolName: name, input }),
59
+ });
60
+ const data = await res.json() as {
61
+ text?: string;
62
+ filesChanged?: string[];
63
+ costUsd?: number;
64
+ durationMs?: number;
65
+ sessionId?: string;
66
+ error?: string;
67
+ };
68
+ if (!res.ok || data.error) {
69
+ return {
70
+ isError: true,
71
+ content: [{ type: "text", text: data.error || `Agent tool ${name} failed.` }],
72
+ };
73
+ }
74
+
75
+ const files = data.filesChanged?.length ? `\n\nFiles changed:\n${data.filesChanged.map((f) => `- ${f}`).join("\n")}` : "";
76
+ const metrics = `\n\nSub-session: ${data.sessionId || "unknown"}\nCost: $${(data.costUsd || 0).toFixed(4)}\nDuration: ${data.durationMs || 0}ms`;
77
+ return {
78
+ content: [{ type: "text", text: `${data.text || "Sub-agent completed without text output."}${files}${metrics}` }],
79
+ };
80
+ }
81
+
82
+ async function handle(req: JsonRpcRequest): Promise<void> {
83
+ switch (req.method) {
84
+ case "initialize":
85
+ respond(req.id, {
86
+ protocolVersion: "2024-11-05",
87
+ capabilities: { tools: {} },
88
+ serverInfo: { name: "campfire-agents", version: "1.0.0" },
89
+ });
90
+ return;
91
+ case "notifications/initialized":
92
+ return;
93
+ case "tools/list":
94
+ respond(req.id, { tools: mcpTools() });
95
+ return;
96
+ case "tools/call": {
97
+ const params = req.params ?? {};
98
+ const name = typeof params.name === "string" ? params.name : "";
99
+ const args = params.arguments && typeof params.arguments === "object" ? params.arguments as Record<string, unknown> : {};
100
+ if (!name) {
101
+ respondError(req.id, -32602, "Tool name is required.");
102
+ return;
103
+ }
104
+ respond(req.id, await callTool(name, args));
105
+ return;
106
+ }
107
+ default:
108
+ if (req.id !== undefined && req.id !== null) respondError(req.id, -32601, `Unknown method: ${req.method || ""}`);
109
+ }
110
+ }
111
+
112
+ let buffer = Buffer.alloc(0);
113
+
114
+ function parseJsonRequest(body: string): void {
115
+ try {
116
+ void handle(JSON.parse(body) as JsonRpcRequest);
117
+ } catch (err) {
118
+ process.stderr.write(`campfire-agents MCP parse error: ${err}\n`);
119
+ }
120
+ }
121
+
122
+ function consumeContentLengthFrames(): boolean {
123
+ let consumed = false;
124
+ while (buffer.length > 0) {
125
+ const headerEnd = buffer.indexOf("\r\n\r\n");
126
+ if (headerEnd === -1) return consumed;
127
+ const header = buffer.subarray(0, headerEnd).toString("utf-8");
128
+ const match = /Content-Length:\s*(\d+)/i.exec(header);
129
+ if (!match) {
130
+ return consumed;
131
+ }
132
+ framingMode = "content-length";
133
+ const length = Number(match[1]);
134
+ const start = headerEnd + 4;
135
+ if (buffer.length < start + length) return consumed;
136
+ const body = buffer.subarray(start, start + length).toString("utf-8");
137
+ buffer = buffer.subarray(start + length);
138
+ consumed = true;
139
+ parseJsonRequest(body);
140
+ }
141
+ return consumed;
142
+ }
143
+
144
+ function consumeNdjsonFrames(): void {
145
+ while (buffer.length > 0) {
146
+ const newline = buffer.indexOf("\n");
147
+ if (newline === -1) return;
148
+ framingMode = "ndjson";
149
+ const line = buffer.subarray(0, newline).toString("utf-8").trim();
150
+ buffer = buffer.subarray(newline + 1);
151
+ if (!line) continue;
152
+ parseJsonRequest(line);
153
+ }
154
+ }
155
+
156
+ function consumeFrames(): void {
157
+ while (buffer.length > 0) {
158
+ if (/^\s*Content-Length:/i.test(buffer.toString("utf-8", 0, Math.min(buffer.length, 32)))) {
159
+ const consumed = consumeContentLengthFrames();
160
+ if (!consumed) return;
161
+ continue;
162
+ }
163
+ consumeNdjsonFrames();
164
+ return;
165
+ }
166
+ }
167
+
168
+ process.stdin.on("data", (chunk) => {
169
+ buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
170
+ consumeFrames();
171
+ });
@@ -0,0 +1,76 @@
1
+ import type { BackendType, McpServerConfig } from "./session-types.js";
2
+
3
+ export interface AgentToolDefinition {
4
+ name: string;
5
+ backendType: BackendType;
6
+ description: string;
7
+ input_schema: {
8
+ type: "object";
9
+ properties: Record<string, unknown>;
10
+ required: string[];
11
+ };
12
+ }
13
+
14
+ const TOOL_BACKENDS: Array<{ backendType: BackendType; label: string; strengths: string }> = [
15
+ { backendType: "codex", label: "Codex", strengths: "focused code generation, scripts, tests, and small refactors" },
16
+ { backendType: "goose", label: "Goose", strengths: "local development workflows and shell-oriented implementation tasks" },
17
+ { backendType: "aider", label: "Aider", strengths: "direct repository patching and concise file edits" },
18
+ { backendType: "openhands", label: "OpenHands", strengths: "autonomous implementation work in a project workspace" },
19
+ { backendType: "claude", label: "Claude Code", strengths: "architecture, review, broad refactors, and high-context reasoning" },
20
+ ];
21
+
22
+ export function toolNameForBackend(backendType: BackendType): string {
23
+ return `ask_${backendType}`;
24
+ }
25
+
26
+ export function backendFromAskTool(toolName: string): BackendType | null {
27
+ if (!toolName.startsWith("ask_")) return null;
28
+ const backend = toolName.slice(4) as BackendType;
29
+ return TOOL_BACKENDS.some((entry) => entry.backendType === backend) ? backend : null;
30
+ }
31
+
32
+ export function generateAgentToolDefinitions(backends: BackendType[] = TOOL_BACKENDS.map((entry) => entry.backendType)): AgentToolDefinition[] {
33
+ const enabled = new Set(backends);
34
+ return TOOL_BACKENDS
35
+ .filter((entry) => enabled.has(entry.backendType))
36
+ .map((entry) => ({
37
+ name: toolNameForBackend(entry.backendType),
38
+ backendType: entry.backendType,
39
+ description: `Delegate a one-turn coding subtask to ${entry.label}. Best for ${entry.strengths}. Runs in the same working directory as the lead session.`,
40
+ input_schema: {
41
+ type: "object",
42
+ properties: {
43
+ prompt: {
44
+ type: "string",
45
+ description: `Clear, specific task description for ${entry.label}.`,
46
+ },
47
+ timeout_seconds: {
48
+ type: "number",
49
+ description: "Maximum seconds to wait for the sub-agent. Defaults to 300.",
50
+ default: 300,
51
+ },
52
+ },
53
+ required: ["prompt"],
54
+ },
55
+ }));
56
+ }
57
+
58
+ export function createAgentMcpServerConfig(options: {
59
+ port: number;
60
+ token: string;
61
+ packageRoot: string;
62
+ parentSessionId: string;
63
+ backends?: BackendType[];
64
+ }): McpServerConfig {
65
+ return {
66
+ type: "stdio",
67
+ command: "bun",
68
+ args: [`${options.packageRoot}/server/agent-mcp-stdio.ts`],
69
+ env: {
70
+ CAMPFIRE_AGENT_MCP_URL: `http://127.0.0.1:${options.port}/api/internal/agent-mcp`,
71
+ CAMPFIRE_AGENT_MCP_TOKEN: options.token,
72
+ CAMPFIRE_PARENT_SESSION_ID: options.parentSessionId,
73
+ CAMPFIRE_AGENT_MCP_BACKENDS: (options.backends ?? TOOL_BACKENDS.map((entry) => entry.backendType)).join(","),
74
+ },
75
+ };
76
+ }