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,108 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+ import { existsSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { homedir } from "node:os";
6
+ import { DEFAULT_OPENROUTER_MODEL, getSettings, updateSettings, type CampfireSettings } from "../settings-manager.js";
7
+
8
+ function settingsResponse(s: CampfireSettings) {
9
+ return {
10
+ openrouterApiKeyConfigured: !!s.openrouterApiKey.trim(),
11
+ openrouterModel: s.openrouterModel || DEFAULT_OPENROUTER_MODEL,
12
+ moltbookApiKeyConfigured: !!s.moltbookApiKey?.trim(),
13
+ linearApiKeyConfigured: !!s.linearApiKey?.trim(),
14
+ claudeOAuthTokenConfigured: !!s.claudeOAuthToken?.trim(),
15
+ openaiApiKeyConfigured: !!s.openaiApiKey?.trim(),
16
+ anthropicApiKeyConfigured: !!s.anthropicApiKey?.trim(),
17
+ onboardingCompleted: s.onboardingCompleted,
18
+ };
19
+ }
20
+
21
+ function resolveClaudeMethod(
22
+ credentials: boolean, oauthEnv: boolean, apiKeyEnv: boolean,
23
+ anthropicStored: boolean, tokenStored: boolean,
24
+ ): string | null {
25
+ if (credentials) return "subscription";
26
+ if (oauthEnv) return "oauth-token";
27
+ if (apiKeyEnv || anthropicStored) return "api-key";
28
+ if (tokenStored) return "oauth-token";
29
+ return null;
30
+ }
31
+
32
+ function resolveCodexMethod(auth: boolean, apiKeyEnv: boolean, keyStored: boolean): string | null {
33
+ if (auth) return "subscription";
34
+ if (apiKeyEnv || keyStored) return "api-key";
35
+ return null;
36
+ }
37
+
38
+ export function registerSettingsRoutes(api: Hono, _deps: RouteDeps): void {
39
+ api.get("/settings", (c) => {
40
+ const settings = getSettings();
41
+ return c.json(settingsResponse(settings));
42
+ });
43
+
44
+ api.put("/settings", async (c) => {
45
+ const body = await c.req.json().catch(() => ({}));
46
+
47
+ // Validate string fields
48
+ const STRING_FIELDS = [
49
+ "openrouterApiKey", "openrouterModel", "moltbookApiKey", "linearApiKey",
50
+ "claudeOAuthToken", "openaiApiKey", "anthropicApiKey",
51
+ ] as const;
52
+
53
+ for (const field of STRING_FIELDS) {
54
+ if (body[field] !== undefined && typeof body[field] !== "string") {
55
+ return c.json({ error: `${field} must be a string` }, 400);
56
+ }
57
+ }
58
+
59
+ const hasOnboarding = typeof body.onboardingCompleted === "boolean";
60
+ const hasAnyField = STRING_FIELDS.some((f) => body[f] !== undefined) || hasOnboarding;
61
+ if (!hasAnyField) {
62
+ return c.json({ error: "At least one settings field is required" }, 400);
63
+ }
64
+
65
+ const patch: Record<string, string | boolean> = {};
66
+ if (hasOnboarding) patch.onboardingCompleted = body.onboardingCompleted;
67
+ for (const field of STRING_FIELDS) {
68
+ if (typeof body[field] === "string") {
69
+ patch[field] = field === "openrouterModel"
70
+ ? (body[field].trim() || DEFAULT_OPENROUTER_MODEL)
71
+ : body[field].trim();
72
+ }
73
+ }
74
+
75
+ const settings = updateSettings(patch);
76
+ return c.json(settingsResponse(settings));
77
+ });
78
+
79
+ // ─── Auth detection: check if Claude/Codex are already authenticated ─────
80
+ api.get("/settings/auth-status", (c) => {
81
+ const home = homedir();
82
+ // Claude: ~/.claude/.credentials.json or CLAUDE_CODE_OAUTH_TOKEN env var
83
+ const claudeCredentials = existsSync(join(home, ".claude", ".credentials.json"));
84
+ const claudeOAuthEnv = !!process.env.CLAUDE_CODE_OAUTH_TOKEN;
85
+ const claudeApiKeyEnv = !!process.env.ANTHROPIC_API_KEY;
86
+
87
+ // Codex: ~/.codex/auth.json or OPENAI_API_KEY env var
88
+ const codexAuth = existsSync(join(home, ".codex", "auth.json"));
89
+ const openaiApiKeyEnv = !!process.env.OPENAI_API_KEY;
90
+
91
+ // Also check global settings for stored tokens
92
+ const settings = getSettings();
93
+ const claudeTokenStored = !!settings.claudeOAuthToken?.trim();
94
+ const openaiKeyStored = !!settings.openaiApiKey?.trim();
95
+ const anthropicKeyStored = !!settings.anthropicApiKey?.trim();
96
+
97
+ return c.json({
98
+ claude: {
99
+ authenticated: claudeCredentials || claudeOAuthEnv || claudeApiKeyEnv || claudeTokenStored || anthropicKeyStored,
100
+ method: resolveClaudeMethod(claudeCredentials, claudeOAuthEnv, claudeApiKeyEnv, anthropicKeyStored, claudeTokenStored),
101
+ },
102
+ codex: {
103
+ authenticated: codexAuth || openaiApiKeyEnv || openaiKeyStored,
104
+ method: resolveCodexMethod(codexAuth, openaiApiKeyEnv, openaiKeyStored),
105
+ },
106
+ });
107
+ });
108
+ }
@@ -0,0 +1,52 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+ import * as skillsManager from "../skills-manager.js";
4
+
5
+ export function registerSkillsRoutes(api: Hono, _deps: RouteDeps): void {
6
+ // List all installed plugins with their skills and commands
7
+ api.get("/skills", (c) => {
8
+ const plugins = skillsManager.listPlugins();
9
+ const disabled = skillsManager.getDisabledPlugins();
10
+ const enriched = plugins.map((p) => ({
11
+ ...p,
12
+ disabledInCampfire: disabled.includes(p.id),
13
+ }));
14
+ return c.json(enriched);
15
+ });
16
+
17
+ // Get a single plugin by ID
18
+ api.get("/skills/:id", (c) => {
19
+ const id = decodeURIComponent(c.req.param("id"));
20
+ const plugin = skillsManager.getPlugin(id);
21
+ if (!plugin) return c.json({ error: "Plugin not found" }, 404);
22
+ const disabled = skillsManager.isPluginDisabled(id);
23
+ return c.json({ ...plugin, disabledInCampfire: disabled });
24
+ });
25
+
26
+ // Read a skill's SKILL.md content
27
+ api.get("/skills/:id/skill/:name", (c) => {
28
+ const id = decodeURIComponent(c.req.param("id"));
29
+ const name = decodeURIComponent(c.req.param("name"));
30
+ const content = skillsManager.readSkillContent(id, name);
31
+ if (content === null) return c.json({ error: "Skill not found" }, 404);
32
+ return c.json({ content });
33
+ });
34
+
35
+ // Read a command's content
36
+ api.get("/skills/:id/command/:name", (c) => {
37
+ const id = decodeURIComponent(c.req.param("id"));
38
+ const name = decodeURIComponent(c.req.param("name"));
39
+ const content = skillsManager.readCommandContent(id, name);
40
+ if (content === null) return c.json({ error: "Command not found" }, 404);
41
+ return c.json({ content });
42
+ });
43
+
44
+ // Toggle plugin enabled/disabled in Campfire
45
+ api.post("/skills/:id/toggle", async (c) => {
46
+ const id = decodeURIComponent(c.req.param("id"));
47
+ const body = await c.req.json().catch(() => ({}));
48
+ const disabled = body.disabled === true;
49
+ skillsManager.setPluginDisabled(id, disabled);
50
+ return c.json({ ok: true, disabled });
51
+ });
52
+ }
@@ -0,0 +1,243 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+ import { resolveBinary } from "../path-resolver.js";
4
+ import { homedir } from "node:os";
5
+ import { join } from "node:path";
6
+ import { existsSync, readFileSync } from "node:fs";
7
+ import { containerManager } from "../container-manager.js";
8
+ import { getUsageLimits } from "../usage-limits.js";
9
+ import {
10
+ getUpdateState,
11
+ checkForUpdate,
12
+ isUpdateAvailable,
13
+ setUpdateInProgress,
14
+ } from "../update-checker.js";
15
+ import { refreshServiceDefinition } from "../service.js";
16
+
17
+ const UPDATE_CHECK_STALE_MS = 5 * 60 * 1000;
18
+
19
+ export function registerSystemRoutes(api: Hono, deps: RouteDeps): void {
20
+ const { wsBridge, terminalManager } = deps;
21
+
22
+ // ─── Available backends ─────────────────────────────────────
23
+ api.get("/backends", (c) => {
24
+ const backends: Array<{ id: string; name: string; available: boolean }> = [];
25
+ backends.push({ id: "claude", name: "Claude Code", available: resolveBinary("claude") !== null });
26
+ backends.push({ id: "codex", name: "Codex", available: resolveBinary("codex") !== null });
27
+ backends.push({ id: "goose", name: "Goose", available: resolveBinary("goose") !== null });
28
+ backends.push({ id: "aider", name: "Aider", available: resolveBinary("aider") !== null });
29
+ backends.push({ id: "openhands", name: "OpenHands", available: resolveBinary("openhands") !== null });
30
+ backends.push({ id: "openclaw", name: "OpenClaw", available: resolveBinary("openclaw") !== null });
31
+ backends.push({ id: "opencode", name: "OpenCode", available: resolveBinary("opencode") !== null });
32
+ return c.json(backends);
33
+ });
34
+
35
+ api.get("/backends/:id/models", (c) => {
36
+ const backendId = c.req.param("id");
37
+ if (backendId === "codex") {
38
+ const cachePath = join(homedir(), ".codex", "models_cache.json");
39
+ if (!existsSync(cachePath)) {
40
+ return c.json({ error: "Codex models cache not found. Run codex once to populate it." }, 404);
41
+ }
42
+ try {
43
+ const raw = readFileSync(cachePath, "utf-8");
44
+ const cache = JSON.parse(raw) as {
45
+ models: Array<{
46
+ slug: string;
47
+ display_name?: string;
48
+ description?: string;
49
+ visibility?: string;
50
+ priority?: number;
51
+ }>;
52
+ };
53
+ const models = cache.models
54
+ .filter((m) => m.visibility === "list")
55
+ .sort((a, b) => (a.priority ?? 99) - (b.priority ?? 99))
56
+ .map((m) => ({
57
+ value: m.slug,
58
+ label: m.display_name || m.slug,
59
+ description: m.description || "",
60
+ }));
61
+ return c.json(models);
62
+ } catch (e) {
63
+ return c.json({ error: "Failed to parse Codex models cache" }, 500);
64
+ }
65
+ }
66
+ return c.json({ error: "Use frontend defaults for this backend" }, 404);
67
+ });
68
+
69
+ // ─── Containers ─────────────────────────────────────────────────
70
+ api.get("/containers/status", (c) => {
71
+ const available = containerManager.checkDocker();
72
+ const version = containerManager.getDockerVersion();
73
+ return c.json({ available, version });
74
+ });
75
+
76
+ api.get("/containers/images", (c) => {
77
+ const images = containerManager.listImages();
78
+ return c.json(images);
79
+ });
80
+
81
+ api.get("/containers/list", (c) => {
82
+ return c.json(containerManager.listContainers());
83
+ });
84
+
85
+ api.post("/containers/:sessionId/stop", async (c) => {
86
+ const sessionId = c.req.param("sessionId");
87
+ await containerManager.removeContainer(sessionId);
88
+ return c.json({ ok: true });
89
+ });
90
+
91
+ // ─── Usage Limits ─────────────────────────────────────────────────────
92
+ api.get("/usage-limits", async (c) => {
93
+ const limits = await getUsageLimits();
94
+ return c.json(limits);
95
+ });
96
+
97
+ api.get("/sessions/:id/usage-limits", async (c) => {
98
+ const sessionId = c.req.param("id");
99
+ const session = wsBridge.getSession(sessionId);
100
+ const empty = { five_hour: null, seven_day: null, extra_usage: null };
101
+
102
+ if (session?.backendType === "codex") {
103
+ const rl = wsBridge.getCodexRateLimits(sessionId);
104
+ if (!rl) return c.json(empty);
105
+ const mapLimit = (l: { usedPercent: number; windowDurationMins: number; resetsAt: number } | null) => {
106
+ if (!l) return null;
107
+ return {
108
+ utilization: l.usedPercent,
109
+ resets_at: l.resetsAt ? new Date(l.resetsAt * 1000).toISOString() : null,
110
+ };
111
+ };
112
+ return c.json({
113
+ five_hour: mapLimit(rl.primary),
114
+ seven_day: mapLimit(rl.secondary),
115
+ extra_usage: null,
116
+ });
117
+ }
118
+
119
+ const limits = await getUsageLimits();
120
+ return c.json(limits);
121
+ });
122
+
123
+ // ─── Update checking ─────────────────────────────────────────────────
124
+ api.get("/update-check", async (c) => {
125
+ const initialState = getUpdateState();
126
+ const needsRefresh =
127
+ initialState.lastChecked === 0
128
+ || Date.now() - initialState.lastChecked > UPDATE_CHECK_STALE_MS;
129
+ if (needsRefresh) {
130
+ await checkForUpdate();
131
+ }
132
+ const state = getUpdateState();
133
+ return c.json({
134
+ currentVersion: state.currentVersion,
135
+ latestVersion: state.latestVersion,
136
+ updateAvailable: isUpdateAvailable(),
137
+ isServiceMode: state.isServiceMode,
138
+ updateInProgress: state.updateInProgress,
139
+ lastChecked: state.lastChecked,
140
+ });
141
+ });
142
+
143
+ api.post("/update-check", async (c) => {
144
+ await checkForUpdate();
145
+ const state = getUpdateState();
146
+ return c.json({
147
+ currentVersion: state.currentVersion,
148
+ latestVersion: state.latestVersion,
149
+ updateAvailable: isUpdateAvailable(),
150
+ isServiceMode: state.isServiceMode,
151
+ updateInProgress: state.updateInProgress,
152
+ lastChecked: state.lastChecked,
153
+ });
154
+ });
155
+
156
+ api.post("/update", async (c) => {
157
+ const state = getUpdateState();
158
+ if (!state.isServiceMode) {
159
+ return c.json({ error: "Update & restart is only available in service mode" }, 400);
160
+ }
161
+ if (!isUpdateAvailable()) {
162
+ return c.json({ error: "No update available" }, 400);
163
+ }
164
+ if (state.updateInProgress) {
165
+ return c.json({ error: "Update already in progress" }, 409);
166
+ }
167
+
168
+ setUpdateInProgress(true);
169
+
170
+ setTimeout(async () => {
171
+ try {
172
+ console.log(`[update] Updating the-campfire to ${state.latestVersion}...`);
173
+ const proc = Bun.spawn(
174
+ ["bun", "install", "-g", `the-campfire@${state.latestVersion}`],
175
+ { stdout: "pipe", stderr: "pipe" },
176
+ );
177
+ const exitCode = await proc.exited;
178
+ if (exitCode !== 0) {
179
+ const stderr = await new Response(proc.stderr).text();
180
+ console.error(`[update] bun install failed (code ${exitCode}):`, stderr);
181
+ setUpdateInProgress(false);
182
+ return;
183
+ }
184
+ try {
185
+ refreshServiceDefinition();
186
+ console.log("[update] Service definition refreshed.");
187
+ } catch (err) {
188
+ console.warn("[update] Failed to refresh service definition:", err);
189
+ }
190
+ console.log("[update] Update successful, restarting service...");
191
+ const isLinux = process.platform === "linux";
192
+ const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
193
+ const restartCmd = isLinux
194
+ ? ["systemctl", "--user", "restart", "the-campfire.service"]
195
+ : uid !== undefined
196
+ ? ["launchctl", "kickstart", "-k", `gui/${uid}/sh.campfire.app`]
197
+ : ["launchctl", "kickstart", "-k", "sh.campfire.app"];
198
+
199
+ Bun.spawn(restartCmd, {
200
+ stdout: "ignore",
201
+ stderr: "ignore",
202
+ stdin: "ignore",
203
+ env: isLinux
204
+ ? {
205
+ ...process.env,
206
+ XDG_RUNTIME_DIR:
207
+ process.env.XDG_RUNTIME_DIR ||
208
+ `/run/user/${uid ?? 1000}`,
209
+ }
210
+ : undefined,
211
+ });
212
+ setTimeout(() => process.exit(0), 500);
213
+ } catch (err) {
214
+ console.error("[update] Update failed:", err);
215
+ setUpdateInProgress(false);
216
+ }
217
+ }, 100);
218
+
219
+ return c.json({
220
+ ok: true,
221
+ message: "Update started. Server will restart shortly.",
222
+ });
223
+ });
224
+
225
+ // ─── Terminal ──────────────────────────────────────────────────────
226
+ api.get("/terminal", (c) => {
227
+ const info = terminalManager.getInfo();
228
+ if (!info) return c.json({ active: false });
229
+ return c.json({ active: true, terminalId: info.id, cwd: info.cwd });
230
+ });
231
+
232
+ api.post("/terminal/spawn", async (c) => {
233
+ const body = await c.req.json<{ cwd: string; cols?: number; rows?: number }>();
234
+ if (!body.cwd) return c.json({ error: "cwd is required" }, 400);
235
+ const terminalId = terminalManager.spawn(body.cwd, body.cols, body.rows);
236
+ return c.json({ terminalId });
237
+ });
238
+
239
+ api.post("/terminal/kill", (c) => {
240
+ terminalManager.kill();
241
+ return c.json({ ok: true });
242
+ });
243
+ }
@@ -0,0 +1,135 @@
1
+ import type { Hono } from "hono";
2
+ import type { RouteDeps } from "./route-deps.js";
3
+ import * as webhookStore from "../webhook-store.js";
4
+ import type { WebhookCreateInput } from "../webhook-types.js";
5
+
6
+ export function registerWebhookRoutes(api: Hono, deps: RouteDeps): void {
7
+ const { launcher, wsBridge, webhookManager } = deps;
8
+
9
+ api.get("/webhooks", (c) => {
10
+ return c.json(webhookStore.listWebhooks());
11
+ });
12
+
13
+ api.get("/webhooks/:id", (c) => {
14
+ const id = c.req.param("id");
15
+ const webhook = webhookStore.getWebhook(id);
16
+ if (!webhook) return c.json({ error: "Webhook not found" }, 404);
17
+ return c.json(webhook);
18
+ });
19
+
20
+ api.post("/webhooks", async (c) => {
21
+ const data = await c.req.json() as WebhookCreateInput;
22
+ try {
23
+ const webhook = webhookStore.createWebhook(data);
24
+ return c.json(webhook, 201);
25
+ } catch (err) {
26
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
27
+ }
28
+ });
29
+
30
+ api.put("/webhooks/:id", async (c) => {
31
+ const id = c.req.param("id");
32
+ const updates = await c.req.json();
33
+ try {
34
+ const webhook = webhookStore.updateWebhook(id, updates);
35
+ if (!webhook) return c.json({ error: "Webhook not found" }, 404);
36
+ return c.json(webhook);
37
+ } catch (err) {
38
+ return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
39
+ }
40
+ });
41
+
42
+ api.delete("/webhooks/:id", (c) => {
43
+ const id = c.req.param("id");
44
+ const deleted = webhookStore.deleteWebhook(id);
45
+ if (!deleted) return c.json({ error: "Webhook not found" }, 404);
46
+ return c.json({ ok: true });
47
+ });
48
+
49
+ api.post("/webhooks/:id/toggle", (c) => {
50
+ const id = c.req.param("id");
51
+ const webhook = webhookStore.getWebhook(id);
52
+ if (!webhook) return c.json({ error: "Webhook not found" }, 404);
53
+ const updated = webhookStore.updateWebhook(id, { enabled: !webhook.enabled });
54
+ return c.json(updated);
55
+ });
56
+
57
+ api.post("/webhooks/:id/test", async (c) => {
58
+ const id = c.req.param("id");
59
+ const webhook = webhookStore.getWebhook(id);
60
+ if (!webhook) return c.json({ error: "Webhook not found" }, 404);
61
+ if (webhookManager) {
62
+ webhookManager.emit("session.completed", "test-session", {
63
+ backendType: "claude",
64
+ model: "claude-sonnet-4-5-20250929",
65
+ totalCostUsd: 0.05,
66
+ numTurns: 3,
67
+ isError: false,
68
+ test: true,
69
+ });
70
+ }
71
+ return c.json({ ok: true });
72
+ });
73
+
74
+ // ─── OpenClaw Inbound Webhook ───────────────────────────────────
75
+ api.post("/webhooks/openclaw", async (c) => {
76
+ const expectedToken = process.env.CAMPFIRE_OPENCLAW_TOKEN;
77
+ if (expectedToken) {
78
+ const authHeader = c.req.header("Authorization") || "";
79
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
80
+ if (token !== expectedToken) {
81
+ return c.json({ ok: false, error: "Unauthorized" }, 401);
82
+ }
83
+ }
84
+ try {
85
+ const body = await c.req.json() as {
86
+ message?: string;
87
+ name?: string;
88
+ model?: string;
89
+ cwd?: string;
90
+ };
91
+ if (!body.message || !body.message.trim()) {
92
+ return c.json({ ok: false, error: "message is required" }, 400);
93
+ }
94
+ const cwd = body.cwd || process.env.HOME || "/";
95
+ const session = launcher.launch({
96
+ model: body.model || "default",
97
+ permissionMode: "bypassPermissions",
98
+ cwd,
99
+ backendType: "openclaw",
100
+ env: {},
101
+ });
102
+ setTimeout(() => {
103
+ wsBridge.injectUserMessage(session.sessionId, body.message!.trim());
104
+ }, 2000);
105
+ console.log(`[routes] OpenClaw inbound webhook → created session ${session.sessionId} with prompt: "${body.message.trim().slice(0, 80)}..."`);
106
+ return c.json({
107
+ ok: true,
108
+ sessionId: session.sessionId,
109
+ name: body.name || "OpenClaw Hook",
110
+ }, 202);
111
+ } catch (err) {
112
+ console.error("[routes] OpenClaw inbound webhook error:", err);
113
+ return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
114
+ }
115
+ });
116
+
117
+ // ─── OpenClaw Channel Inbound ────────────────────────────────────
118
+ api.post("/openclaw/inbound", async (c) => {
119
+ try {
120
+ const body = await c.req.json() as {
121
+ sessionId?: string;
122
+ senderId?: string;
123
+ text?: string;
124
+ metadata?: Record<string, unknown>;
125
+ };
126
+ if (!body.sessionId || !body.text) {
127
+ return c.json({ ok: false, error: "sessionId and text are required" }, 400);
128
+ }
129
+ wsBridge.injectAgentMessage(body.sessionId, body.text, body.metadata);
130
+ return c.json({ ok: true });
131
+ } catch (err) {
132
+ return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) }, 500);
133
+ }
134
+ });
135
+ }
@@ -0,0 +1,44 @@
1
+ // Re-export shim — the actual route implementations live in routes/*.ts
2
+ // This file preserves the original import path for backwards compatibility.
3
+ import type { CliLauncher } from "./cli-launcher.js";
4
+ import type { WsBridge } from "./ws-bridge.js";
5
+ import type { SessionStore } from "./session-store.js";
6
+ import type { WorktreeTracker } from "./worktree-tracker.js";
7
+ import type { TerminalManager } from "./terminal-manager.js";
8
+ import { createRoutes as _createRoutes } from "./routes/index.js";
9
+
10
+ /**
11
+ * Legacy positional-args signature preserved for backwards compatibility.
12
+ * New code should import from `./routes/index.js` and pass a deps object.
13
+ */
14
+ export function createRoutes(
15
+ launcher: CliLauncher,
16
+ wsBridge: WsBridge,
17
+ sessionStore: SessionStore,
18
+ worktreeTracker: WorktreeTracker,
19
+ terminalManager: TerminalManager,
20
+ prPoller?: import("./pr-poller.js").PRPoller,
21
+ recorder?: import("./recorder.js").RecorderManager,
22
+ cronScheduler?: import("./cron-scheduler.js").CronScheduler,
23
+ webhookManager?: import("./webhook-manager.js").WebhookManager,
24
+ adapterRegistry?: import("./adapter-registry.js").AdapterRegistry,
25
+ agentExecutor?: import("./agent-executor.js").AgentExecutor,
26
+ protocolMonitor?: import("./protocol-monitor.js").ProtocolMonitor,
27
+ agentMcpBridge?: import("./agent-mcp-bridge.js").AgentMcpBridge,
28
+ ) {
29
+ return _createRoutes({
30
+ launcher,
31
+ wsBridge,
32
+ sessionStore,
33
+ worktreeTracker,
34
+ terminalManager,
35
+ prPoller,
36
+ recorder,
37
+ cronScheduler,
38
+ webhookManager,
39
+ adapterRegistry,
40
+ agentExecutor,
41
+ protocolMonitor,
42
+ agentMcpBridge,
43
+ });
44
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Security middleware — headers + rate limiting for the Hono server.
3
+ *
4
+ * Security headers: X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
5
+ * X-XSS-Protection, Strict-Transport-Security, Permissions-Policy.
6
+ *
7
+ * Rate limiting: per-IP sliding window, configurable via env vars.
8
+ */
9
+
10
+ import type { Context, Next } from "hono";
11
+
12
+ // ─── Security Headers ──────────────────────────────────────────────────────
13
+
14
+ export async function securityHeaders(c: Context, next: Next): Promise<void> {
15
+ await next();
16
+
17
+ c.header("X-Content-Type-Options", "nosniff");
18
+ c.header("X-Frame-Options", "SAMEORIGIN");
19
+ c.header("Referrer-Policy", "strict-origin-when-cross-origin");
20
+ c.header("X-XSS-Protection", "1; mode=block");
21
+ c.header("Permissions-Policy", "camera=(), microphone=(), geolocation=()");
22
+
23
+ // HSTS only if behind HTTPS (don't break HTTP dev setups)
24
+ if (c.req.header("x-forwarded-proto") === "https") {
25
+ c.header("Strict-Transport-Security", "max-age=31536000; includeSubDomains");
26
+ }
27
+ }
28
+
29
+ // ─── Rate Limiting ──────────────────────────────────────────────────────────
30
+
31
+ const WINDOW_MS = Number(process.env.CAMPFIRE_RATE_LIMIT_WINDOW_MS) || 60_000;
32
+ const MAX_REQUESTS = Number(process.env.CAMPFIRE_RATE_LIMIT_MAX) || 120;
33
+
34
+ interface RateEntry {
35
+ count: number;
36
+ resetAt: number;
37
+ }
38
+
39
+ const ipBuckets = new Map<string, RateEntry>();
40
+
41
+ // Clean up stale entries every 5 minutes. unref() so the interval never
42
+ // keeps the process (or a test runner) alive on its own.
43
+ setInterval(() => {
44
+ const now = Date.now();
45
+ for (const [ip, entry] of ipBuckets) {
46
+ if (entry.resetAt <= now) ipBuckets.delete(ip);
47
+ }
48
+ }, 5 * 60_000).unref?.();
49
+
50
+ /** Socket-level remote IPs, tagged by the Bun.serve fetch handler before the
51
+ * request reaches Hono. A WeakMap keyed by the raw Request cannot be spoofed
52
+ * by client-supplied headers. */
53
+ const requestIps = new WeakMap<Request, string>();
54
+
55
+ /** Record the actual socket remote address for a request (called from index.ts). */
56
+ export function tagRequestIp(req: Request, ip: string | undefined | null): void {
57
+ if (ip) requestIps.set(req, ip);
58
+ }
59
+
60
+ function isLoopback(ip: string): boolean {
61
+ return ip === "127.0.0.1" || ip === "::1" || ip === "::ffff:127.0.0.1";
62
+ }
63
+
64
+ function getClientIp(c: Context): string {
65
+ const socketIp = requestIps.get(c.req.raw) ?? "";
66
+ // Forwarded headers are only trustworthy when the direct peer is a local
67
+ // reverse proxy — otherwise a client could spoof them to dodge the limiter.
68
+ if (!socketIp || isLoopback(socketIp)) {
69
+ const forwarded = c.req.header("x-forwarded-for")?.split(",")[0]?.trim()
70
+ || c.req.header("x-real-ip");
71
+ if (forwarded) return forwarded;
72
+ }
73
+ return socketIp || "unknown";
74
+ }
75
+
76
+ export async function rateLimiter(c: Context, next: Next): Promise<void> {
77
+ const ip = getClientIp(c);
78
+ const now = Date.now();
79
+
80
+ let entry = ipBuckets.get(ip);
81
+ if (!entry || entry.resetAt <= now) {
82
+ entry = { count: 0, resetAt: now + WINDOW_MS };
83
+ ipBuckets.set(ip, entry);
84
+ }
85
+
86
+ entry.count++;
87
+
88
+ // Set rate limit headers
89
+ const remaining = Math.max(0, MAX_REQUESTS - entry.count);
90
+ c.header("X-RateLimit-Limit", String(MAX_REQUESTS));
91
+ c.header("X-RateLimit-Remaining", String(remaining));
92
+ c.header("X-RateLimit-Reset", String(Math.ceil(entry.resetAt / 1000)));
93
+
94
+ if (entry.count > MAX_REQUESTS) {
95
+ c.header("Retry-After", String(Math.ceil((entry.resetAt - now) / 1000)));
96
+ return c.json({ error: "Too many requests. Please retry later." }, 429) as unknown as void;
97
+ }
98
+
99
+ await next();
100
+ }