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,26 @@
1
+ {
2
+ "name": "Campfire",
3
+ "short_name": "Campfire",
4
+ "description": "Collaborative web UI for AI coding agents",
5
+ "start_url": "/",
6
+ "scope": "/",
7
+ "display": "standalone",
8
+ "background_color": "#2b2a27",
9
+ "theme_color": "#ae5630",
10
+ "orientation": "any",
11
+ "categories": ["developer-tools", "productivity"],
12
+ "icons": [
13
+ {
14
+ "src": "/icon-192.png",
15
+ "sizes": "192x192",
16
+ "type": "image/png",
17
+ "purpose": "any maskable"
18
+ },
19
+ {
20
+ "src": "/icon-512.png",
21
+ "sizes": "512x512",
22
+ "type": "image/png",
23
+ "purpose": "any maskable"
24
+ }
25
+ ]
26
+ }
Binary file
package/dist/sw.js ADDED
@@ -0,0 +1,130 @@
1
+ // Campfire Service Worker — caching + push notifications
2
+ const CACHE_NAME = "campfire-v4";
3
+
4
+ // Assets to pre-cache on install (shell resources)
5
+ const PRECACHE_URLS = [
6
+ "/",
7
+ "/manifest.json",
8
+ "/icon-192.png",
9
+ "/icon-512.png",
10
+ ];
11
+
12
+ // Install: pre-cache shell resources
13
+ self.addEventListener("install", (event) => {
14
+ event.waitUntil(
15
+ caches.open(CACHE_NAME).then((cache) => cache.addAll(PRECACHE_URLS))
16
+ );
17
+ self.skipWaiting();
18
+ });
19
+
20
+ // Activate: clean up old caches
21
+ self.addEventListener("activate", (event) => {
22
+ event.waitUntil(
23
+ caches.keys().then((keys) =>
24
+ Promise.all(
25
+ keys
26
+ .filter((key) => key !== CACHE_NAME)
27
+ .map((key) => caches.delete(key))
28
+ )
29
+ )
30
+ );
31
+ self.clients.claim();
32
+ });
33
+
34
+ // Fetch: network-first for API/WS, cache-first for static assets
35
+ self.addEventListener("fetch", (event) => {
36
+ const url = new URL(event.request.url);
37
+
38
+ // Skip non-GET requests and WebSocket upgrades
39
+ if (event.request.method !== "GET") return;
40
+ // Skip API requests — always go to network
41
+ if (url.pathname.startsWith("/api/") || url.pathname.startsWith("/ws/")) return;
42
+
43
+ // HTML navigations must prefer the network so deploys do not leave tabs
44
+ // pinned to an old hashed JS bundle.
45
+ if (event.request.mode === "navigate") {
46
+ event.respondWith(
47
+ fetch(event.request)
48
+ .then((networkResponse) => {
49
+ if (networkResponse.ok && url.origin === self.location.origin) {
50
+ const clone = networkResponse.clone();
51
+ caches.open(CACHE_NAME).then((cache) => {
52
+ cache.put(event.request, clone);
53
+ });
54
+ }
55
+ return networkResponse;
56
+ })
57
+ .catch(() => caches.match(event.request).then((cached) => cached || new Response("Offline", { status: 503 })))
58
+ );
59
+ return;
60
+ }
61
+
62
+ event.respondWith(
63
+ caches.match(event.request).then((cached) => {
64
+ // Return cached version while fetching update in background (stale-while-revalidate)
65
+ const fetchPromise = fetch(event.request)
66
+ .then((networkResponse) => {
67
+ // Only cache successful responses for same-origin requests
68
+ if (networkResponse.ok && url.origin === self.location.origin) {
69
+ const clone = networkResponse.clone();
70
+ caches.open(CACHE_NAME).then((cache) => {
71
+ cache.put(event.request, clone);
72
+ });
73
+ }
74
+ return networkResponse;
75
+ })
76
+ .catch(() => {
77
+ // Network failed — return cached if available
78
+ return cached || new Response("Offline", { status: 503 });
79
+ });
80
+
81
+ return cached || fetchPromise;
82
+ })
83
+ );
84
+ });
85
+
86
+ // Push notifications for permission requests
87
+ self.addEventListener("push", (event) => {
88
+ if (!event.data) return;
89
+
90
+ let payload;
91
+ try {
92
+ payload = event.data.json();
93
+ } catch {
94
+ payload = { title: "Campfire", body: event.data.text() };
95
+ }
96
+
97
+ const title = payload.title || "Campfire";
98
+ const options = {
99
+ body: payload.body || "Permission request pending",
100
+ icon: "/icon-192.png",
101
+ badge: "/icon-192.png",
102
+ tag: payload.tag || "campfire-notification",
103
+ data: payload.data || {},
104
+ actions: payload.actions || [
105
+ { action: "allow", title: "Allow" },
106
+ { action: "deny", title: "Deny" },
107
+ ],
108
+ vibrate: [200, 100, 200],
109
+ };
110
+
111
+ event.waitUntil(self.registration.showNotification(title, options));
112
+ });
113
+
114
+ // Handle notification click — focus the app or open it
115
+ self.addEventListener("notificationclick", (event) => {
116
+ event.notification.close();
117
+
118
+ event.waitUntil(
119
+ self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clients) => {
120
+ // Focus existing window if available
121
+ for (const client of clients) {
122
+ if (client.url.includes(self.location.origin)) {
123
+ return client.focus();
124
+ }
125
+ }
126
+ // Otherwise open new window
127
+ return self.clients.openWindow("/");
128
+ })
129
+ );
130
+ });
package/package.json ADDED
@@ -0,0 +1,92 @@
1
+ {
2
+ "name": "the-campfire",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "description": "Campfire — collaborative web platform for AI coding agents. Run Claude Code, Codex, Goose, Aider, and more from one browser UI with real-time collaboration, permission voting, and automation.",
6
+ "license": "MIT",
7
+ "author": "The Vibe Company",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/stretchcloud/campfire.git",
11
+ "directory": "web"
12
+ },
13
+ "homepage": "https://github.com/stretchcloud/campfire#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/stretchcloud/campfire/issues"
16
+ },
17
+ "bin": {
18
+ "the-campfire": "./bin/cli.ts"
19
+ },
20
+ "files": [
21
+ "bin/",
22
+ "server/",
23
+ "dist/",
24
+ "!server/**/*.test.ts",
25
+ "!server/protocol/"
26
+ ],
27
+ "engines": {
28
+ "bun": ">=1.0.0"
29
+ },
30
+ "keywords": [
31
+ "claude",
32
+ "claude-code",
33
+ "codex",
34
+ "ai",
35
+ "agent",
36
+ "multi-agent",
37
+ "self-hosted",
38
+ "mcp",
39
+ "web-ui",
40
+ "vibe-coding"
41
+ ],
42
+ "scripts": {
43
+ "dev": "bun dev.ts",
44
+ "dev:api": "bun --watch server/index.ts",
45
+ "dev:vite": "vite",
46
+ "build": "vite build",
47
+ "start": "NODE_ENV=production bun server/index.ts",
48
+ "prepublishOnly": "bun run build",
49
+ "typecheck": "tsc --noEmit",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "test:codex-contract": "vitest run server/codex-protocol-*.test.ts"
53
+ },
54
+ "dependencies": {
55
+ "@lancedb/lancedb": "^0.26.2",
56
+ "@monaco-editor/react": "^4.7.0",
57
+ "croner": "^10.0.1",
58
+ "diff": "^8.0.3",
59
+ "geist": "^1.7.0",
60
+ "hono": "^4.7.0",
61
+ "mermaid": "^11.12.3",
62
+ "posthog-js": "^1.347.2"
63
+ },
64
+ "devDependencies": {
65
+ "@tailwindcss/vite": "^4.0.0",
66
+ "@testing-library/jest-dom": "^6.9.1",
67
+ "@testing-library/react": "^16.3.2",
68
+ "@testing-library/user-event": "^14.6.1",
69
+ "@types/bun": "^1.2.5",
70
+ "@types/diff": "^8.0.0",
71
+ "@types/react": "^19.0.0",
72
+ "@types/react-dom": "^19.0.0",
73
+ "@vitejs/plugin-react": "^4.4.0",
74
+ "@vitest/coverage-v8": "^4.0.18",
75
+ "@xterm/addon-fit": "^0.11.0",
76
+ "@xterm/xterm": "^6.0.0",
77
+ "autoprefixer": "^10.4.21",
78
+ "jsdom": "^28.0.0",
79
+ "postcss": "^8.5.3",
80
+ "react": "^19.0.0",
81
+ "react-arborist": "^3.4.3",
82
+ "react-dom": "^19.0.0",
83
+ "react-markdown": "^10.1.0",
84
+ "react-resizable-panels": "^4.6.2",
85
+ "remark-gfm": "^4.0.1",
86
+ "tailwindcss": "^4.0.0",
87
+ "typescript": "^5.9.3",
88
+ "vite": "^6.3.0",
89
+ "vitest": "^4.0.18",
90
+ "zustand": "^5.0.0"
91
+ }
92
+ }
@@ -0,0 +1,121 @@
1
+ # Adapter Contribution Guide
2
+
3
+ This document explains how to add a new agent backend adapter to Campfire.
4
+
5
+ ## Architecture
6
+
7
+ Campfire uses a unified `AgentAdapter` interface (`adapter-types.ts`) that translates between any agent backend's protocol and the browser's message format. The browser UI is completely unaware of which backend is running.
8
+
9
+ ```
10
+ Browser <-> WsBridge <-> AgentAdapter <-> Backend Process (stdio/websocket)
11
+ ```
12
+
13
+ ## The `AgentAdapter` Interface
14
+
15
+ Every adapter must implement `AgentAdapter` from `adapter-types.ts`:
16
+
17
+ ```typescript
18
+ interface AgentAdapter {
19
+ sendBrowserMessage(msg: BrowserOutgoingMessage): boolean;
20
+ onBrowserMessage(cb: (msg: BrowserIncomingMessage) => void): void;
21
+ onSessionMeta(cb: (meta: AdapterSessionMeta) => void): void;
22
+ onDisconnect(cb: () => void): void;
23
+ onInitError(cb: (error: string) => void): void;
24
+ isConnected(): boolean;
25
+ disconnect(): Promise<void>;
26
+ getBackendSessionId(): string | null;
27
+ }
28
+ ```
29
+
30
+ ## Step-by-Step: Adding a New Adapter
31
+
32
+ ### 1. Create the adapter file
33
+
34
+ Create `web/server/{name}-adapter.ts`. Your adapter class should:
35
+
36
+ - Accept a `Subprocess` (from `Bun.spawn()`) and session ID in the constructor
37
+ - Parse the backend's protocol (JSON-RPC, stdout text, REST, etc.)
38
+ - Translate incoming messages to `BrowserIncomingMessage` types
39
+ - Translate outgoing `BrowserOutgoingMessage` to the backend's protocol
40
+ - Emit `session_init` with a `SessionState` during initialization
41
+
42
+ Key message types to emit:
43
+ - `session_init` — on startup, with session state
44
+ - `stream_event` — for streaming text (`message_start`, `content_block_delta`, etc.)
45
+ - `assistant` — for complete assistant messages with `content` blocks
46
+ - `result` — when a turn completes
47
+ - `permission_request` — when the agent needs tool approval
48
+ - `tool_progress` — for long-running tool calls
49
+ - `error` — for error conditions
50
+
51
+ ### 2. Add to `cli-launcher.ts`
52
+
53
+ - Import your adapter class
54
+ - Add a `spawn{Name}()` method that:
55
+ - Resolves the binary path via `resolveBinary()`
56
+ - Spawns the process with `Bun.spawn()`
57
+ - Creates your adapter instance
58
+ - Calls `this.onAdapter(sessionId, adapter, backendType)`
59
+ - Add your backend to the `launch()` and `relaunch()` dispatch
60
+
61
+ ### 3. Update `session-types.ts`
62
+
63
+ Add your backend to the `BackendType` union:
64
+
65
+ ```typescript
66
+ export type BackendType = "claude" | "codex" | "goose" | "aider" | "openhands" | "your-backend";
67
+ ```
68
+
69
+ ### 4. Update `routes.ts`
70
+
71
+ - Add your backend to the `validBackends` array in session creation
72
+ - Add to the `/backends` endpoint with `resolveBinary()` check
73
+
74
+ ### 5. Update frontend utilities
75
+
76
+ **`src/utils/backends.ts`**:
77
+ - Add `YOUR_MODELS` and `YOUR_MODES` arrays
78
+ - Update `getModelsForBackend()`, `getModesForBackend()`, `getDefaultModel()`, `getDefaultMode()`
79
+
80
+ **`src/api.ts`**:
81
+ - Add your backend to the `CreateSessionOpts.backend` union
82
+ - Add to `CronJobInfo.backendType`
83
+
84
+ **`src/utils/project-grouping.ts`**:
85
+ - Add to the `backendType` union in `SessionItem`
86
+
87
+ **`src/components/CronManager.tsx`**:
88
+ - Add to the backend cycle array
89
+
90
+ ### 6. Write tests
91
+
92
+ Create `web/server/{name}-adapter.test.ts` with tests for:
93
+ - Message parsing/translation
94
+ - Protocol handling
95
+ - Edge cases (malformed input, disconnection)
96
+
97
+ ## Examples
98
+
99
+ ### ACP/JSON-RPC (structured protocol)
100
+
101
+ See `goose-adapter.ts` and `openhands-adapter.ts`. Both use the `JsonRpcTransport` class for stdin/stdout NDJSON communication with the ACP protocol (`initialize` -> `session/new` -> `session/prompt`).
102
+
103
+ ### Raw stdout (unstructured)
104
+
105
+ See `aider-adapter.ts`. Parses unstructured text output, detecting SEARCH/REPLACE edit blocks and prompt markers to determine turn boundaries.
106
+
107
+ ## Tool Name Mapping
108
+
109
+ Map your backend's tool names to Campfire-standard names so the UI renders them correctly:
110
+
111
+ ```typescript
112
+ const toolMap: Record<string, string> = {
113
+ "your_bash_tool": "Bash",
114
+ "your_editor": "Edit",
115
+ "your_reader": "Read",
116
+ "your_writer": "Write",
117
+ "your_search": "Grep",
118
+ };
119
+ ```
120
+
121
+ Standard tool names: `Bash`, `Edit`, `Read`, `Write`, `Glob`, `Grep`, `WebFetch`, `WebSearch`.
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Types for the adapter registry — manages third-party agent adapters
3
+ * installed via npm into ~/.campfire/adapters/.
4
+ *
5
+ * Each adapter package declares a "campfireAdapter" field in its package.json
6
+ * containing an AdapterMetadata object that tells the registry how to launch
7
+ * and configure the backend.
8
+ */
9
+
10
+ export interface AdapterMetadata {
11
+ name: string; // backend ID used in BackendType, e.g. "my-agent"
12
+ displayName: string; // human-readable label for UI
13
+ version: string;
14
+ binaryName?: string; // CLI binary name to resolve (optional)
15
+ models: Array<{ value: string; label: string }>;
16
+ modes: Array<{ value: string; label: string }>;
17
+ protocol: "stdio" | "websocket" | "http";
18
+ description?: string;
19
+ author?: string;
20
+ homepage?: string;
21
+ }
22
+
23
+ export interface InstalledAdapter {
24
+ metadata: AdapterMetadata;
25
+ path: string; // absolute path to adapter directory
26
+ installedAt: number;
27
+ npmPackage: string; // original npm package name used to install
28
+ }
@@ -0,0 +1,293 @@
1
+ /**
2
+ * AdapterRegistry — manages third-party agent adapters installed via npm.
3
+ *
4
+ * Adapters are stored in ~/.campfire/adapters/{name}/ and discovered by
5
+ * scanning for package.json files with a "campfireAdapter" field.
6
+ *
7
+ * Follows the same file-based persistence pattern as cron-store.ts.
8
+ */
9
+
10
+ import {
11
+ mkdirSync,
12
+ readdirSync,
13
+ readFileSync,
14
+ existsSync,
15
+ rmSync,
16
+ renameSync,
17
+ writeFileSync,
18
+ statSync,
19
+ } from "node:fs";
20
+ import { join } from "node:path";
21
+ import { homedir } from "node:os";
22
+ import type { AdapterMetadata, InstalledAdapter } from "./adapter-registry-types.js";
23
+
24
+ // ─── Constants ───────────────────────────────────────────────────────────────
25
+
26
+ /** Only allow safe characters in npm package names to prevent shell injection. */
27
+ const SAFE_NPM_PACKAGE_RE = /^[a-zA-Z0-9@/_.-]+$/;
28
+
29
+ // ─── Registry ────────────────────────────────────────────────────────────────
30
+
31
+ class AdapterRegistry {
32
+ private adaptersDir: string;
33
+ private installed: Map<string, InstalledAdapter>;
34
+
35
+ constructor() {
36
+ this.adaptersDir = join(homedir(), ".campfire", "adapters");
37
+ mkdirSync(this.adaptersDir, { recursive: true });
38
+ this.installed = new Map();
39
+ this.scan();
40
+ }
41
+
42
+ // ─── Scan ────────────────────────────────────────────────────────────────
43
+
44
+ /**
45
+ * Scan ~/.campfire/adapters/ for installed adapter packages.
46
+ * Each subdirectory is expected to contain a package.json with a
47
+ * "campfireAdapter" field. Invalid directories are silently skipped.
48
+ */
49
+ scan(): InstalledAdapter[] {
50
+ this.installed.clear();
51
+
52
+ let entries: string[];
53
+ try {
54
+ entries = readdirSync(this.adaptersDir);
55
+ } catch {
56
+ return [];
57
+ }
58
+
59
+ for (const entry of entries) {
60
+ const dir = join(this.adaptersDir, entry);
61
+ try {
62
+ const stat = statSync(dir);
63
+ if (!stat.isDirectory()) continue;
64
+ } catch {
65
+ continue;
66
+ }
67
+
68
+ const metadata = this.validate(dir);
69
+ if (!metadata) continue;
70
+
71
+ // Read the persisted metadata.json for installedAt / npmPackage
72
+ let installedAt = Date.now();
73
+ let npmPackage = metadata.name;
74
+ const metaJsonPath = join(dir, "metadata.json");
75
+ try {
76
+ const raw = readFileSync(metaJsonPath, "utf-8");
77
+ const parsed = JSON.parse(raw);
78
+ if (typeof parsed.installedAt === "number") installedAt = parsed.installedAt;
79
+ if (typeof parsed.npmPackage === "string") npmPackage = parsed.npmPackage;
80
+ } catch {
81
+ // metadata.json may not exist for manually-placed adapters
82
+ }
83
+
84
+ const adapter: InstalledAdapter = {
85
+ metadata,
86
+ path: dir,
87
+ installedAt,
88
+ npmPackage,
89
+ };
90
+
91
+ this.installed.set(metadata.name, adapter);
92
+ }
93
+
94
+ return Array.from(this.installed.values());
95
+ }
96
+
97
+ // ─── Validate ────────────────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Validate a single adapter directory by reading its package.json and
101
+ * checking for a well-formed "campfireAdapter" field.
102
+ *
103
+ * Returns the validated AdapterMetadata or null if invalid.
104
+ */
105
+ validate(dir: string): AdapterMetadata | null {
106
+ const pkgPath = join(dir, "package.json");
107
+ if (!existsSync(pkgPath)) return null;
108
+
109
+ let pkg: Record<string, unknown>;
110
+ try {
111
+ pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
112
+ } catch {
113
+ return null;
114
+ }
115
+
116
+ const raw = pkg.campfireAdapter;
117
+ if (!raw || typeof raw !== "object") return null;
118
+
119
+ const adapter = raw as Record<string, unknown>;
120
+
121
+ // Required fields
122
+ if (typeof adapter.name !== "string" || !adapter.name.trim()) return null;
123
+ if (typeof adapter.displayName !== "string" || !adapter.displayName.trim()) return null;
124
+
125
+ // Build validated metadata with defaults
126
+ const metadata: AdapterMetadata = {
127
+ name: adapter.name as string,
128
+ displayName: adapter.displayName as string,
129
+ version: typeof adapter.version === "string" ? adapter.version : (typeof pkg.version === "string" ? pkg.version : "0.0.0"),
130
+ protocol: adapter.protocol === "websocket" || adapter.protocol === "http"
131
+ ? adapter.protocol
132
+ : "stdio",
133
+ models: Array.isArray(adapter.models) && adapter.models.length > 0
134
+ ? adapter.models as Array<{ value: string; label: string }>
135
+ : [{ value: "default", label: "Default" }],
136
+ modes: Array.isArray(adapter.modes) && adapter.modes.length > 0
137
+ ? adapter.modes as Array<{ value: string; label: string }>
138
+ : [{ value: "default", label: "Default" }],
139
+ };
140
+
141
+ // Optional fields
142
+ if (typeof adapter.binaryName === "string") metadata.binaryName = adapter.binaryName;
143
+ if (typeof adapter.description === "string") metadata.description = adapter.description;
144
+ if (typeof adapter.author === "string") metadata.author = adapter.author;
145
+ if (typeof adapter.homepage === "string") metadata.homepage = adapter.homepage;
146
+
147
+ return metadata;
148
+ }
149
+
150
+ // ─── Install ─────────────────────────────────────────────────────────────
151
+
152
+ /**
153
+ * Install an adapter from npm into ~/.campfire/adapters/{name}/.
154
+ *
155
+ * Uses `bun add` in a temporary directory, validates the downloaded package
156
+ * has a campfireAdapter field, then moves it into the adapters directory.
157
+ */
158
+ async install(npmPackage: string): Promise<InstalledAdapter> {
159
+ // Sanitize package name to prevent shell injection
160
+ if (!npmPackage || !SAFE_NPM_PACKAGE_RE.test(npmPackage)) {
161
+ throw new Error(
162
+ `Invalid npm package name: "${npmPackage}". Only alphanumeric characters, @, /, _, ., and - are allowed.`,
163
+ );
164
+ }
165
+
166
+ // Create a temporary directory for the install
167
+ const tempDir = join(this.adaptersDir, `.tmp-install-${Date.now()}`);
168
+ mkdirSync(tempDir, { recursive: true });
169
+
170
+ try {
171
+ // Write a minimal package.json so bun add works
172
+ writeFileSync(
173
+ join(tempDir, "package.json"),
174
+ JSON.stringify({ name: "adapter-install-tmp", version: "0.0.0", private: true }),
175
+ "utf-8",
176
+ );
177
+
178
+ // Run bun add in the temp directory. --ignore-scripts prevents the
179
+ // third-party package from executing arbitrary code at install time;
180
+ // adapter code only runs later, when the user launches it as a backend.
181
+ const proc = Bun.spawn(["bun", "add", "--ignore-scripts", npmPackage], {
182
+ cwd: tempDir,
183
+ stdout: "pipe",
184
+ stderr: "pipe",
185
+ });
186
+
187
+ const exitCode = await proc.exited;
188
+ if (exitCode !== 0) {
189
+ const stderr = await new Response(proc.stderr).text();
190
+ throw new Error(`Failed to install "${npmPackage}": ${stderr.trim() || `exit code ${exitCode}`}`);
191
+ }
192
+
193
+ // Find the installed package in node_modules
194
+ // Handle scoped packages like @scope/pkg
195
+ const nodeModulesDir = join(tempDir, "node_modules");
196
+ const pkgName = npmPackage.replace(/@[\d^~>=<. |]+$/, ""); // strip version suffixes
197
+ const pkgDir = join(nodeModulesDir, pkgName);
198
+
199
+ if (!existsSync(pkgDir)) {
200
+ throw new Error(`Package "${npmPackage}" was installed but not found in node_modules`);
201
+ }
202
+
203
+ // Validate the package has campfireAdapter metadata
204
+ const metadata = this.validate(pkgDir);
205
+ if (!metadata) {
206
+ throw new Error(
207
+ `Package "${npmPackage}" does not contain a valid "campfireAdapter" field in its package.json`,
208
+ );
209
+ }
210
+
211
+ // Move to the final location
212
+ const destDir = join(this.adaptersDir, metadata.name);
213
+ if (existsSync(destDir)) {
214
+ rmSync(destDir, { recursive: true, force: true });
215
+ }
216
+ renameSync(pkgDir, destDir);
217
+
218
+ // Persist install metadata
219
+ const installedAt = Date.now();
220
+ writeFileSync(
221
+ join(destDir, "metadata.json"),
222
+ JSON.stringify({ installedAt, npmPackage }, null, 2),
223
+ "utf-8",
224
+ );
225
+
226
+ // Re-scan to pick up the new adapter
227
+ this.scan();
228
+
229
+ const installed = this.installed.get(metadata.name);
230
+ if (!installed) {
231
+ throw new Error(`Adapter "${metadata.name}" was installed but not found after scan`);
232
+ }
233
+
234
+ return installed;
235
+ } finally {
236
+ // Clean up temp directory
237
+ try {
238
+ rmSync(tempDir, { recursive: true, force: true });
239
+ } catch {
240
+ // Best-effort cleanup
241
+ }
242
+ }
243
+ }
244
+
245
+ // ─── Uninstall ───────────────────────────────────────────────────────────
246
+
247
+ /**
248
+ * Uninstall an adapter by name, removing its directory from
249
+ * ~/.campfire/adapters/.
250
+ */
251
+ uninstall(name: string): boolean {
252
+ const dir = join(this.adaptersDir, name);
253
+ if (!existsSync(dir)) {
254
+ this.installed.delete(name);
255
+ return false;
256
+ }
257
+
258
+ try {
259
+ rmSync(dir, { recursive: true, force: true });
260
+ } catch {
261
+ return false;
262
+ }
263
+
264
+ this.installed.delete(name);
265
+ return true;
266
+ }
267
+
268
+ // ─── Queries ─────────────────────────────────────────────────────────────
269
+
270
+ /** List all installed adapters. */
271
+ listInstalled(): InstalledAdapter[] {
272
+ return Array.from(this.installed.values());
273
+ }
274
+
275
+ /** Get a single installed adapter by its backend name, or null. */
276
+ getAdapter(name: string): InstalledAdapter | null {
277
+ return this.installed.get(name) || null;
278
+ }
279
+
280
+ /**
281
+ * Get backend configs for all installed adapters, suitable for the
282
+ * /api/backends endpoint.
283
+ */
284
+ getBackendConfigs(): Array<{ id: string; name: string; available: boolean }> {
285
+ return this.listInstalled().map((a) => ({
286
+ id: a.metadata.name,
287
+ name: a.metadata.displayName,
288
+ available: true, // If installed, it's available
289
+ }));
290
+ }
291
+ }
292
+
293
+ export { AdapterRegistry };