whale-code 6.4.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 (319) hide show
  1. package/README.md +95 -0
  2. package/bin/swag-agent.js +9 -0
  3. package/bin/swagmanager-mcp.js +321 -0
  4. package/dist/cli/app.d.ts +26 -0
  5. package/dist/cli/app.js +64 -0
  6. package/dist/cli/chat/AgentSelector.d.ts +14 -0
  7. package/dist/cli/chat/AgentSelector.js +14 -0
  8. package/dist/cli/chat/ChatApp.d.ts +9 -0
  9. package/dist/cli/chat/ChatApp.js +267 -0
  10. package/dist/cli/chat/ChatInput.d.ts +39 -0
  11. package/dist/cli/chat/ChatInput.js +509 -0
  12. package/dist/cli/chat/MarkdownText.d.ts +10 -0
  13. package/dist/cli/chat/MarkdownText.js +20 -0
  14. package/dist/cli/chat/MessageList.d.ts +37 -0
  15. package/dist/cli/chat/MessageList.js +80 -0
  16. package/dist/cli/chat/ModelSelector.d.ts +20 -0
  17. package/dist/cli/chat/ModelSelector.js +73 -0
  18. package/dist/cli/chat/RewindViewer.d.ts +26 -0
  19. package/dist/cli/chat/RewindViewer.js +185 -0
  20. package/dist/cli/chat/StoreSelector.d.ts +14 -0
  21. package/dist/cli/chat/StoreSelector.js +24 -0
  22. package/dist/cli/chat/StreamingText.d.ts +12 -0
  23. package/dist/cli/chat/StreamingText.js +12 -0
  24. package/dist/cli/chat/SubagentPanel.d.ts +45 -0
  25. package/dist/cli/chat/SubagentPanel.js +110 -0
  26. package/dist/cli/chat/TeamPanel.d.ts +21 -0
  27. package/dist/cli/chat/TeamPanel.js +42 -0
  28. package/dist/cli/chat/ToolIndicator.d.ts +25 -0
  29. package/dist/cli/chat/ToolIndicator.js +436 -0
  30. package/dist/cli/chat/hooks/useAgentLoop.d.ts +39 -0
  31. package/dist/cli/chat/hooks/useAgentLoop.js +382 -0
  32. package/dist/cli/chat/hooks/useSlashCommands.d.ts +37 -0
  33. package/dist/cli/chat/hooks/useSlashCommands.js +387 -0
  34. package/dist/cli/commands/config-cmd.d.ts +10 -0
  35. package/dist/cli/commands/config-cmd.js +99 -0
  36. package/dist/cli/commands/doctor.d.ts +14 -0
  37. package/dist/cli/commands/doctor.js +172 -0
  38. package/dist/cli/commands/init.d.ts +16 -0
  39. package/dist/cli/commands/init.js +278 -0
  40. package/dist/cli/commands/mcp.d.ts +12 -0
  41. package/dist/cli/commands/mcp.js +162 -0
  42. package/dist/cli/login/LoginApp.d.ts +7 -0
  43. package/dist/cli/login/LoginApp.js +157 -0
  44. package/dist/cli/print-mode.d.ts +31 -0
  45. package/dist/cli/print-mode.js +202 -0
  46. package/dist/cli/serve-mode.d.ts +37 -0
  47. package/dist/cli/serve-mode.js +636 -0
  48. package/dist/cli/services/agent-definitions.d.ts +25 -0
  49. package/dist/cli/services/agent-definitions.js +91 -0
  50. package/dist/cli/services/agent-events.d.ts +178 -0
  51. package/dist/cli/services/agent-events.js +175 -0
  52. package/dist/cli/services/agent-loop.d.ts +90 -0
  53. package/dist/cli/services/agent-loop.js +762 -0
  54. package/dist/cli/services/agent-worker-base.d.ts +97 -0
  55. package/dist/cli/services/agent-worker-base.js +220 -0
  56. package/dist/cli/services/auth-service.d.ts +30 -0
  57. package/dist/cli/services/auth-service.js +160 -0
  58. package/dist/cli/services/background-processes.d.ts +126 -0
  59. package/dist/cli/services/background-processes.js +318 -0
  60. package/dist/cli/services/browser-auth.d.ts +24 -0
  61. package/dist/cli/services/browser-auth.js +180 -0
  62. package/dist/cli/services/claude-md-loader.d.ts +16 -0
  63. package/dist/cli/services/claude-md-loader.js +58 -0
  64. package/dist/cli/services/config-store.d.ts +47 -0
  65. package/dist/cli/services/config-store.js +79 -0
  66. package/dist/cli/services/debug-log.d.ts +10 -0
  67. package/dist/cli/services/debug-log.js +52 -0
  68. package/dist/cli/services/error-logger.d.ts +58 -0
  69. package/dist/cli/services/error-logger.js +269 -0
  70. package/dist/cli/services/file-history.d.ts +21 -0
  71. package/dist/cli/services/file-history.js +83 -0
  72. package/dist/cli/services/format-server-response.d.ts +16 -0
  73. package/dist/cli/services/format-server-response.js +440 -0
  74. package/dist/cli/services/git-context.d.ts +11 -0
  75. package/dist/cli/services/git-context.js +66 -0
  76. package/dist/cli/services/hooks.d.ts +85 -0
  77. package/dist/cli/services/hooks.js +258 -0
  78. package/dist/cli/services/interactive-tools.d.ts +125 -0
  79. package/dist/cli/services/interactive-tools.js +260 -0
  80. package/dist/cli/services/keybinding-manager.d.ts +52 -0
  81. package/dist/cli/services/keybinding-manager.js +115 -0
  82. package/dist/cli/services/local-tools.d.ts +22 -0
  83. package/dist/cli/services/local-tools.js +697 -0
  84. package/dist/cli/services/lsp-manager.d.ts +18 -0
  85. package/dist/cli/services/lsp-manager.js +717 -0
  86. package/dist/cli/services/mcp-client.d.ts +48 -0
  87. package/dist/cli/services/mcp-client.js +157 -0
  88. package/dist/cli/services/memory-manager.d.ts +16 -0
  89. package/dist/cli/services/memory-manager.js +57 -0
  90. package/dist/cli/services/model-manager.d.ts +18 -0
  91. package/dist/cli/services/model-manager.js +71 -0
  92. package/dist/cli/services/model-router.d.ts +26 -0
  93. package/dist/cli/services/model-router.js +149 -0
  94. package/dist/cli/services/permission-modes.d.ts +13 -0
  95. package/dist/cli/services/permission-modes.js +43 -0
  96. package/dist/cli/services/rewind.d.ts +84 -0
  97. package/dist/cli/services/rewind.js +194 -0
  98. package/dist/cli/services/ripgrep.d.ts +28 -0
  99. package/dist/cli/services/ripgrep.js +138 -0
  100. package/dist/cli/services/sandbox.d.ts +29 -0
  101. package/dist/cli/services/sandbox.js +97 -0
  102. package/dist/cli/services/server-tools.d.ts +61 -0
  103. package/dist/cli/services/server-tools.js +543 -0
  104. package/dist/cli/services/session-persistence.d.ts +23 -0
  105. package/dist/cli/services/session-persistence.js +99 -0
  106. package/dist/cli/services/subagent-worker.d.ts +19 -0
  107. package/dist/cli/services/subagent-worker.js +41 -0
  108. package/dist/cli/services/subagent.d.ts +47 -0
  109. package/dist/cli/services/subagent.js +647 -0
  110. package/dist/cli/services/system-prompt.d.ts +7 -0
  111. package/dist/cli/services/system-prompt.js +198 -0
  112. package/dist/cli/services/team-lead.d.ts +73 -0
  113. package/dist/cli/services/team-lead.js +512 -0
  114. package/dist/cli/services/team-state.d.ts +77 -0
  115. package/dist/cli/services/team-state.js +398 -0
  116. package/dist/cli/services/teammate.d.ts +31 -0
  117. package/dist/cli/services/teammate.js +689 -0
  118. package/dist/cli/services/telemetry.d.ts +61 -0
  119. package/dist/cli/services/telemetry.js +209 -0
  120. package/dist/cli/services/tools/agent-tools.d.ts +14 -0
  121. package/dist/cli/services/tools/agent-tools.js +347 -0
  122. package/dist/cli/services/tools/file-ops.d.ts +15 -0
  123. package/dist/cli/services/tools/file-ops.js +487 -0
  124. package/dist/cli/services/tools/search-tools.d.ts +8 -0
  125. package/dist/cli/services/tools/search-tools.js +186 -0
  126. package/dist/cli/services/tools/shell-exec.d.ts +10 -0
  127. package/dist/cli/services/tools/shell-exec.js +168 -0
  128. package/dist/cli/services/tools/task-manager.d.ts +28 -0
  129. package/dist/cli/services/tools/task-manager.js +209 -0
  130. package/dist/cli/services/tools/web-tools.d.ts +11 -0
  131. package/dist/cli/services/tools/web-tools.js +395 -0
  132. package/dist/cli/setup/SetupApp.d.ts +9 -0
  133. package/dist/cli/setup/SetupApp.js +191 -0
  134. package/dist/cli/shared/MatrixIntro.d.ts +4 -0
  135. package/dist/cli/shared/MatrixIntro.js +83 -0
  136. package/dist/cli/shared/Theme.d.ts +74 -0
  137. package/dist/cli/shared/Theme.js +127 -0
  138. package/dist/cli/shared/WhaleBanner.d.ts +10 -0
  139. package/dist/cli/shared/WhaleBanner.js +12 -0
  140. package/dist/cli/shared/markdown.d.ts +21 -0
  141. package/dist/cli/shared/markdown.js +756 -0
  142. package/dist/cli/status/StatusApp.d.ts +4 -0
  143. package/dist/cli/status/StatusApp.js +105 -0
  144. package/dist/cli/stores/StoreApp.d.ts +7 -0
  145. package/dist/cli/stores/StoreApp.js +81 -0
  146. package/dist/index.d.ts +15 -0
  147. package/dist/index.js +538 -0
  148. package/dist/local-agent/connection.d.ts +48 -0
  149. package/dist/local-agent/connection.js +332 -0
  150. package/dist/local-agent/discovery.d.ts +18 -0
  151. package/dist/local-agent/discovery.js +146 -0
  152. package/dist/local-agent/executor.d.ts +34 -0
  153. package/dist/local-agent/executor.js +241 -0
  154. package/dist/local-agent/index.d.ts +14 -0
  155. package/dist/local-agent/index.js +198 -0
  156. package/dist/node/adapters/base.d.ts +35 -0
  157. package/dist/node/adapters/base.js +10 -0
  158. package/dist/node/adapters/discord.d.ts +29 -0
  159. package/dist/node/adapters/discord.js +299 -0
  160. package/dist/node/adapters/email.d.ts +23 -0
  161. package/dist/node/adapters/email.js +218 -0
  162. package/dist/node/adapters/imessage.d.ts +17 -0
  163. package/dist/node/adapters/imessage.js +118 -0
  164. package/dist/node/adapters/slack.d.ts +26 -0
  165. package/dist/node/adapters/slack.js +259 -0
  166. package/dist/node/adapters/sms.d.ts +23 -0
  167. package/dist/node/adapters/sms.js +161 -0
  168. package/dist/node/adapters/telegram.d.ts +17 -0
  169. package/dist/node/adapters/telegram.js +101 -0
  170. package/dist/node/adapters/webchat.d.ts +27 -0
  171. package/dist/node/adapters/webchat.js +160 -0
  172. package/dist/node/adapters/whatsapp.d.ts +28 -0
  173. package/dist/node/adapters/whatsapp.js +230 -0
  174. package/dist/node/cli.d.ts +2 -0
  175. package/dist/node/cli.js +325 -0
  176. package/dist/node/config.d.ts +17 -0
  177. package/dist/node/config.js +31 -0
  178. package/dist/node/runtime.d.ts +50 -0
  179. package/dist/node/runtime.js +351 -0
  180. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +11 -0
  181. package/dist/server/handlers/__test-utils__/mock-supabase.js +393 -0
  182. package/dist/server/handlers/analytics.d.ts +17 -0
  183. package/dist/server/handlers/analytics.js +266 -0
  184. package/dist/server/handlers/api-keys.d.ts +6 -0
  185. package/dist/server/handlers/api-keys.js +221 -0
  186. package/dist/server/handlers/billing.d.ts +33 -0
  187. package/dist/server/handlers/billing.js +272 -0
  188. package/dist/server/handlers/browser.d.ts +10 -0
  189. package/dist/server/handlers/browser.js +517 -0
  190. package/dist/server/handlers/catalog.d.ts +99 -0
  191. package/dist/server/handlers/catalog.js +976 -0
  192. package/dist/server/handlers/comms.d.ts +254 -0
  193. package/dist/server/handlers/comms.js +588 -0
  194. package/dist/server/handlers/creations.d.ts +6 -0
  195. package/dist/server/handlers/creations.js +479 -0
  196. package/dist/server/handlers/crm.d.ts +89 -0
  197. package/dist/server/handlers/crm.js +538 -0
  198. package/dist/server/handlers/discovery.d.ts +6 -0
  199. package/dist/server/handlers/discovery.js +288 -0
  200. package/dist/server/handlers/embeddings.d.ts +92 -0
  201. package/dist/server/handlers/embeddings.js +197 -0
  202. package/dist/server/handlers/enrichment.d.ts +8 -0
  203. package/dist/server/handlers/enrichment.js +768 -0
  204. package/dist/server/handlers/image-gen.d.ts +6 -0
  205. package/dist/server/handlers/image-gen.js +409 -0
  206. package/dist/server/handlers/inventory.d.ts +319 -0
  207. package/dist/server/handlers/inventory.js +447 -0
  208. package/dist/server/handlers/kali.d.ts +10 -0
  209. package/dist/server/handlers/kali.js +210 -0
  210. package/dist/server/handlers/llm-providers.d.ts +6 -0
  211. package/dist/server/handlers/llm-providers.js +673 -0
  212. package/dist/server/handlers/local-agent.d.ts +6 -0
  213. package/dist/server/handlers/local-agent.js +118 -0
  214. package/dist/server/handlers/meta-ads.d.ts +111 -0
  215. package/dist/server/handlers/meta-ads.js +2279 -0
  216. package/dist/server/handlers/nodes.d.ts +33 -0
  217. package/dist/server/handlers/nodes.js +699 -0
  218. package/dist/server/handlers/operations.d.ts +138 -0
  219. package/dist/server/handlers/operations.js +131 -0
  220. package/dist/server/handlers/platform.d.ts +23 -0
  221. package/dist/server/handlers/platform.js +227 -0
  222. package/dist/server/handlers/supply-chain.d.ts +19 -0
  223. package/dist/server/handlers/supply-chain.js +327 -0
  224. package/dist/server/handlers/transcription.d.ts +17 -0
  225. package/dist/server/handlers/transcription.js +121 -0
  226. package/dist/server/handlers/video-gen.d.ts +6 -0
  227. package/dist/server/handlers/video-gen.js +466 -0
  228. package/dist/server/handlers/voice.d.ts +8 -0
  229. package/dist/server/handlers/voice.js +1146 -0
  230. package/dist/server/handlers/workflow-steps.d.ts +86 -0
  231. package/dist/server/handlers/workflow-steps.js +2349 -0
  232. package/dist/server/handlers/workflows.d.ts +7 -0
  233. package/dist/server/handlers/workflows.js +989 -0
  234. package/dist/server/index.d.ts +1 -0
  235. package/dist/server/index.js +2427 -0
  236. package/dist/server/lib/batch-client.d.ts +80 -0
  237. package/dist/server/lib/batch-client.js +467 -0
  238. package/dist/server/lib/code-worker-pool.d.ts +31 -0
  239. package/dist/server/lib/code-worker-pool.js +224 -0
  240. package/dist/server/lib/code-worker.d.ts +1 -0
  241. package/dist/server/lib/code-worker.js +188 -0
  242. package/dist/server/lib/compaction-service.d.ts +32 -0
  243. package/dist/server/lib/compaction-service.js +162 -0
  244. package/dist/server/lib/logger.d.ts +19 -0
  245. package/dist/server/lib/logger.js +46 -0
  246. package/dist/server/lib/otel.d.ts +38 -0
  247. package/dist/server/lib/otel.js +126 -0
  248. package/dist/server/lib/pg-rate-limiter.d.ts +21 -0
  249. package/dist/server/lib/pg-rate-limiter.js +86 -0
  250. package/dist/server/lib/prompt-sanitizer.d.ts +37 -0
  251. package/dist/server/lib/prompt-sanitizer.js +177 -0
  252. package/dist/server/lib/provider-capabilities.d.ts +85 -0
  253. package/dist/server/lib/provider-capabilities.js +190 -0
  254. package/dist/server/lib/provider-failover.d.ts +74 -0
  255. package/dist/server/lib/provider-failover.js +210 -0
  256. package/dist/server/lib/rate-limiter.d.ts +39 -0
  257. package/dist/server/lib/rate-limiter.js +147 -0
  258. package/dist/server/lib/server-agent-loop.d.ts +107 -0
  259. package/dist/server/lib/server-agent-loop.js +667 -0
  260. package/dist/server/lib/server-subagent.d.ts +78 -0
  261. package/dist/server/lib/server-subagent.js +203 -0
  262. package/dist/server/lib/session-checkpoint.d.ts +51 -0
  263. package/dist/server/lib/session-checkpoint.js +145 -0
  264. package/dist/server/lib/ssrf-guard.d.ts +13 -0
  265. package/dist/server/lib/ssrf-guard.js +240 -0
  266. package/dist/server/lib/supabase-client.d.ts +7 -0
  267. package/dist/server/lib/supabase-client.js +78 -0
  268. package/dist/server/lib/template-resolver.d.ts +31 -0
  269. package/dist/server/lib/template-resolver.js +215 -0
  270. package/dist/server/lib/utils.d.ts +16 -0
  271. package/dist/server/lib/utils.js +147 -0
  272. package/dist/server/local-agent-gateway.d.ts +82 -0
  273. package/dist/server/local-agent-gateway.js +426 -0
  274. package/dist/server/providers/anthropic.d.ts +20 -0
  275. package/dist/server/providers/anthropic.js +199 -0
  276. package/dist/server/providers/bedrock.d.ts +20 -0
  277. package/dist/server/providers/bedrock.js +194 -0
  278. package/dist/server/providers/gemini.d.ts +24 -0
  279. package/dist/server/providers/gemini.js +486 -0
  280. package/dist/server/providers/openai.d.ts +24 -0
  281. package/dist/server/providers/openai.js +522 -0
  282. package/dist/server/providers/registry.d.ts +32 -0
  283. package/dist/server/providers/registry.js +58 -0
  284. package/dist/server/providers/shared.d.ts +32 -0
  285. package/dist/server/providers/shared.js +124 -0
  286. package/dist/server/providers/types.d.ts +92 -0
  287. package/dist/server/providers/types.js +12 -0
  288. package/dist/server/proxy-handlers.d.ts +6 -0
  289. package/dist/server/proxy-handlers.js +89 -0
  290. package/dist/server/tool-router.d.ts +149 -0
  291. package/dist/server/tool-router.js +803 -0
  292. package/dist/server/validation.d.ts +24 -0
  293. package/dist/server/validation.js +301 -0
  294. package/dist/server/worker.d.ts +19 -0
  295. package/dist/server/worker.js +201 -0
  296. package/dist/setup.d.ts +8 -0
  297. package/dist/setup.js +181 -0
  298. package/dist/shared/agent-core.d.ts +157 -0
  299. package/dist/shared/agent-core.js +534 -0
  300. package/dist/shared/anthropic-types.d.ts +105 -0
  301. package/dist/shared/anthropic-types.js +7 -0
  302. package/dist/shared/api-client.d.ts +90 -0
  303. package/dist/shared/api-client.js +379 -0
  304. package/dist/shared/constants.d.ts +33 -0
  305. package/dist/shared/constants.js +80 -0
  306. package/dist/shared/sse-parser.d.ts +26 -0
  307. package/dist/shared/sse-parser.js +259 -0
  308. package/dist/shared/tool-dispatch.d.ts +52 -0
  309. package/dist/shared/tool-dispatch.js +191 -0
  310. package/dist/shared/types.d.ts +72 -0
  311. package/dist/shared/types.js +7 -0
  312. package/dist/updater.d.ts +25 -0
  313. package/dist/updater.js +140 -0
  314. package/dist/webchat/widget.d.ts +0 -0
  315. package/dist/webchat/widget.js +397 -0
  316. package/package.json +95 -0
  317. package/src/cli/services/builtin-skills/commit.md +19 -0
  318. package/src/cli/services/builtin-skills/review-pr.md +21 -0
  319. package/src/cli/services/builtin-skills/review.md +18 -0
