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
@@ -4,7 +4,11 @@
4
4
  * All consumers should import from ChatApp (re-export facade).
5
5
  */
6
6
  import { useCallback } from "react";
7
- import { execSync } from "child_process";
7
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
8
+ import { join } from "path";
9
+ import { homedir } from "os";
10
+ import { execSync, spawn } from "child_process";
11
+ import { createConnection } from "net";
8
12
  import { setModel, getModel, getModelShortName, loadClaudeMd, getSessionTokens, resetSessionState, saveSession, loadSession, listSessions, addMemory, removeMemory, listMemories, setPermissionMode, getPermissionMode, getServerStatus, mcpClientManager, } from "../../services/agent-loop.js";
9
13
  import { setConversationId } from "../../services/telemetry.js";
10
14
  import { getAllServerToolDefinitions, resetServerToolClient } from "../../services/server-tools.js";
@@ -13,12 +17,58 @@ import { loadAgentDefinitions } from "../../services/agent-definitions.js";
13
17
  import { SLASH_COMMANDS } from "../ChatInput.js";
14
18
  import { MODEL_OPTIONS } from "../ModelSelector.js";
15
19
  import { symbols } from "../../shared/Theme.js";
20
+ import { switchTheme, getThemePresets } from "../../shared/theme-manager.js";
16
21
  import { loadKeybindings } from "../../services/keybinding-manager.js";
17
22
  import { loadConfig, updateConfig } from "../../services/config-store.js";
18
- import { getStoresForUser, getValidToken, selectStore } from "../../services/auth-service.js";
23
+ import { getStoresForUser, getValidToken, selectStore, signOut } from "../../services/auth-service.js";
19
24
  import { getServerToolCount } from "../../services/agent-loop.js";
