-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.ts
More file actions
executable file
·169 lines (148 loc) · 6.62 KB
/
server.ts
File metadata and controls
executable file
·169 lines (148 loc) · 6.62 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/usr/bin/env -S node --experimental-strip-types --no-warnings
/**
* ClaudeBox Server — combined Slack listener + HTTP API.
*
* Modes:
* Full: Slack Socket Mode + HTTP API (default)
* HTTP-only: node server.ts --http-only (no Slack tokens required)
*
* Max 10 concurrent sessions.
*/
// Set env var early so config.ts sees it during import
if (process.argv.includes("--http-only")) process.env.CLAUDEBOX_HTTP_ONLY = "1";
const HTTP_ONLY = process.env.CLAUDEBOX_HTTP_ONLY === "1";
import {
SLACK_APP_TOKEN, HTTP_PORT, INTERNAL_PORT, DOCKER_IMAGE, MAX_CONCURRENT,
CLAUDEBOX_DIR,
} from "./packages/libclaudebox/config.ts";
import { setChannelMaps } from "./packages/libclaudebox/runtime.ts";
// SLACK_BOT_TOKEN via libcreds-host — needed for Slack Bolt App initialization.
import { getSlackBotToken } from "./packages/libcreds-host/index.ts";
const SLACK_BOT_TOKEN = getSlackBotToken();
import { WorktreeStore } from "./packages/libclaudebox/worktree-store.ts";
import { DockerService } from "./packages/libclaudebox/docker.ts";
import { createHttpServer } from "./packages/libclaudebox/http-routes.ts";
import { DmRegistry } from "./packages/libclaudebox/dm-registry.ts";
import { setProfilesDir, buildChannelProfileMap, buildChannelBranchMap, loadAllProfiles } from "./packages/libclaudebox/profile-loader.ts";
import { ProfileRuntime } from "./packages/libclaudebox/profile.ts";
import { startAutoUpdate } from "./packages/libclaudebox/auto-update.ts";
import { join, dirname } from "path";
// Prevent unhandled Slack/WebSocket rejections from crashing the process
process.on("unhandledRejection", (reason: any) => {
const msg = reason?.message || String(reason);
if (msg.includes("invalid_auth") || msg.includes("slack")) {
console.warn(`[UNHANDLED] Slack error suppressed: ${msg}`);
} else {
console.error(`[UNHANDLED] Rejection: ${msg}`);
}
});
async function main() {
// ── Parse --profiles flag ──
const profilesArg = process.argv.find(a => a.startsWith("--profiles="))?.split("=")[1]
|| (process.argv.indexOf("--profiles") >= 0 ? process.argv[process.argv.indexOf("--profiles") + 1] : "");
const requestedProfiles = profilesArg ? profilesArg.split(",").map(p => p.trim()).filter(Boolean) : [];
// ── Discover profiles and build channel maps ──
const rootDir = dirname(import.meta.url.replace("file://", ""));
setProfilesDir(join(rootDir, "profiles"));
const profileMap = await buildChannelProfileMap();
const branchMap = await buildChannelBranchMap();
// Filter to requested profiles if specified
if (requestedProfiles.length > 0) {
for (const [ch, prof] of profileMap) {
if (!requestedProfiles.includes(prof)) profileMap.delete(ch);
}
}
setChannelMaps(
Object.fromEntries(branchMap),
Object.fromEntries(profileMap),
);
console.log("ClaudeBox server starting...");
console.log(` Image: ${DOCKER_IMAGE}`);
console.log(` Profiles: ${requestedProfiles.length ? requestedProfiles.join(", ") : "(all)"}`);
console.log(` Mode: ${HTTP_ONLY ? "HTTP-only" : "Slack + HTTP"}`);
console.log(` HTTP: port ${HTTP_PORT}`);
console.log(` Max concurrent: ${MAX_CONCURRENT}`);
// ── Instantiate services ──
const store = new WorktreeStore();
const docker = new DockerService();
// ── Reconcile stale sessions (async — won't block event loop) ──
let reconciling = false;
const runReconcile = async () => {
if (reconciling) return;
reconciling = true;
try { await store.reconcileAsync(docker); } catch (e: any) {
console.error(`[RECONCILE] Error: ${e.message}`);
} finally { reconciling = false; }
};
runReconcile();
setInterval(runReconcile, 60_000);
// ── DM registry ──
const dmRegistry = new DmRegistry(join(CLAUDEBOX_DIR, "dm-registry.json"));
// ── Worktree GC — keep workspace dirs under 100GB, clean oldest first ──
let gcRunning = false;
const runGC = async () => {
if (gcRunning) return;
gcRunning = true;
try {
const cleaned = await store.gcWorktreesAsync(100, 1); // 100GB budget, min 1 day old
if (cleaned.length > 0) console.log(`[GC] Cleaned ${cleaned.length} worktrees: ${cleaned.join(", ")}`);
} catch (e: any) {
console.error(`[GC] Error: ${e.message}`);
} finally { gcRunning = false; }
};
setTimeout(runGC, 30_000);
setInterval(runGC, 6 * 60 * 60 * 1000); // every 6h instead of daily — more gradual
// ── Slack app (skipped in HTTP-only mode) ──
if (!HTTP_ONLY) {
try {
const { App } = await import("@slack/bolt");
const { registerSlackHandlers } = await import("./packages/libclaudebox/slack/handlers.ts");
const slackApp = new App({
token: SLACK_BOT_TOKEN,
appToken: SLACK_APP_TOKEN,
socketMode: true,
port: HTTP_PORT + 1, // Bolt creates its own HTTP server; avoid conflicting with ours
});
slackApp.error(async (error) => {
console.error(`[SLACK_ERROR] ${error.message || error}`);
});
slackApp.use(async ({ body, next }) => {
const eventType = (body as any)?.event?.type || (body as any)?.type || "unknown";
const channelType = (body as any)?.event?.channel_type || "";
console.log(`[SLACK_RAW] type=${eventType} channel_type=${channelType}`);
await next();
});
registerSlackHandlers(slackApp, store, docker, dmRegistry);
await slackApp.start();
console.log(" Slack connected.");
} catch (e: any) {
console.warn(` Slack failed: ${e.message} (HTTP server will still run)`);
}
} else {
console.log(" Slack: skipped (HTTP-only mode)");
}
// ── Load profiles and set up runtime ──
const profiles = await loadAllProfiles(requestedProfiles.length ? requestedProfiles : undefined);
const profileRuntime = new ProfileRuntime(docker, store);
for (const profile of profiles) {
await profileRuntime.loadProfile(profile);
}
const profileRoutes = profileRuntime.getRoutes();
if (profileRoutes.length) console.log(` Profile routes: ${profileRoutes.length} endpoints`);
// ── HTTP servers ──
const { public: publicServer, internal: internalServer } = createHttpServer(store, docker, profileRuntime, dmRegistry);
publicServer.listen(HTTP_PORT, () => {
console.log(` HTTP (public) listening on :${HTTP_PORT}`);
});
internalServer.listen(INTERNAL_PORT, () => {
console.log(` HTTP (internal) listening on :${INTERNAL_PORT}`);
});
// ── Auto-update (polls origin/next, restarts on new commits) ──
if (process.argv.includes("--auto-update")) {
startAutoUpdate(rootDir);
}
}
main().catch((e) => {
console.error("Fatal:", e);
process.exit(1);
});