@axhub/genie 0.2.5 → 0.2.7

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 (139) hide show
  1. package/dist/assets/App-BWSqiXAT.js +220 -0
  2. package/dist/assets/App-DrlLKa8f.css +1 -0
  3. package/dist/assets/ReviewApp-nz3mbArg.js +1 -0
  4. package/dist/assets/{_basePickBy-CFRQvihx.js → _basePickBy-C19AekOu.js} +1 -1
  5. package/dist/assets/{_baseUniq-Dhh8nCvs.js → _baseUniq-JsnevLw_.js} +1 -1
  6. package/dist/assets/{arc-DQ0v3dU4.js → arc-BLpcuBlf.js} +1 -1
  7. package/dist/assets/architectureDiagram-2XIMDMQ5-CarjBOOv.js +36 -0
  8. package/dist/assets/{blockDiagram-WCTKOSBZ-Bbxhj5KC.js → blockDiagram-WCTKOSBZ-DQBLwsUS.js} +3 -3
  9. package/dist/assets/c4Diagram-IC4MRINW-CGobwBIj.js +10 -0
  10. package/dist/assets/channel-DkFNxV_H.js +1 -0
  11. package/dist/assets/{chunk-4BX2VUAB-DlvtrM0q.js → chunk-4BX2VUAB-De63kbgc.js} +1 -1
  12. package/dist/assets/{chunk-55IACEB6-DJUSHyTa.js → chunk-55IACEB6-DtTDDdM9.js} +1 -1
  13. package/dist/assets/{chunk-FMBD7UC4-C6Ch-htf.js → chunk-FMBD7UC4-DHuwd8tw.js} +1 -1
  14. package/dist/assets/{chunk-JSJVCQXG-DzQIht58.js → chunk-JSJVCQXG-BgytFtmO.js} +1 -1
  15. package/dist/assets/{chunk-KX2RTZJC-C05jARMH.js → chunk-KX2RTZJC-nZdp86aN.js} +1 -1
  16. package/dist/assets/chunk-NQ4KR5QH-CMH6EDP2.js +220 -0
  17. package/dist/assets/{chunk-QZHKN3VN-jxti9HTX.js → chunk-QZHKN3VN-DvUQ3mnO.js} +1 -1
  18. package/dist/assets/chunk-WL4C6EOR-Dn7db_6t.js +189 -0
  19. package/dist/assets/classDiagram-VBA2DB6C-DtwCEe8S.js +1 -0
  20. package/dist/assets/classDiagram-v2-RAHNMMFH-DtwCEe8S.js +1 -0
  21. package/dist/assets/clone-C0lCEIEO.js +1 -0
  22. package/dist/assets/cose-bilkent-S5V4N54A-DD_nzqsz.js +1 -0
  23. package/dist/assets/cytoscape.esm-5J0xJHOV.js +321 -0
  24. package/dist/assets/{dagre-KLK3FWXG-DJ3dNSYk.js → dagre-KLK3FWXG-CHYIvW47.js} +1 -1
  25. package/dist/assets/diagram-E7M64L7V-TVdvHtGc.js +24 -0
  26. package/dist/assets/{diagram-IFDJBPK2-Da6K4aP-.js → diagram-IFDJBPK2-Dzsiln_C.js} +1 -1
  27. package/dist/assets/{diagram-P4PSJMXO-vZZKB92A.js → diagram-P4PSJMXO-DKnGbUpE.js} +1 -1
  28. package/dist/assets/erDiagram-INFDFZHY-5Kw0bByo.js +70 -0
  29. package/dist/assets/{flowDiagram-PKNHOUZH-DUV13pHi.js → flowDiagram-PKNHOUZH-BAZ2-jKp.js} +4 -4
  30. package/dist/assets/ganttDiagram-A5KZAMGK-CsADFkcq.js +292 -0
  31. package/dist/assets/{gitGraphDiagram-K3NZZRJ6-BZ5gW69I.js → gitGraphDiagram-K3NZZRJ6-BflpyjGy.js} +1 -1
  32. package/dist/assets/{graph-BbvHswRd.js → graph-suelaXFh.js} +1 -1
  33. package/dist/assets/highlighted-body-OFNGDK62-CZrBMazC.js +1 -0
  34. package/dist/assets/index-B01NxbUv.css +1 -0
  35. package/dist/assets/index-DW5pGgQ_.js +2 -0
  36. package/dist/assets/{infoDiagram-LFFYTUFH-8auUIPKW.js → infoDiagram-LFFYTUFH-pfD1FA3p.js} +1 -1
  37. package/dist/assets/ishikawaDiagram-PHBUUO56-ndm9snwO.js +70 -0
  38. package/dist/assets/journeyDiagram-4ABVD52K-HgF2t7z5.js +139 -0
  39. package/dist/assets/{kanban-definition-K7BYSVSG-Bappd2YO.js → kanban-definition-K7BYSVSG-FWinmur1.js} +5 -5
  40. package/dist/assets/{layout-BmbfFZKy.js → layout-vcz43XvZ.js} +1 -1
  41. package/dist/assets/{linear-WZnF-PT6.js → linear-le4gc0vx.js} +1 -1
  42. package/dist/assets/mermaid-GHXKKRXX-CK8m3lad.js +870 -0
  43. package/dist/assets/mindmap-definition-YRQLILUH-CNq9SKj4.js +68 -0
  44. package/dist/assets/{pieDiagram-SKSYHLDU-uxjlAy1t.js → pieDiagram-SKSYHLDU-C7PKDh3b.js} +2 -2
  45. package/dist/assets/quadrantDiagram-337W2JSQ-B7FnztNO.js +7 -0
  46. package/dist/assets/requirementDiagram-Z7DCOOCP-Bl_BM2Th.js +73 -0
  47. package/dist/assets/{sankeyDiagram-WA2Y5GQK-2-FHHM-R.js → sankeyDiagram-WA2Y5GQK-4gulcOP4.js} +3 -3
  48. package/dist/assets/sequenceDiagram-2WXFIKYE-VEuJDwyJ.js +145 -0
  49. package/dist/assets/{stateDiagram-RAJIS63D-DoW8U53H.js → stateDiagram-RAJIS63D-CB4Vl7qM.js} +1 -1
  50. package/dist/assets/stateDiagram-v2-FVOUBMTO-C85ucl39.js +1 -0
  51. package/dist/assets/timeline-definition-YZTLITO2-BPGKhi7f.js +61 -0
  52. package/dist/assets/{treemap-KZPCXAKY-ajdAP-72.js → treemap-KZPCXAKY-DZSEE6Hz.js} +58 -58
  53. package/dist/assets/vendor-codemirror-CyOKkaQZ.js +31 -0
  54. package/dist/assets/vendor-react-CP4yFTs7.js +8 -0
  55. package/dist/assets/vendor-xterm-DfcmCpbH.js +66 -0
  56. package/dist/assets/{vennDiagram-LZ73GAT5-C9If0AT0.js → vennDiagram-LZ73GAT5-8E_G06fI.js} +4 -4
  57. package/dist/assets/xychartDiagram-JWTSCODW-CbBk50-O.js +7 -0
  58. package/dist/favicon.png +0 -0
  59. package/dist/icons/icon-128x128.png +0 -0
  60. package/dist/icons/icon-144x144.png +0 -0
  61. package/dist/icons/icon-152x152.png +0 -0
  62. package/dist/icons/icon-192x192.png +0 -0
  63. package/dist/icons/icon-384x384.png +0 -0
  64. package/dist/icons/icon-512x512.png +0 -0
  65. package/dist/icons/icon-72x72.png +0 -0
  66. package/dist/icons/icon-96x96.png +0 -0
  67. package/dist/index.html +4 -5
  68. package/dist/logo-128.png +0 -0
  69. package/dist/logo-256.png +0 -0
  70. package/dist/logo-32.png +0 -0
  71. package/dist/logo-512.png +0 -0
  72. package/dist/logo-64.png +0 -0
  73. package/package.json +2 -1
  74. package/server/_legacy-providers/README.md +30 -0
  75. package/server/_legacy-providers/claude-sdk.js +956 -0
  76. package/server/_legacy-providers/gemini-cli.js +368 -0
  77. package/server/_legacy-providers/openai-codex.js +705 -0
  78. package/server/_legacy-providers/opencode-cli.js +674 -0
  79. package/server/acp-runtime/client.js +1805 -0
  80. package/server/acp-runtime/client.test.js +688 -0
  81. package/server/acp-runtime/index.js +419 -0
  82. package/server/acp-runtime/registry.js +45 -0
  83. package/server/acp-runtime/session-store.js +254 -0
  84. package/server/acp-runtime/session-store.test.js +89 -0
  85. package/server/channels/runtime/AgentRuntimeAdapter.js +21 -70
  86. package/server/claude-sdk.js +24 -944
  87. package/server/cli.js +11 -5
  88. package/server/external-agent/service.js +77 -63
  89. package/server/gemini-cli.js +23 -360
  90. package/server/index.js +54 -46
  91. package/server/openai-codex.js +24 -698
  92. package/server/opencode-cli.js +70 -640
  93. package/server/routes/agent.js +2 -0
  94. package/server/routes/codex.js +5 -5
  95. package/server/routes/git.js +3 -20
  96. package/server/routes/mcp.js +18 -34
  97. package/server/routes/session-core.js +44 -10
  98. package/server/session-core/abortSession.js +2 -18
  99. package/server/session-core/eventStore.js +5 -1
  100. package/server/session-core/providerAdapters.js +98 -10
  101. package/server/session-core/providerDiscovery.js +2 -2
  102. package/server/session-core/runtimeState.js +16 -17
  103. package/server/session-core/runtimeWriter.js +19 -12
  104. package/server/utils/codexPath.js +3 -1
  105. package/server/utils/spawnCommand.js +7 -0
  106. package/shared/conversationEvents.js +347 -10
  107. package/shared/conversationEvents.test.js +403 -0
  108. package/dist/assets/App-BxazfNJn.js +0 -484
  109. package/dist/assets/App-qxJ8_QYu.css +0 -32
  110. package/dist/assets/ReviewApp-CsqTAlGU.js +0 -1
  111. package/dist/assets/architectureDiagram-2XIMDMQ5-DmUHdvQH.js +0 -36
  112. package/dist/assets/c4Diagram-IC4MRINW-BOivDlQU.js +0 -10
  113. package/dist/assets/channel-Cj8xVD0X.js +0 -1
  114. package/dist/assets/chunk-NQ4KR5QH-Ci-n7jfu.js +0 -220
  115. package/dist/assets/chunk-WL4C6EOR-C559Mk71.js +0 -189
  116. package/dist/assets/classDiagram-VBA2DB6C-CI2zklxw.js +0 -1
  117. package/dist/assets/classDiagram-v2-RAHNMMFH-CI2zklxw.js +0 -1
  118. package/dist/assets/clone-BEVqubrI.js +0 -1
  119. package/dist/assets/cose-bilkent-S5V4N54A-DNO9ncXL.js +0 -1
  120. package/dist/assets/cytoscape.esm-2ZfV8NB5.js +0 -331
  121. package/dist/assets/diagram-E7M64L7V-Ba_LGLun.js +0 -24
  122. package/dist/assets/erDiagram-INFDFZHY-Csb8dFdP.js +0 -70
  123. package/dist/assets/ganttDiagram-A5KZAMGK-B5Kv9Wfz.js +0 -292
  124. package/dist/assets/highlighted-body-TPN3WLV5-DZJajMGm.js +0 -1
  125. package/dist/assets/index-BFX9lxRB.css +0 -1
  126. package/dist/assets/index-BiErUGrv.js +0 -2
  127. package/dist/assets/ishikawaDiagram-PHBUUO56-JmsNlo2I.js +0 -70
  128. package/dist/assets/journeyDiagram-4ABVD52K-Cuudv7Vv.js +0 -139
  129. package/dist/assets/mermaid-O7DHMXV3-D-2fQRvw.js +0 -988
  130. package/dist/assets/mindmap-definition-YRQLILUH-BQHnzzud.js +0 -68
  131. package/dist/assets/quadrantDiagram-337W2JSQ-DpwZU-f_.js +0 -7
  132. package/dist/assets/requirementDiagram-Z7DCOOCP-C_9ClOWm.js +0 -73
  133. package/dist/assets/sequenceDiagram-2WXFIKYE-egns-0XI.js +0 -145
  134. package/dist/assets/stateDiagram-v2-FVOUBMTO-BoFZZ4Ds.js +0 -1
  135. package/dist/assets/timeline-definition-YZTLITO2-chPa8ppH.js +0 -61
  136. package/dist/assets/vendor-codemirror-Dz7_EqNA.js +0 -39
  137. package/dist/assets/vendor-react-Cpt6D04s.js +0 -59
  138. package/dist/assets/vendor-xterm-DfaPXD3y.js +0 -66
  139. package/dist/assets/xychartDiagram-JWTSCODW-DD42U6Or.js +0 -7
