whale-code 6.4.0 → 6.5.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 (187) hide show
  1. package/bin/swagmanager-mcp.js +7 -0
  2. package/dist/cli/app.js +30 -2
  3. package/dist/cli/chat/ChatApp.d.ts +4 -4
  4. package/dist/cli/chat/ChatApp.js +114 -44
  5. package/dist/cli/chat/ChatInput.d.ts +13 -6
  6. package/dist/cli/chat/ChatInput.js +433 -89
  7. package/dist/cli/chat/MemoryManager.d.ts +15 -0
  8. package/dist/cli/chat/MemoryManager.js +61 -0
  9. package/dist/cli/chat/MessageList.d.ts +8 -0
  10. package/dist/cli/chat/MessageList.js +1 -1
  11. package/dist/cli/chat/NodeManager.d.ts +30 -0
  12. package/dist/cli/chat/NodeManager.js +89 -0
  13. package/dist/cli/chat/NodeSelector.d.ts +19 -0
  14. package/dist/cli/chat/NodeSelector.js +37 -0
  15. package/dist/cli/chat/PlanApproval.d.ts +17 -0
  16. package/dist/cli/chat/PlanApproval.js +82 -0
  17. package/dist/cli/chat/SessionManager.d.ts +16 -0
  18. package/dist/cli/chat/SessionManager.js +43 -0
  19. package/dist/cli/chat/SlashMenu.d.ts +38 -0
  20. package/dist/cli/chat/SlashMenu.js +208 -0
  21. package/dist/cli/chat/StatusBar.d.ts +16 -0
  22. package/dist/cli/chat/StatusBar.js +22 -0
  23. package/dist/cli/chat/ThemeSelector.d.ts +14 -0
  24. package/dist/cli/chat/ThemeSelector.js +29 -0
  25. package/dist/cli/chat/ToolIndicator.d.ts +8 -0
  26. package/dist/cli/chat/ToolIndicator.js +33 -9
  27. package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
  28. package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
  29. package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
  30. package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
  31. package/dist/cli/commands/config-cmd.js +4 -25
  32. package/dist/cli/commands/db.d.ts +13 -0
  33. package/dist/cli/commands/db.js +243 -0
  34. package/dist/cli/commands/doctor.js +6 -9
  35. package/dist/cli/commands/mcp.js +1 -20
  36. package/dist/cli/services/agent-events.d.ts +22 -1
  37. package/dist/cli/services/agent-events.js +9 -0
  38. package/dist/cli/services/agent-loop.js +66 -2
  39. package/dist/cli/services/agent-worker-base.js +21 -6
  40. package/dist/cli/services/api-retry.d.ts +25 -0
  41. package/dist/cli/services/api-retry.js +91 -0
  42. package/dist/cli/services/auth-service.d.ts +1 -1
  43. package/dist/cli/services/auth-service.js +40 -19
  44. package/dist/cli/services/background-processes.js +26 -2
  45. package/dist/cli/services/config-store.d.ts +13 -1
  46. package/dist/cli/services/config-store.js +116 -13
  47. package/dist/cli/services/format-server-response.js +12 -6
  48. package/dist/cli/services/ink-resize-fix.d.ts +18 -0
  49. package/dist/cli/services/ink-resize-fix.js +66 -0
  50. package/dist/cli/services/interactive-tools.d.ts +14 -0
  51. package/dist/cli/services/interactive-tools.js +47 -2
  52. package/dist/cli/services/keybinding-manager.js +1 -1
  53. package/dist/cli/services/local-tools.js +35 -2
  54. package/dist/cli/services/server-tools.js +175 -3
  55. package/dist/cli/services/subagent.js +15 -3
  56. package/dist/cli/services/system-prompt.js +5 -3
  57. package/dist/cli/services/task-decomposer.d.ts +35 -0
  58. package/dist/cli/services/task-decomposer.js +199 -0
  59. package/dist/cli/services/team-lead.d.ts +18 -0
  60. package/dist/cli/services/team-lead.js +80 -0
  61. package/dist/cli/services/teammate.js +5 -5
  62. package/dist/cli/services/telemetry.d.ts +8 -2
  63. package/dist/cli/services/telemetry.js +116 -92
  64. package/dist/cli/services/tools/agent-tools.d.ts +1 -0
  65. package/dist/cli/services/tools/agent-tools.js +50 -4
  66. package/dist/cli/services/tools/file-ops.d.ts +2 -0
  67. package/dist/cli/services/tools/file-ops.js +71 -19
  68. package/dist/cli/services/tools/shell-exec.js +22 -12
  69. package/dist/cli/shared/Theme.d.ts +1 -2
  70. package/dist/cli/shared/Theme.js +1 -1
  71. package/dist/cli/shared/WhaleBanner.d.ts +4 -1
  72. package/dist/cli/shared/WhaleBanner.js +12 -8
  73. package/dist/cli/shared/markdown.d.ts +5 -4
  74. package/dist/cli/shared/markdown.js +376 -334
  75. package/dist/cli/shared/theme-manager.d.ts +27 -0
  76. package/dist/cli/shared/theme-manager.js +178 -0
  77. package/dist/cli/shared/theme-presets.d.ts +16 -0
  78. package/dist/cli/shared/theme-presets.js +265 -0
  79. package/dist/index.js +0 -51
  80. package/dist/node/adapters/imessage.d.ts +10 -0
  81. package/dist/node/adapters/imessage.js +45 -6
  82. package/dist/node/cli.js +459 -8
  83. package/dist/node/config.d.ts +17 -0
  84. package/dist/node/gateway-client.d.ts +55 -0
  85. package/dist/node/gateway-client.js +201 -0
  86. package/dist/node/portal/clipboard.d.ts +28 -0
  87. package/dist/node/portal/clipboard.js +183 -0
  88. package/dist/node/portal/discovery.d.ts +29 -0
  89. package/dist/node/portal/discovery.js +61 -0
  90. package/dist/node/portal/forward.d.ts +30 -0
  91. package/dist/node/portal/forward.js +90 -0
  92. package/dist/node/portal/index.d.ts +47 -0
  93. package/dist/node/portal/index.js +250 -0
  94. package/dist/node/portal/multiplexer.d.ts +48 -0
  95. package/dist/node/portal/multiplexer.js +207 -0
  96. package/dist/node/portal/permissions.d.ts +36 -0
  97. package/dist/node/portal/permissions.js +131 -0
  98. package/dist/node/portal/protocol.d.ts +140 -0
  99. package/dist/node/portal/protocol.js +193 -0
  100. package/dist/node/portal/screen.d.ts +18 -0
  101. package/dist/node/portal/screen.js +93 -0
  102. package/dist/node/portal/session.d.ts +68 -0
  103. package/dist/node/portal/session.js +127 -0
  104. package/dist/node/portal/shell.d.ts +26 -0
  105. package/dist/node/portal/shell.js +142 -0
  106. package/dist/node/portal/stream.d.ts +43 -0
  107. package/dist/node/portal/stream.js +90 -0
  108. package/dist/node/portal/transfer.d.ts +33 -0
  109. package/dist/node/portal/transfer.js +231 -0
  110. package/dist/node/portal/ui.d.ts +16 -0
  111. package/dist/node/portal/ui.js +148 -0
  112. package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
  113. package/dist/node/remote-desktop/compile-helper.js +73 -0
  114. package/dist/node/remote-desktop/index.d.ts +67 -0
  115. package/dist/node/remote-desktop/index.js +220 -0
  116. package/dist/node/remote-desktop/protocol.d.ts +96 -0
  117. package/dist/node/remote-desktop/protocol.js +67 -0
  118. package/dist/node/runtime.d.ts +8 -1
  119. package/dist/node/runtime.js +117 -9
  120. package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
  121. package/dist/server/handlers/__test-utils__/test-db.js +128 -0
  122. package/dist/server/handlers/api-keys.js +26 -2
  123. package/dist/server/handlers/browser.d.ts +0 -4
  124. package/dist/server/handlers/browser.js +0 -46
  125. package/dist/server/handlers/catalog.js +37 -14
  126. package/dist/server/handlers/clickhouse.d.ts +10 -0
  127. package/dist/server/handlers/clickhouse.js +215 -0
  128. package/dist/server/handlers/comms.d.ts +308 -4
  129. package/dist/server/handlers/comms.js +444 -11
  130. package/dist/server/handlers/creations.js +1 -1
  131. package/dist/server/handlers/crm.d.ts +54 -8
  132. package/dist/server/handlers/crm.js +353 -68
  133. package/dist/server/handlers/embeddings.js +3 -3
  134. package/dist/server/handlers/enrichment.js +39 -55
  135. package/dist/server/handlers/inventory.js +1 -1
  136. package/dist/server/handlers/kali.d.ts +9 -1
  137. package/dist/server/handlers/kali.js +50 -1
  138. package/dist/server/handlers/media.d.ts +8 -0
  139. package/dist/server/handlers/media.js +902 -0
  140. package/dist/server/handlers/meta-ads.js +6 -3
  141. package/dist/server/handlers/nodes.d.ts +2 -0
  142. package/dist/server/handlers/nodes.js +331 -40
  143. package/dist/server/handlers/operations.d.ts +4 -6
  144. package/dist/server/handlers/operations.js +99 -38
  145. package/dist/server/handlers/platform.js +224 -107
  146. package/dist/server/handlers/remove-bg.d.ts +6 -0
  147. package/dist/server/handlers/remove-bg.js +96 -0
  148. package/dist/server/handlers/storefront.d.ts +6 -0
  149. package/dist/server/handlers/storefront.js +477 -0
  150. package/dist/server/handlers/supply-chain.js +21 -3
  151. package/dist/server/handlers/workflow-steps.js +87 -31
  152. package/dist/server/handlers/workflows.js +4 -1
  153. package/dist/server/index.js +334 -88
  154. package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
  155. package/dist/server/lib/clickhouse-buffer.js +175 -0
  156. package/dist/server/lib/clickhouse-client.d.ts +112 -0
  157. package/dist/server/lib/clickhouse-client.js +141 -0
  158. package/dist/server/lib/coa-renderer.d.ts +91 -0
  159. package/dist/server/lib/coa-renderer.js +411 -0
  160. package/dist/server/lib/compaction-service.js +45 -1
  161. package/dist/server/lib/pdf-renderer.d.ts +143 -0
  162. package/dist/server/lib/pdf-renderer.js +867 -0
  163. package/dist/server/lib/react-pdf-layout.d.ts +40 -0
  164. package/dist/server/lib/react-pdf-layout.js +437 -0
  165. package/dist/server/lib/server-agent-loop.d.ts +2 -0
  166. package/dist/server/lib/server-agent-loop.js +61 -15
  167. package/dist/server/lib/server-subagent.d.ts +3 -0
  168. package/dist/server/lib/server-subagent.js +7 -4
  169. package/dist/server/lib/supabase-client.js +51 -3
  170. package/dist/server/lib/template-resolver.js +14 -4
  171. package/dist/server/lib/utils.js +15 -0
  172. package/dist/server/local-agent-gateway.d.ts +44 -0
  173. package/dist/server/local-agent-gateway.js +389 -49
  174. package/dist/server/providers/anthropic.js +12 -2
  175. package/dist/server/providers/gemini.js +17 -2
  176. package/dist/server/proxy-handlers.js +151 -0
  177. package/dist/server/tool-router.d.ts +2 -2
  178. package/dist/server/tool-router.js +25 -35
  179. package/dist/shared/agent-core.d.ts +5 -2
  180. package/dist/shared/agent-core.js +30 -4
  181. package/dist/shared/api-client.js +54 -3
  182. package/dist/shared/sse-parser.d.ts +1 -1
  183. package/dist/shared/sse-parser.js +5 -2
  184. package/dist/shared/tool-dispatch.js +1 -1
  185. package/package.json +16 -10
  186. package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
  187. package/dist/server/handlers/__test-utils__/mock-supabase.js +0 -393
