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
@@ -0,0 +1,201 @@
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 { WebSocket } from "ws";
9
+ import { EventEmitter } from "node:events";
10
+ import os from "node:os";
11
+ export class GatewayClient extends EventEmitter {
12
+ ws = null;
13
+ config;
14
+ reconnectDelay = 1000;
15
+ maxReconnectDelay = 60_000;
16
+ running = false;
17
+ agentId = null;
18
+ commandHandlers = new Map();
19
+ binaryHandler = null;
20
+ constructor(config) {
21
+ super();
22
+ this.config = config;
23
+ }
24
+ /**
25
+ * Register a handler for a specific command type (e.g. "exec", "cluster_command").
26
+ */
27
+ onCommand(type, handler) {
28
+ this.commandHandlers.set(type, handler);
29
+ }
30
+ /**
31
+ * Connect to the server's WebSocket gateway.
32
+ */
33
+ start() {
34
+ this.running = true;
35
+ this.connect();
36
+ }
37
+ /**
38
+ * Disconnect and stop reconnecting.
39
+ */
40
+ stop() {
41
+ this.running = false;
42
+ if (this.ws) {
43
+ this.ws.close(1000, "Node shutting down");
44
+ this.ws = null;
45
+ }
46
+ }
47
+ /**
48
+ * Register a handler for binary messages (portal frames).
49
+ */
50
+ onBinary(handler) {
51
+ this.binaryHandler = handler;
52
+ }
53
+ /**
54
+ * Send raw binary data over the WebSocket (for portal frames).
55
+ */
56
+ sendBinary(data) {
57
+ if (this.ws?.readyState === WebSocket.OPEN) {
58
+ this.ws.send(data);
59
+ }
60
+ }
61
+ /**
62
+ * Send a JSON message over the WebSocket.
63
+ */
64
+ sendJson(msg) {
65
+ this.send(msg);
66
+ }
67
+ get isConnected() {
68
+ return this.ws?.readyState === WebSocket.OPEN;
69
+ }
70
+ // ============================================================================
71
+ // INTERNAL
72
+ // ============================================================================
73
+ connect() {
74
+ if (!this.running)
75
+ return;
76
+ // Convert https:// to wss:// and http:// to ws://
77
+ const wsUrl = this.config.serverUrl
78
+ .replace(/^https:\/\//, "wss://")
79
+ .replace(/^http:\/\//, "ws://")
80
+ + "/agent/ws";
81
+ try {
82
+ this.ws = new WebSocket(wsUrl);
83
+ }
84
+ catch (err) {
85
+ console.error(`[gateway] Failed to create WebSocket:`, err.message);
86
+ this.scheduleReconnect();
87
+ return;
88
+ }
89
+ this.ws.on("open", () => {
90
+ console.log(`[gateway] Connected to ${wsUrl}`);
91
+ this.reconnectDelay = 1000; // Reset backoff
92
+ // Send auth
93
+ this.send({
94
+ type: "auth",
95
+ api_key: this.config.apiKey,
96
+ capabilities: this.config.capabilities || [],
97
+ platform: process.platform,
98
+ hostname: os.hostname(),
99
+ version: this.config.version || "1.0.0",
100
+ });
101
+ });
102
+ this.ws.on("message", async (raw, isBinary) => {
103
+ // Handle binary messages (portal frames)
104
+ if (isBinary || (Buffer.isBuffer(raw) && raw.length > 0 && raw[0] === 0x01)) {
105
+ const buf = Buffer.isBuffer(raw) ? raw : Buffer.from(raw);
106
+ if (this.binaryHandler) {
107
+ this.binaryHandler(buf);
108
+ }
109
+ return;
110
+ }
111
+ let msg;
112
+ try {
113
+ msg = JSON.parse(raw.toString());
114
+ }
115
+ catch {
116
+ return;
117
+ }
118
+ // Handle auth response
119
+ if (msg.type === "authenticated") {
120
+ this.agentId = msg.agent_id;
121
+ console.log(`[gateway] Authenticated as agent ${this.agentId}`);
122
+ this.emit("connected", this.agentId);
123
+ return;
124
+ }
125
+ // Handle ping from server
126
+ if (msg.type === "ping") {
127
+ this.send({ type: "pong" });
128
+ return;
129
+ }
130
+ // Handle errors
131
+ if (msg.type === "error") {
132
+ console.error(`[gateway] Server error: ${msg.error}`);
133
+ this.emit("error", msg.error);
134
+ return;
135
+ }
136
+ // Handle commands (exec, cluster_command, tool_exec, discover)
137
+ const handler = this.commandHandlers.get(msg.type);
138
+ if (handler) {
139
+ if (msg.request_id) {
140
+ // Request-response pattern
141
+ try {
142
+ const result = await handler(msg);
143
+ if (result !== null) {
144
+ this.send({
145
+ type: "result",
146
+ request_id: msg.request_id,
147
+ success: true,
148
+ ...result,
149
+ });
150
+ }
151
+ }
152
+ catch (err) {
153
+ this.send({
154
+ type: "result",
155
+ request_id: msg.request_id,
156
+ success: false,
157
+ error: err.message,
158
+ });
159
+ }
160
+ }
161
+ else {
162
+ // Fire-and-forget pattern (portal messages)
163
+ handler(msg).catch(() => { });
164
+ }
165
+ return;
166
+ }
167
+ // Unknown message type
168
+ if (msg.type && msg.type !== "error") {
169
+ console.log(`[gateway] Unhandled message type: ${msg.type}`);
170
+ }
171
+ });
172
+ this.ws.on("close", (code, reason) => {
173
+ console.log(`[gateway] Disconnected (code=${code} reason=${reason?.toString() || "none"})`);
174
+ this.agentId = null;
175
+ this.emit("disconnected", code, reason?.toString());
176
+ this.scheduleReconnect();
177
+ });
178
+ this.ws.on("error", (err) => {
179
+ // Suppress ECONNREFUSED spam — the reconnect handles it
180
+ if (err.code !== "ECONNREFUSED") {
181
+ console.error(`[gateway] WebSocket error: ${err.message}`);
182
+ }
183
+ });
184
+ }
185
+ scheduleReconnect() {
186
+ if (!this.running)
187
+ return;
188
+ setTimeout(() => {
189
+ if (this.running) {
190
+ this.connect();
191
+ }
192
+ }, this.reconnectDelay);
193
+ // Exponential backoff
194
+ this.reconnectDelay = Math.min(this.reconnectDelay * 2, this.maxReconnectDelay);
195
+ }
196
+ send(msg) {
197
+ if (this.ws?.readyState === WebSocket.OPEN) {
198
+ this.ws.send(JSON.stringify(msg));
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * ClipboardChannel — clipboard sync between nodes.
3
+ *
4
+ * macOS: pbcopy / pbpaste via child_process
5
+ * Modes: push (one-shot send), pull (one-shot receive), sync (bidirectional)
6
+ */
7
+ import type { PortalStream } from "./stream.js";
8
+ import type { SynClipboard } from "./protocol.js";
9
+ import type { PortalSession } from "./session.js";
10
+ /**
11
+ * Send local clipboard to remote node (one-shot).
12
+ */
13
+ export declare function pushClipboard(session: PortalSession): Promise<void>;
14
+ /**
15
+ * Get clipboard from remote node (one-shot).
16
+ */
17
+ export declare function pullClipboard(session: PortalSession): Promise<string>;
18
+ /**
19
+ * Start bidirectional clipboard sync.
20
+ * Returns a cleanup function to stop syncing.
21
+ */
22
+ export declare function syncClipboard(session: PortalSession): Promise<{
23
+ cleanup: () => void;
24
+ }>;
25
+ /**
26
+ * Handle an incoming clipboard stream on the target node.
27
+ */
28
+ export declare function handleClipboard(stream: PortalStream, meta: SynClipboard): void;
@@ -0,0 +1,183 @@
1
+ /**
2
+ * ClipboardChannel — clipboard sync between nodes.
3
+ *
4
+ * macOS: pbcopy / pbpaste via child_process
5
+ * Modes: push (one-shot send), pull (one-shot receive), sync (bidirectional)
6
+ */
7
+ import { execSync } from "node:child_process";
8
+ import { createHash } from "node:crypto";
9
+ const SYNC_POLL_MS = 500;
10
+ // ============================================================================
11
+ // CLIPBOARD I/O (macOS)
12
+ // ============================================================================
13
+ function readClipboard() {
14
+ try {
15
+ if (process.platform === "darwin") {
16
+ return execSync("pbpaste", { encoding: "utf-8", timeout: 2000 });
17
+ }
18
+ // Linux xclip fallback
19
+ return execSync("xclip -selection clipboard -o", { encoding: "utf-8", timeout: 2000 });
20
+ }
21
+ catch {
22
+ return "";
23
+ }
24
+ }
25
+ function writeClipboard(text) {
26
+ try {
27
+ if (process.platform === "darwin") {
28
+ execSync("pbcopy", { input: text, timeout: 2000 });
29
+ }
30
+ else {
31
+ execSync("xclip -selection clipboard", { input: text, timeout: 2000 });
32
+ }
33
+ }
34
+ catch {
35
+ // best effort
36
+ }
37
+ }
38
+ function hashContent(text) {
39
+ return createHash("md5").update(text).digest("hex");
40
+ }
41
+ // ============================================================================
42
+ // INITIATOR SIDE
43
+ // ============================================================================
44
+ /**
45
+ * Send local clipboard to remote node (one-shot).
46
+ */
47
+ export async function pushClipboard(session) {
48
+ const stream = session.openStream({ channel: "clipboard", mode: "push" });
49
+ await new Promise((resolve, reject) => {
50
+ const timeout = setTimeout(() => reject(new Error("Clipboard open timed out")), 5000);
51
+ session.once("stream:ack", (s) => {
52
+ if (s.streamId === stream.streamId) {
53
+ clearTimeout(timeout);
54
+ resolve();
55
+ }
56
+ });
57
+ });
58
+ const content = readClipboard();
59
+ stream.write(Buffer.from(content));
60
+ stream.end();
61
+ }
62
+ /**
63
+ * Get clipboard from remote node (one-shot).
64
+ */
65
+ export async function pullClipboard(session) {
66
+ const stream = session.openStream({ channel: "clipboard", mode: "pull" });
67
+ await new Promise((resolve, reject) => {
68
+ const timeout = setTimeout(() => reject(new Error("Clipboard open timed out")), 5000);
69
+ session.once("stream:ack", (s) => {
70
+ if (s.streamId === stream.streamId) {
71
+ clearTimeout(timeout);
72
+ resolve();
73
+ }
74
+ });
75
+ });
76
+ return new Promise((resolve) => {
77
+ const chunks = [];
78
+ stream.on("data", (chunk) => chunks.push(chunk));
79
+ stream.on("end", () => {
80
+ const content = Buffer.concat(chunks).toString();
81
+ writeClipboard(content);
82
+ resolve(content);
83
+ });
84
+ });
85
+ }
86
+ /**
87
+ * Start bidirectional clipboard sync.
88
+ * Returns a cleanup function to stop syncing.
89
+ */
90
+ export async function syncClipboard(session) {
91
+ const stream = session.openStream({ channel: "clipboard", mode: "sync" });
92
+ await new Promise((resolve, reject) => {
93
+ const timeout = setTimeout(() => reject(new Error("Clipboard sync timed out")), 5000);
94
+ session.once("stream:ack", (s) => {
95
+ if (s.streamId === stream.streamId) {
96
+ clearTimeout(timeout);
97
+ resolve();
98
+ }
99
+ });
100
+ });
101
+ let lastHash = hashContent(readClipboard());
102
+ let running = true;
103
+ // Poll local clipboard and send changes
104
+ const pollTimer = setInterval(() => {
105
+ if (!running)
106
+ return;
107
+ const content = readClipboard();
108
+ const hash = hashContent(content);
109
+ if (hash !== lastHash) {
110
+ lastHash = hash;
111
+ if (!stream.destroyed) {
112
+ stream.write(Buffer.from(content));
113
+ }
114
+ }
115
+ }, SYNC_POLL_MS);
116
+ // Receive remote clipboard changes
117
+ stream.on("data", (data) => {
118
+ const content = data.toString();
119
+ const hash = hashContent(content);
120
+ if (hash !== lastHash) {
121
+ lastHash = hash;
122
+ writeClipboard(content);
123
+ }
124
+ });
125
+ const cleanup = () => {
126
+ running = false;
127
+ clearInterval(pollTimer);
128
+ if (!stream.destroyed)
129
+ stream.end();
130
+ };
131
+ stream.on("end", cleanup);
132
+ stream.on("error", cleanup);
133
+ return { cleanup };
134
+ }
135
+ // ============================================================================
136
+ // TARGET SIDE
137
+ // ============================================================================
138
+ /**
139
+ * Handle an incoming clipboard stream on the target node.
140
+ */
141
+ export function handleClipboard(stream, meta) {
142
+ if (meta.mode === "push") {
143
+ // Receive clipboard content and write locally
144
+ const chunks = [];
145
+ stream.on("data", (chunk) => chunks.push(chunk));
146
+ stream.on("end", () => {
147
+ writeClipboard(Buffer.concat(chunks).toString());
148
+ });
149
+ return;
150
+ }
151
+ if (meta.mode === "pull") {
152
+ // Send local clipboard content
153
+ const content = readClipboard();
154
+ stream.write(Buffer.from(content));
155
+ stream.end();
156
+ return;
157
+ }
158
+ // Sync mode — bidirectional
159
+ let lastHash = hashContent(readClipboard());
160
+ const pollTimer = setInterval(() => {
161
+ const content = readClipboard();
162
+ const hash = hashContent(content);
163
+ if (hash !== lastHash) {
164
+ lastHash = hash;
165
+ if (!stream.destroyed) {
166
+ stream.write(Buffer.from(content));
167
+ }
168
+ }
169
+ }, SYNC_POLL_MS);
170
+ stream.on("data", (data) => {
171
+ const content = data.toString();
172
+ const hash = hashContent(content);
173
+ if (hash !== lastHash) {
174
+ lastHash = hash;
175
+ writeClipboard(content);
176
+ }
177
+ });
178
+ const cleanup = () => {
179
+ clearInterval(pollTimer);
180
+ };
181
+ stream.on("end", cleanup);
182
+ stream.on("error", cleanup);
183
+ }
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Portal Discovery — find online nodes in the user's store.
3
+ *
4
+ * Queries the nodes table via the server API. Nodes with a heartbeat
5
+ * within the last 2 minutes are considered online.
6
+ */
7
+ export interface DiscoveredNode {
8
+ id: string;
9
+ name: string;
10
+ hostname: string;
11
+ platform: string;
12
+ status: "online" | "offline";
13
+ last_heartbeat: string;
14
+ capabilities: string[];
15
+ }
16
+ export interface DiscoveryOptions {
17
+ serverUrl: string;
18
+ apiKey: string;
19
+ storeId: string;
20
+ includeOffline?: boolean;
21
+ }
22
+ /**
23
+ * Discover nodes in the same store via the server's heartbeat-based status.
24
+ */
25
+ export declare function discoverNodes(opts: DiscoveryOptions): Promise<DiscoveredNode[]>;
26
+ /**
27
+ * Find a specific node by name or partial match.
28
+ */
29
+ export declare function findNode(name: string, opts: DiscoveryOptions): Promise<DiscoveredNode | null>;
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Portal Discovery — find online nodes in the user's store.
3
+ *
4
+ * Queries the nodes table via the server API. Nodes with a heartbeat
5
+ * within the last 2 minutes are considered online.
6
+ */
7
+ /**
8
+ * Discover nodes in the same store via the server's heartbeat-based status.
9
+ */
10
+ export async function discoverNodes(opts) {
11
+ const url = `${opts.serverUrl}/nodes?store_id=${opts.storeId}`;
12
+ const res = await fetch(url, {
13
+ headers: {
14
+ Authorization: `Bearer ${opts.apiKey}`,
15
+ "Content-Type": "application/json",
16
+ },
17
+ signal: AbortSignal.timeout(10_000),
18
+ });
19
+ if (!res.ok) {
20
+ throw new Error(`Discovery failed: ${res.status} ${res.statusText}`);
21
+ }
22
+ const data = await res.json();
23
+ if (!data.success || !data.nodes) {
24
+ throw new Error(data.error || "No nodes found");
25
+ }
26
+ const now = Date.now();
27
+ const ONLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes
28
+ const nodes = data.nodes.map((n) => {
29
+ const lastHb = n.last_heartbeat ? new Date(n.last_heartbeat).getTime() : 0;
30
+ const isOnline = n.status === "online" && (now - lastHb) < ONLINE_THRESHOLD_MS;
31
+ return {
32
+ id: n.id,
33
+ name: n.name || n.hostname || "unnamed",
34
+ hostname: n.hardware?.hostname || n.hostname || "unknown",
35
+ platform: n.hardware?.os || n.platform || "unknown",
36
+ status: isOnline ? "online" : "offline",
37
+ last_heartbeat: n.last_heartbeat || "",
38
+ capabilities: n.capabilities || [],
39
+ };
40
+ });
41
+ if (!opts.includeOffline) {
42
+ return nodes.filter(n => n.status === "online");
43
+ }
44
+ return nodes;
45
+ }
46
+ /**
47
+ * Find a specific node by name or partial match.
48
+ */
49
+ export async function findNode(name, opts) {
50
+ const nodes = await discoverNodes({ ...opts, includeOffline: false });
51
+ const lower = name.toLowerCase();
52
+ // Exact match on name or hostname
53
+ const exact = nodes.find(n => n.name.toLowerCase() === lower ||
54
+ n.hostname.toLowerCase() === lower);
55
+ if (exact)
56
+ return exact;
57
+ // Partial match
58
+ const partial = nodes.find(n => n.name.toLowerCase().includes(lower) ||
59
+ n.hostname.toLowerCase().includes(lower));
60
+ return partial || null;
61
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * ForwardChannel — TCP port forwarding over portal streams.
3
+ *
4
+ * Local forward: net.createServer on local port → pipe to portal stream → remote net.connect
5
+ * Remote expose: reverse of above
6
+ */
7
+ import { type Server } from "node:net";
8
+ import type { PortalStream } from "./stream.js";
9
+ import type { SynForward } from "./protocol.js";
10
+ import type { PortalSession } from "./session.js";
11
+ export interface ForwardInfo {
12
+ localPort: number;
13
+ remotePort: number;
14
+ connections: number;
15
+ server: Server;
16
+ }
17
+ /**
18
+ * Forward a local port to a remote port on the target node.
19
+ * Returns a ForwardInfo with the local server.
20
+ */
21
+ export declare function startForward(session: PortalSession, localPort: number, remotePort: number, description?: string): Promise<ForwardInfo>;
22
+ /**
23
+ * Handle an incoming port forward stream on the target node.
24
+ * Connects to the local port and pipes data.
25
+ */
26
+ export declare function handleForward(stream: PortalStream, meta: SynForward): void;
27
+ /**
28
+ * Stop a port forward.
29
+ */
30
+ export declare function stopForward(info: ForwardInfo): Promise<void>;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * ForwardChannel — TCP port forwarding over portal streams.
3
+ *
4
+ * Local forward: net.createServer on local port → pipe to portal stream → remote net.connect
5
+ * Remote expose: reverse of above
6
+ */
7
+ import { createServer, connect } from "node:net";
8
+ // ============================================================================
9
+ // INITIATOR SIDE — local port → remote port
10
+ // ============================================================================
11
+ /**
12
+ * Forward a local port to a remote port on the target node.
13
+ * Returns a ForwardInfo with the local server.
14
+ */
15
+ export async function startForward(session, localPort, remotePort, description) {
16
+ let connections = 0;
17
+ const server = createServer((socket) => {
18
+ connections++;
19
+ // Open a new stream for each TCP connection
20
+ const stream = session.openStream({
21
+ channel: "forward",
22
+ remotePort,
23
+ description,
24
+ });
25
+ // Pipe bidirectionally
26
+ socket.pipe(stream);
27
+ stream.pipe(socket);
28
+ socket.on("error", () => {
29
+ if (!stream.destroyed)
30
+ stream.destroy();
31
+ });
32
+ stream.on("error", () => {
33
+ if (!socket.destroyed)
34
+ socket.destroy();
35
+ });
36
+ socket.on("close", () => {
37
+ if (!stream.destroyed)
38
+ stream.end();
39
+ connections--;
40
+ });
41
+ stream.on("close", () => {
42
+ if (!socket.destroyed)
43
+ socket.end();
44
+ });
45
+ });
46
+ await new Promise((resolve, reject) => {
47
+ server.listen(localPort, "127.0.0.1", () => resolve());
48
+ server.on("error", reject);
49
+ });
50
+ return { localPort, remotePort, connections, server };
51
+ }
52
+ // ============================================================================
53
+ // TARGET SIDE — handle incoming forward stream
54
+ // ============================================================================
55
+ /**
56
+ * Handle an incoming port forward stream on the target node.
57
+ * Connects to the local port and pipes data.
58
+ */
59
+ export function handleForward(stream, meta) {
60
+ const socket = connect(meta.remotePort, "127.0.0.1");
61
+ socket.on("connect", () => {
62
+ socket.pipe(stream);
63
+ stream.pipe(socket);
64
+ });
65
+ socket.on("error", (err) => {
66
+ if (!stream.destroyed) {
67
+ stream.destroy(new Error(`Connection to port ${meta.remotePort} failed: ${err.message}`));
68
+ }
69
+ });
70
+ stream.on("error", () => {
71
+ if (!socket.destroyed)
72
+ socket.destroy();
73
+ });
74
+ socket.on("close", () => {
75
+ if (!stream.destroyed)
76
+ stream.end();
77
+ });
78
+ stream.on("close", () => {
79
+ if (!socket.destroyed)
80
+ socket.end();
81
+ });
82
+ }
83
+ /**
84
+ * Stop a port forward.
85
+ */
86
+ export function stopForward(info) {
87
+ return new Promise((resolve) => {
88
+ info.server.close(() => resolve());
89
+ });
90
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * PortalManager — coordinator for portal connections.
3
+ *
4
+ * Manages portal sessions, handles incoming requests on the target side,
5
+ * and provides the public API used by the CLI and runtime.
6
+ */
7
+ import { EventEmitter } from "node:events";
8
+ import type { GatewayClient } from "../gateway-client.js";
9
+ import type { NodeConfig } from "../config.js";
10
+ import type { ScreenCaptureService } from "../remote-desktop/index.js";
11
+ import { PortalSession } from "./session.js";
12
+ export interface PortalManagerConfig {
13
+ nodeConfig: NodeConfig;
14
+ gateway: GatewayClient;
15
+ screenCapture?: ScreenCaptureService | null;
16
+ receiveDir?: string;
17
+ autoAcceptAdmins?: boolean;
18
+ maxSessions?: number;
19
+ }
20
+ export declare class PortalManager extends EventEmitter {
21
+ private config;
22
+ private sessions;
23
+ private gateway;
24
+ constructor(config: PortalManagerConfig);
25
+ /**
26
+ * Open a portal to a target node.
27
+ * Returns a PortalSession that can be used to open streams.
28
+ */
29
+ connect(targetNodeId: string, capabilities: string[]): Promise<PortalSession>;
30
+ /**
31
+ * Get all active sessions.
32
+ */
33
+ getSessions(): Array<ReturnType<PortalSession["getInfo"]>>;
34
+ /**
35
+ * Close a session by ID.
36
+ */
37
+ closeSession(sessionId: string): boolean;
38
+ /**
39
+ * Close all sessions.
40
+ */
41
+ closeAll(): void;
42
+ private setupHandlers;
43
+ private handlePortalRequest;
44
+ private handleIncomingStream;
45
+ private routeBinaryFrame;
46
+ private sendJson;
47
+ }