@virtengine/openfleet 0.25.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.
Files changed (120) hide show
  1. package/.env.example +914 -0
  2. package/LICENSE +190 -0
  3. package/README.md +500 -0
  4. package/agent-endpoint.mjs +918 -0
  5. package/agent-hook-bridge.mjs +230 -0
  6. package/agent-hooks.mjs +1188 -0
  7. package/agent-pool.mjs +2403 -0
  8. package/agent-prompts.mjs +689 -0
  9. package/agent-sdk.mjs +141 -0
  10. package/anomaly-detector.mjs +1195 -0
  11. package/autofix.mjs +1294 -0
  12. package/claude-shell.mjs +708 -0
  13. package/cli.mjs +906 -0
  14. package/codex-config.mjs +1274 -0
  15. package/codex-model-profiles.mjs +135 -0
  16. package/codex-shell.mjs +762 -0
  17. package/config-doctor.mjs +613 -0
  18. package/config.mjs +1720 -0
  19. package/conflict-resolver.mjs +248 -0
  20. package/container-runner.mjs +450 -0
  21. package/copilot-shell.mjs +827 -0
  22. package/daemon-restart-policy.mjs +56 -0
  23. package/diff-stats.mjs +282 -0
  24. package/error-detector.mjs +829 -0
  25. package/fetch-runtime.mjs +34 -0
  26. package/fleet-coordinator.mjs +838 -0
  27. package/get-telegram-chat-id.mjs +71 -0
  28. package/git-safety.mjs +170 -0
  29. package/github-reconciler.mjs +403 -0
  30. package/hook-profiles.mjs +651 -0
  31. package/kanban-adapter.mjs +4491 -0
  32. package/lib/logger.mjs +645 -0
  33. package/maintenance.mjs +828 -0
  34. package/merge-strategy.mjs +1171 -0
  35. package/monitor.mjs +12207 -0
  36. package/openfleet.config.example.json +115 -0
  37. package/openfleet.schema.json +465 -0
  38. package/package.json +203 -0
  39. package/postinstall.mjs +187 -0
  40. package/pr-cleanup-daemon.mjs +978 -0
  41. package/preflight.mjs +408 -0
  42. package/prepublish-check.mjs +90 -0
  43. package/presence.mjs +328 -0
  44. package/primary-agent.mjs +282 -0
  45. package/publish.mjs +151 -0
  46. package/repo-root.mjs +29 -0
  47. package/restart-controller.mjs +100 -0
  48. package/review-agent.mjs +557 -0
  49. package/rotate-agent-logs.sh +133 -0
  50. package/sdk-conflict-resolver.mjs +973 -0
  51. package/session-tracker.mjs +880 -0
  52. package/setup.mjs +3937 -0
  53. package/shared-knowledge.mjs +410 -0
  54. package/shared-state-manager.mjs +841 -0
  55. package/shared-workspace-cli.mjs +199 -0
  56. package/shared-workspace-registry.mjs +537 -0
  57. package/shared-workspaces.json +18 -0
  58. package/startup-service.mjs +1070 -0
  59. package/sync-engine.mjs +1063 -0
  60. package/task-archiver.mjs +801 -0
  61. package/task-assessment.mjs +550 -0
  62. package/task-claims.mjs +924 -0
  63. package/task-complexity.mjs +581 -0
  64. package/task-executor.mjs +5111 -0
  65. package/task-store.mjs +753 -0
  66. package/telegram-bot.mjs +9281 -0
  67. package/telegram-sentinel.mjs +2010 -0
  68. package/ui/app.js +867 -0
  69. package/ui/app.legacy.js +1464 -0
  70. package/ui/app.monolith.js +2488 -0
  71. package/ui/components/charts.js +226 -0
  72. package/ui/components/chat-view.js +567 -0
  73. package/ui/components/command-palette.js +587 -0
  74. package/ui/components/diff-viewer.js +190 -0
  75. package/ui/components/forms.js +327 -0
  76. package/ui/components/kanban-board.js +451 -0
  77. package/ui/components/session-list.js +305 -0
  78. package/ui/components/shared.js +473 -0
  79. package/ui/index.html +70 -0
  80. package/ui/modules/api.js +297 -0
  81. package/ui/modules/icons.js +461 -0
  82. package/ui/modules/router.js +81 -0
  83. package/ui/modules/settings-schema.js +261 -0
  84. package/ui/modules/state.js +679 -0
  85. package/ui/modules/telegram.js +331 -0
  86. package/ui/modules/utils.js +270 -0
  87. package/ui/styles/animations.css +140 -0
  88. package/ui/styles/base.css +98 -0
  89. package/ui/styles/components.css +1915 -0
  90. package/ui/styles/kanban.css +286 -0
  91. package/ui/styles/layout.css +809 -0
  92. package/ui/styles/sessions.css +827 -0
  93. package/ui/styles/variables.css +188 -0
  94. package/ui/styles.css +141 -0
  95. package/ui/styles.monolith.css +1046 -0
  96. package/ui/tabs/agents.js +1417 -0
  97. package/ui/tabs/chat.js +74 -0
  98. package/ui/tabs/control.js +887 -0
  99. package/ui/tabs/dashboard.js +515 -0
  100. package/ui/tabs/infra.js +537 -0
  101. package/ui/tabs/logs.js +783 -0
  102. package/ui/tabs/settings.js +1487 -0
  103. package/ui/tabs/tasks.js +1385 -0
  104. package/ui-server.mjs +4073 -0
  105. package/update-check.mjs +465 -0
  106. package/utils.mjs +172 -0
  107. package/ve-kanban.mjs +654 -0
  108. package/ve-kanban.ps1 +1365 -0
  109. package/ve-kanban.sh +18 -0
  110. package/ve-orchestrator.mjs +340 -0
  111. package/ve-orchestrator.ps1 +6546 -0
  112. package/ve-orchestrator.sh +18 -0
  113. package/vibe-kanban-wrapper.mjs +41 -0
  114. package/vk-error-resolver.mjs +470 -0
  115. package/vk-log-stream.mjs +914 -0
  116. package/whatsapp-channel.mjs +520 -0
  117. package/workspace-monitor.mjs +581 -0
  118. package/workspace-reaper.mjs +405 -0
  119. package/workspace-registry.mjs +238 -0
  120. package/worktree-manager.mjs +1266 -0
