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/LICENSE +21 -0
- package/README.md +484 -0
- package/dist/bot-routes.js +196 -0
- package/dist/bot-store.js +297 -0
- package/dist/client-html.js +2241 -0
- package/dist/client-vendor.js +32 -0
- package/dist/client.js +118 -0
- package/dist/dictation.js +128 -0
- package/dist/env-file.js +79 -0
- package/dist/index.js +41 -0
- package/dist/jarvis-tools.js +143 -0
- package/dist/jarvis.js +341 -0
- package/dist/ls.js +46 -0
- package/dist/progress.js +33 -0
- package/dist/relay-client.js +216 -0
- package/dist/relay-types.js +2 -0
- package/dist/server-common.js +663 -0
- package/dist/server.js +530 -0
- package/dist/start-claude-code.js +557 -0
- package/dist/start-codex.js +886 -0
- package/dist/start-opencode.js +452 -0
- package/dist/start.js +742 -0
- package/dist/sync.js +15 -0
- package/dist/trophy.js +7 -0
- package/dist/turns.js +91 -0
- package/dist/workspace.js +152 -0
- package/package.json +55 -0
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
|
+
}
|