25
+ // ── Module-level helpers ──
26
+ function readNodeConfig() {
27
+ const p = join(homedir(), ".whaletools", "config.json");
28
+ if (!existsSync(p))
29
+ return { registered: false };
30
+ try {
31
+ const cfg = JSON.parse(readFileSync(p, "utf-8"));
32
+ return { registered: true, nodeId: cfg.node_id };
33
+ }
34
+ catch {
35
+ return { registered: false };
36
+ }
37
+ }
38
+ // ── Port helpers for node auto-start ──
39
+ function checkPort(port) {
40
+ return new Promise((resolve) => {
41
+ const sock = createConnection({ port, host: "127.0.0.1" }, () => {
42
+ sock.destroy();
43
+ resolve(true);
44
+ });
45
+ sock.on("error", () => resolve(false));
46
+ sock.setTimeout(1000, () => {
47
+ sock.destroy();
48
+ resolve(false);
49
+ });
50
+ });
51
+ }
52
+ function waitForPort(port, timeout) {
53
+ const start = Date.now();
54
+ return new Promise((resolve) => {
55
+ const poll = () => {
56
+ if (Date.now() - start > timeout) {
57
+ resolve(false);
58
+ return;
59
+ }
60
+ checkPort(port).then((up) => {
61
+ if (up)
62
+ resolve(true);
63
+ else
64
+ setTimeout(poll, 500);
65
+ });
66
+ };
67
+ poll();
68
+ });
69
+ }
20
70
  export function useSlashCommands(deps) {
21
- const { exit, toolsExpanded, serverToolsAvailable, sessionId, thinkingEnabled, conversationRef, setMessages, setStreamingText, setActiveTools, setTeamState, setStoreList, setStoreSelectMode, setModelSelectMode, setCurrentModel, setSessionId, setThinkingEnabled, setUserLabel, setServerToolsAvailable, setShowRewind, rewindCheckpointCount, PKG_NAME, PKG_VERSION, } = deps;
71
+ const { exit, toolsExpanded, serverToolsAvailable, sessionId, thinkingEnabled, conversationRef, setMessages, setStreamingText, setActiveTools, setTeamState, setStoreList, setStoreSelectMode, setModelSelectMode, setCurrentModel, setSessionId, setThinkingEnabled, setUserLabel, setServerToolsAvailable, setShowRewind, rewindCheckpointCount, setNodeSelectMode, setNodeList, setSessionManagerMode, setSessionList, setMemoryManagerMode, setMemoryList, setThemeSelectMode, setNodeManagerMode, setNodeManagerStatus, PKG_NAME, PKG_VERSION, } = deps;
22
72
  const handleCommand = useCallback(async (command) => {
23
73
  const parts = command.trim().split(/\s+/);
24
74
  const cmd = parts[0];
@@ -70,7 +120,7 @@ export function useSlashCommands(deps) {
70
120
  ` mode ${getPermissionMode()}`,
71
121
  ` thinking ${thinkingEnabled ? "on" : "off"} (^T)`,
72
122
  ` context ${conversationRef.current.length} messages`,
73
- ` expand ${toolsExpanded ? "on" : "off"} (^E)`,
123
+ ` expand ${toolsExpanded ? "on" : "off"} (^O)`,
74
124
  ];
75
125
  setMessages((prev) => [...prev, { role: "assistant", text: lines.join("\n") }]);
76
126
  break;
@@ -192,11 +242,8 @@ export function useSlashCommands(deps) {
192
242
  setMessages((prev) => [...prev, { role: "assistant", text: " No saved sessions." }]);
193
243
  }
194
244
  else {
195
- const lines = sessions.map((s, i) => ` ${String(i + 1).padStart(2)}. ${s.title.slice(0, 40).padEnd(42)} ${s.messageCount} msgs ${s.updatedAt.slice(0, 10)}`);
196
- setMessages((prev) => [...prev, {
197
- role: "assistant",
198
- text: ` Saved sessions:\n${lines.join("\n")}\n\n Use /resume to load a session.`,
199
- }]);
245
+ setSessionList(sessions);
246
+ setSessionManagerMode(true);
200
247
  }
201
248
  break;
202
249
  }
@@ -280,11 +327,8 @@ export function useSlashCommands(deps) {
280
327
  setMessages((prev) => [...prev, { role: "assistant", text: " No memories stored. Use /remember <fact> to add one." }]);
281
328
  }
282
329
  else {
283
- const lines = memories.map((m, i) => ` ${i + 1}. ${m}`);
284
- setMessages((prev) => [...prev, {
285
- role: "assistant",
286
- text: ` ${memories.length} remembered fact${memories.length !== 1 ? "s" : ""}:\n${lines.join("\n")}`,
287
- }]);
330
+ setMemoryList(memories);
331
+ setMemoryManagerMode(true);
288
332
  }
289
333
  break;
290
334
  }
@@ -344,6 +388,15 @@ export function useSlashCommands(deps) {
344
388
  }
345
389
  break;
346
390
  }
391
+ case "/logout": {
392
+ signOut();
393
+ setMessages((prev) => [...prev, {
394
+ role: "assistant",
395
+ text: ` ${symbols.check} Logged out. Goodbye!`,
396
+ }]);
397
+ setTimeout(() => exit(), 500);
398
+ break;
399
+ }
347
400
  case "/tools": {
348
401
  const lines = [];
349
402
  lines.push(` Local (${LOCAL_TOOL_DEFINITIONS.length})`);
@@ -364,6 +417,46 @@ export function useSlashCommands(deps) {
364
417
  setMessages((prev) => [...prev, { role: "assistant", text: lines.join("\n") }]);
365
418
  break;
366
419
  }
420
+ case "/theme": {
421
+ if (args) {
422
+ const success = switchTheme(args);
423
+ if (success) {
424
+ const preset = getThemePresets().find((t) => t.id === args);
425
+ setMessages((prev) => [...prev, {
426
+ role: "assistant",
427
+ text: ` ${symbols.check} Theme: ${preset?.label || args}`,
428
+ }]);
429
+ }
430
+ else {
431
+ const available = getThemePresets().map((t) => t.id).join(", ");
432
+ setMessages((prev) => [...prev, {
433
+ role: "assistant",
434
+ text: ` ${symbols.cross} Unknown theme: ${args}\n Available: ${available}`,
435
+ }]);
436
+ }
437
+ }
438
+ else {
439
+ setThemeSelectMode(true);
440
+ }
441
+ break;
442
+ }
443
+ // ── Node Commands ──
444
+ case "/nodes": {
445
+ const nodeConfigPath = join(homedir(), ".whaletools", "config.json");
446
+ const registered = existsSync(nodeConfigPath);
447
+ let nodeId;
448
+ if (registered) {
449
+ try {
450
+ const cfg = JSON.parse(readFileSync(nodeConfigPath, "utf-8"));
451
+ nodeId = cfg.node_id;
452
+ }
453
+ catch { /* ignore parse errors */ }
454
+ }
455
+ const running = registered ? await checkPort(7890) : false;
456
+ setNodeManagerStatus({ registered, running, nodeId, port: 7890 });
457
+ setNodeManagerMode(true);
458
+ break;
459
+ }
367
460
  }
