jarvis-hq 0.0.3

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/dist/server.js ADDED
@@ -0,0 +1,530 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.handleRequest = handleRequest;
4
+ exports.start = start;
5
+ const crypto_1 = require("crypto");
6
+ const server_common_1 = require("./server-common");
7
+ const start_claude_code_1 = require("./start-claude-code");
8
+ const start_opencode_1 = require("./start-opencode");
9
+ const start_codex_1 = require("./start-codex");
10
+ const relay_client_1 = require("./relay-client");
11
+ const bot_routes_1 = require("./bot-routes");
12
+ const dictation_1 = require("./dictation");
13
+ const bot_store_1 = require("./bot-store");
14
+ const start_claude_code_2 = require("./start-claude-code");
15
+ const turns_1 = require("./turns");
16
+ const jarvis_1 = require("./jarvis");
17
+ async function handleRequest(req, res, availableAgents, workspaceCwd) {
18
+ const url = req.url ?? "/";
19
+ const method = req.method ?? "GET";
20
+ const path = url.split("?")[0];
21
+ const query = (0, server_common_1.parseQuery)(url);
22
+ // CORS preflight
23
+ if (method === "OPTIONS") {
24
+ res.writeHead(204, {
25
+ "Access-Control-Allow-Origin": "*",
26
+ "Access-Control-Allow-Methods": "GET, POST, PATCH, OPTIONS",
27
+ "Access-Control-Allow-Headers": "Content-Type, Last-Event-ID, X-Client-Version, X-Daytona-Skip-Preview-Warning",
28
+ });
29
+ res.end();
30
+ return;
31
+ }
32
+ // SPA served by createHttpServer's listener — only handle API routes here
33
+ if (method === "GET" && (path === "/" || path === ""))
34
+ return;
35
+ try {
36
+ // Workspace + file system routes
37
+ if (await (0, server_common_1.handleWorkspaceRoutes)(req, res, workspaceCwd, availableAgents))
38
+ return;
39
+ // Bot hub: /bots and /threads
40
+ if (await (0, bot_routes_1.handleBotRoutes)(req, res, workspaceCwd))
41
+ return;
42
+ // Voice input: /dictate and /dictate/status
43
+ if (await (0, dictation_1.handleDictationRoutes)(req, res))
44
+ return;
45
+ // --- Jarvis ---
46
+ // GET /jarvis — the entry point: Jarvis's bot and the projects it knows.
47
+ if (method === "GET" && path === "/jarvis") {
48
+ const bot = (0, jarvis_1.ensureJarvisBot)();
49
+ (0, server_common_1.jsonOk)(res, { bot, dir: (0, jarvis_1.jarvisDir)(), projects: (0, jarvis_1.listProjects)() });
50
+ return;
51
+ }
52
+ // POST /jarvis/ask — a fresh Jarvis conversation from the home screen: opens
53
+ // a thread and sends the first message in one round trip.
54
+ if (method === "POST" && path === "/jarvis/ask") {
55
+ const body = await (0, server_common_1.readBody)(req);
56
+ const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
57
+ if (!prompt) {
58
+ (0, server_common_1.jsonError)(res, 400, "prompt is required");
59
+ return;
60
+ }
61
+ if (!availableAgents.includes("claude-code")) {
62
+ (0, server_common_1.jsonError)(res, 400, "Agent 'claude-code' is not available");
63
+ return;
64
+ }
65
+ const bot = (0, jarvis_1.ensureJarvisBot)();
66
+ const thread = (0, bot_store_1.createThread)(bot.id, bot.repoPath ?? (0, jarvis_1.jarvisDir)());
67
+ const { store } = (0, turns_1.beginThreadTurn)(thread, bot, { prompt });
68
+ (0, server_common_1.jsonOk)(res, { thread: (0, bot_store_1.getThread)(thread.id) ?? thread, sessionId: store.gitbotId });
69
+ return;
70
+ }
71
+ // GET /sessions/active — threads with a turn in flight, so a client that
72
+ // was closed while work ran can rejoin it.
73
+ if (method === "GET" && path === "/sessions/active") {
74
+ (0, server_common_1.jsonOk)(res, { active: (0, turns_1.activeTurns)() });
75
+ return;
76
+ }
77
+ // GET /sessions
78
+ if (method === "GET" && path === "/sessions") {
79
+ const repoPath = query.repoPath ?? workspaceCwd;
80
+ const agent = query.agent;
81
+ if (!agent || agent === "claude-code") {
82
+ const list = await (0, start_claude_code_1.listSessions)(repoPath);
83
+ (0, server_common_1.jsonOk)(res, { sessions: list });
84
+ return;
85
+ }
86
+ if (agent === "opencode") {
87
+ const list = await (0, start_opencode_1.listSessions)(repoPath);
88
+ (0, server_common_1.jsonOk)(res, { sessions: list });
89
+ return;
90
+ }
91
+ if (agent === "codex") {
92
+ const list = await (0, start_codex_1.listSessions)(repoPath);
93
+ (0, server_common_1.jsonOk)(res, { sessions: list });
94
+ return;
95
+ }
96
+ (0, server_common_1.jsonOk)(res, { sessions: [] });
97
+ return;
98
+ }
99
+ // GET /sessions/:id/history
100
+ const historyId = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/history$/, "");
101
+ if (method === "GET" && path.endsWith("/history") && historyId) {
102
+ const store = server_common_1.sessions.get(historyId);
103
+ if (!store) {
104
+ const agentParam = query.agent;
105
+ if (agentParam === "opencode") {
106
+ const history = await (0, start_opencode_1.getSessionHistory)(historyId, query.repoPath ?? workspaceCwd);
107
+ (0, server_common_1.jsonOk)(res, { messages: history });
108
+ }
109
+ else if (agentParam === "codex") {
110
+ const repoPath = query.repoPath ?? workspaceCwd;
111
+ const history = await (0, start_codex_1.loadTranscript)(historyId, repoPath);
112
+ (0, server_common_1.jsonOk)(res, { messages: history });
113
+ }
114
+ else {
115
+ const repoPath = query.repoPath ?? workspaceCwd;
116
+ const history = await (0, start_claude_code_1.loadTranscript)(historyId, repoPath);
117
+ (0, server_common_1.jsonOk)(res, { messages: history });
118
+ }
119
+ return;
120
+ }
121
+ if (store.agent === "opencode" && store.sdkSessionId) {
122
+ const history = await (0, start_opencode_1.getSessionHistory)(store.sdkSessionId, store.repoPath);
123
+ (0, server_common_1.jsonOk)(res, { messages: history });
124
+ }
125
+ else if (store.agent === "codex") {
126
+ if (!store.sdkSessionId) {
127
+ (0, server_common_1.jsonOk)(res, { messages: [] });
128
+ return;
129
+ }
130
+ const history = await (0, start_codex_1.loadTranscript)(store.sdkSessionId, store.repoPath);
131
+ (0, server_common_1.jsonOk)(res, { messages: history });
132
+ }
133
+ else {
134
+ const history = await (0, start_claude_code_1.loadTranscript)(store.sdkSessionId ?? historyId, store.repoPath);
135
+ (0, server_common_1.jsonOk)(res, { messages: history });
136
+ }
137
+ return;
138
+ }
139
+ // GET /sessions/:id/config
140
+ const configId = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/config$/, "");
141
+ if (method === "GET" && path.endsWith("/config") && configId) {
142
+ const store = server_common_1.sessions.get(configId)
143
+ ?? [...server_common_1.sessions.values()].find(s => s.sdkSessionId === configId);
144
+ if (!store) {
145
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
146
+ return;
147
+ }
148
+ (0, server_common_1.jsonOk)(res, {
149
+ gitbotId: store.gitbotId,
150
+ sessionId: store.sdkSessionId,
151
+ agent: store.agent,
152
+ model: store.model ?? null,
153
+ mode: store.mode ?? null,
154
+ permissionMode: store.permissionMode,
155
+ });
156
+ return;
157
+ }
158
+ // GET /sessions/:id/status
159
+ const statusId = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/status$/, "");
160
+ if (method === "GET" && path.endsWith("/status") && statusId) {
161
+ const store = server_common_1.sessions.get(statusId)
162
+ ?? [...server_common_1.sessions.values()].find(s => s.sdkSessionId === statusId);
163
+ if (!store) {
164
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
165
+ return;
166
+ }
167
+ (0, server_common_1.jsonOk)(res, { streaming: store.status === "running", sdkSessionId: store.sdkSessionId ?? null });
168
+ return;
169
+ }
170
+ // GET /sessions/:id/permissions — which tool requests are still awaiting an
171
+ // answer. A client rejoining a running turn replays events it has already
172
+ // seen, and must not re-offer approvals that were resolved while it was away.
173
+ const permsId = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/permissions$/, "");
174
+ if (method === "GET" && path.endsWith("/permissions") && permsId) {
175
+ const store = server_common_1.sessions.get(permsId)
176
+ ?? [...server_common_1.sessions.values()].find(s => s.sdkSessionId === permsId);
177
+ if (!store) {
178
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
179
+ return;
180
+ }
181
+ (0, server_common_1.jsonOk)(res, { pending: [...store.pendingPermissions.keys()] });
182
+ return;
183
+ }
184
+ // POST /sessions/:id/abort
185
+ const abortId = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/abort$/, "");
186
+ if (method === "POST" && path.endsWith("/abort") && abortId) {
187
+ const store = server_common_1.sessions.get(abortId)
188
+ ?? [...server_common_1.sessions.values()].find(s => s.sdkSessionId === abortId);
189
+ if (!store) {
190
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
191
+ return;
192
+ }
193
+ if (store.status !== "running") {
194
+ (0, server_common_1.jsonOk)(res, { ok: true });
195
+ return;
196
+ }
197
+ if (store.agent === "claude-code" && store.abortController) {
198
+ store.abortController.abort();
199
+ }
200
+ else if (store.agent === "codex" && store.abortController) {
201
+ store.abortController.abort();
202
+ }
203
+ else if (store.agent === "opencode" && store.sdkSessionId) {
204
+ await (0, start_opencode_1.abortSession)(store.sdkSessionId, store.repoPath).catch(() => { });
205
+ store.status = "done";
206
+ store.pendingPermissions.clear();
207
+ (0, server_common_1.notifyPermissionsChanged)();
208
+ (0, server_common_1.emitEvent)(store, "aborted", { message: "Aborted by user" });
209
+ (0, server_common_1.scheduleCleanup)(store);
210
+ }
211
+ console.log(`[abort] session ${abortId}`);
212
+ (0, server_common_1.jsonOk)(res, { ok: true });
213
+ return;
214
+ }
215
+ // POST /sessions/:id/permission
216
+ const permBase = (0, server_common_1.parsePathParam)(path, "/sessions/")?.replace(/\/permission$/, "");
217
+ if (method === "POST" && path.endsWith("/permission") && permBase) {
218
+ const store = server_common_1.sessions.get(permBase);
219
+ if (!store) {
220
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
221
+ return;
222
+ }
223
+ const body = await (0, server_common_1.readBody)(req);
224
+ const { toolUseID, approved, updatedInput } = body;
225
+ if (!toolUseID) {
226
+ (0, server_common_1.jsonError)(res, 400, "toolUseID is required");
227
+ return;
228
+ }
229
+ console.log(`[permission] id=${toolUseID} approved=${approved}`);
230
+ if (store.agent === "claude-code") {
231
+ const pending = store.pendingPermissions.get(toolUseID);
232
+ if (pending) {
233
+ store.pendingPermissions.delete(toolUseID);
234
+ (0, server_common_1.notifyPermissionsChanged)();
235
+ pending.resolve(approved
236
+ ? { behavior: "allow", updatedInput: updatedInput ?? pending.input }
237
+ : { behavior: "deny", message: "User denied" });
238
+ }
239
+ }
240
+ else if (store.agent === "opencode" && store.sdkSessionId) {
241
+ const pending = store.pendingPermissions.get(toolUseID);
242
+ if (pending) {
243
+ store.pendingPermissions.delete(toolUseID);
244
+ (0, server_common_1.notifyPermissionsChanged)();
245
+ // For subagent permissions, respond on the child sdkSessionId that actually raised the request.
246
+ const respondSdkId = pending.askedBySdkSessionId ?? store.sdkSessionId;
247
+ await (0, start_opencode_1.respondPermission)(respondSdkId, toolUseID, approved, store.repoPath).catch((err) => {
248
+ console.error("Permission response failed:", err.message);
249
+ });
250
+ }
251
+ }
252
+ (0, server_common_1.jsonOk)(res, { ok: true });
253
+ return;
254
+ }
255
+ // POST /chat
256
+ if (method === "POST" && path === "/chat") {
257
+ const body = await (0, server_common_1.readBody)(req);
258
+ const { repoPath, agent, sessionId: existingId, model, permissionMode, mode } = body;
259
+ const { prompt, attachments, threadId } = body;
260
+ // attachments: Array<{ url: string }> | undefined
261
+ // A threadId comes from the bot hub: the thread supplies the folder, the
262
+ // resume handle and the bot preset, so the client need not repeat them.
263
+ if (threadId) {
264
+ const thread = (0, bot_store_1.getThread)(threadId);
265
+ if (!thread) {
266
+ (0, server_common_1.jsonError)(res, 404, "Thread not found");
267
+ return;
268
+ }
269
+ const bot = (0, bot_store_1.getBot)(thread.botId);
270
+ if (!bot) {
271
+ (0, server_common_1.jsonError)(res, 404, "Bot not found");
272
+ return;
273
+ }
274
+ if (!availableAgents.includes("claude-code")) {
275
+ (0, server_common_1.jsonError)(res, 400, "Agent 'claude-code' is not available");
276
+ return;
277
+ }
278
+ if (!prompt && (!attachments || attachments.length === 0)) {
279
+ (0, server_common_1.jsonError)(res, 400, "prompt or attachments is required");
280
+ return;
281
+ }
282
+ // Work waits on setup; the setup thread itself is exempt, since it is
283
+ // the thing that clears the block.
284
+ if (thread.kind !== "setup" && (0, bot_store_1.botNeedsSetup)(bot)) {
285
+ (0, server_common_1.jsonError)(res, 409, `${bot.name} still needs to set up this machine`, {
286
+ setupRequired: true,
287
+ setupThreadId: bot.setupThreadId,
288
+ });
289
+ return;
290
+ }
291
+ try {
292
+ const { store } = (0, turns_1.beginThreadTurn)(thread, bot, {
293
+ prompt: prompt ?? "",
294
+ attachments,
295
+ model,
296
+ permissionMode: permissionMode,
297
+ mode,
298
+ });
299
+ (0, server_common_1.jsonOk)(res, { sessionId: store.gitbotId });
300
+ }
301
+ catch (err) {
302
+ if (err instanceof turns_1.TurnBusyError)
303
+ (0, server_common_1.jsonError)(res, 409, err.message);
304
+ else
305
+ throw err;
306
+ }
307
+ return;
308
+ }
309
+ if (!repoPath) {
310
+ (0, server_common_1.jsonError)(res, 400, "repoPath is required");
311
+ return;
312
+ }
313
+ if (!prompt && (!attachments || attachments.length === 0)) {
314
+ (0, server_common_1.jsonError)(res, 400, "prompt or attachments is required");
315
+ return;
316
+ }
317
+ if (attachments != null && (!Array.isArray(attachments) || attachments.some((a) => typeof a?.url !== "string" || !a.url))) {
318
+ (0, server_common_1.jsonError)(res, 400, "attachments must be an array of { url: string }");
319
+ return;
320
+ }
321
+ if (agent !== "claude-code" && agent !== "opencode" && agent !== "codex") {
322
+ (0, server_common_1.jsonError)(res, 400, "agent must be claude-code, opencode, or codex");
323
+ return;
324
+ }
325
+ if (!availableAgents.includes(agent)) {
326
+ (0, server_common_1.jsonError)(res, 400, `Agent '${agent}' is not available`);
327
+ return;
328
+ }
329
+ let store = existingId ? server_common_1.sessions.get(existingId) : undefined;
330
+ if (store) {
331
+ if (store.status === "running") {
332
+ (0, server_common_1.jsonError)(res, 409, "Session is already running");
333
+ return;
334
+ }
335
+ store.status = "running";
336
+ (0, server_common_1.notifyPermissionsChanged)();
337
+ store.events = [];
338
+ store.seq = 0;
339
+ if (model)
340
+ store.model = model;
341
+ if (mode)
342
+ store.mode = mode;
343
+ if (permissionMode)
344
+ store.permissionMode = permissionMode;
345
+ (0, server_common_1.emitEvent)(store, 'user_prompt', { prompt: prompt ?? '', ...(attachments?.length ? { attachments } : {}) });
346
+ }
347
+ else {
348
+ const gitbotId = existingId ?? (0, crypto_1.randomUUID)();
349
+ store = (0, server_common_1.createSession)(gitbotId, agent, repoPath, model, mode, permissionMode);
350
+ if (existingId) {
351
+ store.sdkSessionId = existingId;
352
+ }
353
+ (0, server_common_1.emitEvent)(store, 'user_prompt', { prompt: prompt ?? '', ...(attachments?.length ? { attachments } : {}) });
354
+ (0, server_common_1.notifyPermissionsChanged)();
355
+ }
356
+ const s = store;
357
+ (0, server_common_1.notifySessionStarted)();
358
+ if (agent === "claude-code") {
359
+ (0, start_claude_code_1.runAgent)(s).catch((err) => {
360
+ console.error("[runAgent] unhandled:", err);
361
+ }).finally(() => (0, server_common_1.notifySessionEnded)());
362
+ }
363
+ else if (agent === "codex") {
364
+ (0, start_codex_1.runAgent)(s).catch((err) => {
365
+ }).finally(() => (0, server_common_1.notifySessionEnded)());
366
+ }
367
+ else if (agent === "opencode") {
368
+ (0, start_opencode_1.runAgent)(s).catch((err) => {
369
+ console.error("[runAgent] unhandled:", err);
370
+ }).finally(() => (0, server_common_1.notifySessionEnded)());
371
+ }
372
+ else {
373
+ (0, start_opencode_1.runAgent)(s).catch((err) => {
374
+ console.error("[runAgent] unhandled:", err);
375
+ }).finally(() => (0, server_common_1.notifySessionEnded)());
376
+ }
377
+ (0, server_common_1.jsonOk)(res, { sessionId: s.gitbotId });
378
+ return;
379
+ }
380
+ // GET /events?sessionId=X
381
+ if (method === "GET" && path === "/events") {
382
+ const sessionId = query.sessionId;
383
+ if (!sessionId) {
384
+ (0, server_common_1.jsonError)(res, 400, "sessionId is required");
385
+ return;
386
+ }
387
+ const store = server_common_1.sessions.get(sessionId)
388
+ ?? [...server_common_1.sessions.values()].find(s => s.sdkSessionId === sessionId);
389
+ if (!store) {
390
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
391
+ return;
392
+ }
393
+ const lastSeq = parseInt(req.headers["last-event-id"] ?? "0", 10) || 0;
394
+ res.writeHead(200, (0, server_common_1.sseHeaders)());
395
+ for (const event of store.events) {
396
+ if (event.seq > lastSeq) {
397
+ (0, server_common_1.writeSseEvent)(res, event);
398
+ }
399
+ }
400
+ if (store.status !== "running") {
401
+ res.end();
402
+ return;
403
+ }
404
+ const listener = (event) => {
405
+ (0, server_common_1.writeSseEvent)(res, event);
406
+ if (event.type === "done" || event.type === "error" || event.type === "aborted") {
407
+ res.end();
408
+ }
409
+ };
410
+ store.emitter.on("event", listener);
411
+ req.on("close", () => {
412
+ store.emitter.off("event", listener);
413
+ });
414
+ return;
415
+ }
416
+ // GET /permissions/events
417
+ if (method === "GET" && path === "/permissions/events") {
418
+ res.writeHead(200, (0, server_common_1.sseHeaders)());
419
+ const sendDump = (permissions, sessions) => {
420
+ if (res.writableEnded)
421
+ return;
422
+ res.write(`event: permissions\ndata: ${JSON.stringify({ permissions, sessions })}\n\n`);
423
+ };
424
+ sendDump((0, server_common_1.buildPermissionsDump)(), (0, server_common_1.buildSessionsDump)());
425
+ server_common_1.permissionsEmitter.on("update", sendDump);
426
+ req.on("close", () => {
427
+ server_common_1.permissionsEmitter.off("update", sendDump);
428
+ });
429
+ return;
430
+ }
431
+ // PATCH /sessions/:id — update session settings mid-run
432
+ if (method === "PATCH" && path.startsWith("/sessions/")) {
433
+ const sessionId = path.slice("/sessions/".length);
434
+ const store = server_common_1.sessions.get(sessionId);
435
+ if (!store) {
436
+ (0, server_common_1.jsonError)(res, 404, "Session not found");
437
+ return;
438
+ }
439
+ const body = await (0, server_common_1.readBody)(req);
440
+ if (body.permissionMode !== undefined) {
441
+ const valid = ["ask-permissions", "allow-all-edits", "yolo"];
442
+ if (!valid.includes(body.permissionMode)) {
443
+ (0, server_common_1.jsonError)(res, 400, "Invalid permissionMode");
444
+ return;
445
+ }
446
+ store.permissionMode = body.permissionMode;
447
+ if (store.agent === "claude-code") {
448
+ for (const [id, perm] of store.pendingPermissions) {
449
+ if ((0, server_common_1.shouldAutoApprove)(store.agent, perm.toolName, store.permissionMode)) {
450
+ store.pendingPermissions.delete(id);
451
+ perm.resolve({ behavior: "allow", updatedInput: perm.input });
452
+ }
453
+ }
454
+ (0, server_common_1.notifyPermissionsChanged)();
455
+ }
456
+ else if (store.agent === "codex") {
457
+ // codex applies approvalPolicy at thread start — mode change takes effect next turn
458
+ }
459
+ else if (store.agent === "opencode" && store.sdkSessionId) {
460
+ for (const [id, perm] of store.pendingPermissions) {
461
+ if ((0, server_common_1.shouldAutoApprove)(store.agent, perm.toolName, store.permissionMode)) {
462
+ store.pendingPermissions.delete(id);
463
+ const respondSdkId = perm.askedBySdkSessionId ?? store.sdkSessionId;
464
+ await (0, start_opencode_1.respondPermission)(respondSdkId, id, true, store.repoPath).catch(() => { });
465
+ }
466
+ }
467
+ (0, server_common_1.notifyPermissionsChanged)();
468
+ }
469
+ }
470
+ (0, server_common_1.jsonOk)(res, { sessionId: store.gitbotId, permissionMode: store.permissionMode });
471
+ return;
472
+ }
473
+ (0, server_common_1.jsonError)(res, 404, "Not found");
474
+ }
475
+ catch (err) {
476
+ console.error("[request] unhandled error:", err.message);
477
+ if (!res.headersSent) {
478
+ (0, server_common_1.jsonError)(res, 500, "Internal server error");
479
+ }
480
+ }
481
+ }
482
+ async function start(network = "local", portOverride, caffeinate = false, relayUrl) {
483
+ const workspaceCwd = process.cwd();
484
+ console.log(`jarvis — starting in ${workspaceCwd}`);
485
+ // Claude Code refuses to spawn inside another Claude Code session, which would
486
+ // otherwise surface only as an opaque "exited with code 1" on the first message.
487
+ if (process.env.CLAUDECODE) {
488
+ console.warn(` warning: CLAUDECODE is set — this shell is inside a Claude Code session.`);
489
+ console.warn(` The claude-code agent will refuse to start. Run jarvis from a plain terminal,`);
490
+ console.warn(` or launch it with: env -u CLAUDECODE jarvis start -p <port>`);
491
+ }
492
+ (0, start_claude_code_2.setWorkspaceCwd)(workspaceCwd);
493
+ // Jarvis's world exists before anyone speaks to it: its directory, its bot,
494
+ // and a bot per known project.
495
+ try {
496
+ const jarvis = (0, jarvis_1.ensureJarvisBot)();
497
+ (0, jarvis_1.syncProjectBots)();
498
+ console.log(` jarvis: ${(0, jarvis_1.jarvisDir)()} (${(0, jarvis_1.listProjects)().length} projects, bot ${jarvis.id.slice(0, 8)})`);
499
+ }
500
+ catch (err) {
501
+ console.warn(` jarvis: could not initialise — ${err?.message ?? err}`);
502
+ }
503
+ const claudeAvailable = await (0, start_claude_code_1.initAgent)();
504
+ const opencodeAvailable = await (0, start_opencode_1.initAgent)();
505
+ const codexAvailable = await (0, start_codex_1.initAgent)();
506
+ const availableAgents = [
507
+ ...(claudeAvailable ? ["claude-code"] : []),
508
+ ...(opencodeAvailable ? ["opencode"] : []),
509
+ ...(codexAvailable ? ["codex"] : []),
510
+ ];
511
+ console.log(` available agents: ${availableAgents.join(", ") || "none"}`);
512
+ if (relayUrl) {
513
+ const caffeinatePid = (0, server_common_1.maybeCaffeinate)(caffeinate);
514
+ (0, server_common_1.setupShutdown)(() => { }, caffeinatePid);
515
+ await (0, relay_client_1.startRelayMode)(relayUrl, availableAgents, workspaceCwd);
516
+ return;
517
+ }
518
+ const { server, caffeinatePid } = await (0, server_common_1.createHttpServer)({
519
+ portOverride,
520
+ caffeinate,
521
+ network,
522
+ label: "jarvis server",
523
+ });
524
+ server.on("request", (req, res) => {
525
+ handleRequest(req, res, availableAgents, workspaceCwd);
526
+ });
527
+ (0, server_common_1.setupShutdown)(() => {
528
+ server.close(() => process.exit(0));
529
+ }, caffeinatePid);
530
+ }