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,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 };
|