368
461
  }, [exit, toolsExpanded, serverToolsAvailable, sessionId, thinkingEnabled, rewindCheckpointCount]);
369
462
  // Store select handlers
@@ -383,5 +476,151 @@ export function useSlashCommands(deps) {
383
476
  setStoreSelectMode(false);
384
477
  setStoreList([]);
385
478
  }, []);
386
- return { handleCommand, handleStoreSelect, handleStoreCancel };
479
+ // Node select handlers
480
+ const handleNodeSelect = useCallback((node) => {
481
+ setNodeSelectMode(false);
482
+ setNodeList([]);
483
+ }, []);
484
+ const handleNodeCancel = useCallback(() => {
485
+ setNodeSelectMode(false);
486
+ setNodeList([]);
487
+ }, []);
488
+ // Session manager handler
489
+ const handleSessionSelect = useCallback((session) => {
490
+ setSessionManagerMode(false);
491
+ const loaded = loadSession(session.id);
492
+ if (loaded) {
493
+ conversationRef.current = loaded.messages;
494
+ setSessionId(session.id);
495
+ setConversationId(session.id);
496
+ setTodoSessionId(session.id);
497
+ loadTodos(session.id);
498
+ if (loaded.meta.model)
499
+ setModel(loaded.meta.model);
500
+ setMessages((prev) => [...prev, {
501
+ role: "assistant",
502
+ text: ` ${symbols.check} Resumed: ${session.title}\n ${session.messageCount} messages, model: ${getModelShortName()}`,
503
+ }]);
504
+ }
505
+ else {
506
+ setMessages((prev) => [...prev, { role: "assistant", text: " Failed to load session." }]);
507
+ }
508
+ }, []);
509
+ // Memory manager handler
510
+ const handleMemoryDelete = useCallback((index) => {
511
+ setMemoryList((prev) => {
512
+ const memory = prev[index];
513
+ if (memory) {
514
+ removeMemory(memory);
515
+ setMessages((msgs) => [...msgs, { role: "assistant", text: ` ${symbols.check} Forgot: ${memory}` }]);
516
+ }
517
+ const next = prev.filter((_, i) => i !== index);
518
+ if (next.length === 0) {
519
+ setMemoryManagerMode(false);
520
+ }
521
+ return next;
522
+ });
523
+ }, []);
524
+ // ── Node manager helpers ──
525
+ // Node toggle: start or stop — returns result for NodeManager to display
526
+ const handleNodeToggle = useCallback(async () => {
527
+ const { nodeId } = readNodeConfig();
528
+ const running = await checkPort(7890);
529
+ if (running) {
530
+ // Stop
531
+ try {
532
+ execSync("lsof -ti :7890 | xargs kill", { encoding: "utf-8" });
533
+ return {
534
+ success: true,
535
+ message: "Node stopped",
536
+ newStatus: { registered: true, running: false, nodeId, port: 7890 },
537
+ };
538
+ }
539
+ catch {
540
+ return {
541
+ success: false,
542
+ message: "Failed to stop node",
543
+ newStatus: { registered: true, running: true, nodeId, port: 7890 },
544
+ };
545
+ }
546
+ }
547
+ else {
548
+ // Start
549
+ spawn("whalenode", ["node", "start"], {
550
+ detached: true,
551
+ stdio: "ignore",
552
+ cwd: homedir(),
553
+ }).unref();
554
+ const ready = await waitForPort(7890, 8000);
555
+ return {
556
+ success: ready,
557
+ message: ready ? "Node started" : "Node failed to start",
558
+ newStatus: { registered: true, running: ready, nodeId, port: 7890 },
559
+ };
560
+ }
561
+ }, []);
562
+ // Node register: register this machine, returns result for NodeManager
563
+ const handleNodeRegister = useCallback(async () => {
564
+ const token = await getValidToken();
565
+ const config = loadConfig();
566
+ if (!token || !config.store_id) {
567
+ return {
568
+ success: false,
569
+ message: "Not logged in — run: whale login",
570
+ newStatus: { registered: false, running: false },
571
+ };
572
+ }
573
+ try {
574
+ const hostname = (await import("os")).hostname();
575
+ const res = await fetch(`https://whale-agent.fly.dev/nodes/register`, {
576
+ method: "POST",
577
+ headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
578
+ body: JSON.stringify({ name: hostname, store_id: config.store_id }),
579
+ });
580
+ const data = await res.json();
581
+ if (!data.success) {
582
+ return {
583
+ success: false,
584
+ message: `Registration failed: ${data.error || "unknown"}`,
585
+ newStatus: { registered: false, running: false },
586
+ };
587
+ }
588
+ const nodeConfig = {
589
+ node_id: data.node.id,
590
+ api_key: data.api_key,
591
+ store_id: config.store_id,
592
+ server_url: "https://whale-agent.fly.dev",
593
+ channels: [],
594
+ };
595
+ const configDir = join(homedir(), ".whaletools");
596
+ if (!existsSync(configDir))
597
+ mkdirSync(configDir, { recursive: true });
598
+ writeFileSync(join(configDir, "config.json"), JSON.stringify(nodeConfig, null, 2));
599
+ return {
600
+ success: true,
601
+ message: `Registered: ${data.node.id}`,
602
+ newStatus: { registered: true, running: false, nodeId: data.node.id, port: 7890 },
603
+ };
604
+ }
605
+ catch {
606
+ return {
607
+ success: false,
608
+ message: "Registration failed",
609
+ newStatus: { registered: false, running: false },
610
+ };
611
+ }
612
+ }, []);
613
+ // Node manager close: dismiss panel + optionally show result in chat
614
+ const handleNodeClose = useCallback((resultMessage) => {
615
+ setNodeManagerMode(false);
616
+ if (resultMessage) {
617
+ setMessages((prev) => [...prev, { role: "assistant", text: ` ${resultMessage}` }]);
618
+ }
619
+ }, []);
620
+ return {
621
+ handleCommand, handleStoreSelect, handleStoreCancel,
622
+ handleNodeSelect, handleNodeCancel,
623
+ handleSessionSelect, handleMemoryDelete,
624
+ handleNodeToggle, handleNodeRegister, handleNodeClose,
625
+ };
387
626
  }