@@ -1,956 +1,36 @@
1
- /**
2
- * Claude SDK Integration
3
- *
4
- * This module provides SDK-based integration with Claude using the @anthropic-ai/claude-agent-sdk.
5
- * It mirrors the interface of claude-cli.js but uses the SDK internally for better performance
6
- * and maintainability.
7
- *
8
- * Key features:
9
- * - Direct SDK integration without child processes
10
- * - Session management with abort capability
11
- * - Options mapping between CLI and SDK formats
12
- * - WebSocket message streaming
13
- */
14
-
15
- import { query } from '@anthropic-ai/claude-agent-sdk';
16
- // Used to mint unique approval request IDs when randomUUID is not available.
17
- // This keeps parallel tool approvals from colliding; it does not add any crypto/security guarantees.
18
- import crypto from 'crypto';
19
- import { promises as fs } from 'fs';
20
- import path from 'path';
21
- import os from 'os';
1
+ import {
2
+ abortAgentSession,
3
+ executeAgentPrompt,
4
+ getActiveAgentSessions,
5
+ isAgentSessionActive,
6
+ resolveAgentPermission
7
+ } from './acp-runtime/index.js';
22
8
  import { CLAUDE_MODELS } from '../shared/modelConstants.js';
23
- import { resolveWorkingDirectory } from './utils/defaultWorkingDirectory.js';
24
-
25
- // Session tracking: Map of session IDs to active query instances
26
- const activeSessions = new Map();
27
- // In-memory registry of pending tool approvals keyed by requestId.
28
- // This does not persist approvals or share across processes; it exists so the
29
- // SDK can pause tool execution while the UI decides what to do.
30
- const pendingToolApprovals = new Map();
31
-
32
- // Default approval timeout kept under the SDK's 60s control timeout.
33
- // This does not change SDK limits; it only defines how long we wait for the UI,
34
- // introduced to avoid hanging the run when no decision arrives.
35
- const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
36
- const CLAUDE_STREAM_START_TIMEOUT_MS = parseInt(process.env.CLAUDE_STREAM_START_TIMEOUT_MS, 10) || 20000;
37
- const CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS = parseInt(process.env.CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS, 10) || 45000;
38
-
39
- // Generate a stable request ID for UI approval flows.
40
- // This does not encode tool details or get shown to users; it exists so the UI
41
- // can respond to the correct pending request without collisions.
42
- function createRequestId() {
43
- // if clause is used because randomUUID is not available in older Node.js versions
44
- if (typeof crypto.randomUUID === 'function') {
45
- return crypto.randomUUID();
46
- }
47
- return crypto.randomBytes(16).toString('hex');
48
- }
49
-
50
- // Wait for a UI approval decision, honoring SDK cancellation.
51
- // This does not auto-approve or auto-deny; it only resolves with UI input,
52
- // and it cleans up the pending map to avoid leaks, introduced to prevent
53
- // replying after the SDK cancels the control request.
54
- function waitForToolApproval(requestId, options = {}) {
55
- const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
56
-
57
- return new Promise(resolve => {
58
- let settled = false;
59
-
60
- const finalize = (decision) => {
61
- if (settled) return;
62
- settled = true;
63
- cleanup();
64
- resolve(decision);
65
- };
66
-
67
- const cleanup = () => {
68
- pendingToolApprovals.delete(requestId);
69
- clearTimeout(timeout);
70
- if (signal && abortHandler) {
71
- signal.removeEventListener('abort', abortHandler);
72
- }
73
- };
74
-
75
- // Timeout is local to this process; it does not override SDK timing.
76
- // It exists to prevent the UI prompt from lingering indefinitely.
77
- const timeout = setTimeout(() => {
78
- onCancel?.('timeout');
79
- finalize(null);
80
- }, timeoutMs);
81
9
 
82
- const abortHandler = () => {
83
- // If the SDK cancels the control request, stop waiting to avoid
84
- // replying after the process is no longer ready for writes.
85
- onCancel?.('cancelled');
86
- finalize({ cancelled: true });
87
- };
88
-
89
- if (signal) {
90
- if (signal.aborted) {
91
- onCancel?.('cancelled');
92
- finalize({ cancelled: true });
93
- return;
94
- }
95
- signal.addEventListener('abort', abortHandler, { once: true });
96
- }
97
-
98
- pendingToolApprovals.set(requestId, (decision) => {
99
- finalize(decision);
100
- });
10
+ export async function queryClaudeSDK(command, options = {}, writer) {
11
+ return executeAgentPrompt({
12
+ agentKey: 'claude',
13
+ command,
14
+ options: {
15
+ ...options,
16
+ model: options.model || CLAUDE_MODELS.DEFAULT
17
+ },
18
+ writer
101
19
  });
102
20
  }
103
21
 
104
- // Resolve a pending approval. This does not validate the decision payload;
105
- // validation and tool matching remain in canUseTool, which keeps this as a
106
- // lightweight WebSocket -> SDK relay.
107
- function resolveToolApproval(requestId, decision) {
108
- const resolver = pendingToolApprovals.get(requestId);
109
- if (resolver) {
110
- resolver(decision);
111
- }
22
+ export async function abortClaudeSDKSession(sessionId) {
23
+ return abortAgentSession('claude', sessionId);
112
24
  }
113
25
 
114
- // Match stored permission entries against a tool + input combo.
115
- // This only supports exact tool names and the Bash(command:*) shorthand
116
- // used by the UI; it intentionally does not implement full glob semantics,
117
- // introduced to stay consistent with the UI's "Allow rule" format.
118
- function matchesToolPermission(entry, toolName, input) {
119
- if (!entry || !toolName) {
120
- return false;
121
- }
122
-
123
- if (entry === toolName) {
124
- return true;
125
- }
126
-
127
- const bashMatch = entry.match(/^Bash\((.+):\*\)$/);
128
- if (toolName === 'Bash' && bashMatch) {
129
- const allowedPrefix = bashMatch[1];
130
- let command = '';
131
-
132
- if (typeof input === 'string') {
133
- command = input.trim();
134
- } else if (input && typeof input === 'object' && typeof input.command === 'string') {
135
- command = input.command.trim();
136
- }
137
-
138
- if (!command) {
139
- return false;
140
- }
141
-
142
- return command.startsWith(allowedPrefix);
143
- }
144
-
145
- return false;
26
+ export function isClaudeSDKSessionActive(sessionId) {
27
+ return isAgentSessionActive('claude', sessionId);
146
28
  }
147
29
 
148
- /**
149
- * Maps CLI options to SDK-compatible options format
150
- * @param {Object} options - CLI options
151
- * @returns {Object} SDK-compatible options
152
- */
153
- function mapCliOptionsToSDK(options = {}) {
154
- const { sessionId, cwd, toolsSettings, permissionMode, images } = options;
155
-
156
- const sdkOptions = {};
157
-
158
- // Map working directory
159
- if (cwd) {
160
- sdkOptions.cwd = cwd;
161
- }
162
-
163
- // Map permission mode
164
- if (permissionMode && permissionMode !== 'default') {
165
- sdkOptions.permissionMode = permissionMode;
166
- }
167
-
168
- // Map tool settings
169
- const settings = toolsSettings || {
170
- allowedTools: [],
171
- disallowedTools: [],
172
- skipPermissions: false
173
- };
174
-
175
- // Handle tool permissions
176
- if (settings.skipPermissions && permissionMode !== 'plan') {
177
- // When skipping permissions, use bypassPermissions mode
178
- sdkOptions.permissionMode = 'bypassPermissions';
179
- }
180
-
181
- // Map allowed tools (always set to avoid implicit "allow all" defaults).
182
- // This does not grant permissions by itself; it just configures the SDK,
183
- // introduced because leaving it undefined made the SDK treat it as "all tools allowed."
184
- let allowedTools = [...(settings.allowedTools || [])];
185
-
186
- // Add plan mode default tools
187
- if (permissionMode === 'plan') {
188
- const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
189
- for (const tool of planModeTools) {
190
- if (!allowedTools.includes(tool)) {
191
- allowedTools.push(tool);
192
- }
193
- }
194
- }
195
-
196
- sdkOptions.allowedTools = allowedTools;
197
-
198
- // Map disallowed tools (always set so the SDK doesn't treat "undefined" as permissive).
199
- // This does not override allowlists; it only feeds the canUseTool gate.
200
- sdkOptions.disallowedTools = settings.disallowedTools || [];
201
-
202
- // Map model (default to sonnet)
203
- // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
204
- sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
205
- console.log(`Using model: ${sdkOptions.model}`);
206
-
207
- // Map system prompt configuration
208
- sdkOptions.systemPrompt = {
209
- type: 'preset',
210
- preset: 'claude_code' // Required to use CLAUDE.md
211
- };
212
-
213
- // Map setting sources for CLAUDE.md loading
214
- // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
215
- sdkOptions.settingSources = ['project', 'user', 'local'];
216
-
217
- // Map resume session
218
- if (sessionId) {
219
- sdkOptions.resume = sessionId;
220
- }
221
-
222
- return sdkOptions;
30
+ export function getActiveClaudeSDKSessions() {
31
+ return getActiveAgentSessions('claude');
223
32
  }
224
33
 
225
- async function loadClaudeSettingsEnv() {
226
- try {
227
- const settingsPath = path.join(os.homedir(), '.claude', 'settings.json');
228
- const content = await fs.readFile(settingsPath, 'utf8');
229
- const parsed = JSON.parse(content);
230
- const env = parsed?.env;
231
-
232
- if (!env || typeof env !== 'object' || Array.isArray(env)) {
233
- return null;
234
- }
235
-
236
- const sanitizedEnv = {};
237
- for (const [key, value] of Object.entries(env)) {
238
- if (value !== undefined && value !== null) {
239
- sanitizedEnv[key] = String(value);
240
- }
241
- }
242
-
243
- return Object.keys(sanitizedEnv).length > 0 ? sanitizedEnv : null;
244
- } catch {
245
- return null;
246
- }
247
- }
248
-
249
- function getDisabledClaudeSdkMcpServers() {
250
- const configured = typeof process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS === 'string'
251
- ? process.env.CLAUDE_SDK_DISABLED_MCP_SERVERS
252
- : 'pencil';
253
-
254
- return new Set(
255
- configured
256
- .split(',')
257
- .map((value) => value.trim())
258
- .filter(Boolean)
259
- );
260
- }
261
-
262
- /**
263
- * Adds a session to the active sessions map
264
- * @param {string} sessionId - Session identifier
265
- * @param {Object} queryInstance - SDK query instance
266
- * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
267
- * @param {string} tempDir - Temp directory for cleanup
268
- */
269
- function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
270
- activeSessions.set(sessionId, {
271
- instance: queryInstance,
272
- startTime: Date.now(),
273
- status: 'active',
274
- tempImagePaths,
275
- tempDir
276
- });
277
- }
278
-
279
- /**
280
- * Removes a session from the active sessions map
281
- * @param {string} sessionId - Session identifier
282
- */
283
- function removeSession(sessionId) {
284
- activeSessions.delete(sessionId);
285
- }
286
-
287
- /**
288
- * Gets a session from the active sessions map
289
- * @param {string} sessionId - Session identifier
290
- * @returns {Object|undefined} Session data or undefined
291
- */
292
- function getSession(sessionId) {
293
- return activeSessions.get(sessionId);
294
- }
295
-
296
- /**
297
- * Gets all active session IDs
298
- * @returns {Array<string>} Array of active session IDs
299
- */
300
- function getAllSessions() {
301
- return Array.from(activeSessions.keys());
302
- }
303
-
304
- /**
305
- * Transforms SDK messages to WebSocket format expected by frontend
306
- * @param {Object} sdkMessage - SDK message object
307
- * @returns {Object} Transformed message ready for WebSocket
308
- */
309
- function transformMessage(sdkMessage) {
310
- // SDK messages are already in a format compatible with the frontend
311
- // The CLI sends them wrapped in {type: 'claude-response', data: message}
312
- // We'll do the same here to maintain compatibility
313
- return sdkMessage;
314
- }
315
-
316
- function messageHasMeaningfulClaudeOutput(message) {
317
- if (!message || typeof message !== 'object') {
318
- return false;
319
- }
320
-
321
- if (message.type === 'system' && message.subtype === 'init') {
322
- return false;
323
- }
324
-
325
- if (message.type === 'content_block_delta') {
326
- return typeof message.delta?.text === 'string' && message.delta.text.trim().length > 0;
327
- }
328
-
329
- if (message.type === 'thinking') {
330
- return typeof message.message?.content === 'string' && message.message.content.trim().length > 0;
331
- }
332
-
333
- if (message.type === 'tool_use' || message.type === 'tool_result') {
334
- return true;
335
- }
336
-
337
- const contentParts = Array.isArray(message.content)
338
- ? message.content
339
- : Array.isArray(message.message?.content)
340
- ? message.message.content
341
- : null;
342
-
343
- if (contentParts) {
344
- return contentParts.some((part) => (
345
- (part?.type === 'text' && typeof part.text === 'string' && part.text.trim().length > 0) ||
346
- part?.type === 'tool_use' ||
347
- part?.type === 'tool_result'
348
- ));
349
- }
350
-
351
- if (typeof message.content === 'string' && message.content.trim().length > 0) {
352
- return true;
353
- }
354
-
355
- return false;
356
- }
357
-
358
- /**
359
- * Extracts token usage from SDK result messages
360
- * @param {Object} resultMessage - SDK result message
361
- * @returns {Object|null} Token budget object or null
362
- */
363
- function extractTokenBudget(resultMessage) {
364
- if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
365
- return null;
366
- }
367
-
368
- // Get the first model's usage data
369
- const modelKey = Object.keys(resultMessage.modelUsage)[0];
370
- const modelData = resultMessage.modelUsage[modelKey];
371
-
372
- if (!modelData) {
373
- return null;
374
- }
375
-
376
- // Use cumulative tokens if available (tracks total for the session)
377
- // Otherwise fall back to per-request tokens
378
- const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
379
- const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
380
- const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
381
- const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
382
-
383
- // Total used = input + output + cache tokens
384
- const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
385
-
386
- // Use configured context window budget from environment (default 160000)
387
- // This is the user's budget limit, not the model's context window
388
- const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
389
-
390
- console.log(`Token calculation: input=${inputTokens}, output=${outputTokens}, cache=${cacheReadTokens + cacheCreationTokens}, total=${totalUsed}/${contextWindow}`);
391
-
392
- return {
393
- used: totalUsed,
394
- total: contextWindow
395
- };
396
- }
397
-
398
- /**
399
- * Handles image processing for SDK queries
400
- * Saves base64 images to temporary files and returns modified prompt with file paths
401
- * @param {string} command - Original user prompt
402
- * @param {Array} images - Array of image objects with base64 data
403
- * @param {string} cwd - Working directory for temp file creation
404
- * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
405
- */
406
- async function handleImages(command, images, cwd) {
407
- const tempImagePaths = [];
408
- let tempDir = null;
409
-
410
- if (!images || images.length === 0) {
411
- return { modifiedCommand: command, tempImagePaths, tempDir };
412
- }
413
-
414
- try {
415
- // Create temp directory in the project directory
416
- const workingDir = cwd || process.cwd();
417
- tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
418
- await fs.mkdir(tempDir, { recursive: true });
419
-
420
- // Save each image to a temp file
421
- for (const [index, image] of images.entries()) {
422
- // Extract base64 data and mime type
423
- const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
424
- if (!matches) {
425
- console.error('Invalid image data format');
426
- continue;
427
- }
428
-
429
- const [, mimeType, base64Data] = matches;
430
- const extension = mimeType.split('/')[1] || 'png';
431
- const filename = `image_${index}.${extension}`;
432
- const filepath = path.join(tempDir, filename);
433
-
434
- // Write base64 data to file
435
- await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
436
- tempImagePaths.push(filepath);
437
- }
438
-
439
- // Include the full image paths in the prompt
440
- let modifiedCommand = command;
441
- if (tempImagePaths.length > 0 && command && command.trim()) {
442
- const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
443
- modifiedCommand = command + imageNote;
444
- }
445
-
446
- console.log(`Processed ${tempImagePaths.length} images to temp directory: ${tempDir}`);
447
- return { modifiedCommand, tempImagePaths, tempDir };
448
- } catch (error) {
449
- console.error('Error processing images for SDK:', error);
450
- return { modifiedCommand: command, tempImagePaths, tempDir };
451
- }
34
+ export function resolveToolApproval(requestId, decision) {
35
+ return resolveAgentPermission(requestId, decision);
452
36
  }
453
-
454
- /**
455
- * Cleans up temporary image files
456
- * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
457
- * @param {string} tempDir - Temp directory to remove
458
- */
459
- async function cleanupTempFiles(tempImagePaths, tempDir) {
460
- if (!tempImagePaths || tempImagePaths.length === 0) {
461
- return;
462
- }
463
-
464
- try {
465
- // Delete individual temp files
466
- for (const imagePath of tempImagePaths) {
467
- await fs.unlink(imagePath).catch(err =>
468
- console.error(`Failed to delete temp image ${imagePath}:`, err)
469
- );
470
- }
471
-
472
- // Delete temp directory
473
- if (tempDir) {
474
- await fs.rm(tempDir, { recursive: true, force: true }).catch(err =>
475
- console.error(`Failed to delete temp directory ${tempDir}:`, err)
476
- );
477
- }
478
-
479
- console.log(`Cleaned up ${tempImagePaths.length} temp image files`);
480
- } catch (error) {
481
- console.error('Error during temp file cleanup:', error);
482
- }
483
- }
484
-
485
- /**
486
- * Loads MCP server configurations from ~/.claude.json
487
- * @param {string} cwd - Current working directory for project-specific configs
488
- * @returns {Object|null} MCP servers object or null if none found
489
- */
490
- async function loadMcpConfig(cwd) {
491
- try {
492
- const claudeConfigPath = path.join(os.homedir(), '.claude.json');
493
-
494
- // Check if config file exists
495
- try {
496
- await fs.access(claudeConfigPath);
497
- } catch (error) {
498
- // File doesn't exist, return null
499
- console.log('No ~/.claude.json found, proceeding without MCP servers');
500
- return null;
501
- }
502
-
503
- // Read and parse config file
504
- let claudeConfig;
505
- try {
506
- const configContent = await fs.readFile(claudeConfigPath, 'utf8');
507
- claudeConfig = JSON.parse(configContent);
508
- } catch (error) {
509
- console.error('Failed to parse ~/.claude.json:', error.message);
510
- return null;
511
- }
512
-
513
- // Extract MCP servers (merge global and project-specific)
514
- let mcpServers = {};
515
-
516
- // Add global MCP servers
517
- if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
518
- mcpServers = { ...claudeConfig.mcpServers };
519
- console.log(`Loaded ${Object.keys(mcpServers).length} global MCP servers`);
520
- }
521
-
522
- // Add/override with project-specific MCP servers
523
- if (claudeConfig.claudeProjects && cwd) {
524
- const projectConfig = claudeConfig.claudeProjects[cwd];
525
- if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
526
- mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
527
- console.log(`Loaded ${Object.keys(projectConfig.mcpServers).length} project-specific MCP servers`);
528
- }
529
- }
530
-
531
- // Return null if no servers found
532
- if (Object.keys(mcpServers).length === 0) {
533
- console.log('No MCP servers configured');
534
- return null;
535
- }
536
-
537
- const disabledServers = getDisabledClaudeSdkMcpServers();
538
- if (disabledServers.size > 0) {
539
- for (const serverName of Object.keys(mcpServers)) {
540
- if (disabledServers.has(serverName)) {
541
- delete mcpServers[serverName];
542
- }
543
- }
544
- }
545
-
546
- if (Object.keys(mcpServers).length === 0) {
547
- console.log('All configured MCP servers were filtered for Claude SDK runs');
548
- return null;
549
- }
550
-
551
- console.log(`Total MCP servers loaded: ${Object.keys(mcpServers).length}`);
552
- return mcpServers;
553
- } catch (error) {
554
- console.error('Error loading MCP config:', error.message);
555
- return null;
556
- }
557
- }
558
-
559
- /**
560
- * Executes a Claude query using the SDK
561
- * @param {string} command - User prompt/command
562
- * @param {Object} options - Query options
563
- * @param {Object} ws - WebSocket connection
564
- * @returns {Promise<void>}
565
- */
566
- async function queryClaudeSDK(command, options = {}, ws) {
567
- const workingDirectory = resolveWorkingDirectory(options);
568
- const effectiveOptions = {
569
- ...options,
570
- cwd: workingDirectory
571
- };
572
- const { sessionId } = options;
573
- let capturedSessionId = sessionId;
574
- let sessionCreatedSent = false;
575
- let tempImagePaths = [];
576
- let tempDir = null;
577
- let queryInstance = null;
578
- let startupTimeoutId = null;
579
- let meaningfulOutputTimeoutId = null;
580
- let timeoutTriggered = false;
581
- let timeoutErrorMessage = null;
582
-
583
- const clearStartupTimeout = () => {
584
- if (startupTimeoutId) {
585
- clearTimeout(startupTimeoutId);
586
- startupTimeoutId = null;
587
- }
588
- };
589
-
590
- const clearMeaningfulOutputTimeout = () => {
591
- if (meaningfulOutputTimeoutId) {
592
- clearTimeout(meaningfulOutputTimeoutId);
593
- meaningfulOutputTimeoutId = null;
594
- }
595
- };
596
-
597
- const interruptTimedOutQuery = () => {
598
- if (!queryInstance || typeof queryInstance.interrupt !== 'function') {
599
- return;
600
- }
601
-
602
- Promise.resolve(queryInstance.interrupt()).catch((interruptError) => {
603
- console.warn('Failed to interrupt Claude SDK query after timeout:', interruptError);
604
- });
605
- };
606
-
607
- const triggerStreamTimeout = (message) => {
608
- if (timeoutTriggered) {
609
- return;
610
- }
611
-
612
- timeoutTriggered = true;
613
- timeoutErrorMessage = message;
614
- interruptTimedOutQuery();
615
- };
616
-
617
- const armStartupTimeout = () => {
618
- clearStartupTimeout();
619
-
620
- if (!Number.isFinite(CLAUDE_STREAM_START_TIMEOUT_MS) || CLAUDE_STREAM_START_TIMEOUT_MS <= 0) {
621
- return;
622
- }
623
-
624
- startupTimeoutId = setTimeout(() => {
625
- triggerStreamTimeout(
626
- `Claude did not produce any stream events within ${Math.round(CLAUDE_STREAM_START_TIMEOUT_MS / 1000)} seconds. ` +
627
- 'This usually means the Claude client or upstream service is unavailable.'
628
- );
629
- }, CLAUDE_STREAM_START_TIMEOUT_MS);
630
- };
631
-
632
- const armMeaningfulOutputTimeout = () => {
633
- clearMeaningfulOutputTimeout();
634
-
635
- if (!Number.isFinite(CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS) || CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS <= 0) {
636
- return;
637
- }
638
-
639
- meaningfulOutputTimeoutId = setTimeout(() => {
640
- triggerStreamTimeout(
641
- `Claude did not produce any assistant output within ${Math.round(CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS / 1000)} seconds. ` +
642
- 'The upstream Claude service may be unavailable or stuck before the first response.'
643
- );
644
- }, CLAUDE_MEANINGFUL_OUTPUT_TIMEOUT_MS);
645
- };
646
-
647
- try {
648
- // Map CLI options to SDK format
649
- const sdkOptions = mapCliOptionsToSDK(effectiveOptions);
650
-
651
- const claudeSettingsEnv = await loadClaudeSettingsEnv();
652
- if (claudeSettingsEnv) {
653
- sdkOptions.env = {
654
- ...claudeSettingsEnv,
655
- ...process.env
656
- };
657
- }
658
-
659
- // Load MCP configuration
660
- const mcpServers = await loadMcpConfig(effectiveOptions.cwd);
661
- if (mcpServers) {
662
- sdkOptions.mcpServers = mcpServers;
663
- sdkOptions.strictMcpConfig = true;
664
- }
665
-
666
- // Handle images - save to temp files and modify prompt
667
- const imageResult = await handleImages(command, effectiveOptions.images, effectiveOptions.cwd);
668
- const finalCommand = imageResult.modifiedCommand;
669
- tempImagePaths = imageResult.tempImagePaths;
670
- tempDir = imageResult.tempDir;
671
-
672
- // Gate tool usage with explicit UI approval when not auto-approved.
673
- // This does not render UI or persist permissions; it only bridges to the UI
674
- // via WebSocket and waits for the response, introduced so tool calls pause
675
- // instead of auto-running when the allowlist is empty.
676
- sdkOptions.canUseTool = async (toolName, input, context) => {
677
- if (sdkOptions.permissionMode === 'bypassPermissions') {
678
- return { behavior: 'allow', updatedInput: input };
679
- }
680
-
681
- const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
682
- matchesToolPermission(entry, toolName, input)
683
- );
684
- if (isDisallowed) {
685
- return { behavior: 'deny', message: 'Tool disallowed by settings' };
686
- }
687
-
688
- const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
689
- matchesToolPermission(entry, toolName, input)
690
- );
691
- if (isAllowed) {
692
- return { behavior: 'allow', updatedInput: input };
693
- }
694
-
695
- const requestId = createRequestId();
696
- ws.send({
697
- type: 'claude-permission-request',
698
- requestId,
699
- toolName,
700
- input,
701
- sessionId: capturedSessionId || sessionId || null
702
- });
703
-
704
- // Wait for the UI; if the SDK cancels, notify the UI so it can dismiss the banner.
705
- // This does not retry or resurface the prompt; it just reflects the cancellation.
706
- const decision = await waitForToolApproval(requestId, {
707
- signal: context?.signal,
708
- onCancel: (reason) => {
709
- ws.send({
710
- type: 'claude-permission-cancelled',
711
- requestId,
712
- reason,
713
- sessionId: capturedSessionId || sessionId || null
714
- });
715
- }
716
- });
717
- if (!decision) {
718
- return { behavior: 'deny', message: 'Permission request timed out' };
719
- }
720
-
721
- if (decision.cancelled) {
722
- return { behavior: 'deny', message: 'Permission request cancelled' };
723
- }
724
-
725
- if (decision.allow) {
726
- // rememberEntry only updates this run's in-memory allowlist to prevent
727
- // repeated prompts in the same session; persistence is handled by the UI.
728
- if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
729
- if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
730
- sdkOptions.allowedTools.push(decision.rememberEntry);
731
- }
732
- if (Array.isArray(sdkOptions.disallowedTools)) {
733
- sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
734
- }
735
- }
736
- ws.send({
737
- type: 'claude-permission-resolved',
738
- requestId,
739
- status: 'approved',
740
- toolName,
741
- sessionId: capturedSessionId || sessionId || null
742
- });
743
- return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
744
- }
745
-
746
- ws.send({
747
- type: 'claude-permission-resolved',
748
- requestId,
749
- status: 'denied',
750
- toolName,
751
- message: decision.message ?? 'User denied tool use',
752
- sessionId: capturedSessionId || sessionId || null
753
- });
754
- return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
755
- };
756
-
757
- // Create SDK query instance
758
- queryInstance = query({
759
- prompt: finalCommand,
760
- options: sdkOptions
761
- });
762
-
763
- // Track the query instance for abort capability
764
- if (capturedSessionId) {
765
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
766
- }
767
-
768
- // Process streaming messages
769
- console.log('Starting async generator loop for session:', capturedSessionId || 'NEW');
770
- armStartupTimeout();
771
- armMeaningfulOutputTimeout();
772
- for await (const message of queryInstance) {
773
- clearStartupTimeout();
774
-
775
- // Capture session ID from first message
776
- if (message.session_id && !capturedSessionId) {
777
-
778
- capturedSessionId = message.session_id;
779
- addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
780
-
781
- // Set session ID on writer
782
- if (ws.setSessionId && typeof ws.setSessionId === 'function') {
783
- ws.setSessionId(capturedSessionId);
784
- }
785
-
786
- // Send session-created event only once for new sessions
787
- if (!sessionId && !sessionCreatedSent) {
788
- sessionCreatedSent = true;
789
- ws.send({
790
- type: 'session-created',
791
- sessionId: capturedSessionId,
792
- provider: 'claude'
793
- });
794
- } else {
795
- console.log('Not sending session-created. sessionId:', sessionId, 'sessionCreatedSent:', sessionCreatedSent);
796
- }
797
- } else {
798
- console.log('No session_id in message or already captured. message.session_id:', message.session_id, 'capturedSessionId:', capturedSessionId);
799
- }
800
-
801
- // Transform and send message to WebSocket
802
- const transformedMessage = transformMessage(message);
803
- ws.send({
804
- type: 'claude-response',
805
- data: transformedMessage,
806
- provider: 'claude',
807
- sessionId: capturedSessionId || sessionId || null
808
- });
809
-
810
- if (messageHasMeaningfulClaudeOutput(message)) {
811
- clearMeaningfulOutputTimeout();
812
- }
813
-
814
- // Extract and send token budget updates from result messages
815
- if (message.type === 'result') {
816
- const tokenBudget = extractTokenBudget(message);
817
- if (tokenBudget) {
818
- console.log('Token budget from modelUsage:', tokenBudget);
819
- ws.send({
820
- type: 'token-budget',
821
- data: tokenBudget,
822
- provider: 'claude',
823
- sessionId: capturedSessionId || sessionId || null
824
- });
825
- }
826
- }
827
- }
828
-
829
- if (timeoutTriggered) {
830
- throw new Error(timeoutErrorMessage);
831
- }
832
-
833
- // Clean up session on completion
834
- if (capturedSessionId) {
835
- removeSession(capturedSessionId);
836
- }
837
-
838
- // Clean up temporary image files
839
- await cleanupTempFiles(tempImagePaths, tempDir);
840
-
841
- // Send completion event
842
- console.log('Streaming complete, sending claude-complete event');
843
- ws.send({
844
- type: 'claude-complete',
845
- provider: 'claude',
846
- sessionId: capturedSessionId,
847
- exitCode: 0,
848
- isNewSession: !sessionId && !!command
849
- });
850
- console.log('claude-complete event sent');
851
-
852
- } catch (error) {
853
- console.error('SDK query error:', error);
854
-
855
- // Clean up session on error
856
- if (capturedSessionId) {
857
- removeSession(capturedSessionId);
858
- }
859
-
860
- // Clean up temporary image files on error
861
- await cleanupTempFiles(tempImagePaths, tempDir);
862
-
863
- // Provide actionable diagnostics for EBADF spawn failures.
864
- // This error is a known issue on macOS with Node.js v22+ where
865
- // child_process.spawn fails due to invalid inherited file descriptors,
866
- // especially when running via npx or in nested process contexts.
867
- let userFacingError = timeoutErrorMessage || error.message;
868
- if (error.code === 'EBADF' && error.syscall === 'spawn') {
869
- const nodeVersion = process.version;
870
- const platform = `${process.platform}/${process.arch}`;
871
- userFacingError =
872
- `Claude process failed to start (spawn EBADF). ` +
873
- `This is a known issue on macOS with Node.js v22+. ` +
874
- `Current environment: Node ${nodeVersion}, ${platform}. ` +
875
- `Possible fixes: (1) Downgrade to Node.js v20 LTS, ` +
876
- `(2) Restart the application, ` +
877
- `(3) Run with 'node server/index.js' instead of npx.`;
878
- console.error('[EBADF] Spawn failure diagnostic:', userFacingError);
879
- }
880
-
881
- // Send error to WebSocket
882
- ws.send({
883
- type: 'claude-error',
884
- error: userFacingError,
885
- provider: 'claude',
886
- sessionId: capturedSessionId || sessionId || null
887
- });
888
-
889
- throw error;
890
- } finally {
891
- clearStartupTimeout();
892
- clearMeaningfulOutputTimeout();
893
- }
894
- }
895
-
896
- /**
897
- * Aborts an active SDK session
898
- * @param {string} sessionId - Session identifier
899
- * @returns {boolean} True if session was aborted, false if not found
900
- */
901
- async function abortClaudeSDKSession(sessionId) {
902
- const session = getSession(sessionId);
903
-
904
- if (!session) {
905
- console.log(`Session ${sessionId} not found`);
906
- return false;
907
- }
908
-
909
- try {
910
- console.log(`Aborting SDK session: ${sessionId}`);
911
-
912
- // Call interrupt() on the query instance
913
- await session.instance.interrupt();
914
-
915
- // Update session status
916
- session.status = 'aborted';
917
-
918
- // Clean up temporary image files
919
- await cleanupTempFiles(session.tempImagePaths, session.tempDir);
920
-
921
- // Clean up session
922
- removeSession(sessionId);
923
-
924
- return true;
925
- } catch (error) {
926
- console.error(`Error aborting session ${sessionId}:`, error);
927
- return false;
928
- }
929
- }
930
-
931
- /**
932
- * Checks if an SDK session is currently active
933
- * @param {string} sessionId - Session identifier
934
- * @returns {boolean} True if session is active
935
- */
936
- function isClaudeSDKSessionActive(sessionId) {
937
- const session = getSession(sessionId);
938
- return session && session.status === 'active';
939
- }
940
-
941
- /**
942
- * Gets all active SDK session IDs
943
- * @returns {Array<string>} Array of active session IDs
944
- */
945
- function getActiveClaudeSDKSessions() {
946
- return getAllSessions();
947
- }
948
-
949
- // Export public API
950
- export {
951
- queryClaudeSDK,
952
- abortClaudeSDKSession,
953
- isClaudeSDKSessionActive,
954
- getActiveClaudeSDKSessions,
955
- resolveToolApproval
956
- };