@@ -0,0 +1,447 @@
1
+ // server/handlers/inventory.ts — Inventory management handlers
2
+ export async function handleInventory(sb, args, storeId) {
3
+ const sid = storeId;
4
+ switch (args.action) {
5
+ case "adjust": {
6
+ const productId = args.product_id;
7
+ const locationId = args.location_id;
8
+ const adjustment = args.adjustment;
9
+ const reason = args.reason || "Manual adjustment";
10
+ const { data: product } = await sb.from("products").select("name, sku").eq("id", productId).eq("store_id", sid).single();
11
+ const { data: location } = await sb.from("locations").select("name").eq("id", locationId).eq("store_id", sid).single();
12
+ // Atomic RPC — row-locked, includes audit trail
13
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_adjust", {
14
+ p_store_id: sid, p_product_id: productId, p_location_id: locationId,
15
+ p_adjustment: adjustment, p_reason: reason,
16
+ });
17
+ if (rpcError)
18
+ return { success: false, error: rpcError.message };
19
+ const rpcResult = result;
20
+ if (!rpcResult.success)
21
+ return { success: false, error: rpcResult.error || "Adjust failed" };
22
+ const d = rpcResult.data || {};
23
+ const sign = adjustment >= 0 ? "+" : "";
24
+ return {
25
+ success: true,
26
+ data: {
27
+ intent: `Adjust inventory for ${product?.name || 'product'} at ${location?.name || 'location'}: ${sign}${adjustment} units`,
28
+ product: product ? { id: productId, name: product.name, sku: product.sku } : { id: productId },
29
+ location: location ? { id: locationId, name: location.name } : { id: locationId },
30
+ adjustment, reason,
31
+ before_state: { quantity: d.before },
32
+ after_state: { quantity: d.after },
33
+ change: { from: d.before, to: d.after, delta: adjustment }
34
+ }
35
+ };
36
+ }
37
+ case "set": {
38
+ const productId = args.product_id;
39
+ const locationId = args.location_id;
40
+ const newQty = args.quantity;
41
+ const { data: product } = await sb.from("products").select("name, sku").eq("id", productId).eq("store_id", sid).single();
42
+ const { data: location } = await sb.from("locations").select("name").eq("id", locationId).eq("store_id", sid).single();
43
+ // Atomic RPC — row-locked, includes audit trail
44
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_set", {
45
+ p_store_id: sid, p_product_id: productId, p_location_id: locationId,
46
+ p_quantity: newQty, p_reason: `Set to ${newQty}`,
47
+ });
48
+ if (rpcError)
49
+ return { success: false, error: rpcError.message };
50
+ const rpcResult = result;
51
+ if (!rpcResult.success)
52
+ return { success: false, error: rpcResult.error || "Set failed" };
53
+ const d = rpcResult.data || {};
54
+ const delta = d.delta || 0;
55
+ const sign = delta >= 0 ? "+" : "";
56
+ return {
57
+ success: true,
58
+ data: {
59
+ intent: `Set inventory for ${product?.name || 'product'} at ${location?.name || 'location'} to ${newQty} units`,
60
+ product: product ? { id: productId, name: product.name, sku: product.sku } : { id: productId },
61
+ location: location ? { id: locationId, name: location.name } : { id: locationId },
62
+ before_state: { quantity: d.before },
63
+ after_state: { quantity: d.after },
64
+ change: { from: d.before, to: d.after, delta, description: `${sign}${delta} units` }
65
+ }
66
+ };
67
+ }
68
+ case "transfer": {
69
+ // C3 FIX: Use atomic RPC with row-level locking instead of separate upserts
70
+ const qty = args.quantity;
71
+ const productId = args.product_id;
72
+ const fromLocationId = args.from_location_id;
73
+ const toLocationId = args.to_location_id;
74
+ const { data: product } = await sb.from("products").select("name, sku").eq("id", productId).eq("store_id", sid).single();
75
+ const { data: fromLocation } = await sb.from("locations").select("name").eq("id", fromLocationId).eq("store_id", sid).single();
76
+ const { data: toLocation } = await sb.from("locations").select("name").eq("id", toLocationId).eq("store_id", sid).single();
77
+ // Atomic transfer — row-locked, transactional, no race conditions
78
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_transfer", {
79
+ p_store_id: sid,
80
+ p_product_id: productId,
81
+ p_from_location_id: fromLocationId,
82
+ p_to_location_id: toLocationId,
83
+ p_quantity: qty,
84
+ });
85
+ if (rpcError) {
86
+ return { success: false, error: rpcError.message };
87
+ }
88
+ const rpcResult = result;
89
+ if (!rpcResult.success) {
90
+ return { success: false, error: rpcResult.error || "Transfer failed" };
91
+ }
92
+ const transferData = rpcResult.data || {};
93
+ return {
94
+ success: true,
95
+ data: {
96
+ intent: `Transfer ${qty} units of ${product?.name || 'product'} from ${fromLocation?.name || 'source'} to ${toLocation?.name || 'destination'}`,
97
+ product: product ? { id: productId, name: product.name, sku: product.sku } : { id: productId },
98
+ from_location: fromLocation ? { id: fromLocationId, name: fromLocation.name } : { id: fromLocationId },
99
+ to_location: toLocation ? { id: toLocationId, name: toLocation.name } : { id: toLocationId },
100
+ quantity_transferred: qty,
101
+ before_state: { from_quantity: transferData.source_before, to_quantity: transferData.dest_before, total: (transferData.source_before || 0) + (transferData.dest_before || 0) },
102
+ after_state: { from_quantity: transferData.source_after, to_quantity: transferData.dest_after, total: (transferData.source_after || 0) + (transferData.dest_after || 0) }
103
+ }
104
+ };
105
+ }
106
+ case "bulk_set": {
107
+ const items = args.items;
108
+ if (!items || !Array.isArray(items) || items.length === 0)
109
+ return { success: false, error: "items array is required" };
110
+ const results = [];
111
+ for (const item of items) {
112
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_set", {
113
+ p_store_id: sid, p_product_id: item.product_id, p_location_id: item.location_id,
114
+ p_quantity: item.quantity, p_reason: "Bulk set",
115
+ });
116
+ if (rpcError) {
117
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "no", error: rpcError.message });
118
+ continue;
119
+ }
120
+ const r = result;
121
+ if (!r.success) {
122
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "no", error: r.error });
123
+ continue;
124
+ }
125
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "yes", before: r.data?.before, after: r.data?.after });
126
+ }
127
+ const succeeded = results.filter(r => r.ok === "yes").length;
128
+ return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
129
+ }
130
+ case "bulk_adjust": {
131
+ const items = args.items;
132
+ if (!items || !Array.isArray(items) || items.length === 0)
133
+ return { success: false, error: "items array is required" };
134
+ const results = [];
135
+ for (const item of items) {
136
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_adjust", {
137
+ p_store_id: sid, p_product_id: item.product_id, p_location_id: item.location_id,
138
+ p_adjustment: item.adjustment, p_reason: "Bulk adjustment",
139
+ });
140
+ if (rpcError) {
141
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "no", error: rpcError.message });
142
+ continue;
143
+ }
144
+ const r = result;
145
+ if (!r.success) {
146
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "no", error: r.error });
147
+ continue;
148
+ }
149
+ results.push({ product_id: item.product_id, location_id: item.location_id, ok: "yes", before: r.data?.before, after: r.data?.after });
150
+ }
151
+ const succeeded = results.filter(r => r.ok === "yes").length;
152
+ return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
153
+ }
154
+ case "bulk_clear": {
155
+ const locationId = args.location_id;
156
+ if (!locationId)
157
+ return { success: false, error: "location_id is required" };
158
+ // Fetch items with stock > 0, then use atomic_inventory_set for audit trail
159
+ const { data: items, error: fetchErr } = await sb.from("inventory")
160
+ .select("product_id, quantity").eq("store_id", sid).eq("location_id", locationId).gt("quantity", 0);
161
+ if (fetchErr)
162
+ return { success: false, error: fetchErr.message };
163
+ if (!items || items.length === 0)
164
+ return { success: true, data: { location_id: locationId, items_cleared: 0 } };
165
+ const results = [];
166
+ for (const item of items) {
167
+ const { data: result, error: rpcError } = await sb.rpc("atomic_inventory_set", {
168
+ p_store_id: sid, p_product_id: item.product_id, p_location_id: locationId,
169
+ p_quantity: 0, p_reason: "Bulk clear",
170
+ });
171
+ if (rpcError) {
172
+ results.push({ product_id: item.product_id, success: false, error: rpcError.message });
173
+ continue;
174
+ }
175
+ const r = result;
176
+ if (!r.success) {
177
+ results.push({ product_id: item.product_id, success: false, error: r.error });
178
+ continue;
179
+ }
180
+ results.push({ product_id: item.product_id, success: true, before: r.data?.before });
181
+ }
182
+ const succeeded = results.filter(r => r.success).length;
183
+ return { success: true, data: { location_id: locationId, items_cleared: succeeded, total: items.length, results } };
184
+ }
185
+ default:
186
+ return { success: false, error: `Unknown inventory action: ${args.action}` };
187
+ }
188
+ }
189
+ // Resolve location name/slug to UUID — accepts UUID passthrough, name, or slug
190
+ async function resolveLocationId(sb, sid, input) {
191
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(input)) {
192
+ const { data } = await sb.from("locations").select("id, name").eq("id", input).eq("store_id", sid).single();
193
+ return data;
194
+ }
195
+ const clean = input.replace(/-/g, " ");
196
+ const { data } = await sb.from("locations").select("id, name").eq("store_id", sid)
197
+ .or(`name.ilike.%${clean}%,slug.ilike.%${input}%`).limit(1);
198
+ return data?.[0] || null;
199
+ }
200
+ export async function handleInventoryQuery(sb, args, storeId) {
201
+ const sid = storeId;
202
+ switch (args.action) {
203
+ case "summary": {
204
+ const { data, error } = await sb.from("inventory")
205
+ .select("*, product:products(name, sku), location:locations(name)")
206
+ .eq("store_id", sid).limit(1000);
207
+ if (error)
208
+ return { success: false, error: error.message };
209
+ const byLocation = {};
210
+ for (const row of data || []) {
211
+ const locId = row.location_id;
212
+ if (!byLocation[locId])
213
+ byLocation[locId] = { location_id: locId, location_name: row.location?.name || locId, items: 0, total_qty: 0 };
214
+ byLocation[locId].items++;
215
+ byLocation[locId].total_qty += row.quantity || 0;
216
+ }
217
+ return { success: true, data: { total_items: data?.length || 0, by_location: Object.values(byLocation) } };
218
+ }
219
+ case "velocity": {
220
+ const days = args.days || 30;
221
+ const categoryId = args.category_id;
222
+ const productId = args.product_id;
223
+ const locationId = args.location_id;
224
+ const limit = args.limit || 50;
225
+ const { data, error } = await sb.rpc("get_product_velocity", {
226
+ p_store_id: sid, p_days: days, p_location_id: locationId || null,
227
+ p_category_id: categoryId || null, p_product_id: productId || null, p_limit: limit
228
+ });
229
+ if (error)
230
+ return { success: false, error: error.message };
231
+ const products = (data || []).map((row) => ({
232
+ productId: row.product_id, name: row.product_name, sku: row.product_sku,
233
+ category: row.category_name, locationId: row.location_id, locationName: row.location_name,
234
+ totalQty: row.units_sold, totalRevenue: row.revenue, orderCount: row.order_count,
235
+ velocityPerDay: row.daily_velocity, revenuePerDay: row.daily_revenue,
236
+ currentStock: row.current_stock, daysOfStock: row.days_of_stock,
237
+ avgPrice: row.avg_unit_price, stockAlert: row.stock_status
238
+ }));
239
+ return { success: true, data: { days, filters: { categoryId, locationId, productId }, products } };
240
+ }
241
+ case "by_location": {
242
+ let q = sb.from("inventory").select("product_id, location_id, quantity, product:products(name, sku, category:categories!primary_category_id(name)), location:locations(name)")
243
+ .eq("store_id", sid);
244
+ if (args.location_id || args.location) {
245
+ const locInput = (args.location_id || args.location);
246
+ const loc = await resolveLocationId(sb, sid, locInput);
247
+ if (!loc)
248
+ return { success: false, error: `Location not found: "${locInput}". Use locations tool to list available locations.` };
249
+ q = q.eq("location_id", loc.id);
250
+ }
251
+ if (args.limit)
252
+ q = q.limit(args.limit);
253
+ const { data, error } = await q.order("quantity", { ascending: false });
254
+ if (error)
255
+ return { success: false, error: error.message };
256
+ const flattened = (data || []).map((row) => ({
257
+ product_id: row.product_id,
258
+ product_name: row.product?.name || "—",
259
+ product_sku: row.product?.sku || "—",
260
+ category: row.product?.category?.name || null,
261
+ location_id: row.location_id,
262
+ location_name: row.location?.name || "—",
263
+ quantity: row.quantity,
264
+ }));
265
+ return { success: true, count: flattened.length, data: flattened };
266
+ }
267
+ case "in_stock": {
268
+ let inStockQ = sb.from("inventory")
269
+ .select("product_id, location_id, quantity, product:products(name, sku, category:categories!primary_category_id(name)), location:locations(name)")
270
+ .eq("store_id", sid).gt("quantity", 0);
271
+ if (args.location_id || args.location) {
272
+ const locInput = (args.location_id || args.location);
273
+ const loc = await resolveLocationId(sb, sid, locInput);
274
+ if (!loc)
275
+ return { success: false, error: `Location not found: "${locInput}". Use locations tool to list available locations.` };
276
+ inStockQ = inStockQ.eq("location_id", loc.id);
277
+ }
278
+ if (args.limit)
279
+ inStockQ = inStockQ.limit(args.limit);
280
+ const { data, error } = await inStockQ.order("quantity", { ascending: false });
281
+ if (error)
282
+ return { success: false, error: error.message };
283
+ const flattened = (data || []).map((row) => ({
284
+ product_id: row.product_id,
285
+ product_name: row.product?.name || "—",
286
+ product_sku: row.product?.sku || "—",
287
+ category: row.product?.category?.name || null,
288
+ location_id: row.location_id,
289
+ location_name: row.location?.name || "—",
290
+ quantity: row.quantity,
291
+ }));
292
+ return { success: true, count: flattened.length, data: flattened };
293
+ }
294
+ case "by_category": {
295
+ // Pre-grouped inventory by category — compact summary the LLM can use directly
296
+ let catQ = sb.from("inventory")
297
+ .select("product_id, location_id, quantity, product:products!inner(name, sku, status, primary_category_id, category:categories!primary_category_id(name, parent_id)), location:locations(name)")
298
+ .eq("store_id", sid).gt("quantity", 0)
299
+ .eq("product.status", "published");
300
+ // Optional location filter
301
+ let locationName = null;
302
+ if (args.location_id || args.location) {
303
+ const locInput = (args.location_id || args.location);
304
+ const loc = await resolveLocationId(sb, sid, locInput);
305
+ if (!loc)
306
+ return { success: false, error: `Location not found: "${locInput}". Use locations tool to list available locations.` };
307
+ catQ = catQ.eq("location_id", loc.id);
308
+ locationName = loc.name;
309
+ }
310
+ const { data, error } = await catQ;
311
+ // Optional category filter — includes sub-categories
312
+ let categoryFilter = null;
313
+ if (args.category) {
314
+ const catInput = args.category;
315
+ const isUuid = /^[0-9a-f]{8}-/i.test(catInput);
316
+ let parentId = null;
317
+ if (isUuid) {
318
+ parentId = catInput;
319
+ }
320
+ else {
321
+ const { data: cats } = await sb.from("categories").select("id").ilike("name", `%${catInput}%`).eq("store_id", sid).limit(1);
322
+ if (cats?.length)
323
+ parentId = cats[0].id;
324
+ }
325
+ if (parentId) {
326
+ const { data: children } = await sb.from("categories").select("id").eq("parent_id", parentId).eq("store_id", sid);
327
+ categoryFilter = new Set([parentId, ...(children || []).map(c => c.id)]);
328
+ }
329
+ else {
330
+ // Category name didn't match anything — filter out everything
331
+ categoryFilter = new Set();
332
+ }
333
+ }
334
+ if (error)
335
+ return { success: false, error: error.message };
336
+ // Group by category
337
+ const catMap = {};
338
+ for (const row of data || []) {
339
+ // Apply category filter if set
340
+ if (categoryFilter) {
341
+ const prodCatId = row.product?.primary_category_id;
342
+ if (!prodCatId || !categoryFilter.has(prodCatId))
343
+ continue;
344
+ }
345
+ const cat = row.product?.category?.name || "Uncategorized";
346
+ const pName = row.product?.name || "—";
347
+ const pSku = row.product?.sku || "—";
348
+ const locName = row.location?.name || "—";
349
+ const qty = row.quantity || 0;
350
+ if (!catMap[cat])
351
+ catMap[cat] = { products: {}, total_qty: 0 };
352
+ catMap[cat].total_qty += qty;
353
+ const pKey = row.product_id;
354
+ if (!catMap[cat].products[pKey])
355
+ catMap[cat].products[pKey] = { name: pName, sku: pSku, total_qty: 0, locations: [] };
356
+ catMap[cat].products[pKey].total_qty += qty;
357
+ if (!catMap[cat].products[pKey].locations.includes(locName))
358
+ catMap[cat].products[pKey].locations.push(locName);
359
+ }
360
+ // Format as pre-rendered markdown — the CLI formatter drops nested arrays,
361
+ // so we render products-per-category here to prevent the agent from losing
362
+ // the product→category mapping and reconstructing it incorrectly.
363
+ const categories = Object.entries(catMap)
364
+ .sort((a, b) => b[1].total_qty - a[1].total_qty)
365
+ .map(([cat, info]) => ({
366
+ category: cat,
367
+ total_qty: Math.round(info.total_qty * 100) / 100,
368
+ product_count: Object.keys(info.products).length,
369
+ products: Object.values(info.products)
370
+ .sort((a, b) => b.total_qty - a.total_qty)
371
+ .map(p => ({ name: p.name, sku: p.sku, qty: Math.round(p.total_qty * 100) / 100 })),
372
+ }));
373
+ const totalProducts = categories.reduce((s, c) => s + c.product_count, 0);
374
+ const header = locationName ? `Inventory at ${locationName}` : "Inventory by Category";
375
+ const lines = [
376
+ `## ${header}${args.category ? ` — ${args.category}` : ""}`,
377
+ `${categories.length} categories, ${totalProducts} products in stock\n`,
378
+ ];
379
+ for (const cat of categories) {
380
+ lines.push(`### ${cat.category} (${cat.product_count} products, ${cat.total_qty} units)`);
381
+ lines.push("| Product | SKU | Qty |");
382
+ lines.push("| --- | --- | ---: |");
383
+ for (const p of cat.products) {
384
+ lines.push(`| ${p.name} | ${p.sku} | ${p.qty} |`);
385
+ }
386
+ lines.push("");
387
+ }
388
+ lines.push("This is the COMPLETE inventory breakdown. Every in-stock product is listed under its correct category. Do NOT re-fetch individual categories or call in_stock — all data is here.");
389
+ return { success: true, data: lines.join("\n") };
390
+ }
391
+ case "out_of_stock": {
392
+ // Find all published products with zero or no inventory across all locations
393
+ const { data: products, error: pErr } = await sb.from("products")
394
+ .select("id, name, sku, category:categories!primary_category_id(name)").eq("store_id", sid).eq("status", "published");
395
+ if (pErr)
396
+ return { success: false, error: pErr.message };
397
+ const { data: inv } = await sb.from("inventory")
398
+ .select("product_id, quantity").eq("store_id", sid);
399
+ // Sum stock per product
400
+ const stockMap = {};
401
+ for (const row of inv || []) {
402
+ stockMap[row.product_id] = (stockMap[row.product_id] || 0) + (row.quantity || 0);
403
+ }
404
+ const outOfStock = (products || [])
405
+ .filter(p => !stockMap[p.id] || stockMap[p.id] <= 0)
406
+ .map(p => ({ product_id: p.id, name: p.name, sku: p.sku, category: p.category?.name || null, total_stock: stockMap[p.id] || 0 }));
407
+ return { success: true, count: outOfStock.length, total_products: products?.length || 0, data: outOfStock };
408
+ }
409
+ default:
410
+ return { success: false, error: `Unknown inventory_query action: ${args.action}` };
411
+ }
412
+ }
413
+ export async function handleInventoryAudit(sb, args, storeId) {
414
+ const sid = storeId;
415
+ switch (args.action) {
416
+ case "start": {
417
+ const { data, error } = await sb.from("inventory_audits")
418
+ .insert({ store_id: sid, location_id: args.location_id, status: "in_progress", started_at: new Date().toISOString() })
419
+ .select().single();
420
+ return error ? { success: false, error: error.message } : { success: true, data };
421
+ }
422
+ case "count": {
423
+ // Verify the parent audit belongs to this store before updating items
424
+ const { data: audit } = await sb.from("inventory_audits").select("id").eq("id", args.audit_id).eq("store_id", sid).single();
425
+ if (!audit)
426
+ return { success: false, error: "Audit not found" };
427
+ const { data, error } = await sb.from("inventory_audit_items")
428
+ .update({ counted_quantity: args.counted })
429
+ .eq("audit_id", args.audit_id).eq("product_id", args.product_id).select().single();
430
+ return error ? { success: false, error: error.message } : { success: true, data };
431
+ }
432
+ case "complete": {
433
+ const { data, error } = await sb.from("inventory_audits")
434
+ .update({ status: "completed", completed_at: new Date().toISOString() })
435
+ .eq("id", args.audit_id).eq("store_id", sid).select().single();
436
+ return error ? { success: false, error: error.message } : { success: true, data };
437
+ }
438
+ case "summary": {
439
+ const { data, error } = await sb.from("inventory_audits")
440
+ .select("*, items:inventory_audit_items(*)").eq("store_id", sid)
441
+ .order("created_at", { ascending: false }).limit(args.limit || 5);
442
+ return error ? { success: false, error: error.message } : { success: true, data };
443
+ }
444
+ default:
445
+ return { success: false, error: `Unknown inventory_audit action: ${args.action}` };
446
+ }
447
+ }
@@ -0,0 +1,10 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export interface KaliProgressEvent {
3
+ type: "stdout" | "stderr";
4
+ data: string;
5
+ }
6
+ export declare function handleKali(_sb: SupabaseClient, args: Record<string, unknown>, _storeId?: string, onToolProgress?: (progress: KaliProgressEvent) => void): Promise<{
7
+ success: boolean;
8
+ data?: unknown;
9
+ error?: string;
10
+ }>;
@@ -0,0 +1,210 @@
1
+ // server/handlers/kali.ts — Kali Linux remote execution
2
+ // Proxies commands to a dedicated Fly.io Kali machine via internal network.
3
+ // Pattern: same as browser.ts — external execution environment, handler is a thin proxy.
4
+ // Supports NDJSON streaming for exec actions via exec_stream on kali-box.
5
+ const KALI_BOX_URL = process.env.KALI_BOX_URL || "http://kali-box.internal:8080";
6
+ const KALI_AUTH_TOKEN = process.env.KALI_AUTH_TOKEN || "";
7
+ const MAX_OUTPUT_CHARS = 500 * 1024; // 500KB safety cap — context_management handles limits
8
+ const VALID_ACTIONS = new Set([
9
+ "exec", "exec_stream", "exec_bg", "bg_status", "bg_kill", "bg_list",
10
+ "upload", "download", "info", "sessions", "reset",
11
+ ]);
12
+ const FORWARD_KEYS = [
13
+ "command", "session_id", "timeout", "job_id",
14
+ "path", "content", "encoding", "tail",
15
+ ];
16
+ function truncate(text) {
17
+ if (!text || text.length <= MAX_OUTPUT_CHARS)
18
+ return text;
19
+ return text.substring(0, MAX_OUTPUT_CHARS) + `\n...[truncated, ${text.length} total chars]`;
20
+ }
21
+ /**
22
+ * Read NDJSON lines from a ReadableStream (Node.js or web).
23
+ * Yields parsed JSON objects, one per newline-delimited line.
24
+ */
25
+ async function* readNDJSON(body) {
26
+ const reader = body.getReader();
27
+ const decoder = new TextDecoder();
28
+ let buffer = "";
29
+ try {
30
+ while (true) {
31
+ const { done, value } = await reader.read();
32
+ if (done)
33
+ break;
34
+ buffer += decoder.decode(value, { stream: true });
35
+ // Process complete lines
36
+ let newlineIdx;
37
+ while ((newlineIdx = buffer.indexOf("\n")) !== -1) {
38
+ const line = buffer.substring(0, newlineIdx).trim();
39
+ buffer = buffer.substring(newlineIdx + 1);
40
+ if (line) {
41
+ try {
42
+ yield JSON.parse(line);
43
+ }
44
+ catch {
45
+ // Skip malformed JSON lines
46
+ }
47
+ }
48
+ }
49
+ }
50
+ // Process any remaining buffer
51
+ const remaining = buffer.trim();
52
+ if (remaining) {
53
+ try {
54
+ yield JSON.parse(remaining);
55
+ }
56
+ catch {
57
+ // Skip malformed final fragment
58
+ }
59
+ }
60
+ }
61
+ finally {
62
+ reader.releaseLock();
63
+ }
64
+ }
65
+ export async function handleKali(_sb, args, _storeId, onToolProgress) {
66
+ const action = args.action;
67
+ if (!action) {
68
+ return { success: false, error: "action is required" };
69
+ }
70
+ if (!VALID_ACTIONS.has(action)) {
71
+ return { success: false, error: `Unknown action: ${action}. Valid: ${[...VALID_ACTIONS].join(", ")}` };
72
+ }
73
+ if ((action === "exec" || action === "exec_stream" || action === "exec_bg") && !args.command) {
74
+ return { success: false, error: "command is required for exec/exec_bg" };
75
+ }
76
+ if (!KALI_AUTH_TOKEN) {
77
+ return { success: false, error: "KALI_AUTH_TOKEN not configured on whale-agent" };
78
+ }
79
+ // Build payload — only forward recognized keys
80
+ const payload = { action };
81
+ for (const key of FORWARD_KEYS) {
82
+ if (args[key] !== undefined)
83
+ payload[key] = args[key];
84
+ }
85
+ // Command timeout + HTTP buffer
86
+ const cmdTimeout = Math.min(args.timeout || 30000, 600000);
87
+ const httpTimeout = cmdTimeout + 10000; // 10s buffer over command timeout
88
+ // ── Streaming path: exec actions use exec_stream on kali-box ──────
89
+ if ((action === "exec" || action === "exec_stream") && onToolProgress) {
90
+ payload.action = "exec_stream";
91
+ payload.timeout = cmdTimeout;
92
+ try {
93
+ const controller = new AbortController();
94
+ const timer = setTimeout(() => controller.abort(), httpTimeout);
95
+ const resp = await fetch(KALI_BOX_URL, {
96
+ method: "POST",
97
+ headers: {
98
+ "Content-Type": "application/json",
99
+ Authorization: `Bearer ${KALI_AUTH_TOKEN}`,
100
+ },
101
+ body: JSON.stringify(payload),
102
+ signal: controller.signal,
103
+ });
104
+ clearTimeout(timer);
105
+ if (!resp.ok) {
106
+ const text = await resp.text().catch(() => "");
107
+ return { success: false, error: `Kali box HTTP ${resp.status}: ${text.substring(0, 500)}` };
108
+ }
109
+ if (!resp.body) {
110
+ return { success: false, error: "Kali box returned no response body for exec_stream" };
111
+ }
112
+ // Read NDJSON stream — emit progress for each chunk, accumulate for final result
113
+ let stdout = "";
114
+ let stderr = "";
115
+ let doneEvent = null;
116
+ for await (const line of readNDJSON(resp.body)) {
117
+ if (line.type === "stdout") {
118
+ const data = String(line.data || "");
119
+ stdout += data;
120
+ // Filter out the CWD marker line from progress events
121
+ if (!data.includes("__KALI_END_")) {
122
+ onToolProgress({ type: "stdout", data });
123
+ }
124
+ }
125
+ else if (line.type === "stderr") {
126
+ const data = String(line.data || "");
127
+ stderr += data;
128
+ onToolProgress({ type: "stderr", data });
129
+ }
130
+ else if (line.type === "done") {
131
+ doneEvent = line;
132
+ }
133
+ }
134
+ // Strip CWD marker from accumulated stdout (same as non-streaming path)
135
+ const markerIdx = stdout.lastIndexOf("__KALI_END_");
136
+ if (markerIdx !== -1) {
137
+ stdout = stdout.substring(0, markerIdx);
138
+ }
139
+ // Build final result (same shape as non-streaming exec)
140
+ const exitCode = doneEvent ? doneEvent.exit_code ?? -1 : -1;
141
+ const result = {
142
+ success: exitCode === 0,
143
+ stdout: truncate(stdout.replace(/\s+$/, "")),
144
+ stderr: truncate(stderr.replace(/\s+$/, "")),
145
+ exit_code: exitCode,
146
+ killed: doneEvent?.killed ?? false,
147
+ cwd: doneEvent?.cwd ?? "",
148
+ duration_ms: doneEvent?.duration_ms ?? 0,
149
+ };
150
+ return { success: exitCode === 0, data: result };
151
+ }
152
+ catch (err) {
153
+ if (err instanceof Error && err.name === "AbortError") {
154
+ return { success: false, error: `Kali box streaming request timed out after ${httpTimeout}ms` };
155
+ }
156
+ const msg = err instanceof Error ? err.message : String(err);
157
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed")) {
158
+ return {
159
+ success: false,
160
+ error: `Cannot reach kali-box at ${KALI_BOX_URL}. Ensure the kali-box Fly.io app is running (fly status -a kali-box).`,
161
+ };
162
+ }
163
+ return { success: false, error: `Kali box streaming error: ${msg}` };
164
+ }
165
+ }
166
+ // ── Standard path: non-exec actions or no progress callback ───────
167
+ if (action === "exec")
168
+ payload.timeout = cmdTimeout;
169
+ try {
170
+ const controller = new AbortController();
171
+ const timer = setTimeout(() => controller.abort(), httpTimeout);
172
+ const resp = await fetch(KALI_BOX_URL, {
173
+ method: "POST",
174
+ headers: {
175
+ "Content-Type": "application/json",
176
+ Authorization: `Bearer ${KALI_AUTH_TOKEN}`,
177
+ },
178
+ body: JSON.stringify(payload),
179
+ signal: controller.signal,
180
+ });
181
+ clearTimeout(timer);
182
+ if (!resp.ok) {
183
+ const text = await resp.text().catch(() => "");
184
+ return { success: false, error: `Kali box HTTP ${resp.status}: ${text.substring(0, 500)}` };
185
+ }
186
+ const result = (await resp.json());
187
+ // Truncate large text fields before returning to agent
188
+ if (typeof result.stdout === "string")
189
+ result.stdout = truncate(result.stdout);
190
+ if (typeof result.stderr === "string")
191
+ result.stderr = truncate(result.stderr);
192
+ if (typeof result.content === "string")
193
+ result.content = truncate(result.content);
194
+ return { success: result.success !== false, data: result };
195
+ }
196
+ catch (err) {
197
+ if (err instanceof Error && err.name === "AbortError") {
198
+ return { success: false, error: `Kali box request timed out after ${httpTimeout}ms` };
199
+ }
200
+ const msg = err instanceof Error ? err.message : String(err);
201
+ // Friendly message for connection failures
202
+ if (msg.includes("ECONNREFUSED") || msg.includes("ENOTFOUND") || msg.includes("fetch failed")) {
203
+ return {
204
+ success: false,
205
+ error: `Cannot reach kali-box at ${KALI_BOX_URL}. Ensure the kali-box Fly.io app is running (fly status -a kali-box).`,
206
+ };
207
+ }
208
+ return { success: false, error: `Kali box error: ${msg}` };
209
+ }
210
+ }