@@ -0,0 +1,708 @@
1
+ /**
2
+ * claude-shell.mjs - Persistent Claude agent adapter for openfleet.
3
+ *
4
+ * Uses the Claude Agent SDK (@anthropic-ai/claude-agent-sdk) to run a
5
+ * long-lived session with steering support via streaming input mode.
6
+ */
7
+
8
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
9
+ import { existsSync, readFileSync } from "node:fs";
10
+ import { resolve } from "node:path";
11
+ import { homedir } from "node:os";
12
+ import { fileURLToPath } from "node:url";
13
+ import { resolveRepoRoot } from "./repo-root.mjs";
14
+
15
+ const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
16
+
17
+ // ── Configuration ───────────────────────────────────────────────────────────
18
+
19
+ const DEFAULT_TIMEOUT_MS = 60 * 60 * 1000; // 60 min for agentic tasks
20
+ const STATE_FILE = resolve(__dirname, "logs", "claude-shell-state.json");
21
+ const REPO_ROOT = resolveRepoRoot();
22
+
23
+ // ── State ───────────────────────────────────────────────────────────────────
24
+
25
+ let queryFn = null;
26
+ let activeQuery = null;
27
+ let activeQueue = null;
28
+ let activeTurn = false;
29
+ let activeSessionId = null;
30
+ let turnCount = 0;
31
+
32
+ // Track tool use IDs for mapping tool results to start events.
33
+ const toolUseById = new Map();
34
+
35
+ // ── Helpers ─────────────────────────────────────────────────────────────────
36
+
37
+ function timestamp() {
38
+ return new Date().toISOString();
39
+ }
40
+
41
+ function normalizeList(value) {
42
+ if (!value) return null;
43
+ if (Array.isArray(value)) return value.map(String).map((v) => v.trim());
44
+ return String(value)
45
+ .split(",")
46
+ .map((v) => v.trim())
47
+ .filter(Boolean);
48
+ }
49
+
50
+ function envFlagEnabled(value) {
51
+ const raw = String(value ?? "")
52
+ .trim()
53
+ .toLowerCase();
54
+ return ["1", "true", "yes", "on", "y"].includes(raw);
55
+ }
56
+
57
+ function resolveClaudeTransport() {
58
+ const raw = String(process.env.CLAUDE_TRANSPORT || "auto")
59
+ .trim()
60
+ .toLowerCase();
61
+ if (["auto", "sdk", "cli"].includes(raw)) {
62
+ return raw;
63
+ }
64
+ console.warn(
65
+ `[claude-shell] invalid CLAUDE_TRANSPORT='${raw}', defaulting to 'auto'`,
66
+ );
67
+ return "auto";
68
+ }
69
+
70
+ function getToolName(block) {
71
+ return block?.name || block?.tool_name || block?.toolName || "";
72
+ }
73
+
74
+ function getToolUseId(block) {
75
+ return block?.tool_use_id || block?.toolUseId || block?.id || null;
76
+ }
77
+
78
+ function extractTextBlocks(content) {
79
+ if (!Array.isArray(content)) return "";
80
+ return content
81
+ .filter((b) => b?.type === "text" && typeof b.text === "string")
82
+ .map((b) => b.text)
83
+ .join("");
84
+ }
85
+
86
+ function extractResultText(block) {
87
+ if (!block) return "";
88
+ if (typeof block.content === "string") return block.content;
89
+ if (Array.isArray(block.content)) {
90
+ return block.content
91
+ .map((c) => (typeof c === "string" ? c : c?.text || ""))
92
+ .join("");
93
+ }
94
+ return "";
95
+ }
96
+
97
+ function formatEvent(event) {
98
+ if (!event) return null;
99
+ if (
100
+ event.type === "item.started" &&
101
+ event.item?.type === "command_execution"
102
+ ) {
103
+ return `⚡ Running: \`${event.item.command}\``;
104
+ }
105
+ if (
106
+ event.type === "item.completed" &&
107
+ event.item?.type === "command_execution"
108
+ ) {
109
+ const status = event.item.exit_code === 0 ? "✅" : "❌";
110
+ return `${status} Command done: \`${event.item.command}\``;
111
+ }
112
+ if (event.type === "item.started" && event.item?.type === "mcp_tool_call") {
113
+ return `🔌 MCP [${event.item.server}/${event.item.tool}]`;
114
+ }
115
+ if (event.type === "item.completed" && event.item?.type === "mcp_tool_call") {
116
+ const status = event.item.status === "completed" ? "✅" : "❌";
117
+ return `${status} MCP [${event.item.server}/${event.item.tool}]`;
118
+ }
119
+ if (event.type === "item.started" && event.item?.type === "web_search") {
120
+ return `🔍 Searching: ${event.item.query || ""}`;
121
+ }
122
+ if (event.type === "item.updated" && event.item?.type === "reasoning") {
123
+ return event.item.text ? `💭 ${event.item.text.slice(0, 300)}` : null;
124
+ }
125
+ return null;
126
+ }
127
+
128
+ function makeUserMessage(text) {
129
+ return {
130
+ type: "user",
131
+ session_id: activeSessionId || "",
132
+ message: {
133
+ role: "user",
134
+ content: [{ type: "text", text }],
135
+ },
136
+ parent_tool_use_id: null,
137
+ };
138
+ }
139
+
140
+ function createMessageQueue() {
141
+ const queue = [];
142
+ let resolver = null;
143
+ let closed = false;
144
+
145
+ async function* iterator() {
146
+ while (true) {
147
+ if (queue.length > 0) {
148
+ yield queue.shift();
149
+ continue;
150
+ }
151
+ if (closed) return;
152
+ await new Promise((resolve) => {
153
+ resolver = resolve;
154
+ });
155
+ resolver = null;
156
+ }
157
+ }
158
+
159
+ function push(message) {
160
+ if (closed) return false;
161
+ queue.push(message);
162
+ if (resolver) {
163
+ resolver();
164
+ resolver = null;
165
+ }
166
+ return true;
167
+ }
168
+
169
+ function close() {
170
+ closed = true;
171
+ if (resolver) {
172
+ resolver();
173
+ resolver = null;
174
+ }
175
+ }
176
+
177
+ function size() {
178
+ return queue.length;
179
+ }
180
+
181
+ return { iterator, push, close, size };
182
+ }
183
+
184
+ /**
185
+ * Detect Claude API key from multiple sources (auth passthrough).
186
+ * Priority: ENV > CLI config > undefined (SDK will prompt).
187
+ */
188
+ function detectClaudeAuth() {
189
+ // 1. Direct API key env vars (highest priority)
190
+ const envKey =
191
+ process.env.ANTHROPIC_API_KEY ||
192
+ process.env.CLAUDE_API_KEY ||
193
+ process.env.CLAUDE_KEY;
194
+ if (envKey) {
195
+ console.log("[claude-shell] using API key from environment");
196
+ return envKey;
197
+ }
198
+
199
+ // 2. Try to read from Claude CLI config (~/.config/claude/)
200
+ try {
201
+ const configPath = resolve(homedir(), ".config", "claude", "config.json");
202
+ if (existsSync(configPath)) {
203
+ const config = JSON.parse(readFileSync(configPath, "utf8"));
204
+ if (config.api_key) {
205
+ console.log("[claude-shell] using API key from CLI config");
206
+ return config.api_key;
207
+ }
208
+ }
209
+ } catch {
210
+ // Config not found or invalid
211
+ }
212
+
213
+ console.log("[claude-shell] no pre-auth detected, SDK may prompt");
214
+ return undefined;
215
+ }
216
+
217
+ function buildOptions() {
218
+ const options = {
219
+ cwd: REPO_ROOT,
220
+ settingSources: ["user", "project"],
221
+ permissionMode: process.env.CLAUDE_PERMISSION_MODE || "bypassPermissions",
222
+ };
223
+
224
+ // Auth passthrough: detect API key from multiple sources
225
+ const apiKey = detectClaudeAuth();
226
+ if (apiKey) {
227
+ options.apiKey = apiKey;
228
+ }
229
+
230
+ const model =
231
+ process.env.CLAUDE_MODEL ||
232
+ process.env.CLAUDE_CODE_MODEL ||
233
+ process.env.ANTHROPIC_MODEL ||
234
+ "";
235
+ if (model) options.model = model;
236
+
237
+ const maxTurns = Number(process.env.CLAUDE_MAX_TURNS || "0");
238
+ if (Number.isFinite(maxTurns) && maxTurns > 0) {
239
+ options.maxTurns = maxTurns;
240
+ }
241
+
242
+ const includePartial = envFlagEnabled(process.env.CLAUDE_INCLUDE_PARTIAL);
243
+ if (includePartial) {
244
+ options.includePartialMessages = true;
245
+ }
246
+
247
+ const allowedTools = normalizeList(process.env.CLAUDE_ALLOWED_TOOLS);
248
+ if (allowedTools && allowedTools.length > 0) {
249
+ options.allowedTools = allowedTools;
250
+ }
251
+
252
+ return options;
253
+ }
254
+
255
+ function buildCommandForTool(name, input) {
256
+ const toolName = String(name || "");
257
+ if (toolName === "Bash") {
258
+ return input?.command || input?.cmd || input?.script || "(bash)";
259
+ }
260
+ if (toolName === "Read") {
261
+ const path = input?.path || input?.file_path || input?.file || "";
262
+ return path ? `cat ${path}` : "cat";
263
+ }
264
+ if (toolName === "Grep") {
265
+ const pattern = input?.pattern || input?.query || "";
266
+ const path = input?.path || input?.file_path || "";
267
+ if (pattern && path) return `rg \"${pattern}\" ${path}`;
268
+ if (pattern) return `rg \"${pattern}\"`;
269
+ return "rg";
270
+ }
271
+ if (toolName === "Glob") {
272
+ const pattern = input?.pattern || input?.path || "";
273
+ return pattern ? `ls ${pattern}` : "ls";
274
+ }
275
+ if (toolName === "WebSearch") {
276
+ return input?.query || "";
277
+ }
278
+ if (toolName.startsWith("mcp__")) {
279
+ return toolName;
280
+ }
281
+ return toolName || "tool";
282
+ }
283
+
284
+ function parseMcpName(name) {
285
+ if (!name.startsWith("mcp__")) return null;
286
+ const parts = name.split("__").filter(Boolean);
287
+ if (parts.length < 3) return null;
288
+ return { server: parts[1], tool: parts.slice(2).join("__") };
289
+ }
290
+
291
+ function buildToolStartEvent(toolName, input) {
292
+ if (!toolName) return null;
293
+ if (toolName === "WebSearch") {
294
+ return {
295
+ type: "item.started",
296
+ item: { type: "web_search", query: input?.query || "" },
297
+ };
298
+ }
299
+ if (toolName.startsWith("mcp__")) {
300
+ const parsed = parseMcpName(toolName);
301
+ if (!parsed) return null;
302
+ return {
303
+ type: "item.started",
304
+ item: { type: "mcp_tool_call", server: parsed.server, tool: parsed.tool },
305
+ };
306
+ }
307
+
308
+ const command = buildCommandForTool(toolName, input);
309
+ if (!command) return null;
310
+ return {
311
+ type: "item.started",
312
+ item: { type: "command_execution", command },
313
+ };
314
+ }
315
+
316
+ function buildToolResultEvent(toolName, toolInput, resultBlock) {
317
+ if (!toolName) return null;
318
+ const isError = !!resultBlock?.is_error;
319
+
320
+ if (toolName.startsWith("mcp__")) {
321
+ const parsed = parseMcpName(toolName);
322
+ if (!parsed) return null;
323
+ return {
324
+ type: "item.completed",
325
+ item: {
326
+ type: "mcp_tool_call",
327
+ server: parsed.server,
328
+ tool: parsed.tool,
329
+ status: isError ? "failed" : "completed",
330
+ },
331
+ };
332
+ }
333
+
334
+ if (toolName === "Write" || toolName === "Edit") {
335
+ const path =
336
+ toolInput?.path ||
337
+ toolInput?.file_path ||
338
+ toolInput?.file ||
339
+ toolInput?.filename ||
340
+ "";
341
+ if (!path) return null;
342
+ return {
343
+ type: "item.completed",
344
+ item: {
345
+ type: "file_change",
346
+ changes: [
347
+ {
348
+ path,
349
+ kind: toolName === "Write" ? "add" : "update",
350
+ additions: 0,
351
+ deletions: 0,
352
+ },
353
+ ],
354
+ },
355
+ };
356
+ }
357
+
358
+ const command = buildCommandForTool(toolName, toolInput);
359
+ if (!command) return null;
360
+ return {
361
+ type: "item.completed",
362
+ item: {
363
+ type: "command_execution",
364
+ command,
365
+ exit_code: isError ? 1 : 0,
366
+ aggregated_output: extractResultText(resultBlock),
367
+ },
368
+ };
369
+ }
370
+
371
+ // ── SDK Loading ─────────────────────────────────────────────────────────────
372
+
373
+ async function loadClaudeSdk() {
374
+ if (queryFn) return queryFn;
375
+ const transport = resolveClaudeTransport();
376
+ if (transport === "cli") {
377
+ console.warn(
378
+ "[claude-shell] CLAUDE_TRANSPORT=cli uses SDK compatibility mode with session-id resume",
379
+ );
380
+ }
381
+ try {
382
+ const mod = await import("@anthropic-ai/claude-agent-sdk");
383
+ queryFn = mod.query;
384
+ if (!queryFn) {
385
+ throw new Error("query() not found in Claude SDK");
386
+ }
387
+ console.log("[claude-shell] SDK loaded successfully");
388
+ return queryFn;
389
+ } catch (err) {
390
+ console.error(`[claude-shell] failed to load SDK: ${err.message}`);
391
+ return null;
392
+ }
393
+ }
394
+
395
+ // ── State Persistence ───────────────────────────────────────────────────────
396
+
397
+ async function loadState() {
398
+ try {
399
+ const raw = await readFile(STATE_FILE, "utf8");
400
+ const data = JSON.parse(raw);
401
+ activeSessionId = data.sessionId || null;
402
+ turnCount = data.turnCount || 0;
403
+ console.log(
404
+ `[claude-shell] loaded state: sessionId=${activeSessionId}, turns=${turnCount}`,
405
+ );
406
+ } catch {
407
+ activeSessionId = null;
408
+ turnCount = 0;
409
+ }
410
+ }
411
+
412
+ async function saveState() {
413
+ try {
414
+ await mkdir(resolve(__dirname, "logs"), { recursive: true });
415
+ await writeFile(
416
+ STATE_FILE,
417
+ JSON.stringify(
418
+ {
419
+ sessionId: activeSessionId,
420
+ turnCount,
421
+ updatedAt: timestamp(),
422
+ },
423
+ null,
424
+ 2,
425
+ ),
426
+ "utf8",
427
+ );
428
+ } catch (err) {
429
+ console.warn(`[claude-shell] failed to save state: ${err.message}`);
430
+ }
431
+ }
432
+
433
+ function buildPrompt(userMessage, statusData) {
434
+ if (!statusData) {
435
+ return `# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with \"Ready\" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
436
+ }
437
+ const statusSnippet = JSON.stringify(statusData, null, 2).slice(0, 2000);
438
+ return `[Orchestrator Status]\n\`\`\`json\n${statusSnippet}\n\`\`\`\n\n# YOUR TASK — EXECUTE NOW\n\n${userMessage}\n\n---\nDo NOT respond with \"Ready\" or ask what to do. EXECUTE this task. Read files, run commands, produce detailed output.`;
439
+ }
440
+
441
+ // ── Main Execution ─────────────────────────────────────────────────────────
442
+
443
+ /**
444
+ * Send a message to the Claude agent and stream events back.
445
+ *
446
+ * @param {string} userMessage
447
+ * @param {object} options
448
+ * @param {function} options.onEvent
449
+ * @param {object} options.statusData
450
+ * @param {number} options.timeoutMs
451
+ * @param {boolean} options.sendRawEvents
452
+ * @param {AbortController} options.abortController
453
+ * @returns {Promise<{finalResponse: string, items: Array, usage: object|null}>}
454
+ */
455
+ export async function execClaudePrompt(userMessage, options = {}) {
456
+ const {
457
+ onEvent = null,
458
+ statusData = null,
459
+ timeoutMs = DEFAULT_TIMEOUT_MS,
460
+ sendRawEvents = false,
461
+ abortController = null,
462
+ } = options;
463
+
464
+ if (activeTurn) {
465
+ return {
466
+ finalResponse:
467
+ "⏳ Agent is still executing a previous task. Please wait.",
468
+ items: [],
469
+ usage: null,
470
+ };
471
+ }
472
+
473
+ const query = await loadClaudeSdk();
474
+ if (!query) {
475
+ return {
476
+ finalResponse: "❌ Claude SDK not available.",
477
+ items: [],
478
+ usage: null,
479
+ };
480
+ }
481
+
482
+ activeTurn = true;
483
+ toolUseById.clear();
484
+
485
+ const transport = resolveClaudeTransport();
486
+ const shouldResume = transport === "cli";
487
+
488
+ // Fresh-session mode (default): avoid token overflow from accumulated context.
489
+ // CLI compatibility mode: keep and reuse session_id for continuation.
490
+ if (activeSessionId && !shouldResume) {
491
+ console.log(
492
+ `[claude-shell] discarding previous session ${activeSessionId} — creating fresh session per task`,
493
+ );
494
+ activeSessionId = null;
495
+ } else if (activeSessionId && shouldResume) {
496
+ console.log(`[claude-shell] resuming session ${activeSessionId}`);
497
+ }
498
+
499
+ const controller = abortController || new AbortController();
500
+ let abortReason = null;
501
+ const onAbort = () => {
502
+ abortReason = controller.signal.reason || "aborted";
503
+ try {
504
+ if (activeQuery?.interrupt) {
505
+ void activeQuery.interrupt();
506
+ }
507
+ } catch {
508
+ /* best effort */
509
+ }
510
+ if (activeQueue) {
511
+ activeQueue.close();
512
+ }
513
+ };
514
+ controller.signal.addEventListener("abort", onAbort, { once: true });
515
+
516
+ const timer = setTimeout(() => {
517
+ try {
518
+ controller.abort("timeout");
519
+ } catch {
520
+ /* noop */
521
+ }
522
+ }, timeoutMs);
523
+
524
+ let finalResponse = "";
525
+ const allItems = [];
526
+
527
+ try {
528
+ const queue = createMessageQueue();
529
+ activeQueue = queue;
530
+ queue.push(makeUserMessage(buildPrompt(userMessage, statusData)));
531
+
532
+ const optionsPayload = buildOptions();
533
+
534
+ activeQuery = query({
535
+ prompt: queue.iterator(),
536
+ options: optionsPayload,
537
+ });
538
+
539
+ for await (const message of activeQuery) {
540
+ const sessionId = message?.session_id || message?.sessionId;
541
+ if (sessionId && sessionId !== activeSessionId) {
542
+ activeSessionId = sessionId;
543
+ await saveState();
544
+ }
545
+
546
+ const contentBlocks = message?.message?.content || message?.content || [];
547
+
548
+ if (message?.type === "assistant" && Array.isArray(contentBlocks)) {
549
+ const text = extractTextBlocks(contentBlocks);
550
+ if (text) {
551
+ finalResponse += text + "\n";
552
+ }
553
+
554
+ for (const block of contentBlocks) {
555
+ if (!block) continue;
556
+
557
+ if (block.type === "thinking" && block.text) {
558
+ const event = {
559
+ type: "item.updated",
560
+ item: { type: "reasoning", text: block.text },
561
+ };
562
+ if (onEvent) {
563
+ const formatted = formatEvent(event);
564
+ if (formatted || sendRawEvents) {
565
+ await onEvent(formatted, event);
566
+ }
567
+ }
568
+ continue;
569
+ }
570
+
571
+ if (block.type === "tool_use") {
572
+ const toolName = getToolName(block);
573
+ const toolId = getToolUseId(block);
574
+ if (toolId) {
575
+ toolUseById.set(toolId, { name: toolName, input: block.input });
576
+ }
577
+ const event = buildToolStartEvent(toolName, block.input);
578
+ if (event && onEvent) {
579
+ const formatted = formatEvent(event);
580
+ if (formatted || sendRawEvents) {
581
+ await onEvent(formatted, event);
582
+ }
583
+ }
584
+ continue;
585
+ }
586
+
587
+ if (block.type === "tool_result") {
588
+ const toolId = block.tool_use_id || block.toolUseId;
589
+ const toolData = toolUseById.get(toolId) || {};
590
+ const event = buildToolResultEvent(
591
+ toolData.name,
592
+ toolData.input,
593
+ block,
594
+ );
595
+ if (event) {
596
+ allItems.push(event.item);
597
+ if (onEvent) {
598
+ const formatted = formatEvent(event);
599
+ if (formatted || sendRawEvents) {
600
+ await onEvent(formatted, event);
601
+ }
602
+ }
603
+ }
604
+ continue;
605
+ }
606
+ }
607
+ }
608
+
609
+ if (message?.type === "result" && message?.result) {
610
+ if (!finalResponse) {
611
+ finalResponse = message.result;
612
+ }
613
+ }
614
+ }
615
+
616
+ clearTimeout(timer);
617
+ turnCount += 1;
618
+ await saveState();
619
+
620
+ return {
621
+ finalResponse:
622
+ finalResponse.trim() || "(Agent completed with no text output)",
623
+ items: allItems,
624
+ usage: null,
625
+ };
626
+ } catch (err) {
627
+ clearTimeout(timer);
628
+ if (controller.signal.aborted) {
629
+ const reason = abortReason || controller.signal.reason;
630
+ const msg =
631
+ reason === "user_stop"
632
+ ? "🛑 Agent stopped by user."
633
+ : `⏱️ Agent timed out after ${timeoutMs / 1000}s`;
634
+ return { finalResponse: msg, items: [], usage: null };
635
+ }
636
+ const message = err?.message || String(err || "unknown error");
637
+ return {
638
+ finalResponse: `❌ Claude agent failed: ${message}`,
639
+ items: [],
640
+ usage: null,
641
+ };
642
+ } finally {
643
+ if (activeQueue) activeQueue.close();
644
+ activeQueue = null;
645
+ activeQuery = null;
646
+ activeTurn = false;
647
+ }
648
+ }
649
+
650
+ /**
651
+ * Try to steer an in-flight agent without stopping the run.
652
+ */
653
+ export async function steerClaudePrompt(message) {
654
+ if (!activeTurn || !activeQueue) {
655
+ return { ok: false, reason: "no_active_session" };
656
+ }
657
+ const ok = activeQueue.push(makeUserMessage(message));
658
+ if (ok) {
659
+ return { ok: true, mode: "enqueue" };
660
+ }
661
+ return { ok: false, reason: "queue_closed" };
662
+ }
663
+
664
+ /**
665
+ * Check if a turn is currently in flight.
666
+ */
667
+ export function isClaudeBusy() {
668
+ return !!activeTurn;
669
+ }
670
+
671
+ /**
672
+ * Get session info for display.
673
+ */
674
+ export function getSessionInfo() {
675
+ return {
676
+ sessionId: activeSessionId,
677
+ turnCount,
678
+ isActive: !!activeQuery,
679
+ isBusy: !!activeTurn,
680
+ };
681
+ }
682
+
683
+ /**
684
+ * Reset the session — starts a fresh conversation.
685
+ */
686
+ export async function resetClaudeSession() {
687
+ activeQuery = null;
688
+ activeQueue = null;
689
+ activeSessionId = null;
690
+ turnCount = 0;
691
+ activeTurn = false;
692
+ await saveState();
693
+ console.log("[claude-shell] session reset");
694
+ }
695
+
696
+ // ── Initialization ──────────────────────────────────────────────────────────
697
+
698
+ export async function initClaudeShell() {
699
+ await loadState();
700
+ const loaded = await loadClaudeSdk();
701
+ if (loaded) {
702
+ console.log("[claude-shell] initialised with Claude SDK");
703
+ } else {
704
+ console.warn(
705
+ "[claude-shell] initialised WITHOUT Claude SDK — agent will not work",
706
+ );
707
+ }
708
+ }