@@ -7,29 +7,10 @@
7
7
  * whale config <key> <value> Write single key
8
8
  * whale config --reset Reset to defaults
9
9
  */
10
- import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
11
- import { join } from "path";
12
- import { homedir } from "os";
13
- const CONFIG_PATH = join(homedir(), ".swagmanager", "config.json");
10
+ import { loadConfig, saveConfig, clearConfig, getConfigPath } from "../services/config-store.js";
14
11
  const DIM = "\x1b[2m";
15
12
  const BOLD = "\x1b[1m";
16
13
  const RESET = "\x1b[0m";
17
- function loadConfig() {
18
- if (!existsSync(CONFIG_PATH))
19
- return {};
20
- try {
21
- return JSON.parse(readFileSync(CONFIG_PATH, "utf-8"));
22
- }
23
- catch {
24
- return {};
25
- }
26
- }
27
- function saveConfig(config) {
28
- const dir = join(homedir(), ".swagmanager");
29
- if (!existsSync(dir))
30
- mkdirSync(dir, { recursive: true });
31
- writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), { mode: 0o600 });
32
- }
33
14
  function maskSecret(key, value) {
34
15
  if (key.includes("key") || key.includes("token") || key.includes("secret")) {
35
16
  if (value.length > 8) {
@@ -41,19 +22,17 @@ function maskSecret(key, value) {
41
22
  }
42
23
  export async function runConfigCommand(args, flags) {
43
24
  if (flags.reset) {
44
- const dir = join(homedir(), ".swagmanager");
45
- if (!existsSync(dir))
46
- mkdirSync(dir, { recursive: true });
47
- writeFileSync(CONFIG_PATH, JSON.stringify({}, null, 2), { mode: 0o600 });
25
+ clearConfig();
48
26
  console.log("Configuration reset to defaults.");
49
27
  return;
50
28
  }
29
+ const configPath = getConfigPath();
51
30
  const key = args[0];
52
31
  const value = args[1];
53
32
  if (!key) {
54
33
  // Show all settings
55
34
  const config = loadConfig();
56
- console.log(`\n ${BOLD}whale config${RESET} ${DIM}(${CONFIG_PATH})${RESET}\n`);
35
+ console.log(`\n ${BOLD}whale config${RESET} ${DIM}(${configPath})${RESET}\n`);
57
36
  if (Object.keys(config).length === 0) {
58
37
  console.log(" No configuration set.");
59
38
  console.log(` ${DIM}Set a value: whale config <key> <value>${RESET}`);
@@ -0,0 +1,13 @@
1
+ /**
2
+ * whale db — Local Supabase database management
3
+ *
4
+ * Subcommands:
5
+ * whale db status Check if local Supabase is running
6
+ * whale db start Start local Supabase
7
+ * whale db stop Stop local Supabase
8
+ * whale db reset Drop + recreate + seed local DB
9
+ * whale db sync Pull latest production schema, apply locally
10
+ * whale db seed Re-run seed.sql without full reset
11
+ * whale db test Run full test suite against local DB
12
+ */
13
+ export declare function runDbCommand(args: string[]): Promise<void>;
@@ -0,0 +1,243 @@
1
+ /**
2
+ * whale db — Local Supabase database management
3
+ *
4
+ * Subcommands:
5
+ * whale db status Check if local Supabase is running
6
+ * whale db start Start local Supabase
7
+ * whale db stop Stop local Supabase
8
+ * whale db reset Drop + recreate + seed local DB
9
+ * whale db sync Pull latest production schema, apply locally
10
+ * whale db seed Re-run seed.sql without full reset
11
+ * whale db test Run full test suite against local DB
12
+ */
13
+ import { execSync, spawn } from "child_process";
14
+ import { existsSync, readFileSync } from "fs";
15
+ import { join, resolve, dirname } from "path";
16
+ import { fileURLToPath } from "url";
17
+ const __filename = fileURLToPath(import.meta.url);
18
+ const __dirname = dirname(__filename);
19
+ // ── ANSI colors ──
20
+ const GREEN = "\x1b[32m";
21
+ const RED = "\x1b[31m";
22
+ const YELLOW = "\x1b[33m";
23
+ const CYAN = "\x1b[36m";
24
+ const BOLD = "\x1b[1m";
25
+ const DIM = "\x1b[2m";
26
+ const RESET = "\x1b[0m";
27
+ const CHECK = `${GREEN}✓${RESET}`;
28
+ const CROSS = `${RED}✗${RESET}`;
29
+ const WARN = `${YELLOW}!${RESET}`;
30
+ const DB_URL = "postgresql://postgres:postgres@127.0.0.1:54322/postgres";
31
+ function findEcosystemRoot() {
32
+ // Walk up from __dirname to find the directory containing supabase/config.toml
33
+ let dir = resolve(__dirname, "..", "..", "..");
34
+ for (let i = 0; i < 5; i++) {
35
+ if (existsSync(join(dir, "supabase", "config.toml"))) {
36
+ return dir;
37
+ }
38
+ dir = resolve(dir, "..");
39
+ }
40
+ // Fallback
41
+ return resolve(__dirname, "..", "..", "..");
42
+ }
43
+ function isLocalRunning() {
44
+ try {
45
+ execSync(`PGPASSWORD=postgres psql "${DB_URL}" -c "SELECT 1;" 2>/dev/null`, {
46
+ encoding: "utf-8",
47
+ timeout: 5000,
48
+ stdio: ["pipe", "pipe", "pipe"],
49
+ });
50
+ return true;
51
+ }
52
+ catch {
53
+ return false;
54
+ }
55
+ }
56
+ function runShell(cmd, opts) {
57
+ return execSync(cmd, {
58
+ encoding: "utf-8",
59
+ timeout: opts?.timeout ?? 30000,
60
+ cwd: opts?.cwd,
61
+ stdio: ["pipe", "pipe", "pipe"],
62
+ }).trim();
63
+ }
64
+ function spawnInteractive(cmd, args, cwd) {
65
+ return new Promise((resolve) => {
66
+ const child = spawn(cmd, args, {
67
+ cwd,
68
+ stdio: "inherit",
69
+ shell: true,
70
+ });
71
+ child.on("close", (code) => resolve(code ?? 1));
72
+ });
73
+ }
74
+ // ── Subcommands ──
75
+ async function dbStatus(root) {
76
+ console.log(`\n ${BOLD}whale db status${RESET}\n`);
77
+ const supaDir = join(root, "supabase");
78
+ // Check Docker
79
+ let dockerOk = false;
80
+ try {
81
+ runShell("docker info 2>/dev/null", { timeout: 5000 });
82
+ dockerOk = true;
83
+ }
84
+ catch { /* noop */ }
85
+ console.log(` ${dockerOk ? CHECK : CROSS} Docker ${DIM}${dockerOk ? "running" : "not running"}${RESET}`);
86
+ // Check local Supabase
87
+ const running = isLocalRunning();
88
+ console.log(` ${running ? CHECK : CROSS} Local Supabase ${DIM}${running ? "running (127.0.0.1:54321)" : "not running"}${RESET}`);
89
+ // Table count
90
+ if (running) {
91
+ try {
92
+ const count = runShell(`PGPASSWORD=postgres psql "${DB_URL}" -t -c "SELECT count(*) FROM pg_tables WHERE schemaname='public';"`, { timeout: 5000 }).trim();
93
+ console.log(` ${CHECK} Tables ${DIM}${count} in public schema${RESET}`);
94
+ }
95
+ catch {
96
+ console.log(` ${WARN} Tables ${DIM}could not query${RESET}`);
97
+ }
98
+ }
99
+ // Last sync
100
+ const syncFile = join(supaDir, ".last-sync");
101
+ if (existsSync(syncFile)) {
102
+ const ts = readFileSync(syncFile, "utf-8").trim();
103
+ console.log(` ${CHECK} Last sync ${DIM}${ts}${RESET}`);
104
+ }
105
+ else {
106
+ console.log(` ${WARN} Last sync ${DIM}never (run: whale db sync)${RESET}`);
107
+ }
108
+ // Config
109
+ const configOk = existsSync(join(supaDir, "config.toml"));
110
+ console.log(` ${configOk ? CHECK : CROSS} Config ${DIM}${configOk ? "supabase/config.toml" : "missing"}${RESET}`);
111
+ console.log();
112
+ }
113
+ async function dbStart(root) {
114
+ const supaDir = join(root, "supabase");
115
+ if (!existsSync(join(supaDir, "config.toml"))) {
116
+ console.error(`${RED}ERROR: supabase/config.toml not found in ${root}${RESET}`);
117
+ process.exit(1);
118
+ }
119
+ if (isLocalRunning()) {
120
+ console.log(`${GREEN}Local Supabase is already running.${RESET}`);
121
+ return;
122
+ }
123
+ console.log(`${CYAN}Starting local Supabase...${RESET}`);
124
+ const code = await spawnInteractive("supabase", ["start", "--workdir", "."], supaDir);
125
+ if (code !== 0) {
126
+ console.error(`${RED}Failed to start Supabase (exit ${code})${RESET}`);
127
+ process.exit(1);
128
+ }
129
+ }
130
+ async function dbStop(root) {
131
+ const supaDir = join(root, "supabase");
132
+ console.log(`${CYAN}Stopping local Supabase...${RESET}`);
133
+ const code = await spawnInteractive("supabase", ["stop", "--workdir", "."], supaDir);
134
+ if (code !== 0) {
135
+ console.error(`${RED}Failed to stop Supabase (exit ${code})${RESET}`);
136
+ process.exit(1);
137
+ }
138
+ console.log(`${GREEN}Local Supabase stopped.${RESET}`);
139
+ }
140
+ async function dbReset(root) {
141
+ const script = join(root, "scripts", "reset-test-db.sh");
142
+ if (!existsSync(script)) {
143
+ console.error(`${RED}ERROR: scripts/reset-test-db.sh not found${RESET}`);
144
+ process.exit(1);
145
+ }
146
+ console.log(`${CYAN}Resetting local database...${RESET}`);
147
+ const code = await spawnInteractive("bash", [script]);
148
+ if (code !== 0) {
149
+ console.error(`${RED}Reset failed (exit ${code})${RESET}`);
150
+ process.exit(1);
151
+ }
152
+ }
153
+ async function dbSync(root) {
154
+ const script = join(root, "scripts", "sync-local-db.sh");
155
+ if (!existsSync(script)) {
156
+ console.error(`${RED}ERROR: scripts/sync-local-db.sh not found${RESET}`);
157
+ process.exit(1);
158
+ }
159
+ console.log(`${CYAN}Syncing production schema to local...${RESET}`);
160
+ const code = await spawnInteractive("bash", [script]);
161
+ if (code !== 0) {
162
+ console.error(`${RED}Sync failed (exit ${code})${RESET}`);
163
+ process.exit(1);
164
+ }
165
+ }
166
+ async function dbSeed(root) {
167
+ const seedFile = join(root, "supabase", "seed.sql");
168
+ if (!existsSync(seedFile)) {
169
+ console.error(`${RED}ERROR: supabase/seed.sql not found${RESET}`);
170
+ process.exit(1);
171
+ }
172
+ if (!isLocalRunning()) {
173
+ console.error(`${RED}ERROR: Local Supabase is not running. Run: whale db start${RESET}`);
174
+ process.exit(1);
175
+ }
176
+ console.log(`${CYAN}Re-seeding local database...${RESET}`);
177
+ try {
178
+ runShell(`PGPASSWORD=postgres psql "${DB_URL}" -f "${seedFile}"`, { timeout: 15000 });
179
+ console.log(`${GREEN}Seed data applied.${RESET}`);
180
+ }
181
+ catch (err) {
182
+ console.error(`${RED}Seed failed: ${err.message?.slice(0, 200)}${RESET}`);
183
+ process.exit(1);
184
+ }
185
+ }
186
+ async function dbTest(root) {
187
+ const script = join(root, "scripts", "run-all-tests.sh");
188
+ if (!existsSync(script)) {
189
+ console.error(`${RED}ERROR: scripts/run-all-tests.sh not found${RESET}`);
190
+ process.exit(1);
191
+ }
192
+ console.log(`${CYAN}Running full test suite against local DB...${RESET}\n`);
193
+ const code = await spawnInteractive("bash", [script]);
194
+ process.exit(code);
195
+ }
196
+ // ── Help ──
197
+ function showDbHelp() {
198
+ console.log(`
199
+ ${BOLD}whale db${RESET} — Local Supabase management
200
+
201
+ ${BOLD}Subcommands:${RESET}
202
+ whale db status${DIM} Check if local Supabase is running${RESET}
203
+ whale db start${DIM} Start local Supabase${RESET}
204
+ whale db stop${DIM} Stop local Supabase${RESET}
205
+ whale db reset${DIM} Drop + recreate + seed local DB${RESET}
206
+ whale db sync${DIM} Pull prod schema → apply locally${RESET}
207
+ whale db seed${DIM} Re-run seed.sql without full reset${RESET}
208
+ whale db test${DIM} Run full test suite against local DB${RESET}
209
+ `);
210
+ }
211
+ // ── Router ──
212
+ export async function runDbCommand(args) {
213
+ const sub = args[0];
214
+ const root = findEcosystemRoot();
215
+ switch (sub) {
216
+ case "status":
217
+ return dbStatus(root);
218
+ case "start":
219
+ return dbStart(root);
220
+ case "stop":
221
+ return dbStop(root);
222
+ case "reset":
223
+ return dbReset(root);
224
+ case "sync":
225
+ return dbSync(root);
226
+ case "seed":
227
+ return dbSeed(root);
228
+ case "test":
229
+ return dbTest(root);
230
+ case "help":
231
+ case "--help":
232
+ case "-h":
233
+ showDbHelp();
234
+ return;
235
+ case undefined:
236
+ showDbHelp();
237
+ return;
238
+ default:
239
+ console.error(`${RED}Unknown db subcommand: ${sub}${RESET}`);
240
+ showDbHelp();
241
+ process.exit(1);
242
+ }
243
+ }
@@ -99,12 +99,9 @@ async function checkServerTools() {
99
99
  }
100
100
  }
101
101
  async function checkMcpServers() {
102
- const configPath = join(homedir(), ".swagmanager", "config.json");
103
- if (!existsSync(configPath)) {
104
- return { status: "warn", label: "MCP servers", detail: "none configured" };
105
- }
106
102
  try {
107
- const config = JSON.parse(readFileSync(configPath, "utf-8"));
103
+ const { loadConfig } = await import("../services/config-store.js");
104
+ const config = loadConfig();
108
105
  const servers = config.mcpServers || {};
109
106
  const count = Object.keys(servers).length;
110
107
  if (count === 0) {
@@ -117,18 +114,18 @@ async function checkMcpServers() {
117
114
  }
118
115
  }
119
116
  async function checkDiskSpace() {
120
- const dir = join(homedir(), ".swagmanager");
117
+ const dir = join(homedir(), ".whaletools");
121
118
  if (!existsSync(dir)) {
122
- return { status: "ok", label: "Data dir", detail: "~/.swagmanager/ (not created yet)" };
119
+ return { status: "ok", label: "Data dir", detail: "~/.whaletools/ (not created yet)" };
123
120
  }
124
121
  try {
125
122
  // Get total size
126
123
  const output = execSync(`du -sh "${dir}" 2>/dev/null`, { encoding: "utf-8", timeout: 5000 }).trim();
127
124
  const size = output.split("\t")[0];
128
- return { status: "ok", label: "Data dir", detail: `~/.swagmanager/ (${size})` };
125
+ return { status: "ok", label: "Data dir", detail: `~/.whaletools/ (${size})` };
129
126
  }
130
127
  catch {
131
- return { status: "ok", label: "Data dir", detail: "~/.swagmanager/" };
128
+ return { status: "ok", label: "Data dir", detail: "~/.whaletools/" };
132
129
  }
133
130
  }
134
131
  async function checkVersion() {