@@ -1,5 +1,5 @@
1
1
  import type { NodeConfig } from "./config.js";
2
- import type { MessagePayload } from "./adapters/base.js";
2
+ import type { BaseAdapter, MessagePayload } from "./adapters/base.js";
3
3
  export type NodeStatus = "starting" | "connected" | "degraded" | "disconnected";
4
4
  export interface NodeRuntimeStats {
5
5
  status: NodeStatus;
@@ -32,10 +32,17 @@ export declare class NodeRuntime {
32
32
  private heartbeatsFailed;
33
33
  private messagesRelayed;
34
34
  private messagesFailed;
35
+ private gatewayClient;
36
+ private screenCapture;
37
+ private portalManager;
35
38
  constructor(config: NodeConfig);
36
39
  getStats(): NodeRuntimeStats;
37
40
  start(): Promise<void>;
38
41
  stop(): Promise<void>;
42
+ /**
43
+ * Get a channel adapter by ID.
44
+ */
45
+ getAdapter(channelId: string): BaseAdapter | undefined;
39
46
  private startAdapter;
40
47
  /** Fetch with retry and exponential backoff */
41
48
  private fetchWithRetry;
@@ -8,6 +8,9 @@ import { WhatsAppAdapter } from "./adapters/whatsapp.js";
8
8
  import { SmsAdapter } from "./adapters/sms.js";
9
9
  import { EmailAdapter } from "./adapters/email.js";
10
10
  import { captureError, addBreadcrumb } from "../cli/services/error-logger.js";
11
+ import { GatewayClient } from "./gateway-client.js";
12
+ import { ScreenCaptureService } from "./remote-desktop/index.js";
13
+ import { PortalManager } from "./portal/index.js";
11
14
  const HEARTBEAT_INTERVAL_MS = 60_000;
12
15
  const POLL_INTERVAL_MS = 2_000;
13
16
  const VERSION = "1.1.0";
@@ -30,6 +33,12 @@ export class NodeRuntime {
30
33
  heartbeatsFailed = 0;
31
34
  messagesRelayed = 0;
32
35
  messagesFailed = 0;
36
+ // Server gateway connection
37
+ gatewayClient = null;
38
+ // Screen capture service (macOS only)
39
+ screenCapture = null;
40
+ // Portal manager (node-to-node connectivity)
41
+ portalManager = null;
33
42
  constructor(config) {
34
43
  this.config = config;
35
44
  }
@@ -71,10 +80,79 @@ export class NodeRuntime {
71
80
  for (const ch of this.config.channels) {
72
81
  await this.startAdapter(ch);
73
82
  }
83
+ // Connect to server gateway for receiving commands
84
+ this.gatewayClient = new GatewayClient({
85
+ serverUrl: this.config.server_url,
86
+ apiKey: this.config.api_key,
87
+ version: VERSION,
88
+ });
89
+ // Register exec command handler
90
+ this.gatewayClient.onCommand("exec", async (msg) => {
91
+ const { execSync } = await import("node:child_process");
92
+ try {
93
+ const stdout = execSync(msg.command, {
94
+ timeout: msg.timeout || 30_000,
95
+ cwd: process.env.HOME,
96
+ encoding: "utf-8",
97
+ maxBuffer: 512 * 1024,
98
+ });
99
+ return { stdout, exit_code: 0 };
100
+ }
101
+ catch (err) {
102
+ return {
103
+ stdout: err.stdout || "",
104
+ stderr: err.stderr || err.message,
105
+ exit_code: err.status || 1,
106
+ };
107
+ }
108
+ });
109
+ // Register remote desktop command handler on gateway — forward input events
110
+ this.gatewayClient.onCommand("remote_desktop", async (msg) => {
111
+ if (!this.screenCapture) {
112
+ return { error: "Screen capture not enabled" };
113
+ }
114
+ if (msg.type === "input") {
115
+ this.screenCapture.injectInput(msg);
116
+ return null;
117
+ }
118
+ // Return capture status for session requests
119
+ return {
120
+ type: "remote_desktop_response",
121
+ success: true,
122
+ ...this.screenCapture.getStatus(),
123
+ };
124
+ });
125
+ this.gatewayClient.start();
126
+ // Start screen capture service (macOS only)
127
+ if (process.platform === "darwin" && this.config.remote_desktop?.enabled !== false) {
128
+ try {
129
+ this.screenCapture = new ScreenCaptureService(this.config.remote_desktop || {});
130
+ await this.screenCapture.start();
131
+ }
132
+ catch (err) {
133
+ console.warn("[node] Screen capture failed to start:", err.message);
134
+ }
135
+ }
136
+ // Initialize portal manager
137
+ try {
138
+ const portalConfig = this.config.portal || {};
139
+ this.portalManager = new PortalManager({
140
+ nodeConfig: this.config,
141
+ gateway: this.gatewayClient,
142
+ screenCapture: this.screenCapture,
143
+ receiveDir: portalConfig.receive_dir,
144
+ autoAcceptAdmins: portalConfig.auto_accept_admins !== false,
145
+ maxSessions: portalConfig.max_sessions || 5,
146
+ });
147
+ console.log("[node] Portal handler registered");
148
+ }
149
+ catch (err) {
150
+ console.warn("[node] Portal handler failed to initialize:", err.message);
151
+ }
74
152
  // Handle graceful shutdown
75
153
  process.on("SIGTERM", () => this.stop());
76
154
  process.on("SIGINT", () => this.stop());
77
- console.log(`[node] Running with ${this.adapters.size} channel adapter(s). Status: ${this.status}. Press Ctrl+C to stop.`);
155
+ console.log(`[node] Running with ${this.adapters.size} channel adapter(s). Gateway: connecting. Press Ctrl+C to stop.`);
78
156
  }
79
157
  async stop() {
80
158
  console.log(`[node] Shutting down...`);
@@ -84,6 +162,19 @@ export class NodeRuntime {
84
162
  clearInterval(this.heartbeatTimer);
85
163
  for (const [, timer] of this.pollTimers)
86
164
  clearInterval(timer);
165
+ // Stop portal sessions
166
+ if (this.portalManager) {
167
+ this.portalManager.closeAll();
168
+ this.portalManager = null;
169
+ }
170
+ // Stop screen capture
171
+ if (this.screenCapture) {
172
+ await this.screenCapture.stop();
173
+ this.screenCapture = null;
174
+ }
175
+ // Disconnect from server gateway
176
+ if (this.gatewayClient)
177
+ this.gatewayClient.stop();
87
178
  for (const [name, adapter] of this.adapters) {
88
179
  console.log(`[node] Stopping adapter: ${name}`);
89
180
  await adapter.stop();
@@ -91,6 +182,12 @@ export class NodeRuntime {
91
182
  console.log(`[node] Stopped.`);
92
183
  process.exit(0);
93
184
  }
185
+ /**
186
+ * Get a channel adapter by ID.
187
+ */
188
+ getAdapter(channelId) {
189
+ return this.adapters.get(channelId);
190
+ }
94
191
  async startAdapter(ch) {
95
192
  let adapter;
96
193
  switch (ch.type) {
@@ -282,12 +379,21 @@ export class NodeRuntime {
282
379
  }
283
380
  async sendHeartbeat() {
284
381
  try {
285
- const channelStatuses = Array.from(this.adapters.entries()).map(([, adapter]) => ({
286
- type: adapter.type,
287
- name: adapter.name,
288
- status: adapter.isRunning() ? "active" : "error",
289
- stats: adapter.getStats(),
290
- }));
382
+ const channelStatuses = Array.from(this.adapters.entries()).map(([, adapter]) => {
383
+ const base = {
384
+ type: adapter.type,
385
+ name: adapter.name,
386
+ status: adapter.isRunning() ? "active" : "error",
387
+ stats: adapter.getStats(),
388
+ };
389
+ // Include diagnostics for iMessage adapters
390
+ if (adapter.type === "imessage" && "getLastError" in adapter) {
391
+ const imsg = adapter;
392
+ base.last_error = imsg.getLastError();
393
+ base.restart_attempts = imsg.getRestartAttempts();
394
+ }
395
+ return base;
396
+ });
291
397
  const res = await this.fetchWithRetry(`${this.config.server_url}/nodes/heartbeat`, {
292
398
  method: "POST",
293
399
  headers: {
@@ -344,8 +450,10 @@ function getHardwareInfo() {
344
450
  };
345
451
  }
346
452
  function getCapabilities() {
347
- const caps = ["messaging", "discord", "slack", "telegram", "webchat", "whatsapp", "sms", "email"];
348
- if (process.platform === "darwin")
453
+ const caps = ["messaging", "discord", "slack", "telegram", "webchat", "whatsapp", "sms", "email", "portal"];
454
+ if (process.platform === "darwin") {
349
455
  caps.push("imessage");
456
+ caps.push("remote_desktop");
457
+ }
350
458
  return caps;
351
459
  }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Real Supabase test helper for handler tests.
3
+ * Replaces mock-supabase.ts — all operations hit the real database.
4
+ *
5
+ * When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
6
+ * exports are stubbed so test files can skip gracefully via:
7
+ * describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
8
+ */
9
+ import { SupabaseClient } from "@supabase/supabase-js";
10
+ /** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
11
+ export declare const HAS_TEST_DB: boolean;
12
+ export declare const TEST_STORE: string;
13
+ /** Unique prefix per test run */
14
+ export declare const RUN_PREFIX: string;
15
+ /** Get the real Supabase client (pass to handlers as `sb`) */
16
+ export declare function getTestClient(): SupabaseClient;
17
+ /** Insert rows and return them with DB-generated fields */
18
+ export declare function seed(table: string, rows: Record<string, unknown>[]): Promise<any[]>;
19
+ /** Delete rows by IDs */
20
+ export declare function deleteByIds(table: string, ids: string[]): Promise<void>;
21
+ /** Delete rows matching column=value */
22
+ export declare function deleteWhere(table: string, column: string, value: unknown): Promise<void>;
23
+ export declare function seedTracked(table: string, rows: Record<string, unknown>[]): Promise<any[]>;
24
+ /** Delete all tracked rows in FK-safe order */
25
+ export declare function cleanup(): Promise<void>;
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Real Supabase test helper for handler tests.
3
+ * Replaces mock-supabase.ts — all operations hit the real database.
4
+ *
5
+ * When SUPABASE_URL / SUPABASE_SERVICE_ROLE_KEY / TEST_STORE_ID are missing,
6
+ * exports are stubbed so test files can skip gracefully via:
7
+ * describe.skipIf(!HAS_TEST_DB)("...", () => { ... })
8
+ */
9
+ import { createClient } from "@supabase/supabase-js";
10
+ import { randomUUID } from "node:crypto";
11
+ const url = process.env.SUPABASE_URL;
12
+ const key = process.env.SUPABASE_SERVICE_ROLE_KEY;
13
+ const storeId = process.env.TEST_STORE_ID;
14
+ /** true when all required env vars are present — use with describe.skipIf(!HAS_TEST_DB) */
15
+ export const HAS_TEST_DB = !!(url && key && storeId);
16
+ const sb = HAS_TEST_DB ? createClient(url, key) : null;
17
+ export const TEST_STORE = storeId ?? "missing-test-store";
18
+ /** Unique prefix per test run */
19
+ export const RUN_PREFIX = `T${randomUUID().slice(0, 6)}`;
20
+ /** Get the real Supabase client (pass to handlers as `sb`) */
21
+ export function getTestClient() {
22
+ return sb;
23
+ }
24
+ /** Insert rows and return them with DB-generated fields */
25
+ export async function seed(table, rows) {
26
+ if (!rows.length)
27
+ return [];
28
+ const { data, error } = await sb.from(table).insert(rows).select();
29
+ if (error)
30
+ throw new Error(`seed(${table}): ${error.message}`);
31
+ return data;
32
+ }
33
+ /** Delete rows by IDs */
34
+ export async function deleteByIds(table, ids) {
35
+ if (!ids.length)
36
+ return;
37
+ const { error } = await sb.from(table).delete().in("id", ids);
38
+ if (error)
39
+ throw new Error(`deleteByIds(${table}): ${error.message}`);
40
+ }
41
+ /** Delete rows matching column=value */
42
+ export async function deleteWhere(table, column, value) {
43
+ const { error } = await sb.from(table).delete().eq(column, value);
44
+ if (error)
45
+ throw new Error(`deleteWhere(${table}): ${error.message}`);
46
+ }
47
+ /** Track seeded data for automatic cleanup */
48
+ const tracked = new Map();
49
+ export async function seedTracked(table, rows) {
50
+ const data = await seed(table, rows);
51
+ const ids = data.map((r) => r.id);
52
+ tracked.set(table, [...(tracked.get(table) || []), ...ids]);
53
+ return data;
54
+ }
55
+ /** Delete all tracked rows in FK-safe order */
56
+ export async function cleanup() {
57
+ const fkOrder = [
58
+ "ai_messages",
59
+ "ai_conversation_checkpoints",
60
+ "ai_conversations",
61
+ "ai_agent_config",
62
+ "platform_secrets",
63
+ "business_audit",
64
+ "workflow_step_runs",
65
+ "workflow_dlq",
66
+ "workflow_runs",
67
+ "workflow_versions",
68
+ "workflow_steps",
69
+ "workflows",
70
+ "error_events",
71
+ "payment_intents",
72
+ "order_items",
73
+ "cart_items",
74
+ "webhook_events",
75
+ "inventory_levels",
76
+ "inventory",
77
+ "email_sends",
78
+ "orders",
79
+ "carts",
80
+ "products",
81
+ "categories",
82
+ "catalogs",
83
+ "creation_collections",
84
+ "customers",
85
+ "store_customer_profiles",
86
+ "user_creation_relationships",
87
+ "creations",
88
+ "locations",
89
+ "webhook_configs",
90
+ "webhook_endpoints",
91
+ "api_keys",
92
+ "platform_users",
93
+ "store_media",
94
+ "purchase_orders",
95
+ "suppliers",
96
+ "stock_transfers",
97
+ "v_daily_sales",
98
+ "meta_ads",
99
+ "meta_ad_sets",
100
+ "meta_campaigns",
101
+ "meta_integrations",
102
+ "channel_messages",
103
+ "channels",
104
+ "node_events",
105
+ "nodes",
106
+ "store_members",
107
+ "store_plans",
108
+ "store_usage",
109
+ "email_templates",
110
+ "email_threads",
111
+ "customer_risk_scores",
112
+ "customer_exposures",
113
+ "customer_breach_records",
114
+ "customer_enrichment_profiles",
115
+ "stores",
116
+ ];
117
+ const orderedTables = [
118
+ ...fkOrder.filter((t) => tracked.has(t)),
119
+ ...[...tracked.keys()].filter((t) => !fkOrder.includes(t)),
120
+ ];
121
+ for (const table of orderedTables) {
122
+ const ids = tracked.get(table);
123
+ if (ids?.length) {
124
+ await deleteByIds(table, ids).catch(() => { });
125
+ }
126
+ }
127
+ tracked.clear();
128
+ }
@@ -1,7 +1,7 @@
1
1
  // server/handlers/api-keys.ts — API Key management: create, list, get, revoke, update
2
2
  // Supports linking keys to creations (TV menus, displays, landing pages)
3
3
  import { createHash, randomUUID } from "node:crypto";
4
- const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, created_at, updated_at";
4
+ const KEY_COLS = "id, name, key_prefix, key_type, scope, is_active, rate_limit_per_minute, rate_limit_per_day, last_used_at, request_count, expires_at, revoked_at, revoked_reason, creation_id, client_store_id, created_at, updated_at";
5
5
  export async function handleAPIKeys(sb, args, storeId) {
6
6
  const sid = storeId;
7
7
  const action = args.action;
@@ -20,6 +20,7 @@ export async function handleAPIKeys(sb, args, storeId) {
20
20
  const rateLimitPerDay = args.rate_limit_per_day || 10000;
21
21
  const expiresAt = args.expires_at;
22
22
  const creationId = args.creation_id;
23
+ const clientStoreId = args.client_store_id;
23
24
  // Resolve owner — use provided owner_user_id or find store owner
24
25
  let ownerUserId = args.owner_user_id;
25
26
  if (!ownerUserId) {
@@ -65,6 +66,7 @@ export async function handleAPIKeys(sb, args, storeId) {
65
66
  rate_limit_per_day: rateLimitPerDay,
66
67
  expires_at: expiresAt || null,
67
68
  creation_id: creationId || null,
69
+ client_store_id: clientStoreId || null,
68
70
  }).select(KEY_COLS).single();
69
71
  if (error)
70
72
  return { success: false, error: error.message };
@@ -74,6 +76,17 @@ export async function handleAPIKeys(sb, args, storeId) {
74
76
  ...data,
75
77
  key_value: keyValue,
76
78
  warning: "Copy this key now. The full key value will NOT be returned again — only the hash is stored.",
79
+ api_gateway: {
80
+ base_url: "https://whale-gateway.fly.dev",
81
+ docs_url: "https://whale-gateway.fly.dev/docs",
82
+ openapi_spec: "https://whale-gateway.fly.dev/openapi.json",
83
+ usage: `curl -H "x-api-key: ${keyValue}" https://whale-gateway.fly.dev/v1/stores/${sid}/products`,
84
+ scopes: scopes,
85
+ rate_limits: {
86
+ per_minute: rateLimitPerMinute,
87
+ per_day: rateLimitPerDay,
88
+ },
89
+ },
77
90
  },
78
91
  };
79
92
  }
@@ -95,7 +108,18 @@ export async function handleAPIKeys(sb, args, storeId) {
95
108
  const { data, error } = await q;
96
109
  if (error)
97
110
  return { success: false, error: error.message };
98
- return { success: true, data: { count: data?.length || 0, keys: data } };
111
+ return {
112
+ success: true,
113
+ data: {
114
+ count: data?.length || 0,
115
+ keys: data,
116
+ api_gateway: {
117
+ base_url: "https://whale-gateway.fly.dev",
118
+ docs_url: "https://whale-gateway.fly.dev/docs",
119
+ openapi_spec: "https://whale-gateway.fly.dev/openapi.json",
120
+ },
121
+ },
122
+ };
99
123
  }
100
124
  // ---- get: Get a single API key by ID ----
101
125
  case "get": {
@@ -1,8 +1,4 @@
1
1
  import type { SupabaseClient } from "@supabase/supabase-js";
2
- export declare function generatePdfFromHtml(html: string, options?: {
3
- format?: string;
4
- landscape?: boolean;
5
- }): Promise<Buffer>;
6
2
  export declare function handleBrowser(_sb: SupabaseClient, args: Record<string, unknown>, _storeId?: string): Promise<{
7
3
  success: boolean;
8
4
  data?: unknown;
@@ -274,52 +274,6 @@ async function actionPdf(url, waitFor) {
274
274
  };
275
275
  });
276
276
  }
277
- // ============================================================================
278
- // PDF FROM HTML (reusable by documents handler)
279
- // ============================================================================
280
- export async function generatePdfFromHtml(html, options) {
281
- // P0 FIX: Strip dangerous tags from user HTML before rendering to prevent
282
- // script execution, SSRF via iframes, and local file reads
283
- const sanitizedHtml = html
284
- .replace(/<script[\s\S]*?<\/script>/gi, "")
285
- .replace(/<script[^>]*\/?>/gi, "")
286
- .replace(/<iframe[\s\S]*?<\/iframe>/gi, "")
287
- .replace(/<iframe[^>]*\/?>/gi, "")
288
- .replace(/<object[\s\S]*?<\/object>/gi, "")
289
- .replace(/<object[^>]*\/?>/gi, "")
290
- .replace(/<embed[^>]*\/?>/gi, "")
291
- .replace(/<link[^>]*\/?>/gi, "");
292
- const browser = await getBrowser();
293
- const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
294
- const page = await context.newPage();
295
- try {
296
- // P0 FIX: Block all network requests to prevent SSRF from headless Chromium.
297
- // Only allow data: URIs (inline images/fonts) and about:blank.
298
- await page.route('**/*', (route) => {
299
- const url = route.request().url();
300
- if (url.startsWith('data:') || url === 'about:blank') {
301
- route.continue();
302
- }
303
- else {
304
- route.abort();
305
- }
306
- });
307
- // P0 FIX: Use domcontentloaded instead of networkidle to avoid waiting for
308
- // (now-blocked) network requests and reduce attack surface
309
- await page.setContent(sanitizedHtml, { waitUntil: "domcontentloaded", timeout: PAGE_TIMEOUT_MS });
310
- const buffer = await page.pdf({
311
- format: options?.format || "A4",
312
- printBackground: true,
313
- landscape: options?.landscape || false,
314
- margin: { top: "1cm", bottom: "1cm", left: "1cm", right: "1cm" },
315
- });
316
- return Buffer.from(buffer);
317
- }
318
- finally {
319
- await page.close().catch(() => { });
320
- await context.close().catch(() => { });
321
- }
322
- }
323
277
  async function callCapMonster(taskType, params) {
324
278
  const apiKey = process.env.CAPMONSTER_API_KEY;
325
279
  if (!apiKey)
@@ -150,7 +150,11 @@ export async function handleProducts(sb, args, storeId) {
150
150
  if (args.pricing_data)
151
151
  insert.pricing_data = args.pricing_data;
152
152
  // custom_fields NOT inserted directly — schema is the source of truth
153
- // Agent-provided field_values are filtered against schema keys post-insert
153
+ // Agent-provided custom_fields are filtered against schema keys post-insert
154
+ if (args.featured_image !== undefined)
155
+ insert.featured_image = args.featured_image;
156
+ if (args.image_gallery !== undefined)
157
+ insert.image_gallery = args.image_gallery;
154
158
  if (args.is_wholesale !== undefined)
155
159
  insert.is_wholesale = args.is_wholesale;
156
160
  if (args.wholesale_only !== undefined)
@@ -203,7 +207,7 @@ export async function handleProducts(sb, args, storeId) {
203
207
  }
204
208
  }
205
209
  // Only accept agent values for keys that exist in the schema
206
- const agentValues = args.field_values || {};
210
+ const agentValues = args.custom_fields || {};
207
211
  for (const [k, v] of Object.entries(agentValues)) {
208
212
  if (schemaKeys.has(k))
209
213
  fieldValues[k] = v;
@@ -279,8 +283,8 @@ export async function handleProducts(sb, args, storeId) {
279
283
  if (args.pricing_data !== undefined)
280
284
  updates.pricing_data = args.pricing_data;
281
285
  // custom_fields filtered to schema keys only (schema = source of truth)
282
- if (args.field_values !== undefined) {
283
- const agentFV = args.field_values;
286
+ if (args.custom_fields !== undefined) {
287
+ const agentFV = args.custom_fields;
284
288
  // Look up product's linked field schema to get allowed keys
285
289
  const { data: pfs } = await sb.from("product_field_schemas").select("field_schema_id").eq("product_id", pid).limit(1);
286
290
  if (pfs?.length) {
@@ -312,6 +316,8 @@ export async function handleProducts(sb, args, storeId) {
312
316
  updates.minimum_wholesale_quantity = args.minimum_wholesale_quantity;
313
317
  if (args.featured_image !== undefined)
314
318
  updates.featured_image = args.featured_image;
319
+ if (args.image_gallery !== undefined)
320
+ updates.image_gallery = args.image_gallery;
315
321
  const updateCatArg = (args.category ?? args.primary_category_id ?? args.category_id);
316
322
  if (updateCatArg !== undefined) {
317
323
  if (!updateCatArg) {
@@ -494,6 +500,14 @@ export async function handleProducts(sb, args, storeId) {
494
500
  return { success: false, error: "name is required" };
495
501
  if (!args.fields || !Array.isArray(args.fields))
496
502
  return { success: false, error: "fields array is required" };
503
+ // Auto-resolve catalog_id: use provided value, or fall back to store's default catalog
504
+ let catalogId = args.catalog_id;
505
+ if (!catalogId) {
506
+ const { data: defaultCatalog } = await sb.from("catalogs")
507
+ .select("id").eq("store_id", sid).eq("is_default", true).single();
508
+ if (defaultCatalog)
509
+ catalogId = defaultCatalog.id;
510
+ }
497
511
  const insert = {
498
512
  store_id: sid,
499
513
  name,
@@ -504,12 +518,12 @@ export async function handleProducts(sb, args, storeId) {
504
518
  insert.description = args.description;
505
519
  if (args.icon)
506
520
  insert.icon = args.icon;
507
- if (args.catalog_id)
508
- insert.catalog_id = args.catalog_id;
521
+ if (catalogId)
522
+ insert.catalog_id = catalogId;
509
523
  if (args.is_public !== undefined)
510
524
  insert.is_public = args.is_public;
511
525
  const { data, error } = await sb.from("field_schemas").insert(insert)
512
- .select("id, name, slug, fields, icon, is_active, created_at").single();
526
+ .select("id, name, slug, fields, icon, is_active, catalog_id, created_at").single();
513
527
  return error ? { success: false, error: error.message } : { success: true, data };
514
528
  }
515
529
  case "update_field_schema": {
@@ -534,7 +548,7 @@ export async function handleProducts(sb, args, storeId) {
534
548
  // Only allow modification of own schemas (prevent IDOR)
535
549
  const { data, error } = await sb.from("field_schemas")
536
550
  .update(updates).eq("id", fsId).eq("store_id", sid)
537
- .select("id, name, slug, fields, icon, is_active, updated_at").single();
551
+ .select("id, name, slug, fields, icon, is_active, catalog_id, updated_at").single();
538
552
  return error ? { success: false, error: error.message } : { success: true, data };
539
553
  }
540
554
  case "delete_field_schema": {
@@ -585,6 +599,14 @@ export async function handleProducts(sb, args, storeId) {
585
599
  return { success: false, error: "name is required" };
586
600
  if (!args.tiers || !Array.isArray(args.tiers))
587
601
  return { success: false, error: "tiers array is required" };
602
+ // Auto-resolve catalog_id: use provided value, or fall back to store's default catalog
603
+ let catalogId = args.catalog_id;
604
+ if (!catalogId) {
605
+ const { data: defaultCatalog } = await sb.from("catalogs")
606
+ .select("id").eq("store_id", sid).eq("is_default", true).single();
607
+ if (defaultCatalog)
608
+ catalogId = defaultCatalog.id;
609
+ }
588
610
  const insert = {
589
611
  store_id: sid,
590
612
  name,
@@ -595,12 +617,12 @@ export async function handleProducts(sb, args, storeId) {
595
617
  insert.description = args.description;
596
618
  if (args.quality_tier)
597
619
  insert.quality_tier = args.quality_tier;
598
- if (args.catalog_id)
599
- insert.catalog_id = args.catalog_id;
620
+ if (catalogId)
621
+ insert.catalog_id = catalogId;
600
622
  if (args.is_public !== undefined)
601
623
  insert.is_public = args.is_public;
602
624
  const { data, error } = await sb.from("pricing_schemas").insert(insert)
603
- .select("id, name, slug, tiers, quality_tier, is_active, created_at").single();
625
+ .select("id, name, slug, tiers, quality_tier, is_active, catalog_id, created_at").single();
604
626
  return error ? { success: false, error: error.message } : { success: true, data };
605
627
  }
606
628
  case "update_pricing_schema": {
@@ -625,7 +647,7 @@ export async function handleProducts(sb, args, storeId) {
625
647
  // Only allow modification of own schemas (prevent IDOR)
626
648
  const { data, error } = await sb.from("pricing_schemas")
627
649
  .update(updates).eq("id", psId).eq("store_id", sid)
628
- .select("id, name, slug, tiers, quality_tier, is_active, updated_at").single();
650
+ .select("id, name, slug, tiers, quality_tier, is_active, catalog_id, updated_at").single();
629
651
  return error ? { success: false, error: error.message } : { success: true, data };
630
652
  }
631
653
  case "delete_pricing_schema": {
@@ -916,7 +938,7 @@ export async function handleProducts(sb, args, storeId) {
916
938
  .update({ product_id: primaryId }).eq("product_id", secondaryId);
917
939
  reassignResults.product_reviews = prErr ? `error: ${prErr.message}` : `moved ${prCount ?? 0} rows`;
918
940
  // 6. Fill in missing fields on primary from secondary
919
- const fillFields = ["description", "short_description", "cost_price", "wholesale_price", "weight", "featured_image"];
941
+ const fillFields = ["description", "short_description", "cost_price", "wholesale_price", "weight", "featured_image", "image_gallery"];
920
942
  const fills = {};
921
943
  for (const field of fillFields) {
922
944
  if (!primary[field] && secondary[field])
@@ -966,8 +988,9 @@ export async function handleCollections(sb, args, storeId) {
966
988
  return error ? { success: false, error: error.message } : { success: true, data };
967
989
  }
968
990
  case "create": {
991
+ const slug = args.slug || (args.name || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
969
992
  const { data, error } = await sb.from("creation_collections")
970
- .insert({ store_id: sid, name: args.name }).select().single();
993
+ .insert({ store_id: sid, name: args.name, slug }).select().single();
971
994
  return error ? { success: false, error: error.message } : { success: true, data };
972
995
  }
973
996
  default:
@@ -0,0 +1,10 @@
1
+ import type { SupabaseClient } from "@supabase/supabase-js";
2
+ /**
3
+ * ClickHouse observability handler — platform-level metrics via direct HTTP queries.
4
+ * Replaces FDW RPCs with direct ClickHouse queries for lower latency.
5
+ */
6
+ export declare function handleClickHouse(sb: SupabaseClient, args: Record<string, unknown>, storeId?: string): Promise<{
7
+ success: boolean;
8
+ data?: unknown;
9
+ error?: string;
10
+ }>;