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.
@@ -0,0 +1,557 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setWorkspaceCwd = setWorkspaceCwd;
4
+ exports.initAgent = initAgent;
5
+ exports.runAgent = runAgent;
6
+ exports.continueAgent = continueAgent;
7
+ exports.loadTranscript = loadTranscript;
8
+ exports.listSessions = listSessions;
9
+ const claude_agent_sdk_1 = require("@anthropic-ai/claude-agent-sdk");
10
+ const fs_1 = require("fs");
11
+ const child_process_1 = require("child_process");
12
+ const promises_1 = require("fs/promises");
13
+ const readline_1 = require("readline");
14
+ const path_1 = require("path");
15
+ const os_1 = require("os");
16
+ const crypto_1 = require("crypto");
17
+ const server_common_1 = require("./server-common");
18
+ const bot_store_1 = require("./bot-store");
19
+ const jarvis_tools_1 = require("./jarvis-tools");
20
+ // Where Jarvis's project checkouts live; set once by the server at startup.
21
+ let workspaceCwd = process.cwd();
22
+ function setWorkspaceCwd(cwd) { workspaceCwd = cwd; }
23
+ async function initAgent() {
24
+ try {
25
+ (0, child_process_1.execSync)("claude --version", { stdio: "ignore" });
26
+ return true;
27
+ }
28
+ catch {
29
+ console.warn(" claude CLI not found — claude-code agent unavailable");
30
+ return false;
31
+ }
32
+ }
33
+ async function runAgent(store) {
34
+ const abortController = new AbortController();
35
+ store.abortController = abortController;
36
+ // The SDK reports a failed spawn as a bare exit code; the child's stderr is
37
+ // where the actual reason lives, so keep the tail of it for the error event.
38
+ const stderrTail = [];
39
+ try {
40
+ let modelLogged = false;
41
+ const lastUserEvent = [...store.events].reverse().find(e => e.type === "user_prompt");
42
+ const promptText = lastUserEvent?.prompt ?? "";
43
+ const attachments = lastUserEvent?.attachments;
44
+ let promptParam;
45
+ if (attachments && attachments.length > 0) {
46
+ const content = [];
47
+ if (promptText)
48
+ content.push({ type: "text", text: promptText });
49
+ for (const att of attachments) {
50
+ content.push({ type: "image", source: { type: "url", url: att.url } });
51
+ }
52
+ const sessionId = store.sdkSessionId ?? (0, crypto_1.randomUUID)();
53
+ async function* multimodalPrompt() {
54
+ yield {
55
+ type: "user",
56
+ message: { role: "user", content },
57
+ parent_tool_use_id: null,
58
+ session_id: sessionId,
59
+ };
60
+ }
61
+ promptParam = multimodalPrompt();
62
+ }
63
+ else {
64
+ promptParam = promptText;
65
+ }
66
+ // A bot is a preset: its instructions ride on top of Claude Code's own system
67
+ // prompt, and its tool lists constrain the run.
68
+ const preset = store.botPreset;
69
+ const append = preset?.setup
70
+ ? setupSystemPrompt(preset)
71
+ : preset?.jarvis
72
+ ? preset.instructions
73
+ : preset?.instructions
74
+ ? botSystemPrompt(preset)
75
+ : undefined;
76
+ // Jarvis gets its dispatch tools in-process. A delegated task can run for
77
+ // many minutes, well past the SDK's default close timeout for tool calls.
78
+ const jarvis = preset?.jarvis
79
+ ? {
80
+ mcpServers: { [jarvis_tools_1.JARVIS_MCP_NAME]: (0, jarvis_tools_1.buildJarvisTools)(store, workspaceCwd) },
81
+ allowedTools: [...jarvis_tools_1.JARVIS_TOOL_NAMES, ...(preset.allowedTools ?? [])],
82
+ env: { ...process.env, CLAUDE_CODE_STREAM_CLOSE_TIMEOUT: String(6 * 60 * 60 * 1000) },
83
+ }
84
+ : {};
85
+ const q = (0, claude_agent_sdk_1.query)({
86
+ prompt: promptParam,
87
+ options: {
88
+ model: store.model ?? "claude-sonnet-4-6",
89
+ permissionMode: store.mode === "plan" ? "plan" : "default",
90
+ abortController,
91
+ includePartialMessages: true,
92
+ cwd: store.repoPath,
93
+ // The SDK loads no filesystem config by default. Opt in so the bot picks up
94
+ // .mcp.json servers (plus CLAUDE.md and permission settings) the way the CLI does.
95
+ settingSources: ["user", "project", "local"],
96
+ stderr: (data) => {
97
+ stderrTail.push(data);
98
+ if (stderrTail.length > 20)
99
+ stderrTail.shift();
100
+ },
101
+ ...(append
102
+ ? { systemPrompt: { type: "preset", preset: "claude_code", append } }
103
+ : {}),
104
+ ...(preset?.allowedTools?.length ? { allowedTools: preset.allowedTools } : {}),
105
+ ...(preset?.disallowedTools?.length ? { disallowedTools: preset.disallowedTools } : {}),
106
+ ...jarvis,
107
+ ...(store.sdkSessionId ? { resume: store.sdkSessionId } : {}),
108
+ canUseTool: (toolName, input, { signal, toolUseID }) => {
109
+ return new Promise((resolve) => {
110
+ console.log(`[canUseTool] tool="${toolName}" mode="${store.permissionMode}" autoApprove=${(0, server_common_1.shouldAutoApprove)(store.agent, toolName, store.permissionMode)}`);
111
+ if ((0, server_common_1.shouldAutoApprove)(store.agent, toolName, store.permissionMode)) {
112
+ resolve({ behavior: "allow", updatedInput: input });
113
+ return;
114
+ }
115
+ store.pendingPermissions.set(toolUseID, { resolve, input, toolName, toolUseID });
116
+ (0, server_common_1.notifyNewPermission)(toolName);
117
+ (0, server_common_1.emitEvent)(store, "permission_request", { toolUseID, toolName, input });
118
+ signal.addEventListener("abort", () => {
119
+ const p = store.pendingPermissions.get(toolUseID);
120
+ if (p) {
121
+ store.pendingPermissions.delete(toolUseID);
122
+ (0, server_common_1.notifyPermissionsChanged)();
123
+ p.resolve({ behavior: "deny", message: "Request aborted" });
124
+ }
125
+ }, { once: true });
126
+ });
127
+ },
128
+ },
129
+ });
130
+ let receivedResult = false;
131
+ // A setup run says how it went in words; the marker is what the hub reads.
132
+ let assistantText = "";
133
+ try {
134
+ for await (const msg of q) {
135
+ if (msg.type === "system" && msg.subtype === "init") {
136
+ const newSdkId = msg.session_id;
137
+ if (newSdkId && !store.sdkSessionId) {
138
+ store.sdkSessionId = newSdkId;
139
+ // Bind the hub thread to its transcript the first time we learn the id.
140
+ if (store.threadId)
141
+ (0, bot_store_1.bindSession)(store.threadId, newSdkId);
142
+ }
143
+ }
144
+ if (msg.type === "result")
145
+ receivedResult = true;
146
+ if (!modelLogged && msg.type === "assistant" && msg.message?.model) {
147
+ modelLogged = true;
148
+ }
149
+ if (preset?.setup && msg.type === "assistant") {
150
+ for (const block of msg.message?.content ?? []) {
151
+ if (block?.type === "text" && block.text)
152
+ assistantText += block.text + "\n";
153
+ }
154
+ }
155
+ const payload = formatMessage(msg);
156
+ if (payload) {
157
+ const items = Array.isArray(payload) ? payload : [payload];
158
+ for (const item of items) {
159
+ (0, server_common_1.emitEvent)(store, item.type, item);
160
+ }
161
+ }
162
+ }
163
+ }
164
+ catch (err) {
165
+ if (err?.name === "AbortError" || abortController.signal.aborted) {
166
+ console.log("[query] aborted");
167
+ (0, server_common_1.emitEvent)(store, "aborted", { message: "Request aborted by user" });
168
+ }
169
+ else {
170
+ throw err;
171
+ }
172
+ }
173
+ if (preset?.setup)
174
+ recordSetupOutcome(preset.id, assistantText);
175
+ if (!receivedResult) {
176
+ console.log("[query] stream ended without result message — treating as error");
177
+ (0, server_common_1.emitEvent)(store, "error", { message: "Claude process exited unexpectedly" });
178
+ store.status = "error";
179
+ (0, server_common_1.scheduleCleanup)(store);
180
+ return;
181
+ }
182
+ }
183
+ catch (err) {
184
+ console.log("[query] outer error:", err?.message, err?.stack);
185
+ const detail = stderrTail.join("").trim();
186
+ (0, server_common_1.emitEvent)(store, "error", {
187
+ message: detail ? `${err?.message ?? "Agent failed"}\n\n${detail}` : (err?.message ?? "Unknown error"),
188
+ });
189
+ store.status = "error";
190
+ (0, server_common_1.scheduleCleanup)(store);
191
+ return;
192
+ }
193
+ finally {
194
+ store.abortController = null;
195
+ store.pendingPermissions.clear();
196
+ (0, server_common_1.notifyPermissionsChanged)();
197
+ }
198
+ store.status = "done";
199
+ (0, server_common_1.notifyPermissionsChanged)();
200
+ (0, server_common_1.emitEvent)(store, "done", {});
201
+ (0, server_common_1.notifySessionDone)(store);
202
+ (0, server_common_1.scheduleCleanup)(store);
203
+ }
204
+ // Re-entrant: called for subsequent prompts on the same session
205
+ async function continueAgent(store, prompt) {
206
+ // Inject the prompt as a stored event for reference (not replayed to SDK)
207
+ // Then run agent — sdkSessionId already set so SDK will resume
208
+ store.events.push({ seq: 0, type: "user_prompt", prompt });
209
+ store.status = "running";
210
+ (0, server_common_1.notifyPermissionsChanged)();
211
+ await runAgent(store);
212
+ }
213
+ /**
214
+ * Frames a bot's instructions as a standing job. The instructions alone read as
215
+ * background colour, so a bare "hi" often gets a greeting back instead of the
216
+ * work; naming the job and saying when to start it makes the first turn reliable.
217
+ */
218
+ function botSystemPrompt(preset) {
219
+ return [
220
+ `You are "${preset.name}", an agent with one standing job in this workspace.`,
221
+ "",
222
+ "YOUR JOB:",
223
+ preset.instructions.trim(),
224
+ "",
225
+ "Start this job on the user's first message of the conversation, whatever that",
226
+ "message says — a greeting such as \"hi\" is a signal to begin, not small talk.",
227
+ "Do not ask what to work on and do not wait for a restatement of the job; the",
228
+ "job above is the request. Afterwards, follow the user's messages as usual,",
229
+ "keeping the job's constraints in force for the rest of the conversation.",
230
+ ].join("\n");
231
+ }
232
+ /**
233
+ * Frames the one-time machine-preparation run. The bot's own job is context
234
+ * here, not the task: the task is making this computer able to do that job.
235
+ * The run stays an ordinary conversation afterwards, so the user can take over
236
+ * when a step needs a human — a password, a licence, a choice of package manager.
237
+ */
238
+ function setupSystemPrompt(preset) {
239
+ return [
240
+ `You are setting up the machine for "${preset.name}", a bot that has just been`,
241
+ "installed here. This thread is the one-time setup run, not the bot's work.",
242
+ "",
243
+ ...(preset.instructions.trim()
244
+ ? ["THE JOB THIS MACHINE IS BEING PREPARED FOR (context only — do not do it now):",
245
+ preset.instructions.trim(), ""]
246
+ : []),
247
+ "SETUP INSTRUCTIONS FROM THE BOT'S AUTHOR:",
248
+ (preset.setupInstructions ?? "").trim(),
249
+ "",
250
+ "HOW TO RUN THIS:",
251
+ "Begin as soon as the user's first message arrives, whatever it says. Check what",
252
+ "is already present before installing anything — a machine that is ready needs no",
253
+ "changes. Prefer the platform's usual package manager, keep changes to what the",
254
+ "instructions call for, and explain anything that touches system state before you",
255
+ "do it. If a step needs the user — a password, a licence key, an account, a choice",
256
+ "you cannot make for them — ask in this thread and wait; this is a normal",
257
+ "conversation and they can answer.",
258
+ "",
259
+ "HOW TO FINISH:",
260
+ "When the machine can do the job, verify it (run the tool, check the version),",
261
+ "then end your final message with a line containing exactly:",
262
+ "SETUP_COMPLETE",
263
+ "If you cannot get there — a missing dependency you may not install, an",
264
+ "unsupported platform, a step the user must do elsewhere — end your final message",
265
+ "with a line of the form:",
266
+ "SETUP_FAILED: <one line saying what is blocked>",
267
+ "Write one of those two markers only when the run has actually reached that",
268
+ "point; never write them while a step is still outstanding, and never mention",
269
+ "them as an example. The user may keep talking to you afterwards, and a later",
270
+ "turn may end with a marker of its own once the situation changes.",
271
+ ].join("\n");
272
+ }
273
+ /**
274
+ * Reads the run's verdict off the end of the transcript. The last marker wins,
275
+ * so a later turn that fixes a failure can flip the bot to ready.
276
+ */
277
+ function recordSetupOutcome(botId, text) {
278
+ const matches = text.match(/^\s*SETUP_(COMPLETE|FAILED)\b/gm);
279
+ if (!matches?.length)
280
+ return;
281
+ const done = /COMPLETE/.test(matches[matches.length - 1]);
282
+ (0, bot_store_1.setSetupStatus)(botId, done ? "complete" : "failed");
283
+ }
284
+ function formatMessage(msg) {
285
+ switch (msg.type) {
286
+ case "system":
287
+ return { type: "system", subtype: msg.subtype, data: msg };
288
+ case "assistant": {
289
+ const payloads = [];
290
+ // Walk the blocks in order so the client can paint text and tool calls
291
+ // where they actually happened; consecutive text blocks coalesce.
292
+ let text = "";
293
+ const flushText = () => {
294
+ if (text)
295
+ payloads.push({ type: "assistant", content: text });
296
+ text = "";
297
+ };
298
+ for (const block of msg.message.content) {
299
+ if (block.type === "text") {
300
+ text += block.text;
301
+ }
302
+ else if (block.type === "tool_use") {
303
+ flushText();
304
+ payloads.push({
305
+ type: "tool_use",
306
+ tool_name: block.name,
307
+ tool_input: formatToolInput(block.name, block.input),
308
+ });
309
+ }
310
+ }
311
+ flushText();
312
+ return payloads.length === 1 ? payloads[0] : payloads.length > 1 ? payloads : null;
313
+ }
314
+ case "stream_event": {
315
+ const event = msg.event;
316
+ if (event?.type === "content_block_start") {
317
+ if (event.content_block?.type === "thinking") {
318
+ return { type: "status", status: "thinking" };
319
+ }
320
+ if (event.content_block?.type === "tool_use") {
321
+ return { type: "status", status: "tool", tool_name: event.content_block.name };
322
+ }
323
+ }
324
+ return null;
325
+ }
326
+ case "tool_progress": {
327
+ const tp = msg;
328
+ return { type: "status", status: "tool", tool_name: tp.tool_name, elapsed: tp.elapsed_time_seconds };
329
+ }
330
+ case "tool_use_summary": {
331
+ const ts = msg;
332
+ return { type: "status", status: "tool_summary", summary: ts.summary };
333
+ }
334
+ case "result":
335
+ if (msg.subtype === "success") {
336
+ return {
337
+ type: "result",
338
+ subtype: "success",
339
+ result: msg.result,
340
+ cost: msg.total_cost_usd,
341
+ duration_ms: msg.duration_ms,
342
+ num_turns: msg.num_turns,
343
+ };
344
+ }
345
+ return {
346
+ type: "result",
347
+ subtype: msg.subtype,
348
+ errors: "errors" in msg ? msg.errors : undefined,
349
+ cost: msg.total_cost_usd,
350
+ duration_ms: msg.duration_ms,
351
+ };
352
+ default:
353
+ return null;
354
+ }
355
+ }
356
+ function extractText(content) {
357
+ if (typeof content === "string")
358
+ return content;
359
+ if (Array.isArray(content)) {
360
+ return content
361
+ .filter((b) => b.type === "text")
362
+ .map((b) => b.text)
363
+ .join("");
364
+ }
365
+ return "";
366
+ }
367
+ async function loadTranscript(sessionId, cwd) {
368
+ const encodedCwd = cwd.replace(/[/\\_]/g, "-");
369
+ const transcriptPath = (0, path_1.join)((0, os_1.homedir)(), ".claude", "projects", encodedCwd, `${sessionId}.jsonl`);
370
+ if (!(0, fs_1.existsSync)(transcriptPath))
371
+ return [];
372
+ const messages = [];
373
+ try {
374
+ const rl = (0, readline_1.createInterface)({
375
+ input: (0, fs_1.createReadStream)(transcriptPath, { encoding: "utf-8" }),
376
+ crlfDelay: Infinity,
377
+ });
378
+ for await (const line of rl) {
379
+ if (!line)
380
+ continue;
381
+ let entry;
382
+ try {
383
+ entry = JSON.parse(line);
384
+ }
385
+ catch {
386
+ continue;
387
+ }
388
+ if (entry.type === "user" && entry.userType === "external" && !entry.isMeta) {
389
+ const rawContent = entry.message?.content;
390
+ const blocks = [];
391
+ if (typeof rawContent === "string") {
392
+ if (rawContent)
393
+ blocks.push({ type: "text", text: rawContent });
394
+ }
395
+ else if (Array.isArray(rawContent)) {
396
+ for (const b of rawContent) {
397
+ if (b.type === "text" && b.text)
398
+ blocks.push({ type: "text", text: b.text });
399
+ else if (b.type === "image" && b.source?.url)
400
+ blocks.push({ type: "image_url", url: b.source.url });
401
+ }
402
+ }
403
+ if (blocks.length)
404
+ messages.push({ role: "user", content: blocks });
405
+ }
406
+ if (entry.type === "assistant") {
407
+ const rawContent = entry.message?.content;
408
+ const blocks = [];
409
+ if (Array.isArray(rawContent)) {
410
+ for (const b of rawContent) {
411
+ if (b.type === "text" && b.text)
412
+ blocks.push({ type: "text", text: b.text });
413
+ else if (b.type === "tool_use") {
414
+ let tool_input;
415
+ try {
416
+ tool_input = formatToolInput(b.name, b.input);
417
+ }
418
+ catch {
419
+ tool_input = JSON.stringify(b.input) ?? "";
420
+ }
421
+ blocks.push({ type: "tool_use", tool_name: b.name, tool_input });
422
+ }
423
+ }
424
+ }
425
+ if (blocks.length)
426
+ messages.push({ role: "assistant", content: blocks });
427
+ }
428
+ if (entry.type === "result" && entry.subtype === "success" && typeof entry.result === "string" && entry.result.trim()) {
429
+ messages.push({ role: "assistant", content: [{ type: "text", text: entry.result }] });
430
+ }
431
+ }
432
+ return messages;
433
+ }
434
+ catch (err) {
435
+ console.error("Error reading transcript:", err.message);
436
+ return [];
437
+ }
438
+ }
439
+ async function getSessionPreview(filePath) {
440
+ try {
441
+ try {
442
+ const line = (0, child_process_1.execFileSync)("grep", ["-m1", '"custom-title"', filePath], { encoding: "utf-8" }).trim();
443
+ const entry = JSON.parse(line);
444
+ if (entry.type === "custom-title" && typeof entry.customTitle === "string" && entry.customTitle.trim()) {
445
+ return entry.customTitle.trim();
446
+ }
447
+ }
448
+ catch {
449
+ // no custom-title entry found, fall through to preview
450
+ }
451
+ const rl = (0, readline_1.createInterface)({
452
+ input: (0, fs_1.createReadStream)(filePath, { encoding: "utf-8" }),
453
+ crlfDelay: Infinity,
454
+ });
455
+ const parts = [];
456
+ let totalLen = 0;
457
+ for await (const line of rl) {
458
+ if (!line)
459
+ continue;
460
+ let entry;
461
+ try {
462
+ entry = JSON.parse(line);
463
+ }
464
+ catch {
465
+ continue;
466
+ }
467
+ if ((entry.type === "user" && entry.userType === "external" && !entry.isMeta) ||
468
+ entry.type === "assistant") {
469
+ const raw = extractText(entry.message?.content).trim();
470
+ const text = raw.replace(/<[^>]*>/g, "").trim();
471
+ if (!text)
472
+ continue;
473
+ parts.push(text);
474
+ totalLen += (parts.length > 1 ? 3 : 0) + text.length;
475
+ if (totalLen >= 80) {
476
+ rl.close();
477
+ break;
478
+ }
479
+ }
480
+ }
481
+ const preview = parts.join(" — ");
482
+ return preview.length > 80 ? preview.slice(0, 80) + "..." : preview;
483
+ }
484
+ catch {
485
+ return "";
486
+ }
487
+ }
488
+ async function listSessions(cwd) {
489
+ const encodedCwd = cwd.replace(/[/\\_]/g, "-");
490
+ const projectDir = (0, path_1.join)((0, os_1.homedir)(), ".claude", "projects", encodedCwd);
491
+ if (!(0, fs_1.existsSync)(projectDir))
492
+ return [];
493
+ try {
494
+ const files = await (0, promises_1.readdir)(projectDir);
495
+ const jsonlFiles = files.filter((f) => f.endsWith(".jsonl"));
496
+ const sessionList = await Promise.all(jsonlFiles.map(async (f) => {
497
+ const filePath = (0, path_1.join)(projectDir, f);
498
+ const id = f.replace(/\.jsonl$/, "");
499
+ const [preview, fileStat] = await Promise.all([
500
+ getSessionPreview(filePath),
501
+ (0, promises_1.stat)(filePath),
502
+ ]);
503
+ return { id, preview, updatedAt: fileStat.mtime.toISOString() };
504
+ }));
505
+ sessionList.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
506
+ return sessionList;
507
+ }
508
+ catch (err) {
509
+ console.error("Error listing sessions:", err.message);
510
+ return [];
511
+ }
512
+ }
513
+ function formatToolInput(toolName, input) {
514
+ switch (toolName) {
515
+ case "Bash":
516
+ return input.description
517
+ ? `${input.description}: ${input.command}`
518
+ : `${input.command}`;
519
+ case "Read":
520
+ return `${input.file_path}`;
521
+ case "Write":
522
+ return `${input.file_path} (${typeof input.content === "string" ? input.content.length : "?"} chars)`;
523
+ case "Edit":
524
+ return `${input.file_path}`;
525
+ case "Glob":
526
+ return input.path ? `${input.pattern} in ${input.path}` : `${input.pattern}`;
527
+ case "Grep":
528
+ return input.path ? `/${input.pattern}/ in ${input.path}` : `/${input.pattern}/`;
529
+ case "Task":
530
+ return `[${input.subagent_type}] ${input.description}`;
531
+ case "WebFetch":
532
+ return `${input.url}`;
533
+ case "WebSearch":
534
+ return `"${input.query}"`;
535
+ case "NotebookEdit":
536
+ return `${input.notebook_path} (${input.edit_mode || "replace"})`;
537
+ case "TodoWrite": {
538
+ const todos = input.todos;
539
+ if (!Array.isArray(todos))
540
+ return JSON.stringify(input);
541
+ return todos.map((t) => `[${t.status}] ${t.content}`).join(", ");
542
+ }
543
+ case "mcp__jarvis__delegate":
544
+ return `${input.project}: ${input.task}`;
545
+ case "mcp__jarvis__handoff":
546
+ return `${input.project}: ${input.message}`;
547
+ case "mcp__jarvis__read_project":
548
+ case "mcp__jarvis__update_project":
549
+ return `${input.project}`;
550
+ case "mcp__jarvis__create_project":
551
+ return `${input.name}${input.repo ? ` from ${input.repo}` : input.path ? ` at ${input.path}` : ""}`;
552
+ case "mcp__jarvis__list_projects":
553
+ return "";
554
+ default:
555
+ return JSON.stringify(input);
556
+ }
557
+ }