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.
- package/bin/swagmanager-mcp.js +7 -0
- package/dist/cli/app.js +30 -2
- package/dist/cli/chat/ChatApp.d.ts +4 -4
- package/dist/cli/chat/ChatApp.js +114 -44
- package/dist/cli/chat/ChatInput.d.ts +13 -6
- package/dist/cli/chat/ChatInput.js +433 -89
- package/dist/cli/chat/MemoryManager.d.ts +15 -0
- package/dist/cli/chat/MemoryManager.js +61 -0
- package/dist/cli/chat/MessageList.d.ts +8 -0
- package/dist/cli/chat/MessageList.js +1 -1
- package/dist/cli/chat/NodeManager.d.ts +30 -0
- package/dist/cli/chat/NodeManager.js +89 -0
- package/dist/cli/chat/NodeSelector.d.ts +19 -0
- package/dist/cli/chat/NodeSelector.js +37 -0
- package/dist/cli/chat/PlanApproval.d.ts +17 -0
- package/dist/cli/chat/PlanApproval.js +82 -0
- package/dist/cli/chat/SessionManager.d.ts +16 -0
- package/dist/cli/chat/SessionManager.js +43 -0
- package/dist/cli/chat/SlashMenu.d.ts +38 -0
- package/dist/cli/chat/SlashMenu.js +208 -0
- package/dist/cli/chat/StatusBar.d.ts +16 -0
- package/dist/cli/chat/StatusBar.js +22 -0
- package/dist/cli/chat/ThemeSelector.d.ts +14 -0
- package/dist/cli/chat/ThemeSelector.js +29 -0
- package/dist/cli/chat/ToolIndicator.d.ts +8 -0
- package/dist/cli/chat/ToolIndicator.js +33 -9
- package/dist/cli/chat/hooks/useAgentLoop.d.ts +2 -1
- package/dist/cli/chat/hooks/useAgentLoop.js +22 -17
- package/dist/cli/chat/hooks/useSlashCommands.d.ts +19 -0
- package/dist/cli/chat/hooks/useSlashCommands.js +254 -15
- package/dist/cli/commands/config-cmd.js +4 -25
- package/dist/cli/commands/db.d.ts +13 -0
- package/dist/cli/commands/db.js +243 -0
- package/dist/cli/commands/doctor.js +6 -9
- package/dist/cli/commands/mcp.js +1 -20
- package/dist/cli/services/agent-events.d.ts +22 -1
- package/dist/cli/services/agent-events.js +9 -0
- package/dist/cli/services/agent-loop.js +66 -2
- package/dist/cli/services/agent-worker-base.js +21 -6
- package/dist/cli/services/api-retry.d.ts +25 -0
- package/dist/cli/services/api-retry.js +91 -0
- package/dist/cli/services/auth-service.d.ts +1 -1
- package/dist/cli/services/auth-service.js +40 -19
- package/dist/cli/services/background-processes.js +26 -2
- package/dist/cli/services/config-store.d.ts +13 -1
- package/dist/cli/services/config-store.js +116 -13
- package/dist/cli/services/format-server-response.js +12 -6
- package/dist/cli/services/ink-resize-fix.d.ts +18 -0
- package/dist/cli/services/ink-resize-fix.js +66 -0
- package/dist/cli/services/interactive-tools.d.ts +14 -0
- package/dist/cli/services/interactive-tools.js +47 -2
- package/dist/cli/services/keybinding-manager.js +1 -1
- package/dist/cli/services/local-tools.js +35 -2
- package/dist/cli/services/server-tools.js +175 -3
- package/dist/cli/services/subagent.js +15 -3
- package/dist/cli/services/system-prompt.js +5 -3
- package/dist/cli/services/task-decomposer.d.ts +35 -0
- package/dist/cli/services/task-decomposer.js +199 -0
- package/dist/cli/services/team-lead.d.ts +18 -0
- package/dist/cli/services/team-lead.js +80 -0
- package/dist/cli/services/teammate.js +5 -5
- package/dist/cli/services/telemetry.d.ts +8 -2
- package/dist/cli/services/telemetry.js +116 -92
- package/dist/cli/services/tools/agent-tools.d.ts +1 -0
- package/dist/cli/services/tools/agent-tools.js +50 -4
- package/dist/cli/services/tools/file-ops.d.ts +2 -0
- package/dist/cli/services/tools/file-ops.js +71 -19
- package/dist/cli/services/tools/shell-exec.js +22 -12
- package/dist/cli/shared/Theme.d.ts +1 -2
- package/dist/cli/shared/Theme.js +1 -1
- package/dist/cli/shared/WhaleBanner.d.ts +4 -1
- package/dist/cli/shared/WhaleBanner.js +12 -8
- package/dist/cli/shared/markdown.d.ts +5 -4
- package/dist/cli/shared/markdown.js +376 -334
- package/dist/cli/shared/theme-manager.d.ts +27 -0
- package/dist/cli/shared/theme-manager.js +178 -0
- package/dist/cli/shared/theme-presets.d.ts +16 -0
- package/dist/cli/shared/theme-presets.js +265 -0
- package/dist/index.js +0 -51
- package/dist/node/adapters/imessage.d.ts +10 -0
- package/dist/node/adapters/imessage.js +45 -6
- package/dist/node/cli.js +459 -8
- package/dist/node/config.d.ts +17 -0
- package/dist/node/gateway-client.d.ts +55 -0
- package/dist/node/gateway-client.js +201 -0
- package/dist/node/portal/clipboard.d.ts +28 -0
- package/dist/node/portal/clipboard.js +183 -0
- package/dist/node/portal/discovery.d.ts +29 -0
- package/dist/node/portal/discovery.js +61 -0
- package/dist/node/portal/forward.d.ts +30 -0
- package/dist/node/portal/forward.js +90 -0
- package/dist/node/portal/index.d.ts +47 -0
- package/dist/node/portal/index.js +250 -0
- package/dist/node/portal/multiplexer.d.ts +48 -0
- package/dist/node/portal/multiplexer.js +207 -0
- package/dist/node/portal/permissions.d.ts +36 -0
- package/dist/node/portal/permissions.js +131 -0
- package/dist/node/portal/protocol.d.ts +140 -0
- package/dist/node/portal/protocol.js +193 -0
- package/dist/node/portal/screen.d.ts +18 -0
- package/dist/node/portal/screen.js +93 -0
- package/dist/node/portal/session.d.ts +68 -0
- package/dist/node/portal/session.js +127 -0
- package/dist/node/portal/shell.d.ts +26 -0
- package/dist/node/portal/shell.js +142 -0
- package/dist/node/portal/stream.d.ts +43 -0
- package/dist/node/portal/stream.js +90 -0
- package/dist/node/portal/transfer.d.ts +33 -0
- package/dist/node/portal/transfer.js +231 -0
- package/dist/node/portal/ui.d.ts +16 -0
- package/dist/node/portal/ui.js +148 -0
- package/dist/node/remote-desktop/compile-helper.d.ts +13 -0
- package/dist/node/remote-desktop/compile-helper.js +73 -0
- package/dist/node/remote-desktop/index.d.ts +67 -0
- package/dist/node/remote-desktop/index.js +220 -0
- package/dist/node/remote-desktop/protocol.d.ts +96 -0
- package/dist/node/remote-desktop/protocol.js +67 -0
- package/dist/node/runtime.d.ts +8 -1
- package/dist/node/runtime.js +117 -9
- package/dist/server/handlers/__test-utils__/test-db.d.ts +25 -0
- package/dist/server/handlers/__test-utils__/test-db.js +128 -0
- package/dist/server/handlers/api-keys.js +26 -2
- package/dist/server/handlers/browser.d.ts +0 -4
- package/dist/server/handlers/browser.js +0 -46
- package/dist/server/handlers/catalog.js +37 -14
- package/dist/server/handlers/clickhouse.d.ts +10 -0
- package/dist/server/handlers/clickhouse.js +215 -0
- package/dist/server/handlers/comms.d.ts +308 -4
- package/dist/server/handlers/comms.js +444 -11
- package/dist/server/handlers/creations.js +1 -1
- package/dist/server/handlers/crm.d.ts +54 -8
- package/dist/server/handlers/crm.js +353 -68
- package/dist/server/handlers/embeddings.js +3 -3
- package/dist/server/handlers/enrichment.js +39 -55
- package/dist/server/handlers/inventory.js +1 -1
- package/dist/server/handlers/kali.d.ts +9 -1
- package/dist/server/handlers/kali.js +50 -1
- package/dist/server/handlers/media.d.ts +8 -0
- package/dist/server/handlers/media.js +902 -0
- package/dist/server/handlers/meta-ads.js +6 -3
- package/dist/server/handlers/nodes.d.ts +2 -0
- package/dist/server/handlers/nodes.js +331 -40
- package/dist/server/handlers/operations.d.ts +4 -6
- package/dist/server/handlers/operations.js +99 -38
- package/dist/server/handlers/platform.js +224 -107
- package/dist/server/handlers/remove-bg.d.ts +6 -0
- package/dist/server/handlers/remove-bg.js +96 -0
- package/dist/server/handlers/storefront.d.ts +6 -0
- package/dist/server/handlers/storefront.js +477 -0
- package/dist/server/handlers/supply-chain.js +21 -3
- package/dist/server/handlers/workflow-steps.js +87 -31
- package/dist/server/handlers/workflows.js +4 -1
- package/dist/server/index.js +334 -88
- package/dist/server/lib/clickhouse-buffer.d.ts +48 -0
- package/dist/server/lib/clickhouse-buffer.js +175 -0
- package/dist/server/lib/clickhouse-client.d.ts +112 -0
- package/dist/server/lib/clickhouse-client.js +141 -0
- package/dist/server/lib/coa-renderer.d.ts +91 -0
- package/dist/server/lib/coa-renderer.js +411 -0
- package/dist/server/lib/compaction-service.js +45 -1
- package/dist/server/lib/pdf-renderer.d.ts +143 -0
- package/dist/server/lib/pdf-renderer.js +867 -0
- package/dist/server/lib/react-pdf-layout.d.ts +40 -0
- package/dist/server/lib/react-pdf-layout.js +437 -0
- package/dist/server/lib/server-agent-loop.d.ts +2 -0
- package/dist/server/lib/server-agent-loop.js +61 -15
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +7 -4
- package/dist/server/lib/supabase-client.js +51 -3
- package/dist/server/lib/template-resolver.js +14 -4
- package/dist/server/lib/utils.js +15 -0
- package/dist/server/local-agent-gateway.d.ts +44 -0
- package/dist/server/local-agent-gateway.js +389 -49
- package/dist/server/providers/anthropic.js +12 -2
- package/dist/server/providers/gemini.js +17 -2
- package/dist/server/proxy-handlers.js +151 -0
- package/dist/server/tool-router.d.ts +2 -2
- package/dist/server/tool-router.js +25 -35
- package/dist/shared/agent-core.d.ts +5 -2
- package/dist/shared/agent-core.js +30 -4
- package/dist/shared/api-client.js +54 -3
- package/dist/shared/sse-parser.d.ts +1 -1
- package/dist/shared/sse-parser.js +5 -2
- package/dist/shared/tool-dispatch.js +1 -1
- package/package.json +16 -10
- package/dist/server/handlers/__test-utils__/mock-supabase.d.ts +0 -11
- 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;
|