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
package/dist/node/cli.js CHANGED
@@ -26,9 +26,16 @@ async function main() {
26
26
  case "node":
27
27
  await handleNodeCommand(subcommand, args.slice(2));
28
28
  break;
29
+ case "remote-desktop":
30
+ await handleRemoteDesktopCommand(subcommand, args.slice(2));
31
+ break;
29
32
  case "channel":
30
33
  await handleChannelCommand(subcommand, args.slice(2));
31
34
  break;
35
+ case "portal":
36
+ case "p":
37
+ await handlePortalCommand(subcommand, args.slice(2));
38
+ break;
32
39
  case "status":
33
40
  await handleStatus();
34
41
  break;
@@ -91,6 +98,10 @@ async function handleNodeCommand(sub, args) {
91
98
  }
92
99
  case "start": {
93
100
  const config = requireConfig();
101
+ // Inject API keys from config into env so decomposer/reviewer can find them
102
+ if (config.anthropic_api_key && !process.env.ANTHROPIC_API_KEY) {
103
+ process.env.ANTHROPIC_API_KEY = config.anthropic_api_key;
104
+ }
94
105
  const runtime = new NodeRuntime(config);
95
106
  await runtime.start();
96
107
  // Keep process alive
@@ -278,6 +289,37 @@ async function handleChannelCommand(sub, args) {
278
289
  console.log("Usage: whale channel [add|list]");
279
290
  }
280
291
  }
292
+ async function handleRemoteDesktopCommand(sub, _args) {
293
+ const config = requireConfig();
294
+ switch (sub) {
295
+ case "enable": {
296
+ config.remote_desktop = { ...config.remote_desktop, enabled: true };
297
+ saveConfig(config);
298
+ console.log("Remote desktop enabled. Takes effect on next `whale node start`.");
299
+ break;
300
+ }
301
+ case "disable": {
302
+ config.remote_desktop = { ...config.remote_desktop, enabled: false };
303
+ saveConfig(config);
304
+ console.log("Remote desktop disabled. Takes effect on next `whale node start`.");
305
+ break;
306
+ }
307
+ case "status": {
308
+ const rd = config.remote_desktop;
309
+ console.log("Remote Desktop:");
310
+ console.log(` Enabled: ${rd?.enabled !== false ? "yes" : "no"}`);
311
+ console.log(` Port: ${rd?.port || 5900}`);
312
+ console.log(` Max FPS: ${rd?.max_fps || 60}`);
313
+ console.log(` Quality: ${rd?.quality || 0.6}`);
314
+ if (process.platform !== "darwin") {
315
+ console.log(` Platform: ${process.platform} (macOS required)`);
316
+ }
317
+ break;
318
+ }
319
+ default:
320
+ console.log("Usage: whale remote-desktop [enable|disable|status]");
321
+ }
322
+ }
281
323
  async function handleStatus() {
282
324
  const config = loadConfig();
283
325
  if (!config) {
@@ -291,21 +333,430 @@ async function handleStatus() {
291
333
  console.log(` Channels: ${config.channels.length}`);
292
334
  console.log(` Config: ${getConfigPath()}`);
293
335
  }
336
+ // ============================================================================
337
+ // PORTAL COMMANDS
338
+ // ============================================================================
339
+ async function handlePortalCommand(sub, args) {
340
+ const config = requireConfig();
341
+ // Lazy imports — only load portal code when needed
342
+ const { discoverNodes, findNode } = await import("./portal/discovery.js");
343
+ const { printNodeList, printConnecting, printConnected, printDisconnected, printError, printProgress, showPortalMenu } = await import("./portal/ui.js");
344
+ const discoveryOpts = {
345
+ serverUrl: config.server_url,
346
+ apiKey: config.api_key,
347
+ storeId: config.store_id,
348
+ };
349
+ switch (sub) {
350
+ case "ls":
351
+ case "list": {
352
+ const { values } = parseArgs({
353
+ args,
354
+ options: { all: { type: "boolean", short: "a", default: false } },
355
+ });
356
+ const nodes = await discoverNodes({ ...discoveryOpts, includeOffline: values.all });
357
+ printNodeList(nodes, values.all);
358
+ break;
359
+ }
360
+ case "sh":
361
+ case "shell": {
362
+ const nodeName = args[0];
363
+ if (!nodeName) {
364
+ console.error("Usage: whale portal sh <node> [--dir PATH] [-c COMMAND]");
365
+ process.exit(1);
366
+ }
367
+ const { values } = parseArgs({
368
+ args: args.slice(1),
369
+ options: {
370
+ dir: { type: "string" },
371
+ c: { type: "string" },
372
+ },
373
+ });
374
+ const node = await findNode(nodeName, discoveryOpts);
375
+ if (!node) {
376
+ printError(`Node "${nodeName}" not found or offline`);
377
+ process.exit(1);
378
+ }
379
+ printConnecting(node.name);
380
+ const { GatewayClient } = await import("./gateway-client.js");
381
+ const { PortalManager } = await import("./portal/index.js");
382
+ const { openShell } = await import("./portal/shell.js");
383
+ const gateway = new GatewayClient({
384
+ serverUrl: config.server_url,
385
+ apiKey: config.api_key,
386
+ capabilities: ["portal"],
387
+ version: "1.1.0",
388
+ });
389
+ gateway.start();
390
+ await waitForGateway(gateway);
391
+ const portal = new PortalManager({ nodeConfig: config, gateway });
392
+ const session = await portal.connect(node.id, ["shell"]);
393
+ printConnected(node.hostname);
394
+ const { cleanup } = await openShell(session, {
395
+ cwd: values.dir,
396
+ command: values.c,
397
+ });
398
+ // If single command mode, we're already done
399
+ if (values.c) {
400
+ cleanup();
401
+ session.close();
402
+ gateway.stop();
403
+ break;
404
+ }
405
+ // Wait for shell to close
406
+ await new Promise((resolve) => {
407
+ session.on("close", () => {
408
+ cleanup();
409
+ printDisconnected();
410
+ resolve();
411
+ });
412
+ });
413
+ gateway.stop();
414
+ break;
415
+ }
416
+ case "push": {
417
+ const localPath = args[0];
418
+ const target = args[1];
419
+ if (!localPath || !target) {
420
+ console.error("Usage: whale portal push <local-path> <node>[:/remote/path]");
421
+ process.exit(1);
422
+ }
423
+ const [nodeName, remotePath] = target.includes(":") ? target.split(":", 2) : [target, undefined];
424
+ const node = await findNode(nodeName, discoveryOpts);
425
+ if (!node) {
426
+ printError(`Node "${nodeName}" not found or offline`);
427
+ process.exit(1);
428
+ }
429
+ printConnecting(node.name);
430
+ const { GatewayClient } = await import("./gateway-client.js");
431
+ const { PortalManager } = await import("./portal/index.js");
432
+ const { pushFile } = await import("./portal/transfer.js");
433
+ const gateway = new GatewayClient({
434
+ serverUrl: config.server_url,
435
+ apiKey: config.api_key,
436
+ capabilities: ["portal"],
437
+ version: "1.1.0",
438
+ });
439
+ gateway.start();
440
+ await waitForGateway(gateway);
441
+ const portal = new PortalManager({ nodeConfig: config, gateway });
442
+ const session = await portal.connect(node.id, ["file"]);
443
+ printConnected(node.hostname);
444
+ await pushFile(session, localPath, remotePath, printProgress);
445
+ console.log(`\nFile sent to ${node.name}`);
446
+ session.close();
447
+ gateway.stop();
448
+ break;
449
+ }
450
+ case "pull": {
451
+ const source = args[0];
452
+ const localDir = args[1];
453
+ if (!source || !source.includes(":")) {
454
+ console.error("Usage: whale portal pull <node>:/path/to/file [local-dir]");
455
+ process.exit(1);
456
+ }
457
+ const [nodeName, remotePath] = source.split(":", 2);
458
+ const node = await findNode(nodeName, discoveryOpts);
459
+ if (!node) {
460
+ printError(`Node "${nodeName}" not found or offline`);
461
+ process.exit(1);
462
+ }
463
+ printConnecting(node.name);
464
+ const { GatewayClient } = await import("./gateway-client.js");
465
+ const { PortalManager } = await import("./portal/index.js");
466
+ const { pullFile } = await import("./portal/transfer.js");
467
+ const gateway = new GatewayClient({
468
+ serverUrl: config.server_url,
469
+ apiKey: config.api_key,
470
+ capabilities: ["portal"],
471
+ version: "1.1.0",
472
+ });
473
+ gateway.start();
474
+ await waitForGateway(gateway);
475
+ const portal = new PortalManager({ nodeConfig: config, gateway });
476
+ const session = await portal.connect(node.id, ["file"]);
477
+ printConnected(node.hostname);
478
+ const savedPath = await pullFile(session, remotePath, localDir, printProgress);
479
+ console.log(`\nFile saved to ${savedPath}`);
480
+ session.close();
481
+ gateway.stop();
482
+ break;
483
+ }
484
+ case "forward": {
485
+ // Format: 8080:<node>:3000
486
+ const spec = args[0];
487
+ if (!spec) {
488
+ console.error("Usage: whale portal forward <local-port>:<node>:<remote-port>");
489
+ process.exit(1);
490
+ }
491
+ const parts = spec.split(":");
492
+ if (parts.length !== 3) {
493
+ console.error("Format: <local-port>:<node>:<remote-port>");
494
+ process.exit(1);
495
+ }
496
+ const localPort = parseInt(parts[0], 10);
497
+ const nodeName = parts[1];
498
+ const remotePort = parseInt(parts[2], 10);
499
+ const node = await findNode(nodeName, discoveryOpts);
500
+ if (!node) {
501
+ printError(`Node "${nodeName}" not found or offline`);
502
+ process.exit(1);
503
+ }
504
+ printConnecting(node.name);
505
+ const { GatewayClient } = await import("./gateway-client.js");
506
+ const { PortalManager } = await import("./portal/index.js");
507
+ const { startForward } = await import("./portal/forward.js");
508
+ const gateway = new GatewayClient({
509
+ serverUrl: config.server_url,
510
+ apiKey: config.api_key,
511
+ capabilities: ["portal"],
512
+ version: "1.1.0",
513
+ });
514
+ gateway.start();
515
+ await waitForGateway(gateway);
516
+ const portal = new PortalManager({ nodeConfig: config, gateway });
517
+ const session = await portal.connect(node.id, ["forward"]);
518
+ printConnected(node.hostname);
519
+ const info = await startForward(session, localPort, remotePort);
520
+ console.log(`Forwarding localhost:${info.localPort} → ${node.name}:${info.remotePort}`);
521
+ console.log("Press Ctrl+C to stop");
522
+ await new Promise((resolve) => {
523
+ process.on("SIGINT", () => {
524
+ info.server.close();
525
+ session.close();
526
+ gateway.stop();
527
+ resolve();
528
+ });
529
+ });
530
+ break;
531
+ }
532
+ case "screen": {
533
+ const nodeName = args[0];
534
+ if (!nodeName) {
535
+ console.error("Usage: whale portal screen <node> [--control]");
536
+ process.exit(1);
537
+ }
538
+ const { values } = parseArgs({
539
+ args: args.slice(1),
540
+ options: { control: { type: "boolean", default: false } },
541
+ });
542
+ const node = await findNode(nodeName, discoveryOpts);
543
+ if (!node) {
544
+ printError(`Node "${nodeName}" not found or offline`);
545
+ process.exit(1);
546
+ }
547
+ printConnecting(node.name);
548
+ const { GatewayClient } = await import("./gateway-client.js");
549
+ const { PortalManager } = await import("./portal/index.js");
550
+ const { openScreen } = await import("./portal/screen.js");
551
+ const gateway = new GatewayClient({
552
+ serverUrl: config.server_url,
553
+ apiKey: config.api_key,
554
+ capabilities: ["portal"],
555
+ version: "1.1.0",
556
+ });
557
+ gateway.start();
558
+ await waitForGateway(gateway);
559
+ const portal = new PortalManager({ nodeConfig: config, gateway });
560
+ const session = await portal.connect(node.id, ["screen"]);
561
+ printConnected(node.hostname);
562
+ const { stream, cleanup } = await openScreen(session, values.control ?? false);
563
+ console.log(`Screen share active${values.control ? " (control enabled)" : " (view only)"}`);
564
+ console.log("Press Ctrl+C to stop");
565
+ let frameCount = 0;
566
+ stream.on("data", () => {
567
+ frameCount++;
568
+ if (frameCount % 60 === 0) {
569
+ process.stderr.write(`\r Frames: ${frameCount}`);
570
+ }
571
+ });
572
+ await new Promise((resolve) => {
573
+ process.on("SIGINT", () => {
574
+ cleanup();
575
+ session.close();
576
+ gateway.stop();
577
+ console.log(`\n Total frames: ${frameCount}`);
578
+ resolve();
579
+ });
580
+ });
581
+ break;
582
+ }
583
+ case "clip":
584
+ case "clipboard": {
585
+ const { values: clipValues } = parseArgs({
586
+ args,
587
+ options: {
588
+ from: { type: "string" },
589
+ sync: { type: "string" },
590
+ },
591
+ allowPositionals: true,
592
+ });
593
+ const positionals = args.filter(a => !a.startsWith("--"));
594
+ const nodeName = clipValues.from || clipValues.sync || positionals[0];
595
+ if (!nodeName) {
596
+ console.error("Usage: whale portal clip <node> | --from <node> | --sync <node>");
597
+ process.exit(1);
598
+ }
599
+ const node = await findNode(nodeName, discoveryOpts);
600
+ if (!node) {
601
+ printError(`Node "${nodeName}" not found or offline`);
602
+ process.exit(1);
603
+ }
604
+ const { GatewayClient } = await import("./gateway-client.js");
605
+ const { PortalManager } = await import("./portal/index.js");
606
+ const { pushClipboard, pullClipboard, syncClipboard } = await import("./portal/clipboard.js");
607
+ const gateway = new GatewayClient({
608
+ serverUrl: config.server_url,
609
+ apiKey: config.api_key,
610
+ capabilities: ["portal"],
611
+ version: "1.1.0",
612
+ });
613
+ gateway.start();
614
+ await waitForGateway(gateway);
615
+ const portal = new PortalManager({ nodeConfig: config, gateway });
616
+ const session = await portal.connect(node.id, ["clipboard"]);
617
+ if (clipValues.sync) {
618
+ const { cleanup } = await syncClipboard(session);
619
+ console.log(`Clipboard sync active with ${node.name}. Press Ctrl+C to stop.`);
620
+ await new Promise((resolve) => {
621
+ process.on("SIGINT", () => {
622
+ cleanup();
623
+ session.close();
624
+ gateway.stop();
625
+ resolve();
626
+ });
627
+ });
628
+ }
629
+ else if (clipValues.from) {
630
+ const content = await pullClipboard(session);
631
+ console.log(`Clipboard received from ${node.name} (${content.length} chars)`);
632
+ session.close();
633
+ gateway.stop();
634
+ }
635
+ else {
636
+ await pushClipboard(session);
637
+ console.log(`Clipboard sent to ${node.name}`);
638
+ session.close();
639
+ gateway.stop();
640
+ }
641
+ break;
642
+ }
643
+ case "status": {
644
+ // Just show local session info from config
645
+ console.log("Use `whale portal ls` to see online nodes.");
646
+ console.log("Use `whale portal close <session-id>` to close a session.");
647
+ break;
648
+ }
649
+ case "close": {
650
+ const sessionId = args[0];
651
+ if (!sessionId) {
652
+ console.error("Usage: whale portal close <session-id>");
653
+ process.exit(1);
654
+ }
655
+ console.log(`Session ${sessionId} closed.`);
656
+ break;
657
+ }
658
+ default: {
659
+ // If sub looks like a node name, open interactive menu
660
+ if (sub && !sub.startsWith("-")) {
661
+ const node = await findNode(sub, discoveryOpts);
662
+ if (node) {
663
+ const choice = await showPortalMenu(node.name);
664
+ if (choice === "quit")
665
+ break;
666
+ // Re-dispatch based on menu choice
667
+ switch (choice) {
668
+ case "shell":
669
+ await handlePortalCommand("sh", [sub]);
670
+ break;
671
+ case "push":
672
+ console.log("Enter local path to push:");
673
+ // Fall through to push command with readline
674
+ break;
675
+ case "pull":
676
+ console.log("Enter remote path to pull:");
677
+ break;
678
+ case "forward":
679
+ console.log("Enter forward spec (local:remote):");
680
+ break;
681
+ case "screen":
682
+ await handlePortalCommand("screen", [sub]);
683
+ break;
684
+ case "clipboard":
685
+ await handlePortalCommand("clip", [sub]);
686
+ break;
687
+ default:
688
+ printError("Unknown menu option");
689
+ }
690
+ break;
691
+ }
692
+ }
693
+ printPortalHelp();
694
+ }
695
+ }
696
+ }
697
+ function printPortalHelp() {
698
+ console.log(`
699
+ Whale Portal — node-to-node connectivity
700
+
701
+ Commands:
702
+ whale portal ls [--all] List online nodes
703
+ whale portal sh <node> [--dir PATH] [-c CMD] Remote shell
704
+ whale portal push <path> <node>[:/dest] Send file to node
705
+ whale portal pull <node>:/path [local-dir] Get file from node
706
+ whale portal forward <local>:<node>:<remote> Port forwarding
707
+ whale portal screen <node> [--control] Screen share
708
+ whale portal clip <node> Send clipboard to node
709
+ whale portal clip --from <node> Get clipboard from node
710
+ whale portal clip --sync <node> Bidirectional clipboard sync
711
+ whale portal <node> Interactive portal menu
712
+
713
+ Alias: whale p <anything> = whale portal <anything>
714
+ `);
715
+ }
716
+ async function waitForGateway(gateway) {
717
+ if (gateway.isConnected)
718
+ return;
719
+ return new Promise((resolve, reject) => {
720
+ const timeout = setTimeout(() => reject(new Error("Gateway connection timed out")), 15_000);
721
+ gateway.on("connected", () => {
722
+ clearTimeout(timeout);
723
+ resolve();
724
+ });
725
+ gateway.on("error", (err) => {
726
+ clearTimeout(timeout);
727
+ reject(new Error(`Gateway error: ${err}`));
728
+ });
729
+ });
730
+ }
294
731
  function printHelp() {
295
732
  console.log(`
296
733
  WhaleNode CLI v1.1.0 — Bridge local channels to WhaleTools AI agents
297
734
 
298
735
  Commands:
299
- whale node register Register this machine as a WhaleNode
300
- whale node start Start the node (heartbeat + channel adapters)
301
- whale node status Show node status
736
+ whale node register Register this machine as a WhaleNode
737
+ whale node start Start the node (heartbeat + channel adapters)
738
+ whale node status Show node status
739
+
740
+ whale channel add Add a channel adapter
741
+ whale channel list List configured channels
742
+
743
+ whale remote-desktop enable Enable remote desktop (macOS)
744
+ whale remote-desktop disable Disable remote desktop
745
+ whale remote-desktop status Show remote desktop status
302
746
 
303
- whale channel add Add a channel adapter
304
- whale channel list List configured channels
747
+ whale portal ls List online nodes in your store
748
+ whale portal sh <node> Remote shell on a node
749
+ whale portal push <f> <n> Send file to a node
750
+ whale portal pull <n>:/f Get file from a node
751
+ whale portal forward L:N:R Forward local port L to remote port R via node N
752
+ whale portal screen <node> Screen share a node
753
+ whale portal clip <node> Clipboard sync
754
+ whale portal <node> Interactive portal menu
755
+ whale p <...> Alias for whale portal
305
756
 
306
- whale status Quick status check
307
- whale version Show version
308
- whale help Show this help
757
+ whale status Quick status check
758
+ whale version Show version
759
+ whale help Show this help
309
760
 
310
761
  Channel Types:
311
762
  imessage --groups 109,110 --mention @whale
@@ -4,12 +4,29 @@ export interface ChannelConfig {
4
4
  name: string;
5
5
  config: Record<string, unknown>;
6
6
  }
7
+ export interface RemoteDesktopConfig {
8
+ enabled?: boolean;
9
+ port?: number;
10
+ max_fps?: number;
11
+ quality?: number;
12
+ }
13
+ export interface PortalConfig {
14
+ receive_dir?: string;
15
+ auto_accept_admins?: boolean;
16
+ approval_ttl_minutes?: number;
17
+ max_sessions?: number;
18
+ }
7
19
  export interface NodeConfig {
8
20
  node_id: string;
9
21
  api_key: string;
10
22
  store_id: string;
11
23
  server_url: string;
12
24
  channels: ChannelConfig[];
25
+ remote_desktop?: RemoteDesktopConfig;
26
+ portal?: PortalConfig;
27
+ supabase_url?: string;
28
+ supabase_service_key?: string;
29
+ anthropic_api_key?: string;
13
30
  }
14
31
  export declare function getConfigPath(): string;
15
32
  export declare function loadConfig(): NodeConfig | null;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Gateway Client — connects whale-node to the server's local-agent-gateway WebSocket.
3
+ *
4
+ * Authenticates with the node's API key, handles ping/pong keepalive,
5
+ * routes incoming exec/cluster_command messages, responds with results.
6
+ * Auto-reconnects on disconnect with exponential backoff.
7
+ */
8
+ import { EventEmitter } from "node:events";
9
+ export interface GatewayClientConfig {
10
+ serverUrl: string;
11
+ apiKey: string;
12
+ capabilities?: string[];
13
+ version?: string;
14
+ }
15
+ export type CommandHandler = (msg: any) => Promise<any>;
16
+ export type BinaryHandler = (data: Buffer) => void;
17
+ export declare class GatewayClient extends EventEmitter {
18
+ private ws;
19
+ private config;
20
+ private reconnectDelay;
21
+ private maxReconnectDelay;
22
+ private running;
23
+ private agentId;
24
+ private commandHandlers;
25
+ private binaryHandler;
26
+ constructor(config: GatewayClientConfig);
27
+ /**
28
+ * Register a handler for a specific command type (e.g. "exec", "cluster_command").
29
+ */
30
+ onCommand(type: string, handler: CommandHandler): void;
31
+ /**
32
+ * Connect to the server's WebSocket gateway.
33
+ */
34
+ start(): void;
35
+ /**
36
+ * Disconnect and stop reconnecting.
37
+ */
38
+ stop(): void;
39
+ /**
40
+ * Register a handler for binary messages (portal frames).
41
+ */
42
+ onBinary(handler: BinaryHandler): void;
43
+ /**
44
+ * Send raw binary data over the WebSocket (for portal frames).
45
+ */
46
+ sendBinary(data: Buffer): void;
47
+ /**
48
+ * Send a JSON message over the WebSocket.
49
+ */
50
+ sendJson(msg: Record<string, unknown>): void;
51
+ get isConnected(): boolean;
52
+ private connect;
53
+ private scheduleReconnect;
54
+ private send;
55
+ }