upfynai-code 2.9.0 → 2.9.2

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 (229) hide show
  1. package/README.md +91 -66
  2. package/client/dist/api-docs.html +838 -0
  3. package/client/dist/assets/AppContent-BXZDeSIC.js +545 -0
  4. package/client/dist/assets/CanvasFullScreen-mnpCnLZ9.js +1 -0
  5. package/client/dist/assets/CanvasWorkspace-4CqmjAVQ.js +163 -0
  6. package/client/dist/assets/DashboardPanel-zFIFlw56.js +1 -0
  7. package/client/dist/assets/FileTree-B0c_GaB3.js +1 -0
  8. package/client/dist/assets/GitPanel-DUP4zVU4.js +2 -0
  9. package/client/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  10. package/client/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  11. package/client/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  12. package/client/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  13. package/client/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  14. package/client/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  15. package/client/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  16. package/client/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  17. package/client/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  18. package/client/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  19. package/client/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  20. package/client/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  21. package/client/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  22. package/client/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  23. package/client/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  24. package/client/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  25. package/client/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  26. package/client/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  27. package/client/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  28. package/client/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  29. package/client/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  30. package/client/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  31. package/client/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  32. package/client/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  33. package/client/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  34. package/client/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  35. package/client/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  36. package/client/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  37. package/client/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  38. package/client/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  39. package/client/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  40. package/client/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  41. package/client/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  42. package/client/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  43. package/client/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  44. package/client/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  45. package/client/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  46. package/client/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  47. package/client/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  48. package/client/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  49. package/client/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  50. package/client/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  51. package/client/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  52. package/client/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  53. package/client/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  54. package/client/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  55. package/client/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  56. package/client/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  57. package/client/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  58. package/client/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  59. package/client/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  60. package/client/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  61. package/client/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  62. package/client/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  63. package/client/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  64. package/client/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  65. package/client/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  66. package/client/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  67. package/client/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  68. package/client/dist/assets/LoginModal-BRycfsyD.js +13 -0
  69. package/client/dist/assets/MarkdownPreview-DHmk3qzu.js +1 -0
  70. package/client/dist/assets/MermaidBlock-BuBc_G-F.js +2 -0
  71. package/client/dist/assets/Onboarding-BcnaZZ0o.js +1 -0
  72. package/client/dist/assets/PreviewPanel-CqCa92Tf.js +32 -0
  73. package/client/dist/assets/SetupForm-S0g6u5yT.js +1 -0
  74. package/client/dist/assets/WorkflowsPanel-CouH9JDO.js +1 -0
  75. package/client/dist/assets/index-BFuqS0tY.css +1 -0
  76. package/client/dist/assets/index-CNDcVl2g.js +68 -0
  77. package/client/dist/assets/pdf-CE_K4jFx.js +12 -0
  78. package/client/dist/assets/vendor-canvas-BZV40eAE.css +1 -0
  79. package/client/dist/assets/vendor-canvas-D39yWul6.js +49 -0
  80. package/client/dist/assets/vendor-codemirror-CbtmxxaB.js +35 -0
  81. package/client/dist/assets/vendor-diff-DNQpbhrT.js +69 -0
  82. package/client/dist/assets/vendor-i18n-DCFGyhQR.js +1 -0
  83. package/client/dist/assets/vendor-icons-BaD0x9SL.js +711 -0
  84. package/client/dist/assets/vendor-markdown-CimbIo6Y.js +296 -0
  85. package/client/dist/assets/vendor-mermaid-CH7SGc99.js +2556 -0
  86. package/client/dist/assets/vendor-react-96lCPsRK.js +67 -0
  87. package/client/dist/assets/vendor-syntax-DuHI9Ok6.js +16 -0
  88. package/client/dist/assets/vendor-xterm-CZq1hqo1.js +66 -0
  89. package/client/dist/assets/vendor-xterm-qxJ8_QYu.css +32 -0
  90. package/client/dist/clear-cache.html +85 -0
  91. package/client/dist/convert-icons.md +53 -0
  92. package/client/dist/favicon.png +0 -0
  93. package/client/dist/favicon.svg +5 -0
  94. package/client/dist/generate-icons.js +49 -0
  95. package/client/dist/icons/claude-ai-icon.svg +1 -0
  96. package/client/dist/icons/codex-white.svg +3 -0
  97. package/client/dist/icons/codex.svg +3 -0
  98. package/client/dist/icons/cursor-white.svg +12 -0
  99. package/client/dist/icons/cursor.svg +1 -0
  100. package/client/dist/icons/icon-128x128.png +0 -0
  101. package/client/dist/icons/icon-128x128.svg +5 -0
  102. package/client/dist/icons/icon-144x144.png +0 -0
  103. package/client/dist/icons/icon-144x144.svg +5 -0
  104. package/client/dist/icons/icon-152x152.png +0 -0
  105. package/client/dist/icons/icon-152x152.svg +5 -0
  106. package/client/dist/icons/icon-192x192.png +0 -0
  107. package/client/dist/icons/icon-192x192.svg +5 -0
  108. package/client/dist/icons/icon-384x384.png +0 -0
  109. package/client/dist/icons/icon-384x384.svg +5 -0
  110. package/client/dist/icons/icon-512x512.png +0 -0
  111. package/client/dist/icons/icon-512x512.svg +5 -0
  112. package/client/dist/icons/icon-72x72.png +0 -0
  113. package/client/dist/icons/icon-72x72.svg +5 -0
  114. package/client/dist/icons/icon-96x96.png +0 -0
  115. package/client/dist/icons/icon-96x96.svg +5 -0
  116. package/client/dist/icons/icon-template.svg +5 -0
  117. package/client/dist/index.html +119 -0
  118. package/client/dist/logo-128.png +0 -0
  119. package/client/dist/logo-256.png +0 -0
  120. package/client/dist/logo-32.png +0 -0
  121. package/client/dist/logo-512.png +0 -0
  122. package/client/dist/logo-64.png +0 -0
  123. package/client/dist/logo.svg +14 -0
  124. package/client/dist/manifest.json +61 -0
  125. package/client/dist/mcp-docs.html +108 -0
  126. package/client/dist/offline.html +84 -0
  127. package/client/dist/screenshots/cli-selection.png +0 -0
  128. package/client/dist/screenshots/desktop-main.png +0 -0
  129. package/client/dist/screenshots/mobile-chat.png +0 -0
  130. package/client/dist/screenshots/tools-modal.png +0 -0
  131. package/client/dist/sw.js +82 -0
  132. package/commands/upfynai-connect.md +59 -0
  133. package/commands/upfynai-disconnect.md +31 -0
  134. package/commands/upfynai-doctor.md +99 -0
  135. package/commands/upfynai-export.md +49 -0
  136. package/commands/upfynai-local.md +82 -0
  137. package/commands/upfynai-status.md +75 -0
  138. package/commands/upfynai-stop.md +49 -0
  139. package/commands/upfynai-uninstall.md +58 -0
  140. package/commands/upfynai.md +69 -0
  141. package/package.json +143 -82
  142. package/scripts/build-client.js +17 -0
  143. package/scripts/fix-node-pty.js +67 -0
  144. package/scripts/install-commands.js +78 -0
  145. package/server/agent-loop.js +242 -0
  146. package/server/auto-compact.js +99 -0
  147. package/server/claude-sdk.js +797 -0
  148. package/server/cli-ui.js +785 -0
  149. package/server/cli.js +596 -0
  150. package/server/constants/config.js +31 -0
  151. package/server/cursor-cli.js +270 -0
  152. package/server/database/auth.db +0 -0
  153. package/server/database/db.js +1391 -0
  154. package/server/database/init.sql +70 -0
  155. package/server/index.js +3799 -0
  156. package/server/load-env.js +26 -0
  157. package/server/mcp-server.js +621 -0
  158. package/server/middleware/auth.js +176 -0
  159. package/server/middleware/relayHelpers.js +44 -0
  160. package/server/middleware/sandboxRouter.js +174 -0
  161. package/server/openai-codex.js +403 -0
  162. package/server/openrouter.js +137 -0
  163. package/server/projects.js +1807 -0
  164. package/server/provider-factory.js +174 -0
  165. package/server/relay-client.js +379 -0
  166. package/server/routes/agent.js +1226 -0
  167. package/server/routes/auth.js +554 -0
  168. package/server/routes/canvas.js +53 -0
  169. package/server/routes/cli-auth.js +263 -0
  170. package/server/routes/codex.js +396 -0
  171. package/server/routes/commands.js +707 -0
  172. package/server/routes/composio.js +176 -0
  173. package/server/routes/cursor.js +770 -0
  174. package/server/routes/dashboard.js +295 -0
  175. package/server/routes/git.js +1208 -0
  176. package/server/routes/keys.js +34 -0
  177. package/server/routes/mcp-utils.js +48 -0
  178. package/server/routes/mcp.js +661 -0
  179. package/server/routes/payments.js +227 -0
  180. package/server/routes/projects.js +655 -0
  181. package/server/routes/sessions.js +146 -0
  182. package/server/routes/settings.js +261 -0
  183. package/server/routes/taskmaster.js +1928 -0
  184. package/server/routes/user.js +106 -0
  185. package/server/routes/vapi-chat.js +624 -0
  186. package/server/routes/voice.js +235 -0
  187. package/server/routes/webhooks.js +166 -0
  188. package/server/routes/workflows.js +312 -0
  189. package/server/sandbox.js +120 -0
  190. package/server/services/composio.js +204 -0
  191. package/server/services/sessionRegistry.js +139 -0
  192. package/server/services/whisperService.js +84 -0
  193. package/server/services/workflowScheduler.js +206 -0
  194. package/server/tests/relay-flow.test.js +570 -0
  195. package/server/tests/sessions.test.js +259 -0
  196. package/server/utils/commandParser.js +303 -0
  197. package/server/utils/email.js +61 -0
  198. package/server/utils/gitConfig.js +24 -0
  199. package/server/utils/mcp-detector.js +198 -0
  200. package/server/utils/taskmaster-websocket.js +129 -0
  201. package/shared/integrationCatalog.d.ts +12 -0
  202. package/shared/integrationCatalog.js +172 -0
  203. package/shared/modelConstants.js +96 -0
  204. package/bin/cli.js +0 -97
  205. package/dist/agents/claude.js +0 -229
  206. package/dist/agents/codex.js +0 -48
  207. package/dist/agents/cursor.js +0 -48
  208. package/dist/agents/detect.js +0 -51
  209. package/dist/agents/exec.js +0 -31
  210. package/dist/agents/files.js +0 -105
  211. package/dist/agents/git.js +0 -18
  212. package/dist/agents/gitagent.js +0 -67
  213. package/dist/agents/index.js +0 -88
  214. package/dist/agents/shell.js +0 -38
  215. package/dist/agents/utils.js +0 -136
  216. package/scripts/postinstall.js +0 -9
  217. package/scripts/prepublish.js +0 -58
  218. package/src/animation.js +0 -228
  219. package/src/auth.js +0 -122
  220. package/src/config.js +0 -40
  221. package/src/connect.js +0 -416
  222. package/src/launch.js +0 -78
  223. package/src/mcp.js +0 -57
  224. package/src/permissions.js +0 -140
  225. package/src/persistent-shell.js +0 -261
  226. package/src/server.js +0 -54
  227. /package/{dist → shared}/gitagent/index.js +0 -0
  228. /package/{dist → shared}/gitagent/parser.js +0 -0
  229. /package/{dist → shared}/gitagent/prompt-builder.js +0 -0
