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,142 @@
1
+ /**
2
+ * ShellChannel — PTY spawn on target, stdin/stdout bridge on initiator.
3
+ *
4
+ * Initiator side: pipes process.stdin → stream, stream → process.stdout
5
+ * Target side: spawns node-pty, pipes PTY ↔ stream
6
+ */
7
+ // ============================================================================
8
+ // INITIATOR SIDE
9
+ // ============================================================================
10
+ /**
11
+ * Open a remote shell from the initiator. Pipes local terminal to remote PTY.
12
+ */
13
+ export async function openShell(session, opts = {}) {
14
+ const cols = opts.cols || process.stdout.columns || 80;
15
+ const rows = opts.rows || process.stdout.rows || 24;
16
+ const stream = session.openStream({
17
+ channel: "shell",
18
+ term: process.env.TERM || "xterm-256color",
19
+ cols,
20
+ rows,
21
+ cwd: opts.cwd,
22
+ command: opts.command,
23
+ });
24
+ // Wait for ACK
25
+ await new Promise((resolve, reject) => {
26
+ const timeout = setTimeout(() => reject(new Error("Shell open timed out")), 10_000);
27
+ session.once("stream:ack", (s) => {
28
+ if (s.streamId === stream.streamId) {
29
+ clearTimeout(timeout);
30
+ resolve();
31
+ }
32
+ });
33
+ });
34
+ // Pipe stdin → stream → stdout
35
+ const wasRaw = process.stdin.isRaw;
36
+ if (process.stdin.isTTY && !opts.command) {
37
+ process.stdin.setRawMode(true);
38
+ }
39
+ process.stdin.resume();
40
+ const onStdinData = (data) => {
41
+ if (!stream.destroyed) {
42
+ stream.write(data);
43
+ }
44
+ };
45
+ process.stdin.on("data", onStdinData);
46
+ stream.on("data", (data) => {
47
+ process.stdout.write(data);
48
+ });
49
+ // Handle terminal resize
50
+ const onResize = () => {
51
+ const newCols = process.stdout.columns || 80;
52
+ const newRows = process.stdout.rows || 24;
53
+ // Send resize as a control message on stream 0 (JSON)
54
+ const msg = { type: "resize", cols: newCols, rows: newRows };
55
+ // Encode as a DATA frame on the shell stream with a special prefix
56
+ const resizeFrame = Buffer.from(`\x00RESIZE${JSON.stringify(msg)}`);
57
+ if (!stream.destroyed) {
58
+ stream.write(resizeFrame);
59
+ }
60
+ };
61
+ process.stdout.on("resize", onResize);
62
+ const cleanup = () => {
63
+ process.stdin.removeListener("data", onStdinData);
64
+ process.stdout.removeListener("resize", onResize);
65
+ if (process.stdin.isTTY) {
66
+ process.stdin.setRawMode(wasRaw ?? false);
67
+ }
68
+ process.stdin.pause();
69
+ if (!stream.destroyed) {
70
+ stream.end();
71
+ }
72
+ };
73
+ // Auto-cleanup when stream closes
74
+ stream.on("end", cleanup);
75
+ stream.on("error", cleanup);
76
+ // For single command mode, collect output and close
77
+ if (opts.command) {
78
+ return new Promise((resolve) => {
79
+ stream.on("end", () => {
80
+ resolve({ stream, cleanup });
81
+ });
82
+ });
83
+ }
84
+ return { stream, cleanup };
85
+ }
86
+ // ============================================================================
87
+ // TARGET SIDE (handler)
88
+ // ============================================================================
89
+ const RESIZE_PREFIX = Buffer.from("\x00RESIZE");
90
+ /**
91
+ * Handle an incoming shell stream on the target node.
92
+ * Spawns a PTY and bridges it to the stream.
93
+ */
94
+ export async function handleShell(stream, meta) {
95
+ // Dynamic import of node-pty (only needed on target)
96
+ const pty = await import("node-pty");
97
+ const os = await import("node:os");
98
+ const shell = process.env.SHELL || (process.platform === "win32" ? "powershell.exe" : "/bin/bash");
99
+ const cwd = meta.cwd || os.homedir();
100
+ const ptyProcess = pty.spawn(shell, meta.command ? ["-c", meta.command] : [], {
101
+ name: meta.term || "xterm-256color",
102
+ cols: meta.cols || 80,
103
+ rows: meta.rows || 24,
104
+ cwd,
105
+ env: { ...process.env, TERM: meta.term || "xterm-256color" },
106
+ });
107
+ // PTY → stream
108
+ ptyProcess.onData((data) => {
109
+ if (!stream.destroyed) {
110
+ stream.write(Buffer.from(data));
111
+ }
112
+ });
113
+ // Stream → PTY (with resize detection)
114
+ stream.on("data", (data) => {
115
+ // Check for resize control message
116
+ if (data.length > RESIZE_PREFIX.length && data.subarray(0, RESIZE_PREFIX.length).equals(RESIZE_PREFIX)) {
117
+ try {
118
+ const msg = JSON.parse(data.subarray(RESIZE_PREFIX.length).toString());
119
+ ptyProcess.resize(msg.cols, msg.rows);
120
+ }
121
+ catch {
122
+ // Not a resize message, forward as data
123
+ ptyProcess.write(data.toString());
124
+ }
125
+ return;
126
+ }
127
+ ptyProcess.write(data.toString());
128
+ });
129
+ // PTY exit → close stream
130
+ ptyProcess.onExit(() => {
131
+ if (!stream.destroyed) {
132
+ stream.end();
133
+ }
134
+ });
135
+ // Stream close → kill PTY
136
+ stream.on("end", () => {
137
+ ptyProcess.kill();
138
+ });
139
+ stream.on("error", () => {
140
+ ptyProcess.kill();
141
+ });
142
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * PortalStream — Node.js Duplex stream wrapping a multiplexed stream ID.
3
+ *
4
+ * Each channel (shell, file, forward, etc.) gets a PortalStream that behaves
5
+ * like a normal Node.js Duplex. Writing to it sends DATA frames; reading
6
+ * receives DATA frames routed by the multiplexer.
7
+ */
8
+ import { Duplex, type DuplexOptions } from "node:stream";
9
+ import type { SynPayload } from "./protocol.js";
10
+ export interface PortalStreamOptions extends DuplexOptions {
11
+ streamId: number;
12
+ channelMeta?: SynPayload;
13
+ sendFrame: (streamId: number, data: Buffer) => void;
14
+ sendFin: (streamId: number) => void;
15
+ sendRst: (streamId: number, reason?: string) => void;
16
+ }
17
+ export declare class PortalStream extends Duplex {
18
+ readonly streamId: number;
19
+ readonly channelMeta: SynPayload | undefined;
20
+ private _sendFrame;
21
+ private _sendFin;
22
+ private _sendRst;
23
+ private _remoteFinished;
24
+ private _localFinished;
25
+ constructor(opts: PortalStreamOptions);
26
+ /**
27
+ * Called by multiplexer when a DATA frame arrives for this stream.
28
+ */
29
+ pushData(data: Buffer): void;
30
+ /**
31
+ * Called by multiplexer when a FIN frame arrives for this stream.
32
+ */
33
+ pushFin(): void;
34
+ /**
35
+ * Called by multiplexer when a RST frame arrives for this stream.
36
+ */
37
+ pushRst(reason?: string): void;
38
+ get isFullyClosed(): boolean;
39
+ _read(_size: number): void;
40
+ _write(chunk: Buffer, _encoding: string, callback: (error?: Error | null) => void): void;
41
+ _final(callback: (error?: Error | null) => void): void;
42
+ _destroy(error: Error | null, callback: (error?: Error | null) => void): void;
43
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * PortalStream — Node.js Duplex stream wrapping a multiplexed stream ID.
3
+ *
4
+ * Each channel (shell, file, forward, etc.) gets a PortalStream that behaves
5
+ * like a normal Node.js Duplex. Writing to it sends DATA frames; reading
6
+ * receives DATA frames routed by the multiplexer.
7
+ */
8
+ import { Duplex } from "node:stream";
9
+ export class PortalStream extends Duplex {
10
+ streamId;
11
+ channelMeta;
12
+ _sendFrame;
13
+ _sendFin;
14
+ _sendRst;
15
+ _remoteFinished = false;
16
+ _localFinished = false;
17
+ constructor(opts) {
18
+ super({ ...opts, allowHalfOpen: true });
19
+ this.streamId = opts.streamId;
20
+ this.channelMeta = opts.channelMeta;
21
+ this._sendFrame = opts.sendFrame;
22
+ this._sendFin = opts.sendFin;
23
+ this._sendRst = opts.sendRst;
24
+ }
25
+ /**
26
+ * Called by multiplexer when a DATA frame arrives for this stream.
27
+ */
28
+ pushData(data) {
29
+ if (!this._remoteFinished) {
30
+ this.push(data);
31
+ }
32
+ }
33
+ /**
34
+ * Called by multiplexer when a FIN frame arrives for this stream.
35
+ */
36
+ pushFin() {
37
+ this._remoteFinished = true;
38
+ this.push(null); // signal end of readable side
39
+ }
40
+ /**
41
+ * Called by multiplexer when a RST frame arrives for this stream.
42
+ */
43
+ pushRst(reason) {
44
+ this._remoteFinished = true;
45
+ this._localFinished = true;
46
+ this.destroy(new Error(reason || "Stream reset by remote"));
47
+ }
48
+ get isFullyClosed() {
49
+ return this._remoteFinished && this._localFinished;
50
+ }
51
+ // ---- Duplex implementation ----
52
+ _read(_size) {
53
+ // Data is pushed via pushData() — no pull-based reading needed
54
+ }
55
+ _write(chunk, _encoding, callback) {
56
+ if (this._localFinished) {
57
+ callback(new Error("Stream already closed"));
58
+ return;
59
+ }
60
+ try {
61
+ this._sendFrame(this.streamId, chunk);
62
+ callback();
63
+ }
64
+ catch (err) {
65
+ callback(err);
66
+ }
67
+ }
68
+ _final(callback) {
69
+ this._localFinished = true;
70
+ try {
71
+ this._sendFin(this.streamId);
72
+ callback();
73
+ }
74
+ catch (err) {
75
+ callback(err);
76
+ }
77
+ }
78
+ _destroy(error, callback) {
79
+ if (!this._localFinished) {
80
+ this._localFinished = true;
81
+ try {
82
+ this._sendRst(this.streamId, error?.message);
83
+ }
84
+ catch {
85
+ // best effort
86
+ }
87
+ }
88
+ callback(error);
89
+ }
90
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * FileChannel — chunked file push/pull with progress tracking.
3
+ *
4
+ * Push: fs.createReadStream → chunk into 64KB DATA frames → stream
5
+ * Pull: request via SYN → receive DATA frames → fs.createWriteStream
6
+ * Directory: tar pipe (using system tar CLI)
7
+ */
8
+ import type { PortalStream } from "./stream.js";
9
+ import type { SynFilePush, SynFilePull } from "./protocol.js";
10
+ import type { PortalSession } from "./session.js";
11
+ export interface TransferProgress {
12
+ name: string;
13
+ bytesTransferred: number;
14
+ totalBytes: number;
15
+ percent: number;
16
+ }
17
+ export type ProgressCallback = (progress: TransferProgress) => void;
18
+ /**
19
+ * Push a local file or directory to a remote node.
20
+ */
21
+ export declare function pushFile(session: PortalSession, localPath: string, remotePath?: string, onProgress?: ProgressCallback): Promise<void>;
22
+ /**
23
+ * Pull a file from a remote node.
24
+ */
25
+ export declare function pullFile(session: PortalSession, remotePath: string, localDir?: string, onProgress?: ProgressCallback): Promise<string>;
26
+ /**
27
+ * Handle an incoming file push on the target node.
28
+ */
29
+ export declare function handleFilePush(stream: PortalStream, meta: SynFilePush, receiveDir?: string): Promise<string>;
30
+ /**
31
+ * Handle an incoming file pull request on the target node.
32
+ */
33
+ export declare function handleFilePull(stream: PortalStream, meta: SynFilePull): Promise<void>;
@@ -0,0 +1,231 @@
1
+ /**
2
+ * FileChannel — chunked file push/pull with progress tracking.
3
+ *
4
+ * Push: fs.createReadStream → chunk into 64KB DATA frames → stream
5
+ * Pull: request via SYN → receive DATA frames → fs.createWriteStream
6
+ * Directory: tar pipe (using system tar CLI)
7
+ */
8
+ import { createReadStream, createWriteStream, statSync, mkdirSync, existsSync } from "node:fs";
9
+ import { basename, join, dirname } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import { spawn } from "node:child_process";
12
+ import { pipeline } from "node:stream/promises";
13
+ const DEFAULT_RECEIVE_DIR = join(homedir(), "Portal");
14
+ const CHUNK_SIZE = 64 * 1024;
15
+ // ============================================================================
16
+ // PUSH (initiator sends file to target)
17
+ // ============================================================================
18
+ /**
19
+ * Push a local file or directory to a remote node.
20
+ */
21
+ export async function pushFile(session, localPath, remotePath, onProgress) {
22
+ const stat = statSync(localPath);
23
+ const name = basename(localPath);
24
+ if (stat.isDirectory()) {
25
+ return pushDirectory(session, localPath, onProgress);
26
+ }
27
+ const stream = session.openStream({
28
+ channel: "file",
29
+ direction: "push",
30
+ name: remotePath ? basename(remotePath) : name,
31
+ size: stat.size,
32
+ mode: stat.mode,
33
+ mtime: Math.floor(stat.mtimeMs / 1000),
34
+ });
35
+ // Wait for ACK
36
+ await waitForAck(session, stream);
37
+ // Stream file data
38
+ const fileStream = createReadStream(localPath, { highWaterMark: CHUNK_SIZE });
39
+ let transferred = 0;
40
+ fileStream.on("data", (chunk) => {
41
+ const len = typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.length;
42
+ transferred += len;
43
+ onProgress?.({
44
+ name,
45
+ bytesTransferred: transferred,
46
+ totalBytes: stat.size,
47
+ percent: stat.size > 0 ? Math.round((transferred / stat.size) * 100) : 100,
48
+ });
49
+ });
50
+ await pipeline(fileStream, stream);
51
+ }
52
+ /**
53
+ * Push a directory by streaming tar.
54
+ */
55
+ async function pushDirectory(session, localPath, onProgress) {
56
+ const name = basename(localPath) + "/";
57
+ const stream = session.openStream({
58
+ channel: "file",
59
+ direction: "push",
60
+ name,
61
+ size: 0,
62
+ mode: 0o755,
63
+ mtime: Math.floor(Date.now() / 1000),
64
+ isDirectory: true,
65
+ });
66
+ await waitForAck(session, stream);
67
+ // Use system tar to stream directory
68
+ const tar = spawn("tar", ["-cf", "-", "-C", dirname(localPath), basename(localPath)], {
69
+ stdio: ["ignore", "pipe", "pipe"],
70
+ });
71
+ let transferred = 0;
72
+ tar.stdout.on("data", (chunk) => {
73
+ transferred += chunk.length;
74
+ onProgress?.({
75
+ name,
76
+ bytesTransferred: transferred,
77
+ totalBytes: 0,
78
+ percent: 0,
79
+ });
80
+ });
81
+ await pipeline(tar.stdout, stream);
82
+ await new Promise((resolve, reject) => {
83
+ tar.on("exit", (code) => {
84
+ if (code === 0)
85
+ resolve();
86
+ else
87
+ reject(new Error(`tar exited with code ${code}`));
88
+ });
89
+ });
90
+ }
91
+ // ============================================================================
92
+ // PULL (initiator requests file from target)
93
+ // ============================================================================
94
+ /**
95
+ * Pull a file from a remote node.
96
+ */
97
+ export async function pullFile(session, remotePath, localDir, onProgress) {
98
+ const stream = session.openStream({
99
+ channel: "file",
100
+ direction: "pull",
101
+ path: remotePath,
102
+ });
103
+ await waitForAck(session, stream);
104
+ // First data frame contains file metadata as JSON
105
+ const metaBuf = await readFirstChunk(stream);
106
+ const meta = JSON.parse(metaBuf.toString());
107
+ const destDir = localDir || process.cwd();
108
+ const destPath = join(destDir, meta.name);
109
+ if (meta.isDirectory) {
110
+ // Receive tar stream
111
+ mkdirSync(destDir, { recursive: true });
112
+ const tar = spawn("tar", ["-xf", "-", "-C", destDir], {
113
+ stdio: ["pipe", "ignore", "pipe"],
114
+ });
115
+ await pipeline(stream, tar.stdin);
116
+ await new Promise((resolve, reject) => {
117
+ tar.on("exit", (code) => {
118
+ if (code === 0)
119
+ resolve();
120
+ else
121
+ reject(new Error(`tar extract failed with code ${code}`));
122
+ });
123
+ });
124
+ return destPath;
125
+ }
126
+ // Receive file data
127
+ mkdirSync(dirname(destPath), { recursive: true });
128
+ const ws = createWriteStream(destPath, { mode: meta.size > 0 ? undefined : 0o644 });
129
+ let transferred = 0;
130
+ stream.on("data", (chunk) => {
131
+ transferred += chunk.length;
132
+ onProgress?.({
133
+ name: meta.name,
134
+ bytesTransferred: transferred,
135
+ totalBytes: meta.size,
136
+ percent: meta.size > 0 ? Math.round((transferred / meta.size) * 100) : 0,
137
+ });
138
+ });
139
+ await pipeline(stream, ws);
140
+ return destPath;
141
+ }
142
+ // ============================================================================
143
+ // TARGET SIDE (handlers)
144
+ // ============================================================================
145
+ /**
146
+ * Handle an incoming file push on the target node.
147
+ */
148
+ export async function handleFilePush(stream, meta, receiveDir) {
149
+ const destDir = receiveDir || DEFAULT_RECEIVE_DIR;
150
+ mkdirSync(destDir, { recursive: true });
151
+ if (meta.isDirectory) {
152
+ const tar = spawn("tar", ["-xf", "-", "-C", destDir], {
153
+ stdio: ["pipe", "ignore", "pipe"],
154
+ });
155
+ await pipeline(stream, tar.stdin);
156
+ await new Promise((resolve, reject) => {
157
+ tar.on("exit", (code) => {
158
+ if (code === 0)
159
+ resolve();
160
+ else
161
+ reject(new Error(`tar extract failed with code ${code}`));
162
+ });
163
+ });
164
+ return join(destDir, meta.name.replace(/\/$/, ""));
165
+ }
166
+ const destPath = join(destDir, meta.name);
167
+ mkdirSync(dirname(destPath), { recursive: true });
168
+ const ws = createWriteStream(destPath, { mode: meta.mode });
169
+ await pipeline(stream, ws);
170
+ return destPath;
171
+ }
172
+ /**
173
+ * Handle an incoming file pull request on the target node.
174
+ */
175
+ export async function handleFilePull(stream, meta) {
176
+ const path = meta.path;
177
+ if (!existsSync(path)) {
178
+ stream.destroy(new Error(`File not found: ${path}`));
179
+ return;
180
+ }
181
+ const stat = statSync(path);
182
+ const name = basename(path);
183
+ // Send metadata as first chunk
184
+ const metaJson = JSON.stringify({
185
+ name,
186
+ size: stat.size,
187
+ isDirectory: stat.isDirectory(),
188
+ mode: stat.mode,
189
+ mtime: Math.floor(stat.mtimeMs / 1000),
190
+ });
191
+ stream.write(Buffer.from(metaJson));
192
+ if (stat.isDirectory()) {
193
+ // Stream tar
194
+ const tar = spawn("tar", ["-cf", "-", "-C", dirname(path), name], {
195
+ stdio: ["ignore", "pipe", "pipe"],
196
+ });
197
+ await pipeline(tar.stdout, stream);
198
+ }
199
+ else {
200
+ // Stream file
201
+ const rs = createReadStream(path, { highWaterMark: CHUNK_SIZE });
202
+ await pipeline(rs, stream);
203
+ }
204
+ }
205
+ // ============================================================================
206
+ // HELPERS
207
+ // ============================================================================
208
+ function waitForAck(session, stream) {
209
+ return new Promise((resolve, reject) => {
210
+ const timeout = setTimeout(() => reject(new Error("Stream ACK timed out")), 10_000);
211
+ session.once("stream:ack", (s) => {
212
+ if (s.streamId === stream.streamId) {
213
+ clearTimeout(timeout);
214
+ resolve();
215
+ }
216
+ });
217
+ });
218
+ }
219
+ function readFirstChunk(stream) {
220
+ return new Promise((resolve, reject) => {
221
+ const timeout = setTimeout(() => reject(new Error("Timeout waiting for metadata")), 10_000);
222
+ stream.once("data", (chunk) => {
223
+ clearTimeout(timeout);
224
+ resolve(chunk);
225
+ });
226
+ stream.once("error", (err) => {
227
+ clearTimeout(timeout);
228
+ reject(err);
229
+ });
230
+ });
231
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Portal UI — terminal progress bars, status display, and interactive menu.
3
+ */
4
+ import type { DiscoveredNode } from "./discovery.js";
5
+ import type { TransferProgress } from "./transfer.js";
6
+ import type { ForwardInfo } from "./forward.js";
7
+ import type { PortalSession } from "./session.js";
8
+ export declare function printNodeList(nodes: DiscoveredNode[], showOffline?: boolean): void;
9
+ export declare function printProgress(progress: TransferProgress): void;
10
+ export declare function printSessionStatus(sessions: Array<ReturnType<PortalSession["getInfo"]>>): void;
11
+ export declare function printForwardStatus(forwards: ForwardInfo[]): void;
12
+ export declare function showPortalMenu(nodeName: string): Promise<string>;
13
+ export declare function printConnecting(nodeName: string): void;
14
+ export declare function printConnected(hostname: string): void;
15
+ export declare function printDisconnected(reason?: string): void;
16
+ export declare function printError(msg: string): void;