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,410 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { RouteDeps } from "./route-deps.js";
|
|
3
|
+
import * as galleryStore from "../gallery-store.js";
|
|
4
|
+
import * as galleryVotes from "../gallery-votes.js";
|
|
5
|
+
import type { GalleryFilter } from "../gallery-types.js";
|
|
6
|
+
import type { BackendType, BrowserIncomingMessage } from "../session-types.js";
|
|
7
|
+
import * as settingsManager from "../settings-manager.js";
|
|
8
|
+
import { DEFAULT_OPENROUTER_MODEL } from "../settings-manager.js";
|
|
9
|
+
import * as clawhubExport from "../clawhub-export.js";
|
|
10
|
+
|
|
11
|
+
// ─── Session Summarizer for Moltbook Posts ──────────────────────────────────
|
|
12
|
+
|
|
13
|
+
const OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions";
|
|
14
|
+
|
|
15
|
+
/** Extract a condensed conversation transcript from message history. */
|
|
16
|
+
function extractConversationDigest(messages: BrowserIncomingMessage[], maxChars = 3000): string {
|
|
17
|
+
const parts: string[] = [];
|
|
18
|
+
let totalLen = 0;
|
|
19
|
+
|
|
20
|
+
for (const msg of messages) {
|
|
21
|
+
if (totalLen >= maxChars) break;
|
|
22
|
+
|
|
23
|
+
if (msg.type === "user_message") {
|
|
24
|
+
const text = (msg as any).content || "";
|
|
25
|
+
if (text) {
|
|
26
|
+
const snippet = text.slice(0, 400);
|
|
27
|
+
parts.push(`USER: ${snippet}`);
|
|
28
|
+
totalLen += snippet.length + 6;
|
|
29
|
+
}
|
|
30
|
+
} else if (msg.type === "assistant") {
|
|
31
|
+
const content = (msg as any).message?.content;
|
|
32
|
+
if (Array.isArray(content)) {
|
|
33
|
+
for (const block of content) {
|
|
34
|
+
if (totalLen >= maxChars) break;
|
|
35
|
+
if (block.type === "text" && block.text) {
|
|
36
|
+
const snippet = block.text.slice(0, 300);
|
|
37
|
+
parts.push(`ASSISTANT: ${snippet}`);
|
|
38
|
+
totalLen += snippet.length + 11;
|
|
39
|
+
} else if (block.type === "tool_use") {
|
|
40
|
+
const toolInfo = `[Tool: ${block.name}]`;
|
|
41
|
+
parts.push(toolInfo);
|
|
42
|
+
totalLen += toolInfo.length;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return parts.join("\n");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Use OpenRouter to generate a human-readable title and summary for a session. */
|
|
53
|
+
async function generateMoltbookSummary(
|
|
54
|
+
digest: string,
|
|
55
|
+
sessionMeta: { name: string; backend: string; model: string; cost: string; duration: string; turns: number; tags: string[] },
|
|
56
|
+
): Promise<{ title: string; summary: string } | null> {
|
|
57
|
+
const settings = settingsManager.getSettings();
|
|
58
|
+
const apiKey = settings.openrouterApiKey?.trim();
|
|
59
|
+
if (!apiKey) return null;
|
|
60
|
+
|
|
61
|
+
const model = settings.openrouterModel?.trim() || DEFAULT_OPENROUTER_MODEL;
|
|
62
|
+
|
|
63
|
+
const prompt = `You are writing a short social media post about an AI coding session. Based on the conversation transcript below, generate:
|
|
64
|
+
1. A catchy, human-readable title (5-10 words) that describes what was built or accomplished — NOT the session name, but what actually happened.
|
|
65
|
+
2. A brief summary (2-4 sentences) written in a casual, first-person developer voice. Describe what was built, what problems were solved, or what was interesting about the session. Make it sound like a developer sharing their work, not a technical report.
|
|
66
|
+
|
|
67
|
+
Session metadata:
|
|
68
|
+
- Backend: ${sessionMeta.backend} | Model: ${sessionMeta.model}
|
|
69
|
+
- Cost: ${sessionMeta.cost} | Duration: ${sessionMeta.duration} | Turns: ${sessionMeta.turns}
|
|
70
|
+
${sessionMeta.tags.length > 0 ? `- Tags: ${sessionMeta.tags.join(", ")}` : ""}
|
|
71
|
+
|
|
72
|
+
Conversation transcript (condensed):
|
|
73
|
+
${digest}
|
|
74
|
+
|
|
75
|
+
Respond in this exact JSON format:
|
|
76
|
+
{"title": "your title here", "summary": "your summary here"}`;
|
|
77
|
+
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), 20_000);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const res = await fetch(OPENROUTER_URL, {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: {
|
|
85
|
+
"Content-Type": "application/json",
|
|
86
|
+
Authorization: `Bearer ${apiKey}`,
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
model,
|
|
90
|
+
messages: [{ role: "user", content: prompt }],
|
|
91
|
+
temperature: 0.3,
|
|
92
|
+
response_format: { type: "json_object" },
|
|
93
|
+
}),
|
|
94
|
+
signal: controller.signal,
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
console.warn(`[moltbook] OpenRouter summary failed: ${res.status}`);
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const data = await res.json() as {
|
|
103
|
+
choices?: Array<{ message?: { content?: string } }>;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const raw = data.choices?.[0]?.message?.content?.trim();
|
|
107
|
+
if (!raw) return null;
|
|
108
|
+
|
|
109
|
+
const parsed = JSON.parse(raw) as { title?: string; summary?: string };
|
|
110
|
+
if (parsed.title && parsed.summary) {
|
|
111
|
+
return { title: parsed.title, summary: parsed.summary };
|
|
112
|
+
}
|
|
113
|
+
return null;
|
|
114
|
+
} catch (err) {
|
|
115
|
+
console.warn("[moltbook] Failed to generate summary:", err);
|
|
116
|
+
return null;
|
|
117
|
+
} finally {
|
|
118
|
+
clearTimeout(timer);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Public Replay Token Store ──────────────────────────────────────────────
|
|
123
|
+
const publicReplayTokens = new Map<string, { sessionId: string; createdAt: number }>();
|
|
124
|
+
|
|
125
|
+
function generatePublicReplayToken(sessionId: string): string {
|
|
126
|
+
for (const [token, entry] of publicReplayTokens) {
|
|
127
|
+
if (entry.sessionId === sessionId) return token;
|
|
128
|
+
}
|
|
129
|
+
const bytes = new Uint8Array(12);
|
|
130
|
+
crypto.getRandomValues(bytes);
|
|
131
|
+
const token = Array.from(bytes, (b) => b.toString(36).padStart(2, "0")).join("").slice(0, 16);
|
|
132
|
+
publicReplayTokens.set(token, { sessionId, createdAt: Date.now() });
|
|
133
|
+
return token;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function resolvePublicReplayToken(token: string): string | null {
|
|
137
|
+
const entry = publicReplayTokens.get(token);
|
|
138
|
+
if (!entry) return null;
|
|
139
|
+
if (Date.now() - entry.createdAt > 30 * 24 * 60 * 60 * 1000) {
|
|
140
|
+
publicReplayTokens.delete(token);
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
return entry.sessionId;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function registerGalleryRoutes(api: Hono, deps: RouteDeps): void {
|
|
147
|
+
const { launcher, wsBridge, sessionStore } = deps;
|
|
148
|
+
|
|
149
|
+
api.get("/gallery", (c) => {
|
|
150
|
+
const filter: GalleryFilter = {};
|
|
151
|
+
const backend = c.req.query("backend");
|
|
152
|
+
if (backend) filter.backend = backend as BackendType;
|
|
153
|
+
const minCost = c.req.query("minCost");
|
|
154
|
+
if (minCost) filter.minCost = Number(minCost);
|
|
155
|
+
const maxCost = c.req.query("maxCost");
|
|
156
|
+
if (maxCost) filter.maxCost = Number(maxCost);
|
|
157
|
+
const tags = c.req.query("tags");
|
|
158
|
+
if (tags) filter.tags = tags.split(",").filter(Boolean);
|
|
159
|
+
const featured = c.req.query("featured");
|
|
160
|
+
if (featured === "true") filter.featuredOnly = true;
|
|
161
|
+
const sortBy = c.req.query("sortBy");
|
|
162
|
+
if (sortBy) filter.sortBy = sortBy as GalleryFilter["sortBy"];
|
|
163
|
+
const sortOrder = c.req.query("sortOrder");
|
|
164
|
+
if (sortOrder) filter.sortOrder = sortOrder as "asc" | "desc";
|
|
165
|
+
return c.json(galleryStore.listEntries(filter));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
api.get("/gallery/:id", (c) => {
|
|
169
|
+
const entry = galleryStore.getEntry(c.req.param("id"));
|
|
170
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
171
|
+
return c.json(entry);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
api.post("/gallery", async (c) => {
|
|
175
|
+
const body = await c.req.json().catch(() => ({}));
|
|
176
|
+
const { sessionId, name, description, tags } = body;
|
|
177
|
+
if (!sessionId || !name) {
|
|
178
|
+
return c.json({ error: "sessionId and name are required" }, 400);
|
|
179
|
+
}
|
|
180
|
+
const session = launcher.getSession(sessionId);
|
|
181
|
+
if (!session) return c.json({ error: "Session not found" }, 404);
|
|
182
|
+
try {
|
|
183
|
+
const entry = galleryStore.createEntry(
|
|
184
|
+
{ sessionId, name, description: description || "", tags: tags || [] },
|
|
185
|
+
{
|
|
186
|
+
backendType: session.backendType,
|
|
187
|
+
model: session.model,
|
|
188
|
+
totalCostUsd: wsBridge.getSession(sessionId)?.state.total_cost_usd,
|
|
189
|
+
totalLinesAdded: session.totalLinesAdded,
|
|
190
|
+
totalLinesRemoved: session.totalLinesRemoved,
|
|
191
|
+
numTurns: wsBridge.getSession(sessionId)?.state.num_turns,
|
|
192
|
+
repoRoot: session.repoRoot,
|
|
193
|
+
durationMs: Date.now() - session.createdAt,
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
return c.json(entry, 201);
|
|
197
|
+
} catch (e: unknown) {
|
|
198
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
199
|
+
return c.json({ error: msg }, 400);
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
api.put("/gallery/:id", async (c) => {
|
|
204
|
+
const id = c.req.param("id");
|
|
205
|
+
const body = await c.req.json().catch(() => ({}));
|
|
206
|
+
try {
|
|
207
|
+
const entry = galleryStore.updateEntry(id, body);
|
|
208
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
209
|
+
return c.json(entry);
|
|
210
|
+
} catch (e: unknown) {
|
|
211
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
212
|
+
return c.json({ error: msg }, 400);
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
api.delete("/gallery/:id", (c) => {
|
|
217
|
+
const id = c.req.param("id");
|
|
218
|
+
const deleted = galleryStore.deleteEntry(id);
|
|
219
|
+
if (!deleted) return c.json({ error: "Gallery entry not found" }, 404);
|
|
220
|
+
galleryVotes.removeEntryVotes(id);
|
|
221
|
+
return c.json({ ok: true });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
api.post("/gallery/:id/vote", async (c) => {
|
|
225
|
+
const id = c.req.param("id");
|
|
226
|
+
const body = await c.req.json().catch(() => ({}));
|
|
227
|
+
const entry = galleryStore.getEntry(id);
|
|
228
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
229
|
+
const ip = c.req.header("x-forwarded-for") || c.req.header("x-real-ip") || "anonymous";
|
|
230
|
+
const voterId = galleryVotes.getVoterHash(ip);
|
|
231
|
+
const direction = body.direction === -1 ? -1 : 1;
|
|
232
|
+
const newTotal = galleryVotes.recordVote(id, voterId, direction as 1 | -1);
|
|
233
|
+
galleryStore.updateEntry(id, { votes: newTotal });
|
|
234
|
+
return c.json({ votes: newTotal });
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
api.post("/gallery/:id/feature", (c) => {
|
|
238
|
+
const id = c.req.param("id");
|
|
239
|
+
const entry = galleryStore.getEntry(id);
|
|
240
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
241
|
+
const updated = galleryStore.updateEntry(id, { featured: !entry.featured });
|
|
242
|
+
return c.json(updated);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
// ─── ClawHub Integration ────────────────────────────────────────────
|
|
246
|
+
api.get("/clawhub/status", (c) => {
|
|
247
|
+
return c.json({ available: clawhubExport.checkClawHubAvailable() });
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
api.post("/gallery/:id/export-clawhub", async (c) => {
|
|
251
|
+
const id = c.req.param("id");
|
|
252
|
+
const entry = galleryStore.getEntry(id);
|
|
253
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
254
|
+
if (!clawhubExport.checkClawHubAvailable()) {
|
|
255
|
+
return c.json({ error: "clawhub CLI is not installed. Run: npm install -g clawhub" }, 400);
|
|
256
|
+
}
|
|
257
|
+
const body = await c.req.json().catch(() => ({}));
|
|
258
|
+
const result = clawhubExport.exportToClawHub(entry, {
|
|
259
|
+
campfireBaseUrl: body.campfireBaseUrl,
|
|
260
|
+
prompt: body.prompt,
|
|
261
|
+
dryRun: body.dryRun === true,
|
|
262
|
+
});
|
|
263
|
+
if (result.success) {
|
|
264
|
+
return c.json({ ok: true, skillDir: result.skillDir, output: result.output });
|
|
265
|
+
}
|
|
266
|
+
return c.json({ ok: false, error: result.error }, 500);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
api.get("/gallery/:id/skill-preview", (c) => {
|
|
270
|
+
const id = c.req.param("id");
|
|
271
|
+
const entry = galleryStore.getEntry(id);
|
|
272
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
273
|
+
const markdown = clawhubExport.generateSkillMd(entry, {
|
|
274
|
+
campfireBaseUrl: c.req.query("baseUrl"),
|
|
275
|
+
});
|
|
276
|
+
return c.json({ markdown });
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
api.get("/clawhub/search", (c) => {
|
|
280
|
+
const query = c.req.query("q");
|
|
281
|
+
if (!query) return c.json({ error: "query parameter 'q' is required" }, 400);
|
|
282
|
+
if (!clawhubExport.checkClawHubAvailable()) {
|
|
283
|
+
return c.json({ error: "clawhub CLI is not installed" }, 400);
|
|
284
|
+
}
|
|
285
|
+
const results = clawhubExport.searchClawHub(query);
|
|
286
|
+
return c.json(results);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
api.post("/clawhub/install", async (c) => {
|
|
290
|
+
if (!clawhubExport.checkClawHubAvailable()) {
|
|
291
|
+
return c.json({ error: "clawhub CLI is not installed" }, 400);
|
|
292
|
+
}
|
|
293
|
+
const body = await c.req.json().catch(() => ({}));
|
|
294
|
+
const slug = body.slug;
|
|
295
|
+
if (!slug) return c.json({ error: "slug is required" }, 400);
|
|
296
|
+
const result = clawhubExport.installClawHubSkill(slug);
|
|
297
|
+
if (result.success) {
|
|
298
|
+
return c.json({ ok: true, output: result.output });
|
|
299
|
+
}
|
|
300
|
+
return c.json({ ok: false, error: result.error }, 500);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
// ─── Moltbook Integration ──────────────────────────────────────────
|
|
304
|
+
api.get("/moltbook/status", async (c) => {
|
|
305
|
+
const { moltbookApiKey } = settingsManager.getSettings();
|
|
306
|
+
const moltbook = await import("../moltbook-client.js");
|
|
307
|
+
const status = await moltbook.checkMoltbookStatus(moltbookApiKey);
|
|
308
|
+
return c.json(status);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
api.post("/gallery/:id/post-moltbook", async (c) => {
|
|
312
|
+
const id = c.req.param("id");
|
|
313
|
+
const entry = galleryStore.getEntry(id);
|
|
314
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
315
|
+
const { moltbookApiKey } = settingsManager.getSettings();
|
|
316
|
+
if (!moltbookApiKey) {
|
|
317
|
+
return c.json({ error: "Moltbook API key not configured. Add it in Settings." }, 400);
|
|
318
|
+
}
|
|
319
|
+
const body = await c.req.json().catch(() => ({}));
|
|
320
|
+
const baseUrl = body.campfireBaseUrl || `http://localhost:4567`;
|
|
321
|
+
const replayUrl = `${baseUrl}/#/replay/session/${entry.sessionId}`;
|
|
322
|
+
const costStr = entry.totalCostUsd > 0 ? `$${entry.totalCostUsd.toFixed(2)}` : "free";
|
|
323
|
+
const durationMin = Math.round(entry.durationMs / 60_000);
|
|
324
|
+
const durationStr = durationMin >= 60
|
|
325
|
+
? `${Math.floor(durationMin / 60)}h ${durationMin % 60}m`
|
|
326
|
+
: `${durationMin}m`;
|
|
327
|
+
|
|
328
|
+
// Try to generate an AI summary from the session's conversation history
|
|
329
|
+
let postTitle = entry.name;
|
|
330
|
+
let postSummary = entry.description || "";
|
|
331
|
+
|
|
332
|
+
const persisted = sessionStore.load(entry.sessionId);
|
|
333
|
+
if (persisted?.messageHistory?.length) {
|
|
334
|
+
const digest = extractConversationDigest(persisted.messageHistory);
|
|
335
|
+
if (digest.length > 50) {
|
|
336
|
+
const aiSummary = await generateMoltbookSummary(digest, {
|
|
337
|
+
name: entry.name,
|
|
338
|
+
backend: entry.backendType,
|
|
339
|
+
model: entry.model,
|
|
340
|
+
cost: costStr,
|
|
341
|
+
duration: durationStr,
|
|
342
|
+
turns: entry.numTurns,
|
|
343
|
+
tags: entry.tags,
|
|
344
|
+
});
|
|
345
|
+
if (aiSummary) {
|
|
346
|
+
postTitle = aiSummary.title;
|
|
347
|
+
postSummary = aiSummary.summary;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const content = [
|
|
353
|
+
postSummary,
|
|
354
|
+
"",
|
|
355
|
+
`**${entry.backendType}** · ${entry.model} · ${costStr} · ${durationStr} · ${entry.numTurns} turns`,
|
|
356
|
+
"",
|
|
357
|
+
entry.tags.length > 0 ? entry.tags.map(t => `#${t}`).join(" ") : "",
|
|
358
|
+
].filter(Boolean).join("\n");
|
|
359
|
+
|
|
360
|
+
const moltbook = await import("../moltbook-client.js");
|
|
361
|
+
const result = await moltbook.postToMoltbook({
|
|
362
|
+
apiKey: moltbookApiKey,
|
|
363
|
+
title: postTitle,
|
|
364
|
+
content,
|
|
365
|
+
replayUrl,
|
|
366
|
+
submolt: body.submolt || "general",
|
|
367
|
+
});
|
|
368
|
+
if (result.ok) {
|
|
369
|
+
return c.json({ ok: true, postUrl: result.postUrl, postId: result.postId });
|
|
370
|
+
}
|
|
371
|
+
return c.json({ ok: false, error: result.error }, 500);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
// ─── Public Replay ────────────────────────────────────────────────
|
|
375
|
+
api.post("/gallery/:id/public-link", (c) => {
|
|
376
|
+
const id = c.req.param("id");
|
|
377
|
+
const entry = galleryStore.getEntry(id);
|
|
378
|
+
if (!entry) return c.json({ error: "Gallery entry not found" }, 404);
|
|
379
|
+
const token = generatePublicReplayToken(entry.sessionId);
|
|
380
|
+
return c.json({ token, url: `/#/public-replay/${token}` });
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
api.get("/public-replay/:token", (c) => {
|
|
384
|
+
const token = c.req.param("token");
|
|
385
|
+
const sessionId = resolvePublicReplayToken(token);
|
|
386
|
+
if (!sessionId) {
|
|
387
|
+
return c.json({ error: "Invalid or expired replay link" }, 404);
|
|
388
|
+
}
|
|
389
|
+
const persisted = sessionStore.load(sessionId);
|
|
390
|
+
if (!persisted) return c.json({ error: "Session data not found" }, 404);
|
|
391
|
+
const entries = galleryStore.listEntries();
|
|
392
|
+
const galleryEntry = entries.find((e) => e.sessionId === sessionId);
|
|
393
|
+
return c.json({
|
|
394
|
+
messages: persisted.messageHistory || [],
|
|
395
|
+
state: persisted.state || null,
|
|
396
|
+
gallery: galleryEntry
|
|
397
|
+
? {
|
|
398
|
+
name: galleryEntry.name,
|
|
399
|
+
description: galleryEntry.description,
|
|
400
|
+
backendType: galleryEntry.backendType,
|
|
401
|
+
model: galleryEntry.model,
|
|
402
|
+
totalCostUsd: galleryEntry.totalCostUsd,
|
|
403
|
+
durationMs: galleryEntry.durationMs,
|
|
404
|
+
numTurns: galleryEntry.numTurns,
|
|
405
|
+
tags: galleryEntry.tags,
|
|
406
|
+
}
|
|
407
|
+
: null,
|
|
408
|
+
});
|
|
409
|
+
});
|
|
410
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { RouteDeps } from "./route-deps.js";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import * as gitUtils from "../git-utils.js";
|
|
5
|
+
|
|
6
|
+
export function registerGitRoutes(api: Hono, deps: RouteDeps): void {
|
|
7
|
+
const { prPoller } = deps;
|
|
8
|
+
|
|
9
|
+
api.get("/git/repo-info", (c) => {
|
|
10
|
+
const path = c.req.query("path");
|
|
11
|
+
if (!path) return c.json({ error: "path required" }, 400);
|
|
12
|
+
const info = gitUtils.getRepoInfo(path);
|
|
13
|
+
if (!info) return c.json({ error: "Not a git repository" }, 400);
|
|
14
|
+
return c.json(info);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
api.get("/git/branches", (c) => {
|
|
18
|
+
const repoRoot = c.req.query("repoRoot");
|
|
19
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
20
|
+
try {
|
|
21
|
+
return c.json(gitUtils.listBranches(repoRoot));
|
|
22
|
+
} catch (e: unknown) {
|
|
23
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
api.get("/git/worktrees", (c) => {
|
|
28
|
+
const repoRoot = c.req.query("repoRoot");
|
|
29
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
30
|
+
try {
|
|
31
|
+
return c.json(gitUtils.listWorktrees(repoRoot));
|
|
32
|
+
} catch (e: unknown) {
|
|
33
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
api.post("/git/worktree", async (c) => {
|
|
38
|
+
const body = await c.req.json().catch(() => ({}));
|
|
39
|
+
const { repoRoot, branch, baseBranch, createBranch } = body;
|
|
40
|
+
if (!repoRoot || !branch)
|
|
41
|
+
return c.json({ error: "repoRoot and branch required" }, 400);
|
|
42
|
+
try {
|
|
43
|
+
const result = gitUtils.ensureWorktree(repoRoot, branch, {
|
|
44
|
+
baseBranch,
|
|
45
|
+
createBranch,
|
|
46
|
+
});
|
|
47
|
+
return c.json(result);
|
|
48
|
+
} catch (e: unknown) {
|
|
49
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 500);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
api.delete("/git/worktree", async (c) => {
|
|
54
|
+
const body = await c.req.json().catch(() => ({}));
|
|
55
|
+
const { repoRoot, worktreePath, force } = body;
|
|
56
|
+
if (!repoRoot || !worktreePath)
|
|
57
|
+
return c.json({ error: "repoRoot and worktreePath required" }, 400);
|
|
58
|
+
const result = gitUtils.removeWorktree(repoRoot, worktreePath, { force });
|
|
59
|
+
return c.json(result);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
api.post("/git/fetch", async (c) => {
|
|
63
|
+
const body = await c.req.json().catch(() => ({}));
|
|
64
|
+
const { repoRoot } = body;
|
|
65
|
+
if (!repoRoot) return c.json({ error: "repoRoot required" }, 400);
|
|
66
|
+
return c.json(gitUtils.gitFetch(repoRoot));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
api.post("/git/pull", async (c) => {
|
|
70
|
+
const body = await c.req.json().catch(() => ({}));
|
|
71
|
+
const { cwd } = body;
|
|
72
|
+
if (!cwd) return c.json({ error: "cwd required" }, 400);
|
|
73
|
+
const result = gitUtils.gitPull(cwd);
|
|
74
|
+
let git_ahead = 0,
|
|
75
|
+
git_behind = 0;
|
|
76
|
+
try {
|
|
77
|
+
const counts = execSync(
|
|
78
|
+
"git rev-list --left-right --count @{upstream}...HEAD",
|
|
79
|
+
{ cwd, encoding: "utf-8", timeout: 3000 },
|
|
80
|
+
).trim();
|
|
81
|
+
const [behind, ahead] = counts.split(/\s+/).map(Number);
|
|
82
|
+
git_ahead = ahead || 0;
|
|
83
|
+
git_behind = behind || 0;
|
|
84
|
+
} catch {
|
|
85
|
+
/* no upstream */
|
|
86
|
+
}
|
|
87
|
+
return c.json({ ...result, git_ahead, git_behind });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
api.get("/git/pr-status", async (c) => {
|
|
91
|
+
const cwd = c.req.query("cwd");
|
|
92
|
+
const branch = c.req.query("branch");
|
|
93
|
+
if (!cwd || !branch) return c.json({ error: "cwd and branch required" }, 400);
|
|
94
|
+
|
|
95
|
+
if (prPoller) {
|
|
96
|
+
const cached = prPoller.getCached(cwd, branch);
|
|
97
|
+
if (cached) return c.json(cached);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const { isGhAvailable, fetchPRInfoAsync } = await import("../github-pr.js");
|
|
101
|
+
if (!isGhAvailable()) {
|
|
102
|
+
return c.json({ available: false, pr: null });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const pr = await fetchPRInfoAsync(cwd, branch);
|
|
106
|
+
return c.json({ available: true, pr });
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { RouteDeps } from "./route-deps.js";
|
|
3
|
+
import { registerSessionRoutes } from "./session-routes.js";
|
|
4
|
+
import { registerRecordingRoutes } from "./recording-routes.js";
|
|
5
|
+
import { registerFsRoutes } from "./fs-routes.js";
|
|
6
|
+
import { registerEnvRoutes } from "./env-routes.js";
|
|
7
|
+
import { registerSettingsRoutes } from "./settings-routes.js";
|
|
8
|
+
import { registerGitRoutes } from "./git-routes.js";
|
|
9
|
+
import { registerSystemRoutes } from "./system-routes.js";
|
|
10
|
+
import { registerCronRoutes } from "./cron-routes.js";
|
|
11
|
+
import { registerGalleryRoutes } from "./gallery-routes.js";
|
|
12
|
+
import { registerWebhookRoutes } from "./webhook-routes.js";
|
|
13
|
+
import { registerAdapterRoutes } from "./adapter-routes.js";
|
|
14
|
+
import { registerCiRoutes } from "./ci-routes.js";
|
|
15
|
+
import { registerPromptRoutes } from "./prompt-routes.js";
|
|
16
|
+
import { registerLinearRoutes } from "./linear-routes.js";
|
|
17
|
+
import { registerOrchestratorRoutes } from "./orchestrator-routes.js";
|
|
18
|
+
import { registerAuthRoutes, authMiddleware } from "./auth-routes.js";
|
|
19
|
+
import { registerSkillsRoutes } from "./skills-routes.js";
|
|
20
|
+
import { registerFolderRoutes } from "./folder-routes.js";
|
|
21
|
+
import { registerAgentRoutes } from "./agent-routes.js";
|
|
22
|
+
import { registerHubRoutes } from "../recording-hub/hub-routes.js";
|
|
23
|
+
import { registerMonitorRoutes } from "./monitor-routes.js";
|
|
24
|
+
import { registerCommandsRoutes } from "./commands-routes.js";
|
|
25
|
+
import { registerAgentMcpRoutes } from "./agent-mcp-routes.js";
|
|
26
|
+
import { registerRaceRoutes } from "./race-routes.js";
|
|
27
|
+
|
|
28
|
+
export function createRoutes(deps: RouteDeps): Hono {
|
|
29
|
+
const api = new Hono();
|
|
30
|
+
|
|
31
|
+
// Auth routes must be registered BEFORE the middleware
|
|
32
|
+
registerAuthRoutes(api, deps);
|
|
33
|
+
registerAgentMcpRoutes(api, deps);
|
|
34
|
+
|
|
35
|
+
// Auth middleware protects all other routes
|
|
36
|
+
api.use("/*", authMiddleware());
|
|
37
|
+
|
|
38
|
+
registerSessionRoutes(api, deps);
|
|
39
|
+
registerRecordingRoutes(api, deps);
|
|
40
|
+
registerFsRoutes(api, deps);
|
|
41
|
+
registerEnvRoutes(api, deps);
|
|
42
|
+
registerSettingsRoutes(api, deps);
|
|
43
|
+
registerGitRoutes(api, deps);
|
|
44
|
+
registerSystemRoutes(api, deps);
|
|
45
|
+
registerCronRoutes(api, deps);
|
|
46
|
+
registerGalleryRoutes(api, deps);
|
|
47
|
+
registerWebhookRoutes(api, deps);
|
|
48
|
+
registerAdapterRoutes(api, deps);
|
|
49
|
+
registerCiRoutes(api, deps);
|
|
50
|
+
registerPromptRoutes(api, deps);
|
|
51
|
+
registerLinearRoutes(api, deps);
|
|
52
|
+
registerOrchestratorRoutes(api, deps);
|
|
53
|
+
registerSkillsRoutes(api, deps);
|
|
54
|
+
registerFolderRoutes(api, deps);
|
|
55
|
+
registerAgentRoutes(api, deps);
|
|
56
|
+
registerHubRoutes(api, deps);
|
|
57
|
+
registerMonitorRoutes(api, deps);
|
|
58
|
+
registerCommandsRoutes(api, deps);
|
|
59
|
+
registerRaceRoutes(api, deps);
|
|
60
|
+
|
|
61
|
+
return api;
|
|
62
|
+
}
|