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,588 @@
1
+ import { sanitizeFilterValue, escapeCSV, fillTemplate, groupBy } from "../lib/utils.js";
2
+ import { generatePdfFromHtml } from "./browser.js";
3
+ import { validateUrl } from "../lib/ssrf-guard.js";
4
+ const MAX_ATTACHMENT_BYTES = 10 * 1024 * 1024; // 10MB per attachment
5
+ const MAX_TOTAL_ATTACHMENT_BYTES = 25 * 1024 * 1024; // 25MB total
6
+ const MAX_ATTACHMENT_COUNT = 10;
7
+ export async function handleEmail(sb, args, storeId) {
8
+ if (!storeId)
9
+ return { success: false, error: "store_id required" };
10
+ const sid = storeId;
11
+ switch (args.action) {
12
+ case "inbox": {
13
+ let q = sb.from("email_threads").select("*, latest_message:email_inbox(subject, from_email, created_at)")
14
+ .eq("store_id", sid).order("updated_at", { ascending: false }).limit(args.limit || 25);
15
+ if (args.status)
16
+ q = q.eq("status", args.status);
17
+ if (args.mailbox)
18
+ q = q.eq("mailbox", args.mailbox);
19
+ if (args.priority)
20
+ q = q.eq("priority", args.priority);
21
+ const { data, error } = await q;
22
+ if (error)
23
+ return { success: false, error: error.message };
24
+ // Flatten latest_message join so subject/from appear as table columns
25
+ const flattened = (data || []).map((row) => {
26
+ const { latest_message, ...rest } = row;
27
+ return { ...rest, subject: latest_message?.subject || null, from_email: latest_message?.from_email || null };
28
+ });
29
+ return { success: true, data: flattened };
30
+ }
31
+ case "inbox_get": {
32
+ const { data, error } = await sb.from("email_threads")
33
+ .select("*, messages:email_inbox(id, subject, from_email, to_email, body_text, created_at)").eq("id", args.thread_id).eq("store_id", sid).single();
34
+ if (error)
35
+ return { success: false, error: error.message };
36
+ // Pre-format so messages are visible (formatter handles sub-tables but body gets truncated)
37
+ const { messages, ...thread } = data;
38
+ const lines = [
39
+ `## Email Thread`,
40
+ `**Status**: ${thread.status || "—"} | **Mailbox**: ${thread.mailbox || "—"} | **Priority**: ${thread.priority || "—"}`,
41
+ "",
42
+ ];
43
+ for (const msg of (messages || []).sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime())) {
44
+ lines.push(`### ${msg.from_email} → ${msg.to_email || "—"} (${msg.created_at?.slice(0, 16) || "—"})`);
45
+ lines.push(`**Subject**: ${msg.subject || "(no subject)"}`);
46
+ const body = (msg.body_text || "").slice(0, 500);
47
+ lines.push(body + (msg.body_text?.length > 500 ? "..." : ""));
48
+ lines.push("");
49
+ }
50
+ return { success: true, data: lines.join("\n") };
51
+ }
52
+ case "send": {
53
+ // P0 FIX: Per-store email rate limit (100 emails/hour)
54
+ const oneHourAgo = new Date(Date.now() - 3600_000).toISOString();
55
+ const { count: recentSends } = await sb.from("email_sends")
56
+ .select("id", { count: "exact", head: true })
57
+ .eq("store_id", sid)
58
+ .gte("created_at", oneHourAgo);
59
+ if ((recentSends || 0) >= 100) {
60
+ return { success: false, error: "Email rate limit exceeded (100/hour). Try again later." };
61
+ }
62
+ // Invoke send-email edge function
63
+ const sbUrl = process.env["SUPABASE_URL"];
64
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
65
+ try {
66
+ // Resolve URL-based attachments to base64 for Resend API
67
+ let attachments;
68
+ const rawAttachments = args.attachments;
69
+ if (rawAttachments && rawAttachments.length > 0) {
70
+ // P0 FIX: Enforce attachment count limit
71
+ if (rawAttachments.length > MAX_ATTACHMENT_COUNT) {
72
+ throw new Error(`Too many attachments: ${rawAttachments.length} (max ${MAX_ATTACHMENT_COUNT})`);
73
+ }
74
+ attachments = [];
75
+ let totalBytes = 0;
76
+ for (const att of rawAttachments) {
77
+ if (att.url) {
78
+ // P0 FIX: Validate URL to prevent SSRF via attachment URLs
79
+ const ssrfErr = await validateUrl(att.url);
80
+ if (ssrfErr)
81
+ throw new Error(`Attachment URL blocked: ${ssrfErr}`);
82
+ // Fetch the file and convert to base64
83
+ const fileResp = await fetch(att.url);
84
+ if (!fileResp.ok)
85
+ throw new Error(`Failed to fetch attachment: ${att.url} (${fileResp.status})`);
86
+ const buffer = await fileResp.arrayBuffer();
87
+ // P0 FIX: Enforce 10MB per-attachment size limit
88
+ if (buffer.byteLength > MAX_ATTACHMENT_BYTES) {
89
+ throw new Error(`Attachment too large: ${(buffer.byteLength / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
90
+ }
91
+ // P0 FIX: Enforce 25MB total attachment size limit
92
+ totalBytes += buffer.byteLength;
93
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
94
+ throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
95
+ }
96
+ const base64 = Buffer.from(buffer).toString("base64");
97
+ const filename = att.filename || att.url.split("/").pop() || "attachment";
98
+ attachments.push({ filename, content: base64 });
99
+ }
100
+ else if (att.content && att.filename) {
101
+ // Already base64 — decode length to check size
102
+ const contentBytes = Math.ceil((att.content.length * 3) / 4);
103
+ if (contentBytes > MAX_ATTACHMENT_BYTES) {
104
+ throw new Error(`Attachment too large: ${(contentBytes / 1024 / 1024).toFixed(1)}MB (max 10MB)`);
105
+ }
106
+ totalBytes += contentBytes;
107
+ if (totalBytes > MAX_TOTAL_ATTACHMENT_BYTES) {
108
+ throw new Error(`Total attachment size exceeds limit: ${(totalBytes / 1024 / 1024).toFixed(1)}MB (max 25MB)`);
109
+ }
110
+ attachments.push({ filename: att.filename, content: att.content });
111
+ }
112
+ }
113
+ }
114
+ // Resolve {{variable}} placeholders in email fields against template_data
115
+ let emailHtml = args.html;
116
+ let emailSubject = args.subject;
117
+ let emailText = args.text;
118
+ if (args.template_data && typeof args.template_data === "object") {
119
+ const tplData = args.template_data;
120
+ if (emailHtml)
121
+ emailHtml = fillTemplate(emailHtml, tplData);
122
+ if (emailSubject)
123
+ emailSubject = fillTemplate(emailSubject, tplData);
124
+ if (emailText)
125
+ emailText = fillTemplate(emailText, tplData);
126
+ }
127
+ const payload = { to: args.to, subject: emailSubject, html: emailHtml, text: emailText, storeId: sid };
128
+ if (attachments && attachments.length > 0)
129
+ payload.attachments = attachments;
130
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
131
+ method: "POST",
132
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
133
+ body: JSON.stringify(payload)
134
+ });
135
+ const result = await resp.json();
136
+ return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Send failed" };
137
+ }
138
+ catch (err) {
139
+ return { success: false, error: `Email send failed: ${err}` };
140
+ }
141
+ }
142
+ case "send_template": {
143
+ const sbUrl = process.env["SUPABASE_URL"];
144
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
145
+ try {
146
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
147
+ method: "POST",
148
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
149
+ body: JSON.stringify({ to: args.to, template: args.template, template_data: args.template_data, storeId: sid })
150
+ });
151
+ const result = await resp.json();
152
+ return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Send failed" };
153
+ }
154
+ catch (err) {
155
+ return { success: false, error: `Template send failed: ${err}` };
156
+ }
157
+ }
158
+ case "list": {
159
+ const { data, error } = await sb.from("email_sends").select("*")
160
+ .eq("store_id", sid).order("created_at", { ascending: false }).limit(args.limit || 50);
161
+ return error ? { success: false, error: error.message } : { success: true, data };
162
+ }
163
+ case "get": {
164
+ const { data, error } = await sb.from("email_sends")
165
+ .select("*").eq("id", args.email_id).eq("store_id", sid).single();
166
+ return error ? { success: false, error: error.message } : { success: true, data };
167
+ }
168
+ case "templates": {
169
+ const { data, error } = await sb.from("email_templates").select("*")
170
+ .eq("store_id", sid).eq("is_active", true).limit(100);
171
+ return error ? { success: false, error: error.message } : { success: true, data };
172
+ }
173
+ case "inbox_reply": {
174
+ const sbUrl = process.env["SUPABASE_URL"];
175
+ const sbKey = process.env["SUPABASE_SERVICE_ROLE_KEY"];
176
+ try {
177
+ const resp = await fetch(`${sbUrl}/functions/v1/send-email`, {
178
+ method: "POST",
179
+ headers: { "Content-Type": "application/json", "Authorization": `Bearer ${sbKey}` },
180
+ body: JSON.stringify({ to: args.to, subject: args.subject, html: args.html, text: args.text, thread_id: args.thread_id, storeId: sid })
181
+ });
182
+ const result = await resp.json();
183
+ return resp.ok ? { success: true, data: result } : { success: false, error: result.error || "Reply failed" };
184
+ }
185
+ catch (err) {
186
+ return { success: false, error: `Reply failed: ${err}` };
187
+ }
188
+ }
189
+ case "inbox_update": {
190
+ const updates = {};
191
+ if (args.status)
192
+ updates.status = args.status;
193
+ if (args.priority)
194
+ updates.priority = args.priority;
195
+ if (args.intent)
196
+ updates.ai_intent = args.intent;
197
+ if (args.ai_summary)
198
+ updates.ai_summary = args.ai_summary;
199
+ const { data, error } = await sb.from("email_threads")
200
+ .update(updates).eq("id", args.thread_id).eq("store_id", sid).select().single();
201
+ return error ? { success: false, error: error.message } : { success: true, data };
202
+ }
203
+ case "inbox_stats": {
204
+ const { data, error } = await sb.from("email_threads")
205
+ .select("status, mailbox, priority").eq("store_id", sid).limit(1000);
206
+ if (error)
207
+ return { success: false, error: error.message };
208
+ const stats = {
209
+ total: data.length,
210
+ by_status: groupBy(data, "status"),
211
+ by_mailbox: groupBy(data, "mailbox"),
212
+ by_priority: groupBy(data, "priority")
213
+ };
214
+ return { success: true, data: stats };
215
+ }
216
+ case "create_template": {
217
+ if (!args.name)
218
+ return { success: false, error: "name is required" };
219
+ if (!args.subject)
220
+ return { success: false, error: "subject is required" };
221
+ const slug = args.slug || args.name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
222
+ const { data, error } = await sb.from("email_templates").insert({
223
+ store_id: sid,
224
+ name: args.name,
225
+ slug,
226
+ subject: args.subject,
227
+ category: args.category || "general",
228
+ description: args.description || null,
229
+ html_content: args.html_content || null,
230
+ preview_text: args.preview_text || null,
231
+ text_content: args.text_content || null,
232
+ is_active: true,
233
+ }).select("id, name, slug, subject, category, created_at").single();
234
+ if (error)
235
+ return { success: false, error: error.message };
236
+ return { success: true, data };
237
+ }
238
+ case "update_template": {
239
+ if (!args.template_id)
240
+ return { success: false, error: "template_id is required" };
241
+ const updates = {};
242
+ if (args.name !== undefined)
243
+ updates.name = args.name;
244
+ if (args.slug !== undefined)
245
+ updates.slug = args.slug;
246
+ if (args.subject !== undefined)
247
+ updates.subject = args.subject;
248
+ if (args.category !== undefined)
249
+ updates.category = args.category;
250
+ if (args.description !== undefined)
251
+ updates.description = args.description;
252
+ if (args.html_content !== undefined)
253
+ updates.html_content = args.html_content;
254
+ if (args.preview_text !== undefined)
255
+ updates.preview_text = args.preview_text;
256
+ if (args.text_content !== undefined)
257
+ updates.text_content = args.text_content;
258
+ if (Object.keys(updates).length === 0)
259
+ return { success: false, error: "No fields to update" };
260
+ const { data, error } = await sb.from("email_templates")
261
+ .update(updates).eq("id", args.template_id).eq("store_id", sid)
262
+ .select("id, name, slug, subject, category, updated_at").single();
263
+ if (error)
264
+ return { success: false, error: error.message };
265
+ return { success: true, data };
266
+ }
267
+ case "delete_template": {
268
+ if (!args.template_id)
269
+ return { success: false, error: "template_id is required" };
270
+ const { data, error } = await sb.from("email_templates")
271
+ .update({ is_active: false }).eq("id", args.template_id).eq("store_id", sid)
272
+ .select("id, name").single();
273
+ if (error)
274
+ return { success: false, error: error.message };
275
+ return { success: true, data: { ...data, deleted: true } };
276
+ }
277
+ default:
278
+ return { success: false, error: `Unknown email action: ${args.action}. Valid: send, send_template, list, get, templates, inbox, inbox_get, inbox_reply, inbox_update, inbox_stats, create_template, update_template, delete_template` };
279
+ }
280
+ }
281
+ export async function handleDocuments(sb, args, storeId) {
282
+ if (!storeId)
283
+ return { success: false, error: "store_id required" };
284
+ const sid = storeId;
285
+ const action = args.action;
286
+ switch (action) {
287
+ case "create": {
288
+ const docType = args.document_type || "text";
289
+ const name = args.name;
290
+ if (!name)
291
+ return { success: false, error: "name is required" };
292
+ const extMap = { csv: "csv", json: "json", text: "txt", markdown: "md", html: "html", pdf: "pdf" };
293
+ const mimeMap = {
294
+ csv: "text/csv", json: "application/json", text: "text/plain",
295
+ markdown: "text/markdown", html: "text/html", pdf: "application/pdf",
296
+ };
297
+ const ext = extMap[docType] || "txt";
298
+ const mime = mimeMap[docType] || "text/plain";
299
+ const safeName = name.replace(/[^a-zA-Z0-9_\-]/g, "_");
300
+ const fileName = `${safeName}_${Date.now()}.${ext}`;
301
+ const storagePath = `${sid}/${fileName}`;
302
+ let uploadBuffer;
303
+ if (docType === "pdf") {
304
+ // PDF generation from HTML content via Playwright
305
+ const htmlContent = args.html || args.content;
306
+ if (!htmlContent)
307
+ return { success: false, error: "html or content (HTML string) is required for PDF documents" };
308
+ try {
309
+ const pdfBuffer = await generatePdfFromHtml(htmlContent, {
310
+ format: args.format || "A4",
311
+ landscape: args.landscape || false,
312
+ });
313
+ uploadBuffer = new Uint8Array(pdfBuffer);
314
+ }
315
+ catch (err) {
316
+ return { success: false, error: `PDF generation failed: ${err instanceof Error ? err.message : String(err)}` };
317
+ }
318
+ }
319
+ else {
320
+ let content;
321
+ if (docType === "csv") {
322
+ const headers = args.headers;
323
+ const rows = args.rows;
324
+ if (!headers || !rows)
325
+ return { success: false, error: "headers and rows required for CSV" };
326
+ const lines = [headers.map(escapeCSV).join(",")];
327
+ for (const row of rows) {
328
+ if (Array.isArray(row)) {
329
+ lines.push(row.map(escapeCSV).join(","));
330
+ }
331
+ else {
332
+ lines.push(headers.map(h => escapeCSV(row[h])).join(","));
333
+ }
334
+ }
335
+ content = lines.join("\n");
336
+ }
337
+ else if (docType === "json") {
338
+ const jsonData = args.data || args.content;
339
+ content = typeof jsonData === "string" ? jsonData : JSON.stringify(jsonData, null, 2);
340
+ }
341
+ else {
342
+ content = args.content || "";
343
+ }
344
+ uploadBuffer = new TextEncoder().encode(content);
345
+ }
346
+ const { error: uploadErr } = await sb.storage
347
+ .from("documents")
348
+ .upload(storagePath, uploadBuffer, { contentType: mime, upsert: true });
349
+ if (uploadErr)
350
+ return { success: false, error: `Upload failed: ${uploadErr.message}` };
351
+ const { data: urlData } = sb.storage.from("documents").getPublicUrl(storagePath);
352
+ const fileUrl = urlData.publicUrl;
353
+ const sizeBytes = uploadBuffer.length;
354
+ const { data: record, error: insertErr } = await sb.from("store_documents").insert({
355
+ store_id: sid,
356
+ document_type: docType,
357
+ file_name: fileName,
358
+ file_url: fileUrl,
359
+ file_size: sizeBytes,
360
+ file_type: mime,
361
+ document_name: name,
362
+ source_name: "Documents Edge Function",
363
+ document_date: new Date().toISOString().split("T")[0],
364
+ data: { document_type: docType },
365
+ metadata: { size_bytes: sizeBytes },
366
+ }).select("id, document_name, file_url, created_at").single();
367
+ if (insertErr)
368
+ return { success: false, error: insertErr.message };
369
+ return { success: true, data: { id: record.id, name: record.document_name, type: docType, url: record.file_url, file_name: fileName, size: sizeBytes } };
370
+ }
371
+ case "find": {
372
+ let query = sb.from("store_documents")
373
+ .select("id, document_type, document_name, reference_number, file_url, file_type, file_size, created_at, metadata");
374
+ query = query.eq("store_id", sid);
375
+ if (args.document_type)
376
+ query = query.eq("document_type", args.document_type);
377
+ if (args.name) {
378
+ const sn = sanitizeFilterValue(args.name);
379
+ query = query.ilike("document_name", `%${sn}%`);
380
+ }
381
+ query = query.order("created_at", { ascending: false }).limit(args.limit || 50);
382
+ const { data, error } = await query;
383
+ if (error)
384
+ return { success: false, error: error.message };
385
+ return {
386
+ success: true,
387
+ data: {
388
+ count: data?.length || 0,
389
+ documents: (data || []).map(d => ({
390
+ id: d.id, type: d.document_type, name: d.document_name,
391
+ reference: d.reference_number, url: d.file_url,
392
+ size: d.file_size, created: d.created_at,
393
+ })),
394
+ },
395
+ };
396
+ }
397
+ case "delete": {
398
+ if (!args.confirm)
399
+ return { success: false, error: "Set confirm: true to delete" };
400
+ let query = sb.from("store_documents").delete().eq("store_id", sid);
401
+ if (args.document_type)
402
+ query = query.eq("document_type", args.document_type);
403
+ if (args.name) {
404
+ const sn = sanitizeFilterValue(args.name);
405
+ query = query.ilike("document_name", `%${sn}%`);
406
+ }
407
+ const { data, error } = await query.select("id");
408
+ if (error)
409
+ return { success: false, error: error.message };
410
+ return { success: true, data: { deleted: data?.length || 0 } };
411
+ }
412
+ case "create_template": {
413
+ if (!args.name)
414
+ return { success: false, error: "name is required" };
415
+ if (!args.document_type)
416
+ return { success: false, error: "document_type is required" };
417
+ const { data, error } = await sb.from("document_templates").insert({
418
+ store_id: sid || null,
419
+ name: args.name,
420
+ description: args.description || null,
421
+ document_type: args.document_type,
422
+ content: args.content || null,
423
+ headers: args.headers || null,
424
+ schema: args.schema || [],
425
+ metadata: args.data || {},
426
+ }).select("id, name, document_type, created_at").single();
427
+ if (error)
428
+ return { success: false, error: error.message };
429
+ return { success: true, data: { template_id: data.id, name: data.name, type: data.document_type } };
430
+ }
431
+ case "list_templates": {
432
+ let query = sb.from("document_templates")
433
+ .select("id, name, description, document_type, headers, schema, created_at")
434
+ .eq("is_active", true);
435
+ if (sid)
436
+ query = query.or(`store_id.eq.${sid},store_id.is.null`);
437
+ if (args.document_type)
438
+ query = query.eq("document_type", args.document_type);
439
+ if (args.limit)
440
+ query = query.limit(args.limit);
441
+ query = query.order("created_at", { ascending: false });
442
+ const { data, error } = await query;
443
+ if (error)
444
+ return { success: false, error: error.message };
445
+ return {
446
+ success: true,
447
+ data: {
448
+ count: data?.length || 0,
449
+ templates: (data || []).map(t => ({
450
+ id: t.id, name: t.name, description: t.description,
451
+ type: t.document_type, headers: t.headers,
452
+ fields: t.schema?.length || 0,
453
+ })),
454
+ },
455
+ };
456
+ }
457
+ case "from_template": {
458
+ const templateId = args.template_id;
459
+ if (!templateId)
460
+ return { success: false, error: "template_id is required" };
461
+ const { data: template, error: tErr } = await sb.from("document_templates")
462
+ .select("*").eq("id", templateId).or(`store_id.eq.${sid},store_id.is.null`).single();
463
+ if (tErr || !template)
464
+ return { success: false, error: "Template not found" };
465
+ const tData = { ...template.metadata, ...(args.data || {}), date: new Date().toISOString().split("T")[0] };
466
+ const docType = template.document_type;
467
+ const docName = args.name || fillTemplate(template.name, tData);
468
+ let content;
469
+ if (docType === "csv") {
470
+ const headers = template.headers || [];
471
+ const rows = args.rows;
472
+ if (!rows)
473
+ return { success: false, error: "rows required for CSV template" };
474
+ const lines = [headers.map(escapeCSV).join(",")];
475
+ for (const row of rows) {
476
+ if (Array.isArray(row))
477
+ lines.push(row.map(escapeCSV).join(","));
478
+ else
479
+ lines.push(headers.map(h => escapeCSV(row[h])).join(","));
480
+ }
481
+ content = lines.join("\n");
482
+ }
483
+ else if (docType === "json") {
484
+ content = template.content ? fillTemplate(template.content, tData) : JSON.stringify(tData, null, 2);
485
+ }
486
+ else {
487
+ content = template.content ? fillTemplate(template.content, tData) : "";
488
+ }
489
+ const extMap = { csv: "csv", json: "json", text: "txt", markdown: "md", html: "html" };
490
+ const mimeMap = {
491
+ csv: "text/csv", json: "application/json", text: "text/plain",
492
+ markdown: "text/markdown", html: "text/html",
493
+ };
494
+ const ext = extMap[docType] || "txt";
495
+ const mime = mimeMap[docType] || "text/plain";
496
+ const safeName = docName.replace(/[^a-zA-Z0-9_\-]/g, "_");
497
+ const fileName = `${safeName}_${Date.now()}.${ext}`;
498
+ const storagePath = `${sid}/${fileName}`;
499
+ const { error: uploadErr } = await sb.storage
500
+ .from("documents")
501
+ .upload(storagePath, new TextEncoder().encode(content), { contentType: mime, upsert: true });
502
+ if (uploadErr)
503
+ return { success: false, error: `Upload failed: ${uploadErr.message}` };
504
+ const { data: urlData } = sb.storage.from("documents").getPublicUrl(storagePath);
505
+ const sizeBytes = new TextEncoder().encode(content).length;
506
+ const { data: record, error: insertErr } = await sb.from("store_documents").insert({
507
+ store_id: sid,
508
+ document_type: docType,
509
+ file_name: fileName,
510
+ file_url: urlData.publicUrl,
511
+ file_size: sizeBytes,
512
+ file_type: mime,
513
+ document_name: docName,
514
+ source_name: "Documents Edge Function",
515
+ document_date: new Date().toISOString().split("T")[0],
516
+ data: { template_id: template.id, template_name: template.name },
517
+ metadata: { size_bytes: sizeBytes, from_template: true },
518
+ }).select("id, document_name, file_url, created_at").single();
519
+ if (insertErr)
520
+ return { success: false, error: insertErr.message };
521
+ return { success: true, data: { id: record.id, name: record.document_name, type: docType, template: template.name, url: record.file_url, size: sizeBytes } };
522
+ }
523
+ case "list_stores": {
524
+ const { data, error } = await sb.from("stores")
525
+ .select("id, store_name, slug, status, created_at")
526
+ .eq("id", sid)
527
+ .order("store_name");
528
+ if (error)
529
+ return { success: false, error: error.message };
530
+ return { success: true, data: { count: data?.length || 0, stores: data } };
531
+ }
532
+ case "list_profiles": {
533
+ const { data, error } = await sb.from("document_templates")
534
+ .select("id, name, description, document_type, headers, schema, created_at")
535
+ .eq("is_active", true)
536
+ .or(`store_id.eq.${sid},store_id.is.null`)
537
+ .order("name");
538
+ if (error)
539
+ return { success: false, error: error.message };
540
+ return {
541
+ success: true,
542
+ data: {
543
+ count: data?.length || 0,
544
+ profiles: (data || []).map(t => ({
545
+ id: t.id, name: t.name, description: t.description,
546
+ type: t.document_type, headers: t.headers,
547
+ fields: t.schema?.length || 0,
548
+ })),
549
+ },
550
+ };
551
+ }
552
+ case "generate": {
553
+ // Generate a document from a template profile with store data
554
+ const templateId = args.template_id;
555
+ if (!templateId)
556
+ return { success: false, error: "template_id is required. Use list_profiles to find available templates." };
557
+ // Route to from_template with the same args
558
+ return handleDocuments(sb, { ...args, action: "from_template" }, storeId);
559
+ }
560
+ case "bulk_generate": {
561
+ const templateId = args.template_id;
562
+ if (!templateId)
563
+ return { success: false, error: "template_id required" };
564
+ const items = args.items;
565
+ if (!items || !Array.isArray(items) || items.length === 0) {
566
+ return { success: false, error: "items required: array of variable sets for each document" };
567
+ }
568
+ if (items.length > 50) {
569
+ return { success: false, error: "Maximum 50 documents per bulk_generate call" };
570
+ }
571
+ const results = [];
572
+ for (const item of items) {
573
+ const itemArgs = { ...args, action: "from_template", data: item, name: item.name || undefined };
574
+ const result = await handleDocuments(sb, itemArgs, storeId);
575
+ if (result.success && result.data) {
576
+ results.push({ name: result.data.name || "", url: result.data.url || "" });
577
+ }
578
+ else {
579
+ results.push({ name: String(item.name || ""), url: "", error: result.error || "Generation failed" });
580
+ }
581
+ }
582
+ const succeeded = results.filter(r => !r.error).length;
583
+ return { success: true, data: { total: items.length, succeeded, failed: items.length - succeeded, results } };
584
+ }
585
+ default:
586
+ return { success: false, error: `Unknown documents action: ${action}. Valid: create, find, delete, create_template, list_templates, from_template, list_stores, list_profiles, generate` };
587
+ }
588
+ }
@@ -0,0 +1,6 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ export declare function handleCreations(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
3
+ success: boolean;
4
+ data?: unknown;
5
+ error?: string;
6
+ }>;