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,403 @@
|
|
|
1
|
+
import type { Hono } from "hono";
|
|
2
|
+
import type { RouteDeps } from "./route-deps.js";
|
|
3
|
+
import { execSync } from "node:child_process";
|
|
4
|
+
import { readdir, readFile, writeFile, stat } from "node:fs/promises";
|
|
5
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
6
|
+
import { resolve, join, dirname, relative, isAbsolute } from "node:path";
|
|
7
|
+
import { containerManager } from "../container-manager.js";
|
|
8
|
+
import { mapContainerPath } from "../session-git-info.js";
|
|
9
|
+
import { homedir } from "node:os";
|
|
10
|
+
|
|
11
|
+
function execCaptureStdout(
|
|
12
|
+
command: string,
|
|
13
|
+
options: { cwd: string; encoding: "utf-8"; timeout: number },
|
|
14
|
+
): string {
|
|
15
|
+
try {
|
|
16
|
+
return execSync(command, options);
|
|
17
|
+
} catch (err: unknown) {
|
|
18
|
+
const maybe = err as { stdout?: Buffer | string };
|
|
19
|
+
if (typeof maybe.stdout === "string") return maybe.stdout;
|
|
20
|
+
if (maybe.stdout && Buffer.isBuffer(maybe.stdout)) {
|
|
21
|
+
return maybe.stdout.toString("utf-8");
|
|
22
|
+
}
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function resolveBranchDiffBases(repoRoot: string): string[] {
|
|
28
|
+
const options = { cwd: repoRoot, encoding: "utf-8", timeout: 5000 } as const;
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const originHead = execSync("git symbolic-ref refs/remotes/origin/HEAD", options).trim();
|
|
32
|
+
const match = originHead.match(/^refs\/remotes\/origin\/(.+)$/);
|
|
33
|
+
if (match?.[1]) {
|
|
34
|
+
return [`origin/${match[1]}`, match[1]];
|
|
35
|
+
}
|
|
36
|
+
} catch {
|
|
37
|
+
// No remote HEAD ref available
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const branches = execSync("git branch --list main master", options).trim();
|
|
42
|
+
if (branches.includes("main")) return ["main"];
|
|
43
|
+
if (branches.includes("master")) return ["master"];
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return ["main"];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function registerFsRoutes(api: Hono, deps: RouteDeps): void {
|
|
52
|
+
api.get("/fs/list", async (c) => {
|
|
53
|
+
const rawPath = c.req.query("path") || homedir();
|
|
54
|
+
const basePath = resolve(rawPath);
|
|
55
|
+
try {
|
|
56
|
+
const entries = await readdir(basePath, { withFileTypes: true });
|
|
57
|
+
const dirs: { name: string; path: string }[] = [];
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
if (entry.isDirectory() && !entry.name.startsWith(".")) {
|
|
60
|
+
dirs.push({ name: entry.name, path: join(basePath, entry.name) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name));
|
|
64
|
+
return c.json({ path: basePath, dirs, home: homedir() });
|
|
65
|
+
} catch {
|
|
66
|
+
return c.json(
|
|
67
|
+
{ error: "Cannot read directory", path: basePath, dirs: [], home: homedir() },
|
|
68
|
+
400,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
api.get("/fs/home", (c) => {
|
|
74
|
+
const home = homedir();
|
|
75
|
+
const cwd = process.cwd();
|
|
76
|
+
const packageRoot = process.env.__CAMPFIRE_PACKAGE_ROOT;
|
|
77
|
+
const isProjectDir =
|
|
78
|
+
cwd !== home &&
|
|
79
|
+
(!packageRoot || !cwd.startsWith(packageRoot));
|
|
80
|
+
return c.json({ home, cwd: isProjectDir ? cwd : home });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
api.get("/fs/tree", async (c) => {
|
|
84
|
+
const rawPath = c.req.query("path");
|
|
85
|
+
if (!rawPath) return c.json({ error: "path required" }, 400);
|
|
86
|
+
const basePath = resolve(rawPath);
|
|
87
|
+
|
|
88
|
+
interface TreeNode {
|
|
89
|
+
name: string;
|
|
90
|
+
path: string;
|
|
91
|
+
type: "file" | "directory";
|
|
92
|
+
children?: TreeNode[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function buildTree(dir: string, depth: number): Promise<TreeNode[]> {
|
|
96
|
+
if (depth > 10) return [];
|
|
97
|
+
try {
|
|
98
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
99
|
+
const nodes: TreeNode[] = [];
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
102
|
+
const fullPath = join(dir, entry.name);
|
|
103
|
+
if (entry.isDirectory()) {
|
|
104
|
+
const children = await buildTree(fullPath, depth + 1);
|
|
105
|
+
nodes.push({ name: entry.name, path: fullPath, type: "directory", children });
|
|
106
|
+
} else if (entry.isFile()) {
|
|
107
|
+
nodes.push({ name: entry.name, path: fullPath, type: "file" });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
nodes.sort((a, b) => {
|
|
111
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
112
|
+
return a.name.localeCompare(b.name);
|
|
113
|
+
});
|
|
114
|
+
return nodes;
|
|
115
|
+
} catch {
|
|
116
|
+
return [];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const tree = await buildTree(basePath, 0);
|
|
121
|
+
return c.json({ path: basePath, tree });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// Lazy directory listing for workspace file tree (supports on-demand expansion)
|
|
125
|
+
api.get("/fs/list-entries", async (c) => {
|
|
126
|
+
const rawPath = c.req.query("path");
|
|
127
|
+
if (!rawPath) return c.json({ error: "path required" }, 400);
|
|
128
|
+
const showHidden = c.req.query("hidden") === "1";
|
|
129
|
+
const basePath = resolve(rawPath);
|
|
130
|
+
try {
|
|
131
|
+
const entries = await readdir(basePath, { withFileTypes: true });
|
|
132
|
+
const items: { name: string; path: string; type: "file" | "directory"; size?: number }[] = [];
|
|
133
|
+
for (const entry of entries) {
|
|
134
|
+
if (!showHidden && entry.name.startsWith(".")) continue;
|
|
135
|
+
if (entry.name === "node_modules" || entry.name === "__pycache__" || entry.name === ".git") continue;
|
|
136
|
+
const fullPath = join(basePath, entry.name);
|
|
137
|
+
if (entry.isDirectory()) {
|
|
138
|
+
items.push({ name: entry.name, path: fullPath, type: "directory" });
|
|
139
|
+
} else if (entry.isFile()) {
|
|
140
|
+
try {
|
|
141
|
+
const info = await stat(fullPath);
|
|
142
|
+
items.push({ name: entry.name, path: fullPath, type: "file", size: info.size });
|
|
143
|
+
} catch {
|
|
144
|
+
items.push({ name: entry.name, path: fullPath, type: "file" });
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
items.sort((a, b) => {
|
|
149
|
+
if (a.type !== b.type) return a.type === "directory" ? -1 : 1;
|
|
150
|
+
return a.name.localeCompare(b.name);
|
|
151
|
+
});
|
|
152
|
+
return c.json({ path: basePath, entries: items });
|
|
153
|
+
} catch (e: unknown) {
|
|
154
|
+
return c.json({ error: e instanceof Error ? e.message : "Cannot read directory", path: basePath, entries: [] }, 400);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
api.get("/fs/read", async (c) => {
|
|
159
|
+
const filePath = c.req.query("path");
|
|
160
|
+
if (!filePath) return c.json({ error: "path required" }, 400);
|
|
161
|
+
const absPath = resolve(filePath);
|
|
162
|
+
try {
|
|
163
|
+
const info = await stat(absPath);
|
|
164
|
+
if (info.size > 2 * 1024 * 1024) {
|
|
165
|
+
return c.json({ error: "File too large (>2MB)" }, 413);
|
|
166
|
+
}
|
|
167
|
+
const content = await readFile(absPath, "utf-8");
|
|
168
|
+
return c.json({ path: absPath, content });
|
|
169
|
+
} catch (e: unknown) {
|
|
170
|
+
return c.json(
|
|
171
|
+
{ error: e instanceof Error ? e.message : "Cannot read file" },
|
|
172
|
+
404,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
api.put("/fs/write", async (c) => {
|
|
178
|
+
const body = await c.req.json().catch(() => ({}));
|
|
179
|
+
const { path: filePath, content } = body;
|
|
180
|
+
if (!filePath || typeof content !== "string") {
|
|
181
|
+
return c.json({ error: "path and content required" }, 400);
|
|
182
|
+
}
|
|
183
|
+
const absPath = resolve(filePath);
|
|
184
|
+
try {
|
|
185
|
+
await writeFile(absPath, content, "utf-8");
|
|
186
|
+
return c.json({ ok: true, path: absPath });
|
|
187
|
+
} catch (e: unknown) {
|
|
188
|
+
return c.json(
|
|
189
|
+
{ error: e instanceof Error ? e.message : "Cannot write file" },
|
|
190
|
+
500,
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
api.get("/fs/diff", (c) => {
|
|
196
|
+
const filePath = c.req.query("path");
|
|
197
|
+
if (!filePath) return c.json({ error: "path required" }, 400);
|
|
198
|
+
const sessionId = c.req.query("session_id") || undefined;
|
|
199
|
+
let absPath = resolve(filePath);
|
|
200
|
+
if (sessionId) {
|
|
201
|
+
const session = deps.wsBridge.getSession(sessionId);
|
|
202
|
+
const sessionCwd = session?.state.cwd;
|
|
203
|
+
const launchInfo = deps.launcher.getSession(sessionId);
|
|
204
|
+
const hostCwd = launchInfo?.cwd;
|
|
205
|
+
const containerInfo = containerManager.getContainer(sessionId);
|
|
206
|
+
if (containerInfo) {
|
|
207
|
+
absPath = resolve(
|
|
208
|
+
mapContainerPath(absPath, [{
|
|
209
|
+
hostPath: containerInfo.hostCwd,
|
|
210
|
+
containerPath: containerInfo.containerCwd,
|
|
211
|
+
}]),
|
|
212
|
+
);
|
|
213
|
+
} else if (sessionCwd && hostCwd && sessionCwd !== hostCwd) {
|
|
214
|
+
const resolvedSessionCwd = resolve(sessionCwd);
|
|
215
|
+
const resolvedHostCwd = resolve(hostCwd);
|
|
216
|
+
if (absPath === resolvedSessionCwd || absPath.startsWith(`${resolvedSessionCwd}/`)) {
|
|
217
|
+
absPath = resolve(resolvedHostCwd, relative(resolvedSessionCwd, absPath));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
// Optional base ref provided by the frontend (e.g. the commit at session start)
|
|
222
|
+
const baseRef = c.req.query("base");
|
|
223
|
+
// If "known_changed=1", the frontend asserts this file was modified by the session.
|
|
224
|
+
// Enables the synthetic diff fallback when all git strategies return empty.
|
|
225
|
+
const knownChanged = c.req.query("known_changed") === "1";
|
|
226
|
+
const fileExists = existsSync(absPath);
|
|
227
|
+
try {
|
|
228
|
+
const repoRoot = execSync("git rev-parse --show-toplevel", {
|
|
229
|
+
cwd: dirname(absPath),
|
|
230
|
+
encoding: "utf-8",
|
|
231
|
+
timeout: 5000,
|
|
232
|
+
}).trim();
|
|
233
|
+
const relPath = relative(repoRoot, absPath);
|
|
234
|
+
if (!relPath || relPath.startsWith("..") || isAbsolute(relPath)) {
|
|
235
|
+
if (knownChanged) {
|
|
236
|
+
try {
|
|
237
|
+
const content = readFileSync(absPath, "utf-8");
|
|
238
|
+
if (content != null) {
|
|
239
|
+
const lines = content.split("\n");
|
|
240
|
+
const header = [
|
|
241
|
+
`diff --git a/${absPath} b/${absPath}`,
|
|
242
|
+
"new file mode 100644",
|
|
243
|
+
"--- /dev/null",
|
|
244
|
+
`+++ b/${absPath}`,
|
|
245
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
246
|
+
];
|
|
247
|
+
const diff = header.join("\n") + "\n" + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
248
|
+
return c.json({ path: absPath, diff, exists: fileExists });
|
|
249
|
+
}
|
|
250
|
+
} catch {
|
|
251
|
+
// Ignore
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
return c.json({ path: absPath, diff: "", exists: fileExists });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const opts = { cwd: repoRoot, encoding: "utf-8", timeout: 5000 } as const;
|
|
258
|
+
let diff = "";
|
|
259
|
+
|
|
260
|
+
// If a specific base ref was provided (e.g. session start commit), use it
|
|
261
|
+
if (baseRef) {
|
|
262
|
+
try {
|
|
263
|
+
diff = execCaptureStdout(`git diff ${baseRef} -- "${relPath}"`, opts);
|
|
264
|
+
} catch {
|
|
265
|
+
// Invalid ref — fall through to other strategies
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// 1. Try uncommitted working-tree changes (staged + unstaged vs HEAD)
|
|
270
|
+
if (!diff.trim()) {
|
|
271
|
+
try {
|
|
272
|
+
diff = execCaptureStdout(`git diff HEAD -- "${relPath}"`, opts);
|
|
273
|
+
} catch {
|
|
274
|
+
// HEAD may not exist (initial commit) — ignore
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// 2. Try branch-base diff (for feature branches diffing against main)
|
|
279
|
+
if (!diff.trim()) {
|
|
280
|
+
const diffBases = resolveBranchDiffBases(repoRoot);
|
|
281
|
+
for (const base of diffBases) {
|
|
282
|
+
try {
|
|
283
|
+
// Use merge-base to get only the branch's own changes
|
|
284
|
+
const mergeBase = execSync(`git merge-base ${base} HEAD`, opts).trim();
|
|
285
|
+
if (mergeBase) {
|
|
286
|
+
diff = execCaptureStdout(`git diff ${mergeBase} -- "${relPath}"`, opts);
|
|
287
|
+
if (diff.trim()) break;
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// Try next candidate
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 3. Check for untracked files
|
|
296
|
+
if (!diff.trim()) {
|
|
297
|
+
const untracked = execSync(`git ls-files --others --exclude-standard -- "${relPath}"`, {
|
|
298
|
+
cwd: repoRoot,
|
|
299
|
+
encoding: "utf-8",
|
|
300
|
+
timeout: 5000,
|
|
301
|
+
}).trim();
|
|
302
|
+
if (untracked) {
|
|
303
|
+
diff = execCaptureStdout(`git diff --no-index -- /dev/null "${absPath}"`, opts);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// 4. Final fallback: if the frontend asserts this file was changed but all git
|
|
308
|
+
// strategies returned empty (e.g. file created and committed on the same branch),
|
|
309
|
+
// generate a synthetic "new file" diff from the current file contents.
|
|
310
|
+
if (!diff.trim() && knownChanged) {
|
|
311
|
+
try {
|
|
312
|
+
const content = readFileSync(absPath, "utf-8");
|
|
313
|
+
if (content != null) {
|
|
314
|
+
const lines = content.split("\n");
|
|
315
|
+
const header = [
|
|
316
|
+
`diff --git a/${relPath} b/${relPath}`,
|
|
317
|
+
"new file mode 100644",
|
|
318
|
+
"--- /dev/null",
|
|
319
|
+
`+++ b/${relPath}`,
|
|
320
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
321
|
+
];
|
|
322
|
+
diff = header.join("\n") + "\n" + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
323
|
+
}
|
|
324
|
+
} catch {
|
|
325
|
+
// File doesn't exist or can't be read
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return c.json({ path: absPath, diff, exists: fileExists });
|
|
330
|
+
} catch {
|
|
331
|
+
if (knownChanged) {
|
|
332
|
+
try {
|
|
333
|
+
const content = readFileSync(absPath, "utf-8");
|
|
334
|
+
if (content != null) {
|
|
335
|
+
const lines = content.split("\n");
|
|
336
|
+
const header = [
|
|
337
|
+
`diff --git a/${absPath} b/${absPath}`,
|
|
338
|
+
"new file mode 100644",
|
|
339
|
+
"--- /dev/null",
|
|
340
|
+
`+++ b/${absPath}`,
|
|
341
|
+
`@@ -0,0 +1,${lines.length} @@`,
|
|
342
|
+
];
|
|
343
|
+
const diff = header.join("\n") + "\n" + lines.map((l) => `+${l}`).join("\n") + "\n";
|
|
344
|
+
return c.json({ path: absPath, diff, exists: fileExists });
|
|
345
|
+
}
|
|
346
|
+
} catch {
|
|
347
|
+
// Ignore
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
return c.json({ path: absPath, diff: "", exists: fileExists });
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
api.get("/fs/claude-md", async (c) => {
|
|
355
|
+
const cwd = c.req.query("cwd");
|
|
356
|
+
if (!cwd) return c.json({ error: "cwd required" }, 400);
|
|
357
|
+
const resolvedCwd = resolve(cwd);
|
|
358
|
+
|
|
359
|
+
const candidates = [
|
|
360
|
+
join(resolvedCwd, "CLAUDE.md"),
|
|
361
|
+
join(resolvedCwd, ".claude", "CLAUDE.md"),
|
|
362
|
+
];
|
|
363
|
+
|
|
364
|
+
const files: { path: string; content: string }[] = [];
|
|
365
|
+
for (const p of candidates) {
|
|
366
|
+
try {
|
|
367
|
+
const content = await readFile(p, "utf-8");
|
|
368
|
+
files.push({ path: p, content });
|
|
369
|
+
} catch {
|
|
370
|
+
// file doesn't exist — skip
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return c.json({ cwd: resolvedCwd, files });
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
api.put("/fs/claude-md", async (c) => {
|
|
378
|
+
const body = await c.req.json().catch(() => ({}));
|
|
379
|
+
const { path: filePath, content } = body;
|
|
380
|
+
if (!filePath || typeof content !== "string") {
|
|
381
|
+
return c.json({ error: "path and content required" }, 400);
|
|
382
|
+
}
|
|
383
|
+
const base = filePath.split("/").pop();
|
|
384
|
+
if (base !== "CLAUDE.md") {
|
|
385
|
+
return c.json({ error: "Can only write CLAUDE.md files" }, 400);
|
|
386
|
+
}
|
|
387
|
+
const absPath = resolve(filePath);
|
|
388
|
+
if (!absPath.endsWith("/CLAUDE.md") && !absPath.endsWith("/.claude/CLAUDE.md")) {
|
|
389
|
+
return c.json({ error: "Invalid CLAUDE.md path" }, 400);
|
|
390
|
+
}
|
|
391
|
+
try {
|
|
392
|
+
const { mkdir } = await import("node:fs/promises");
|
|
393
|
+
await mkdir(dirname(absPath), { recursive: true });
|
|
394
|
+
await writeFile(absPath, content, "utf-8");
|
|
395
|
+
return c.json({ ok: true, path: absPath });
|
|
396
|
+
} catch (e: unknown) {
|
|
397
|
+
return c.json(
|
|
398
|
+
{ error: e instanceof Error ? e.message : "Cannot write file" },
|
|
399
|
+
500,
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
}
|