@@ -0,0 +1,797 @@
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
+ import crypto from 'crypto';
17
+ import { promises as fs } from 'fs';
18
+ import path from 'path';
19
+ import os from 'os';
20
+ import { CLAUDE_MODELS } from '../shared/modelConstants.js';
21
+ import { parseGitagentRepo } from '../shared/gitagent/parser.js';
22
+ import { buildSystemPromptAppendix, mapGitagentModel, extractAllowedTools } from '../shared/gitagent/prompt-builder.js';
23
+
24
+ const activeSessions = new Map();
25
+ const pendingToolApprovals = new Map();
26
+
27
+ const TOOL_APPROVAL_TIMEOUT_MS = parseInt(process.env.CLAUDE_TOOL_APPROVAL_TIMEOUT_MS, 10) || 55000;
28
+
29
+ const TOOLS_REQUIRING_INTERACTION = new Set(['AskUserQuestion']);
30
+
31
+ // ── Gitagent context cache ──────────────────────────────────────────
32
+ const gitagentCache = new Map();
33
+
34
+ async function fsReadFile(filePath) {
35
+ try { return await fs.readFile(filePath, 'utf8'); } catch { return null; }
36
+ }
37
+ async function fsListDir(dirPath) {
38
+ try { return await fs.readdir(dirPath); } catch { return []; }
39
+ }
40
+
41
+ /**
42
+ * Load and cache a gitagent definition for a project path.
43
+ * Returns { systemPromptAppendix, model, allowedTools, runtime } or null.
44
+ */
45
+ async function loadGitagentContext(projectPath) {
46
+ if (!projectPath) return null;
47
+ if (gitagentCache.has(projectPath)) return gitagentCache.get(projectPath);
48
+
49
+ try {
50
+ const definition = await parseGitagentRepo(projectPath, fsReadFile, fsListDir);
51
+ if (!definition) {
52
+ gitagentCache.set(projectPath, null);
53
+ return null;
54
+ }
55
+
56
+ const ctx = {
57
+ systemPromptAppendix: buildSystemPromptAppendix(definition),
58
+ model: mapGitagentModel(definition.manifest.model),
59
+ allowedTools: extractAllowedTools(definition.skills),
60
+ runtime: definition.manifest.runtime || null,
61
+ definition,
62
+ };
63
+ gitagentCache.set(projectPath, ctx);
64
+ return ctx;
65
+ } catch {
66
+ gitagentCache.set(projectPath, null);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Clear cached gitagent context for a project (called on refresh).
73
+ */
74
+ function clearGitagentCache(projectPath) {
75
+ if (projectPath) {
76
+ gitagentCache.delete(projectPath);
77
+ } else {
78
+ gitagentCache.clear();
79
+ }
80
+ }
81
+
82
+ function createRequestId() {
83
+ if (typeof crypto.randomUUID === 'function') {
84
+ return crypto.randomUUID();
85
+ }
86
+ return crypto.randomBytes(16).toString('hex');
87
+ }
88
+
89
+ function waitForToolApproval(requestId, options = {}) {
90
+ const { timeoutMs = TOOL_APPROVAL_TIMEOUT_MS, signal, onCancel } = options;
91
+
92
+ return new Promise(resolve => {
93
+ let settled = false;
94
+
95
+ const finalize = (decision) => {
96
+ if (settled) return;
97
+ settled = true;
98
+ cleanup();
99
+ resolve(decision);
100
+ };
101
+
102
+ let timeout;
103
+
104
+ const cleanup = () => {
105
+ pendingToolApprovals.delete(requestId);
106
+ if (timeout) clearTimeout(timeout);
107
+ if (signal && abortHandler) {
108
+ signal.removeEventListener('abort', abortHandler);
109
+ }
110
+ };
111
+
112
+ // timeoutMs 0 = wait indefinitely (interactive tools)
113
+ if (timeoutMs > 0) {
114
+ timeout = setTimeout(() => {
115
+ onCancel?.('timeout');
116
+ finalize(null);
117
+ }, timeoutMs);
118
+ }
119
+
120
+ const abortHandler = () => {
121
+ onCancel?.('cancelled');
122
+ finalize({ cancelled: true });
123
+ };
124
+
125
+ if (signal) {
126
+ if (signal.aborted) {
127
+ onCancel?.('cancelled');
128
+ finalize({ cancelled: true });
129
+ return;
130
+ }
131
+ signal.addEventListener('abort', abortHandler, { once: true });
132
+ }
133
+
134
+ pendingToolApprovals.set(requestId, (decision) => {
135
+ finalize(decision);
136
+ });
137
+ });
138
+ }
139
+
140
+ function resolveToolApproval(requestId, decision) {
141
+ const resolver = pendingToolApprovals.get(requestId);
142
+ if (resolver) {
143
+ resolver(decision);
144
+ }
145
+ }
146
+
147
+ // Match stored permission entries against a tool + input combo.
148
+ // This only supports exact tool names and the Bash(command:*) shorthand
149
+ // used by the UI; it intentionally does not implement full glob semantics,
150
+ // introduced to stay consistent with the UI's "Allow rule" format.
151
+ function matchesToolPermission(entry, toolName, input) {
152
+ if (!entry || !toolName) {
153
+ return false;
154
+ }
155
+
156
+ if (entry === toolName) {
157
+ return true;
158
+ }
159
+
160
+ const bashMatch = entry.match(/^Bash\((.+):\*\)$/);
161
+ if (toolName === 'Bash' && bashMatch) {
162
+ const allowedPrefix = bashMatch[1];
163
+ let command = '';
164
+
165
+ if (typeof input === 'string') {
166
+ command = input.trim();
167
+ } else if (input && typeof input === 'object' && typeof input.command === 'string') {
168
+ command = input.command.trim();
169
+ }
170
+
171
+ if (!command) {
172
+ return false;
173
+ }
174
+
175
+ return command.startsWith(allowedPrefix);
176
+ }
177
+
178
+ return false;
179
+ }
180
+
181
+ /**
182
+ * Maps CLI options to SDK-compatible options format
183
+ * @param {Object} options - CLI options
184
+ * @returns {Object} SDK-compatible options
185
+ */
186
+ function mapCliOptionsToSDK(options = {}) {
187
+ const { sessionId, cwd, toolsSettings, permissionMode, images } = options;
188
+
189
+ const sdkOptions = {};
190
+
191
+ // Map working directory
192
+ if (cwd) {
193
+ sdkOptions.cwd = cwd;
194
+ }
195
+
196
+ // Map permission mode
197
+ if (permissionMode && permissionMode !== 'default') {
198
+ sdkOptions.permissionMode = permissionMode;
199
+ }
200
+
201
+ // Map tool settings
202
+ const settings = toolsSettings || {
203
+ allowedTools: [],
204
+ disallowedTools: [],
205
+ skipPermissions: false
206
+ };
207
+
208
+ // Handle tool permissions
209
+ if (settings.skipPermissions && permissionMode !== 'plan') {
210
+ // When skipping permissions, use bypassPermissions mode
211
+ sdkOptions.permissionMode = 'bypassPermissions';
212
+ }
213
+
214
+ let allowedTools = [...(settings.allowedTools || [])];
215
+
216
+ // Add plan mode default tools
217
+ if (permissionMode === 'plan') {
218
+ const planModeTools = ['Read', 'Task', 'exit_plan_mode', 'TodoRead', 'TodoWrite', 'WebFetch', 'WebSearch'];
219
+ for (const tool of planModeTools) {
220
+ if (!allowedTools.includes(tool)) {
221
+ allowedTools.push(tool);
222
+ }
223
+ }
224
+ }
225
+
226
+ sdkOptions.allowedTools = allowedTools;
227
+
228
+ // Use the tools preset to make all default built-in tools available (including AskUserQuestion).
229
+ // This was introduced in SDK 0.1.57. Omitting this preserves existing behavior (all tools available),
230
+ // but being explicit ensures forward compatibility and clarity.
231
+ sdkOptions.tools = { type: 'preset', preset: 'claude_code' };
232
+
233
+ sdkOptions.disallowedTools = settings.disallowedTools || [];
234
+
235
+ // Map model (default to sonnet)
236
+ // Valid models: sonnet, opus, haiku, opusplan, sonnet[1m]
237
+ sdkOptions.model = options.model || CLAUDE_MODELS.DEFAULT;
238
+ // model selected
239
+
240
+ // Map system prompt configuration
241
+ sdkOptions.systemPrompt = {
242
+ type: 'preset',
243
+ preset: 'claude_code' // Required to use CLAUDE.md
244
+ };
245
+
246
+ // Map setting sources for CLAUDE.md loading
247
+ // This loads CLAUDE.md from project, user (~/.config/claude/CLAUDE.md), and local directories
248
+ sdkOptions.settingSources = ['project', 'user', 'local'];
249
+
250
+ // Map resume session
251
+ if (sessionId) {
252
+ sdkOptions.resume = sessionId;
253
+ }
254
+
255
+ return sdkOptions;
256
+ }
257
+
258
+ /**
259
+ * Adds a session to the active sessions map
260
+ * @param {string} sessionId - Session identifier
261
+ * @param {Object} queryInstance - SDK query instance
262
+ * @param {Array<string>} tempImagePaths - Temp image file paths for cleanup
263
+ * @param {string} tempDir - Temp directory for cleanup
264
+ */
265
+ function addSession(sessionId, queryInstance, tempImagePaths = [], tempDir = null) {
266
+ activeSessions.set(sessionId, {
267
+ instance: queryInstance,
268
+ startTime: Date.now(),
269
+ status: 'active',
270
+ tempImagePaths,
271
+ tempDir
272
+ });
273
+ }
274
+
275
+ /**
276
+ * Removes a session from the active sessions map
277
+ * @param {string} sessionId - Session identifier
278
+ */
279
+ function removeSession(sessionId) {
280
+ activeSessions.delete(sessionId);
281
+ }
282
+
283
+ /**
284
+ * Gets a session from the active sessions map
285
+ * @param {string} sessionId - Session identifier
286
+ * @returns {Object|undefined} Session data or undefined
287
+ */
288
+ function getSession(sessionId) {
289
+ return activeSessions.get(sessionId);
290
+ }
291
+
292
+ /**
293
+ * Gets all active session IDs
294
+ * @returns {Array<string>} Array of active session IDs
295
+ */
296
+ function getAllSessions() {
297
+ return Array.from(activeSessions.keys());
298
+ }
299
+
300
+ /**
301
+ * Transforms SDK messages to WebSocket format expected by frontend
302
+ * @param {Object} sdkMessage - SDK message object
303
+ * @returns {Object} Transformed message ready for WebSocket
304
+ */
305
+ function transformMessage(sdkMessage) {
306
+ // Extract parent_tool_use_id for subagent tool grouping
307
+ if (sdkMessage.parent_tool_use_id) {
308
+ return {
309
+ ...sdkMessage,
310
+ parentToolUseId: sdkMessage.parent_tool_use_id
311
+ };
312
+ }
313
+ return sdkMessage;
314
+ }
315
+
316
+ /**
317
+ * Extracts token usage from SDK result messages
318
+ * @param {Object} resultMessage - SDK result message
319
+ * @returns {Object|null} Token budget object or null
320
+ */
321
+ function extractTokenBudget(resultMessage) {
322
+ if (resultMessage.type !== 'result' || !resultMessage.modelUsage) {
323
+ return null;
324
+ }
325
+
326
+ // Get the first model's usage data
327
+ const modelKey = Object.keys(resultMessage.modelUsage)[0];
328
+ const modelData = resultMessage.modelUsage[modelKey];
329
+
330
+ if (!modelData) {
331
+ return null;
332
+ }
333
+
334
+ // Use cumulative tokens if available (tracks total for the session)
335
+ // Otherwise fall back to per-request tokens
336
+ const inputTokens = modelData.cumulativeInputTokens || modelData.inputTokens || 0;
337
+ const outputTokens = modelData.cumulativeOutputTokens || modelData.outputTokens || 0;
338
+ const cacheReadTokens = modelData.cumulativeCacheReadInputTokens || modelData.cacheReadInputTokens || 0;
339
+ const cacheCreationTokens = modelData.cumulativeCacheCreationInputTokens || modelData.cacheCreationInputTokens || 0;
340
+
341
+ // Total used = input + output + cache tokens
342
+ const totalUsed = inputTokens + outputTokens + cacheReadTokens + cacheCreationTokens;
343
+
344
+ // Use configured context window budget from environment (default 160000)
345
+ // This is the user's budget limit, not the model's context window
346
+ const contextWindow = parseInt(process.env.CONTEXT_WINDOW) || 160000;
347
+
348
+ // token calculation done
349
+
350
+ return {
351
+ used: totalUsed,
352
+ total: contextWindow
353
+ };
354
+ }
355
+
356
+ /**
357
+ * Handles image processing for SDK queries
358
+ * Saves base64 images to temporary files and returns modified prompt with file paths
359
+ * @param {string} command - Original user prompt
360
+ * @param {Array} images - Array of image objects with base64 data
361
+ * @param {string} cwd - Working directory for temp file creation
362
+ * @returns {Promise<Object>} {modifiedCommand, tempImagePaths, tempDir}
363
+ */
364
+ async function handleImages(command, images, cwd) {
365
+ const tempImagePaths = [];
366
+ let tempDir = null;
367
+
368
+ if (!images || images.length === 0) {
369
+ return { modifiedCommand: command, tempImagePaths, tempDir };
370
+ }
371
+
372
+ try {
373
+ // Create temp directory in the project directory
374
+ const workingDir = cwd || process.cwd();
375
+ tempDir = path.join(workingDir, '.tmp', 'images', Date.now().toString());
376
+ await fs.mkdir(tempDir, { recursive: true });
377
+
378
+ // Save each image to a temp file
379
+ for (const [index, image] of images.entries()) {
380
+ // Extract base64 data and mime type
381
+ const matches = image.data.match(/^data:([^;]+);base64,(.+)$/);
382
+ if (!matches) {
383
+ // invalid image format
384
+ continue;
385
+ }
386
+
387
+ const [, mimeType, base64Data] = matches;
388
+ const extension = mimeType.split('/')[1] || 'png';
389
+ const filename = `image_${index}.${extension}`;
390
+ const filepath = path.join(tempDir, filename);
391
+
392
+ // Write base64 data to file
393
+ await fs.writeFile(filepath, Buffer.from(base64Data, 'base64'));
394
+ tempImagePaths.push(filepath);
395
+ }
396
+
397
+ // Include the full image paths in the prompt
398
+ let modifiedCommand = command;
399
+ if (tempImagePaths.length > 0 && command && command.trim()) {
400
+ const imageNote = `\n\n[Images provided at the following paths:]\n${tempImagePaths.map((p, i) => `${i + 1}. ${p}`).join('\n')}`;
401
+ modifiedCommand = command + imageNote;
402
+ }
403
+
404
+ // images processed
405
+ return { modifiedCommand, tempImagePaths, tempDir };
406
+ } catch (error) {
407
+ // image processing error
408
+ return { modifiedCommand: command, tempImagePaths, tempDir };
409
+ }
410
+ }
411
+
412
+ /**
413
+ * Cleans up temporary image files
414
+ * @param {Array<string>} tempImagePaths - Array of temp file paths to delete
415
+ * @param {string} tempDir - Temp directory to remove
416
+ */
417
+ async function cleanupTempFiles(tempImagePaths, tempDir) {
418
+ if (!tempImagePaths || tempImagePaths.length === 0) {
419
+ return;
420
+ }
421
+
422
+ try {
423
+ // Delete individual temp files
424
+ for (const imagePath of tempImagePaths) {
425
+ await fs.unlink(imagePath).catch(() => {});
426
+ }
427
+
428
+ // Delete temp directory
429
+ if (tempDir) {
430
+ await fs.rm(tempDir, { recursive: true, force: true }).catch(() => {});
431
+ }
432
+
433
+ // temp files cleaned
434
+ } catch (error) {
435
+ // cleanup error
436
+ }
437
+ }
438
+
439
+ /**
440
+ * Loads MCP server configurations from ~/.claude.json
441
+ * @param {string} cwd - Current working directory for project-specific configs
442
+ * @returns {Object|null} MCP servers object or null if none found
443
+ */
444
+ async function loadMcpConfig(cwd) {
445
+ try {
446
+ const claudeConfigPath = path.join(os.homedir(), '.claude.json');
447
+
448
+ // Check if config file exists
449
+ try {
450
+ await fs.access(claudeConfigPath);
451
+ } catch (error) {
452
+ // File doesn't exist, return null
453
+ // no MCP config found
454
+ return null;
455
+ }
456
+
457
+ // Read and parse config file
458
+ let claudeConfig;
459
+ try {
460
+ const configContent = await fs.readFile(claudeConfigPath, 'utf8');
461
+ claudeConfig = JSON.parse(configContent);
462
+ } catch (error) {
463
+ // MCP config parse error
464
+ return null;
465
+ }
466
+
467
+ // Extract MCP servers (merge global and project-specific)
468
+ let mcpServers = {};
469
+
470
+ // Add global MCP servers
471
+ if (claudeConfig.mcpServers && typeof claudeConfig.mcpServers === 'object') {
472
+ mcpServers = { ...claudeConfig.mcpServers };
473
+ // global MCP servers loaded
474
+ }
475
+
476
+ // Add/override with project-specific MCP servers
477
+ if (claudeConfig.claudeProjects && cwd) {
478
+ const projectConfig = claudeConfig.claudeProjects[cwd];
479
+ if (projectConfig && projectConfig.mcpServers && typeof projectConfig.mcpServers === 'object') {
480
+ mcpServers = { ...mcpServers, ...projectConfig.mcpServers };
481
+ // project MCP servers loaded
482
+ }
483
+ }
484
+
485
+ // Return null if no servers found
486
+ if (Object.keys(mcpServers).length === 0) {
487
+ // no MCP servers configured
488
+ return null;
489
+ }
490
+
491
+ // MCP config loaded
492
+ return mcpServers;
493
+ } catch (error) {
494
+ // MCP config load error
495
+ return null;
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Executes a Claude query using the SDK
501
+ * @param {string} command - User prompt/command
502
+ * @param {Object} options - Query options
503
+ * @param {Object} ws - WebSocket connection
504
+ * @returns {Promise<void>}
505
+ */
506
+ async function queryClaudeSDK(command, options = {}, ws) {
507
+ const { sessionId } = options;
508
+ let capturedSessionId = sessionId;
509
+ let sessionCreatedSent = false;
510
+ let tempImagePaths = [];
511
+ let tempDir = null;
512
+
513
+ try {
514
+ // Map CLI options to SDK format
515
+ const sdkOptions = mapCliOptionsToSDK(options);
516
+
517
+ // Inject gitagent context if the project follows the gitagent spec
518
+ if (options.cwd) {
519
+ const gitagentCtx = await loadGitagentContext(options.cwd);
520
+ if (gitagentCtx) {
521
+ // Append agent definition to the Claude Code preset system prompt
522
+ sdkOptions.systemPrompt = {
523
+ type: 'preset',
524
+ preset: 'claude_code',
525
+ append: gitagentCtx.systemPromptAppendix,
526
+ };
527
+ // Override model if the agent specifies one
528
+ if (gitagentCtx.model) {
529
+ sdkOptions.model = gitagentCtx.model;
530
+ }
531
+ // Merge allowed tools from skills
532
+ for (const t of gitagentCtx.allowedTools) {
533
+ if (!sdkOptions.allowedTools.includes(t)) {
534
+ sdkOptions.allowedTools.push(t);
535
+ }
536
+ }
537
+ // Apply max_turns from runtime config
538
+ if (gitagentCtx.runtime?.max_turns) {
539
+ sdkOptions.maxTurns = gitagentCtx.runtime.max_turns;
540
+ }
541
+ }
542
+ }
543
+
544
+ // Load MCP configuration
545
+ const mcpServers = await loadMcpConfig(options.cwd);
546
+ if (mcpServers) {
547
+ sdkOptions.mcpServers = mcpServers;
548
+ }
549
+
550
+ // Handle images - save to temp files and modify prompt
551
+ const imageResult = await handleImages(command, options.images, options.cwd);
552
+ const finalCommand = imageResult.modifiedCommand;
553
+ tempImagePaths = imageResult.tempImagePaths;
554
+ tempDir = imageResult.tempDir;
555
+
556
+ sdkOptions.canUseTool = async (toolName, input, context) => {
557
+ const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
558
+
559
+ if (!requiresInteraction) {
560
+ if (sdkOptions.permissionMode === 'bypassPermissions') {
561
+ return { behavior: 'allow', updatedInput: input };
562
+ }
563
+
564
+ const isDisallowed = (sdkOptions.disallowedTools || []).some(entry =>
565
+ matchesToolPermission(entry, toolName, input)
566
+ );
567
+ if (isDisallowed) {
568
+ return { behavior: 'deny', message: 'Tool disallowed by settings' };
569
+ }
570
+
571
+ const isAllowed = (sdkOptions.allowedTools || []).some(entry =>
572
+ matchesToolPermission(entry, toolName, input)
573
+ );
574
+ if (isAllowed) {
575
+ return { behavior: 'allow', updatedInput: input };
576
+ }
577
+ }
578
+
579
+ const requestId = createRequestId();
580
+ ws.send({
581
+ type: 'claude-permission-request',
582
+ requestId,
583
+ toolName,
584
+ input,
585
+ sessionId: capturedSessionId || sessionId || null
586
+ });
587
+
588
+ const decision = await waitForToolApproval(requestId, {
589
+ timeoutMs: requiresInteraction ? 0 : undefined,
590
+ signal: context?.signal,
591
+ onCancel: (reason) => {
592
+ ws.send({
593
+ type: 'claude-permission-cancelled',
594
+ requestId,
595
+ reason,
596
+ sessionId: capturedSessionId || sessionId || null
597
+ });
598
+ }
599
+ });
600
+ if (!decision) {
601
+ return { behavior: 'deny', message: 'Permission request timed out' };
602
+ }
603
+
604
+ if (decision.cancelled) {
605
+ return { behavior: 'deny', message: 'Permission request cancelled' };
606
+ }
607
+
608
+ if (decision.allow) {
609
+ if (decision.rememberEntry && typeof decision.rememberEntry === 'string') {
610
+ if (!sdkOptions.allowedTools.includes(decision.rememberEntry)) {
611
+ sdkOptions.allowedTools.push(decision.rememberEntry);
612
+ }
613
+ if (Array.isArray(sdkOptions.disallowedTools)) {
614
+ sdkOptions.disallowedTools = sdkOptions.disallowedTools.filter(entry => entry !== decision.rememberEntry);
615
+ }
616
+ }
617
+ return { behavior: 'allow', updatedInput: decision.updatedInput ?? input };
618
+ }
619
+
620
+ return { behavior: 'deny', message: decision.message ?? 'User denied tool use' };
621
+ };
622
+
623
+ // Set stream-close timeout for interactive tools (Query constructor reads it synchronously). Claude Agent SDK has a default of 5s and this overrides it
624
+ const prevStreamTimeout = process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
625
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = '300000';
626
+
627
+ const queryInstance = query({
628
+ prompt: finalCommand,
629
+ options: sdkOptions
630
+ });
631
+
632
+ // Restore immediately — Query constructor already captured the value
633
+ if (prevStreamTimeout !== undefined) {
634
+ process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT = prevStreamTimeout;
635
+ } else {
636
+ delete process.env.CLAUDE_CODE_STREAM_CLOSE_TIMEOUT;
637
+ }
638
+
639
+ // Track the query instance for abort capability
640
+ if (capturedSessionId) {
641
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
642
+ }
643
+
644
+ // Process streaming messages
645
+ // streaming session started
646
+ for await (const message of queryInstance) {
647
+ // Capture session ID from first message
648
+ if (message.session_id && !capturedSessionId) {
649
+
650
+ capturedSessionId = message.session_id;
651
+ addSession(capturedSessionId, queryInstance, tempImagePaths, tempDir);
652
+
653
+ // Set session ID on writer
654
+ if (ws.setSessionId && typeof ws.setSessionId === 'function') {
655
+ ws.setSessionId(capturedSessionId);
656
+ }
657
+
658
+ // Send session-created event only once for new sessions
659
+ if (!sessionId && !sessionCreatedSent) {
660
+ sessionCreatedSent = true;
661
+ ws.send({
662
+ type: 'session-created',
663
+ sessionId: capturedSessionId
664
+ });
665
+ } else {
666
+ // session-created already sent
667
+ }
668
+ } else {
669
+ // session_id already captured or missing
670
+ }
671
+
672
+ // Transform and send message to WebSocket
673
+ const transformedMessage = transformMessage(message);
674
+ ws.send({
675
+ type: 'claude-response',
676
+ data: transformedMessage,
677
+ sessionId: capturedSessionId || sessionId || null
678
+ });
679
+
680
+ // Extract and send token budget updates from result messages
681
+ if (message.type === 'result') {
682
+ const tokenBudget = extractTokenBudget(message);
683
+ if (tokenBudget) {
684
+ // token budget calculated
685
+ ws.send({
686
+ type: 'token-budget',
687
+ data: tokenBudget,
688
+ sessionId: capturedSessionId || sessionId || null
689
+ });
690
+ }
691
+ }
692
+ }
693
+
694
+ // Clean up session on completion
695
+ if (capturedSessionId) {
696
+ removeSession(capturedSessionId);
697
+ }
698
+
699
+ // Clean up temporary image files
700
+ await cleanupTempFiles(tempImagePaths, tempDir);
701
+
702
+ // Send completion event
703
+ // streaming complete
704
+ ws.send({
705
+ type: 'claude-complete',
706
+ sessionId: capturedSessionId,
707
+ exitCode: 0,
708
+ isNewSession: !sessionId && !!command
709
+ });
710
+ // complete event sent
711
+
712
+ } catch (error) {
713
+ // SDK query error occurred
714
+
715
+ // Clean up session on error
716
+ if (capturedSessionId) {
717
+ removeSession(capturedSessionId);
718
+ }
719
+
720
+ // Clean up temporary image files on error
721
+ await cleanupTempFiles(tempImagePaths, tempDir);
722
+
723
+ // Send error to WebSocket
724
+ ws.send({
725
+ type: 'claude-error',
726
+ error: error.message,
727
+ sessionId: capturedSessionId || sessionId || null
728
+ });
729
+
730
+ throw error;
731
+ }
732
+ }
733
+
734
+ /**
735
+ * Aborts an active SDK session
736
+ * @param {string} sessionId - Session identifier
737
+ * @returns {boolean} True if session was aborted, false if not found
738
+ */
739
+ async function abortClaudeSDKSession(sessionId) {
740
+ const session = getSession(sessionId);
741
+
742
+ if (!session) {
743
+ // session not found
744
+ return false;
745
+ }
746
+
747
+ try {
748
+ // aborting SDK session
749
+
750
+ // Call interrupt() on the query instance
751
+ await session.instance.interrupt();
752
+
753
+ // Update session status
754
+ session.status = 'aborted';
755
+
756
+ // Clean up temporary image files
757
+ await cleanupTempFiles(session.tempImagePaths, session.tempDir);
758
+
759
+ // Clean up session
760
+ removeSession(sessionId);
761
+
762
+ return true;
763
+ } catch (error) {
764
+ // abort error occurred
765
+ return false;
766
+ }
767
+ }
768
+
769
+ /**
770
+ * Checks if an SDK session is currently active
771
+ * @param {string} sessionId - Session identifier
772
+ * @returns {boolean} True if session is active
773
+ */
774
+ function isClaudeSDKSessionActive(sessionId) {
775
+ const session = getSession(sessionId);
776
+ return session && session.status === 'active';
777
+ }
778
+
779
+ /**
780
+ * Gets all active SDK session IDs
781
+ * @returns {Array<string>} Array of active session IDs
782
+ */
783
+ function getActiveClaudeSDKSessions() {
784
+ return getAllSessions();
785
+ }
786
+
787
+ // Export public API
788
+ export {
789
+ queryClaudeSDK,
790
+ abortClaudeSDKSession,
791
+ isClaudeSDKSessionActive,
792
+ getActiveClaudeSDKSessions,
793
+ resolveToolApproval,
794
+ loadGitagentContext,
795
+ clearGitagentCache,
796
+ extractTokenBudget
797
+ };