whale-code 6.4.0 → 6.5.1
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 +51 -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 +65 -8
- 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 +7 -6
- 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 +85 -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 +46 -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 +36 -17
- package/dist/server/lib/server-subagent.d.ts +3 -0
- package/dist/server/lib/server-subagent.js +9 -6
- 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 +25 -2
- package/dist/shared/agent-core.js +66 -5
- 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 +15 -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,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Wire Protocol — frame encoding/decoding for multiplexed portal connections.
|
|
3
|
+
*
|
|
4
|
+
* Relay Envelope (over WebSocket binary):
|
|
5
|
+
* Byte 0: 0x01 (portal frame marker)
|
|
6
|
+
* Bytes 1-16: Session UUID (16 bytes, binary)
|
|
7
|
+
* Bytes 17+: Multiplexed frame (forwarded as-is)
|
|
8
|
+
*
|
|
9
|
+
* Multiplexed Frame (inner protocol, between nodes):
|
|
10
|
+
* ┌──────────┬──────────┬──────────┬──────────┬───────────────┐
|
|
11
|
+
* │ Version │ Type │ Flags │ StreamID │ Length │
|
|
12
|
+
* │ 1 byte │ 1 byte │ 2 bytes │ 4 bytes │ 4 bytes │
|
|
13
|
+
* └──────────┴──────────┴──────────┴──────────┴───────────────┘
|
|
14
|
+
* Total header: 12 bytes + variable payload (max 1MB per frame)
|
|
15
|
+
*/
|
|
16
|
+
export declare const PORTAL_MARKER = 1;
|
|
17
|
+
export declare const PROTOCOL_VERSION = 1;
|
|
18
|
+
export declare const HEADER_SIZE = 12;
|
|
19
|
+
export declare const MAX_PAYLOAD_SIZE: number;
|
|
20
|
+
export declare const SESSION_UUID_SIZE = 16;
|
|
21
|
+
export declare const RELAY_HEADER_SIZE: number;
|
|
22
|
+
export declare enum FrameType {
|
|
23
|
+
DATA = 0,
|
|
24
|
+
SYN = 1,
|
|
25
|
+
ACK = 2,
|
|
26
|
+
FIN = 3,
|
|
27
|
+
RST = 4,
|
|
28
|
+
PING = 5,
|
|
29
|
+
PONG = 6
|
|
30
|
+
}
|
|
31
|
+
export declare enum FrameFlag {
|
|
32
|
+
SYN = 1,
|
|
33
|
+
ACK = 2,
|
|
34
|
+
FIN = 4
|
|
35
|
+
}
|
|
36
|
+
export type ChannelType = "shell" | "file" | "forward" | "clipboard" | "screen";
|
|
37
|
+
export interface SynShell {
|
|
38
|
+
channel: "shell";
|
|
39
|
+
term: string;
|
|
40
|
+
cols: number;
|
|
41
|
+
rows: number;
|
|
42
|
+
cwd?: string;
|
|
43
|
+
command?: string;
|
|
44
|
+
}
|
|
45
|
+
export interface SynFilePush {
|
|
46
|
+
channel: "file";
|
|
47
|
+
direction: "push";
|
|
48
|
+
name: string;
|
|
49
|
+
size: number;
|
|
50
|
+
mode: number;
|
|
51
|
+
mtime: number;
|
|
52
|
+
isDirectory?: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface SynFilePull {
|
|
55
|
+
channel: "file";
|
|
56
|
+
direction: "pull";
|
|
57
|
+
path: string;
|
|
58
|
+
}
|
|
59
|
+
export interface SynForward {
|
|
60
|
+
channel: "forward";
|
|
61
|
+
remotePort: number;
|
|
62
|
+
description?: string;
|
|
63
|
+
}
|
|
64
|
+
export interface SynScreen {
|
|
65
|
+
channel: "screen";
|
|
66
|
+
control: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface SynClipboard {
|
|
69
|
+
channel: "clipboard";
|
|
70
|
+
mode: "push" | "pull" | "sync";
|
|
71
|
+
}
|
|
72
|
+
export type SynPayload = SynShell | SynFilePush | SynFilePull | SynForward | SynScreen | SynClipboard;
|
|
73
|
+
export interface Frame {
|
|
74
|
+
version: number;
|
|
75
|
+
type: FrameType;
|
|
76
|
+
flags: number;
|
|
77
|
+
streamId: number;
|
|
78
|
+
payload: Buffer;
|
|
79
|
+
}
|
|
80
|
+
export interface PortalRequest {
|
|
81
|
+
type: "portal_request";
|
|
82
|
+
session_id: string;
|
|
83
|
+
target_node_id: string;
|
|
84
|
+
capabilities: ChannelType[];
|
|
85
|
+
requester_node_id: string;
|
|
86
|
+
requester_hostname: string;
|
|
87
|
+
requester_role: string;
|
|
88
|
+
}
|
|
89
|
+
export interface PortalAccept {
|
|
90
|
+
type: "portal_accept";
|
|
91
|
+
session_id: string;
|
|
92
|
+
hostname: string;
|
|
93
|
+
}
|
|
94
|
+
export interface PortalReject {
|
|
95
|
+
type: "portal_reject";
|
|
96
|
+
session_id: string;
|
|
97
|
+
reason: string;
|
|
98
|
+
}
|
|
99
|
+
export interface PortalClose {
|
|
100
|
+
type: "portal_close";
|
|
101
|
+
session_id: string;
|
|
102
|
+
}
|
|
103
|
+
export interface ResizeMessage {
|
|
104
|
+
type: "resize";
|
|
105
|
+
cols: number;
|
|
106
|
+
rows: number;
|
|
107
|
+
}
|
|
108
|
+
export type ControlMessage = PortalRequest | PortalAccept | PortalReject | PortalClose | ResizeMessage;
|
|
109
|
+
export declare function encodeFrame(frame: Frame): Buffer;
|
|
110
|
+
export declare function encodeRelayEnvelope(sessionUuid: Buffer, innerFrame: Buffer): Buffer;
|
|
111
|
+
export interface DecodeResult {
|
|
112
|
+
frame: Frame;
|
|
113
|
+
bytesConsumed: number;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Try to decode a frame from the buffer. Returns null if not enough data.
|
|
117
|
+
*/
|
|
118
|
+
export declare function decodeFrame(buf: Buffer, offset?: number): DecodeResult | null;
|
|
119
|
+
/**
|
|
120
|
+
* Parse a relay envelope. Returns session UUID and inner frame buffer.
|
|
121
|
+
*/
|
|
122
|
+
export declare function decodeRelayEnvelope(buf: Buffer): {
|
|
123
|
+
sessionUuid: Buffer;
|
|
124
|
+
innerFrame: Buffer;
|
|
125
|
+
} | null;
|
|
126
|
+
/**
|
|
127
|
+
* Convert a UUID string (with dashes) to a 16-byte buffer.
|
|
128
|
+
*/
|
|
129
|
+
export declare function uuidToBuffer(uuid: string): Buffer;
|
|
130
|
+
/**
|
|
131
|
+
* Convert a 16-byte buffer to a UUID string (with dashes).
|
|
132
|
+
*/
|
|
133
|
+
export declare function bufferToUuid(buf: Buffer): string;
|
|
134
|
+
export declare function makeDataFrame(streamId: number, data: Buffer): Buffer;
|
|
135
|
+
export declare function makeSynFrame(streamId: number, payload: SynPayload): Buffer;
|
|
136
|
+
export declare function makeAckFrame(streamId: number): Buffer;
|
|
137
|
+
export declare function makeFinFrame(streamId: number): Buffer;
|
|
138
|
+
export declare function makeRstFrame(streamId: number, reason?: string): Buffer;
|
|
139
|
+
export declare function makePingFrame(): Buffer;
|
|
140
|
+
export declare function makePongFrame(): Buffer;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Portal Wire Protocol — frame encoding/decoding for multiplexed portal connections.
|
|
3
|
+
*
|
|
4
|
+
* Relay Envelope (over WebSocket binary):
|
|
5
|
+
* Byte 0: 0x01 (portal frame marker)
|
|
6
|
+
* Bytes 1-16: Session UUID (16 bytes, binary)
|
|
7
|
+
* Bytes 17+: Multiplexed frame (forwarded as-is)
|
|
8
|
+
*
|
|
9
|
+
* Multiplexed Frame (inner protocol, between nodes):
|
|
10
|
+
* ┌──────────┬──────────┬──────────┬──────────┬───────────────┐
|
|
11
|
+
* │ Version │ Type │ Flags │ StreamID │ Length │
|
|
12
|
+
* │ 1 byte │ 1 byte │ 2 bytes │ 4 bytes │ 4 bytes │
|
|
13
|
+
* └──────────┴──────────┴──────────┴──────────┴───────────────┘
|
|
14
|
+
* Total header: 12 bytes + variable payload (max 1MB per frame)
|
|
15
|
+
*/
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// CONSTANTS
|
|
18
|
+
// ============================================================================
|
|
19
|
+
export const PORTAL_MARKER = 0x01;
|
|
20
|
+
export const PROTOCOL_VERSION = 0x01;
|
|
21
|
+
export const HEADER_SIZE = 12;
|
|
22
|
+
export const MAX_PAYLOAD_SIZE = 1024 * 1024; // 1MB
|
|
23
|
+
export const SESSION_UUID_SIZE = 16;
|
|
24
|
+
export const RELAY_HEADER_SIZE = 1 + SESSION_UUID_SIZE; // marker + uuid
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// FRAME TYPES
|
|
27
|
+
// ============================================================================
|
|
28
|
+
export var FrameType;
|
|
29
|
+
(function (FrameType) {
|
|
30
|
+
FrameType[FrameType["DATA"] = 0] = "DATA";
|
|
31
|
+
FrameType[FrameType["SYN"] = 1] = "SYN";
|
|
32
|
+
FrameType[FrameType["ACK"] = 2] = "ACK";
|
|
33
|
+
FrameType[FrameType["FIN"] = 3] = "FIN";
|
|
34
|
+
FrameType[FrameType["RST"] = 4] = "RST";
|
|
35
|
+
FrameType[FrameType["PING"] = 5] = "PING";
|
|
36
|
+
FrameType[FrameType["PONG"] = 6] = "PONG";
|
|
37
|
+
})(FrameType || (FrameType = {}));
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// FLAGS
|
|
40
|
+
// ============================================================================
|
|
41
|
+
export var FrameFlag;
|
|
42
|
+
(function (FrameFlag) {
|
|
43
|
+
FrameFlag[FrameFlag["SYN"] = 1] = "SYN";
|
|
44
|
+
FrameFlag[FrameFlag["ACK"] = 2] = "ACK";
|
|
45
|
+
FrameFlag[FrameFlag["FIN"] = 4] = "FIN";
|
|
46
|
+
})(FrameFlag || (FrameFlag = {}));
|
|
47
|
+
// ============================================================================
|
|
48
|
+
// ENCODING
|
|
49
|
+
// ============================================================================
|
|
50
|
+
export function encodeFrame(frame) {
|
|
51
|
+
const payloadLen = frame.payload.length;
|
|
52
|
+
if (payloadLen > MAX_PAYLOAD_SIZE) {
|
|
53
|
+
throw new Error(`Payload too large: ${payloadLen} > ${MAX_PAYLOAD_SIZE}`);
|
|
54
|
+
}
|
|
55
|
+
const buf = Buffer.allocUnsafe(HEADER_SIZE + payloadLen);
|
|
56
|
+
buf[0] = frame.version;
|
|
57
|
+
buf[1] = frame.type;
|
|
58
|
+
buf.writeUInt16BE(frame.flags, 2);
|
|
59
|
+
buf.writeUInt32BE(frame.streamId, 4);
|
|
60
|
+
buf.writeUInt32BE(payloadLen, 8);
|
|
61
|
+
if (payloadLen > 0) {
|
|
62
|
+
frame.payload.copy(buf, HEADER_SIZE);
|
|
63
|
+
}
|
|
64
|
+
return buf;
|
|
65
|
+
}
|
|
66
|
+
export function encodeRelayEnvelope(sessionUuid, innerFrame) {
|
|
67
|
+
const buf = Buffer.allocUnsafe(RELAY_HEADER_SIZE + innerFrame.length);
|
|
68
|
+
buf[0] = PORTAL_MARKER;
|
|
69
|
+
sessionUuid.copy(buf, 1);
|
|
70
|
+
innerFrame.copy(buf, RELAY_HEADER_SIZE);
|
|
71
|
+
return buf;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Try to decode a frame from the buffer. Returns null if not enough data.
|
|
75
|
+
*/
|
|
76
|
+
export function decodeFrame(buf, offset = 0) {
|
|
77
|
+
const remaining = buf.length - offset;
|
|
78
|
+
if (remaining < HEADER_SIZE)
|
|
79
|
+
return null;
|
|
80
|
+
const version = buf[offset];
|
|
81
|
+
const type = buf[offset + 1];
|
|
82
|
+
const flags = buf.readUInt16BE(offset + 2);
|
|
83
|
+
const streamId = buf.readUInt32BE(offset + 4);
|
|
84
|
+
const payloadLen = buf.readUInt32BE(offset + 8);
|
|
85
|
+
if (payloadLen > MAX_PAYLOAD_SIZE) {
|
|
86
|
+
throw new Error(`Invalid payload length: ${payloadLen}`);
|
|
87
|
+
}
|
|
88
|
+
const totalLen = HEADER_SIZE + payloadLen;
|
|
89
|
+
if (remaining < totalLen)
|
|
90
|
+
return null;
|
|
91
|
+
const payload = Buffer.allocUnsafe(payloadLen);
|
|
92
|
+
buf.copy(payload, 0, offset + HEADER_SIZE, offset + totalLen);
|
|
93
|
+
return {
|
|
94
|
+
frame: { version, type, flags, streamId, payload },
|
|
95
|
+
bytesConsumed: totalLen,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Parse a relay envelope. Returns session UUID and inner frame buffer.
|
|
100
|
+
*/
|
|
101
|
+
export function decodeRelayEnvelope(buf) {
|
|
102
|
+
if (buf.length < RELAY_HEADER_SIZE)
|
|
103
|
+
return null;
|
|
104
|
+
if (buf[0] !== PORTAL_MARKER)
|
|
105
|
+
return null;
|
|
106
|
+
const sessionUuid = Buffer.allocUnsafe(SESSION_UUID_SIZE);
|
|
107
|
+
buf.copy(sessionUuid, 0, 1, 1 + SESSION_UUID_SIZE);
|
|
108
|
+
const innerFrame = Buffer.allocUnsafe(buf.length - RELAY_HEADER_SIZE);
|
|
109
|
+
buf.copy(innerFrame, 0, RELAY_HEADER_SIZE);
|
|
110
|
+
return { sessionUuid, innerFrame };
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// UUID HELPERS
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Convert a UUID string (with dashes) to a 16-byte buffer.
|
|
117
|
+
*/
|
|
118
|
+
export function uuidToBuffer(uuid) {
|
|
119
|
+
return Buffer.from(uuid.replace(/-/g, ""), "hex");
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Convert a 16-byte buffer to a UUID string (with dashes).
|
|
123
|
+
*/
|
|
124
|
+
export function bufferToUuid(buf) {
|
|
125
|
+
const hex = buf.toString("hex");
|
|
126
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
127
|
+
}
|
|
128
|
+
// ============================================================================
|
|
129
|
+
// HELPERS
|
|
130
|
+
// ============================================================================
|
|
131
|
+
export function makeDataFrame(streamId, data) {
|
|
132
|
+
return encodeFrame({
|
|
133
|
+
version: PROTOCOL_VERSION,
|
|
134
|
+
type: FrameType.DATA,
|
|
135
|
+
flags: 0,
|
|
136
|
+
streamId,
|
|
137
|
+
payload: data,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export function makeSynFrame(streamId, payload) {
|
|
141
|
+
return encodeFrame({
|
|
142
|
+
version: PROTOCOL_VERSION,
|
|
143
|
+
type: FrameType.SYN,
|
|
144
|
+
flags: FrameFlag.SYN,
|
|
145
|
+
streamId,
|
|
146
|
+
payload: Buffer.from(JSON.stringify(payload)),
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
export function makeAckFrame(streamId) {
|
|
150
|
+
return encodeFrame({
|
|
151
|
+
version: PROTOCOL_VERSION,
|
|
152
|
+
type: FrameType.ACK,
|
|
153
|
+
flags: FrameFlag.ACK,
|
|
154
|
+
streamId,
|
|
155
|
+
payload: Buffer.alloc(0),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
export function makeFinFrame(streamId) {
|
|
159
|
+
return encodeFrame({
|
|
160
|
+
version: PROTOCOL_VERSION,
|
|
161
|
+
type: FrameType.FIN,
|
|
162
|
+
flags: FrameFlag.FIN,
|
|
163
|
+
streamId,
|
|
164
|
+
payload: Buffer.alloc(0),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
export function makeRstFrame(streamId, reason = "") {
|
|
168
|
+
return encodeFrame({
|
|
169
|
+
version: PROTOCOL_VERSION,
|
|
170
|
+
type: FrameType.RST,
|
|
171
|
+
flags: 0,
|
|
172
|
+
streamId,
|
|
173
|
+
payload: reason ? Buffer.from(reason) : Buffer.alloc(0),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
export function makePingFrame() {
|
|
177
|
+
return encodeFrame({
|
|
178
|
+
version: PROTOCOL_VERSION,
|
|
179
|
+
type: FrameType.PING,
|
|
180
|
+
flags: 0,
|
|
181
|
+
streamId: 0,
|
|
182
|
+
payload: Buffer.alloc(0),
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
export function makePongFrame() {
|
|
186
|
+
return encodeFrame({
|
|
187
|
+
version: PROTOCOL_VERSION,
|
|
188
|
+
type: FrameType.PONG,
|
|
189
|
+
flags: 0,
|
|
190
|
+
streamId: 0,
|
|
191
|
+
payload: Buffer.alloc(0),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenChannel — bridges portal stream to ScreenCaptureService.
|
|
3
|
+
*
|
|
4
|
+
* Target side: subscribes to ScreenCaptureService events, forwards H.264 frames
|
|
5
|
+
* over the portal stream. Sends SPS/PPS config + latest frame immediately so the
|
|
6
|
+
* remote viewer gets an instant first frame.
|
|
7
|
+
*
|
|
8
|
+
* Initiator side: opens a portal stream and pipes it to the UI.
|
|
9
|
+
*/
|
|
10
|
+
import type { PortalStream } from "./stream.js";
|
|
11
|
+
import type { SynScreen } from "./protocol.js";
|
|
12
|
+
import type { PortalSession } from "./session.js";
|
|
13
|
+
import type { ScreenCaptureService } from "../remote-desktop/index.js";
|
|
14
|
+
export declare function openScreen(session: PortalSession, control: boolean): Promise<{
|
|
15
|
+
stream: PortalStream;
|
|
16
|
+
cleanup: () => void;
|
|
17
|
+
}>;
|
|
18
|
+
export declare function handleScreen(stream: PortalStream, meta: SynScreen, captureService: ScreenCaptureService | null): void;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScreenChannel — bridges portal stream to ScreenCaptureService.
|
|
3
|
+
*
|
|
4
|
+
* Target side: subscribes to ScreenCaptureService events, forwards H.264 frames
|
|
5
|
+
* over the portal stream. Sends SPS/PPS config + latest frame immediately so the
|
|
6
|
+
* remote viewer gets an instant first frame.
|
|
7
|
+
*
|
|
8
|
+
* Initiator side: opens a portal stream and pipes it to the UI.
|
|
9
|
+
*/
|
|
10
|
+
// ============================================================================
|
|
11
|
+
// INITIATOR SIDE — receive screen frames
|
|
12
|
+
// ============================================================================
|
|
13
|
+
export async function openScreen(session, control) {
|
|
14
|
+
const stream = session.openStream({
|
|
15
|
+
channel: "screen",
|
|
16
|
+
control,
|
|
17
|
+
});
|
|
18
|
+
await new Promise((resolve, reject) => {
|
|
19
|
+
const timeout = setTimeout(() => reject(new Error("Screen open timed out")), 10_000);
|
|
20
|
+
session.once("stream:ack", (s) => {
|
|
21
|
+
if (s.streamId === stream.streamId) {
|
|
22
|
+
clearTimeout(timeout);
|
|
23
|
+
resolve();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (!stream.destroyed)
|
|
29
|
+
stream.end();
|
|
30
|
+
};
|
|
31
|
+
return { stream, cleanup };
|
|
32
|
+
}
|
|
33
|
+
// ============================================================================
|
|
34
|
+
// TARGET SIDE — bridge ScreenCaptureService to portal stream
|
|
35
|
+
// ============================================================================
|
|
36
|
+
export function handleScreen(stream, meta, captureService) {
|
|
37
|
+
if (!captureService) {
|
|
38
|
+
stream.destroy(new Error("Screen capture not available on this node"));
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Send SPS/PPS config immediately so remote decoder can initialize
|
|
42
|
+
const config = captureService.latestConfig;
|
|
43
|
+
if (config) {
|
|
44
|
+
stream.write(config);
|
|
45
|
+
}
|
|
46
|
+
// Send latest frame for instant first image
|
|
47
|
+
const latest = captureService.latestFrame;
|
|
48
|
+
if (latest) {
|
|
49
|
+
stream.write(latest);
|
|
50
|
+
}
|
|
51
|
+
// Forward all subsequent frames
|
|
52
|
+
const onFrame = (data) => {
|
|
53
|
+
if (!stream.destroyed) {
|
|
54
|
+
stream.write(data);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const onConfig = (data) => {
|
|
58
|
+
if (!stream.destroyed) {
|
|
59
|
+
stream.write(data);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
captureService.on("frame", onFrame);
|
|
63
|
+
captureService.on("config", onConfig);
|
|
64
|
+
// Forward control input from remote viewer → capture service
|
|
65
|
+
if (meta.control) {
|
|
66
|
+
stream.on("data", (data) => {
|
|
67
|
+
try {
|
|
68
|
+
const msg = JSON.parse(data.toString());
|
|
69
|
+
if (!msg.type)
|
|
70
|
+
return;
|
|
71
|
+
// Input events (mouse, keyboard, scroll)
|
|
72
|
+
if (msg.type.startsWith("mouse_") || msg.type.startsWith("key_") || msg.type === "scroll") {
|
|
73
|
+
captureService.injectInput(msg);
|
|
74
|
+
}
|
|
75
|
+
// Control messages (keyframe request, quality adjustment)
|
|
76
|
+
else if (msg.type === "request_keyframe" || msg.type === "quality" || msg.type === "set_fps") {
|
|
77
|
+
captureService.sendControlMessage(msg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
// Binary frame data flowing the wrong direction, ignore
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
// Cleanup listeners when stream closes
|
|
86
|
+
const cleanup = () => {
|
|
87
|
+
captureService.removeListener("frame", onFrame);
|
|
88
|
+
captureService.removeListener("config", onConfig);
|
|
89
|
+
};
|
|
90
|
+
stream.on("end", cleanup);
|
|
91
|
+
stream.on("error", cleanup);
|
|
92
|
+
stream.on("close", cleanup);
|
|
93
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PortalSession — manages one peer-to-peer portal connection.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a Multiplexer over the WebSocket transport. Handles session negotiation,
|
|
5
|
+
* binary framing through the relay, and channel lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { type SynPayload } from "./protocol.js";
|
|
9
|
+
import { PortalStream } from "./stream.js";
|
|
10
|
+
export interface PortalSessionConfig {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
isInitiator: boolean;
|
|
13
|
+
sendBinary: (data: Buffer) => void;
|
|
14
|
+
sendJson: (msg: Record<string, unknown>) => void;
|
|
15
|
+
targetNodeId?: string;
|
|
16
|
+
targetHostname?: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class PortalSession extends EventEmitter {
|
|
19
|
+
readonly sessionId: string;
|
|
20
|
+
readonly isInitiator: boolean;
|
|
21
|
+
readonly targetNodeId: string | undefined;
|
|
22
|
+
readonly targetHostname: string | undefined;
|
|
23
|
+
private sessionUuidBuf;
|
|
24
|
+
private multiplexer;
|
|
25
|
+
private sendBinary;
|
|
26
|
+
private sendJson;
|
|
27
|
+
private _accepted;
|
|
28
|
+
private _closed;
|
|
29
|
+
readonly createdAt: Date;
|
|
30
|
+
constructor(config: PortalSessionConfig);
|
|
31
|
+
get accepted(): boolean;
|
|
32
|
+
get closed(): boolean;
|
|
33
|
+
get activeStreams(): number;
|
|
34
|
+
/**
|
|
35
|
+
* Mark session as accepted (called after portal_accept received/sent).
|
|
36
|
+
*/
|
|
37
|
+
accept(): void;
|
|
38
|
+
/**
|
|
39
|
+
* Open a new channel stream (initiator side).
|
|
40
|
+
*/
|
|
41
|
+
openStream(meta: SynPayload): PortalStream;
|
|
42
|
+
/**
|
|
43
|
+
* Feed incoming binary data from the WebSocket.
|
|
44
|
+
* Strips the relay envelope and feeds to multiplexer.
|
|
45
|
+
*/
|
|
46
|
+
receiveRelay(data: Buffer): void;
|
|
47
|
+
/**
|
|
48
|
+
* Feed raw inner frame data directly (no envelope, for testing).
|
|
49
|
+
*/
|
|
50
|
+
receiveRaw(data: Buffer): void;
|
|
51
|
+
/**
|
|
52
|
+
* Close the session.
|
|
53
|
+
*/
|
|
54
|
+
close(reason?: string): void;
|
|
55
|
+
/**
|
|
56
|
+
* Get session info.
|
|
57
|
+
*/
|
|
58
|
+
getInfo(): {
|
|
59
|
+
session_id: string;
|
|
60
|
+
target_node_id?: string;
|
|
61
|
+
target_hostname?: string;
|
|
62
|
+
is_initiator: boolean;
|
|
63
|
+
accepted: boolean;
|
|
64
|
+
active_streams: number;
|
|
65
|
+
created_at: string;
|
|
66
|
+
};
|
|
67
|
+
private sendRelayFrame;
|
|
68
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PortalSession — manages one peer-to-peer portal connection.
|
|
3
|
+
*
|
|
4
|
+
* Wraps a Multiplexer over the WebSocket transport. Handles session negotiation,
|
|
5
|
+
* binary framing through the relay, and channel lifecycle.
|
|
6
|
+
*/
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import { encodeRelayEnvelope, decodeRelayEnvelope, uuidToBuffer, bufferToUuid, } from "./protocol.js";
|
|
9
|
+
import { Multiplexer } from "./multiplexer.js";
|
|
10
|
+
export class PortalSession extends EventEmitter {
|
|
11
|
+
sessionId;
|
|
12
|
+
isInitiator;
|
|
13
|
+
targetNodeId;
|
|
14
|
+
targetHostname;
|
|
15
|
+
sessionUuidBuf;
|
|
16
|
+
multiplexer;
|
|
17
|
+
sendBinary;
|
|
18
|
+
sendJson;
|
|
19
|
+
_accepted = false;
|
|
20
|
+
_closed = false;
|
|
21
|
+
createdAt = new Date();
|
|
22
|
+
constructor(config) {
|
|
23
|
+
super();
|
|
24
|
+
this.sessionId = config.sessionId;
|
|
25
|
+
this.isInitiator = config.isInitiator;
|
|
26
|
+
this.targetNodeId = config.targetNodeId;
|
|
27
|
+
this.targetHostname = config.targetHostname;
|
|
28
|
+
this.sendBinary = config.sendBinary;
|
|
29
|
+
this.sendJson = config.sendJson;
|
|
30
|
+
this.sessionUuidBuf = uuidToBuffer(config.sessionId);
|
|
31
|
+
this.multiplexer = new Multiplexer({
|
|
32
|
+
isInitiator: config.isInitiator,
|
|
33
|
+
sendRaw: (data) => this.sendRelayFrame(data),
|
|
34
|
+
});
|
|
35
|
+
// Forward multiplexer events
|
|
36
|
+
this.multiplexer.on("stream", (stream, meta) => {
|
|
37
|
+
this.emit("stream", stream, meta);
|
|
38
|
+
});
|
|
39
|
+
this.multiplexer.on("stream:ack", (stream) => {
|
|
40
|
+
this.emit("stream:ack", stream);
|
|
41
|
+
});
|
|
42
|
+
this.multiplexer.on("timeout", () => {
|
|
43
|
+
this.close("keepalive timeout");
|
|
44
|
+
});
|
|
45
|
+
this.multiplexer.on("close", () => {
|
|
46
|
+
this._closed = true;
|
|
47
|
+
this.emit("close");
|
|
48
|
+
});
|
|
49
|
+
this.multiplexer.on("error", (err) => {
|
|
50
|
+
this.emit("error", err);
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
get accepted() { return this._accepted; }
|
|
54
|
+
get closed() { return this._closed; }
|
|
55
|
+
get activeStreams() { return this.multiplexer.activeStreams; }
|
|
56
|
+
/**
|
|
57
|
+
* Mark session as accepted (called after portal_accept received/sent).
|
|
58
|
+
*/
|
|
59
|
+
accept() {
|
|
60
|
+
this._accepted = true;
|
|
61
|
+
this.emit("accepted");
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Open a new channel stream (initiator side).
|
|
65
|
+
*/
|
|
66
|
+
openStream(meta) {
|
|
67
|
+
return this.multiplexer.openStream(meta);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Feed incoming binary data from the WebSocket.
|
|
71
|
+
* Strips the relay envelope and feeds to multiplexer.
|
|
72
|
+
*/
|
|
73
|
+
receiveRelay(data) {
|
|
74
|
+
if (this._closed)
|
|
75
|
+
return;
|
|
76
|
+
const decoded = decodeRelayEnvelope(data);
|
|
77
|
+
if (!decoded)
|
|
78
|
+
return;
|
|
79
|
+
// Verify session UUID matches
|
|
80
|
+
const uuid = bufferToUuid(decoded.sessionUuid);
|
|
81
|
+
if (uuid !== this.sessionId)
|
|
82
|
+
return;
|
|
83
|
+
this.multiplexer.receive(decoded.innerFrame);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Feed raw inner frame data directly (no envelope, for testing).
|
|
87
|
+
*/
|
|
88
|
+
receiveRaw(data) {
|
|
89
|
+
if (this._closed)
|
|
90
|
+
return;
|
|
91
|
+
this.multiplexer.receive(data);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Close the session.
|
|
95
|
+
*/
|
|
96
|
+
close(reason = "session closed") {
|
|
97
|
+
if (this._closed)
|
|
98
|
+
return;
|
|
99
|
+
this._closed = true;
|
|
100
|
+
this.sendJson({ type: "portal_close", session_id: this.sessionId });
|
|
101
|
+
this.multiplexer.close();
|
|
102
|
+
this.emit("close", reason);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get session info.
|
|
106
|
+
*/
|
|
107
|
+
getInfo() {
|
|
108
|
+
return {
|
|
109
|
+
session_id: this.sessionId,
|
|
110
|
+
target_node_id: this.targetNodeId,
|
|
111
|
+
target_hostname: this.targetHostname,
|
|
112
|
+
is_initiator: this.isInitiator,
|
|
113
|
+
accepted: this._accepted,
|
|
114
|
+
active_streams: this.activeStreams,
|
|
115
|
+
created_at: this.createdAt.toISOString(),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// INTERNAL
|
|
120
|
+
// ============================================================================
|
|
121
|
+
sendRelayFrame(innerFrame) {
|
|
122
|
+
if (this._closed)
|
|
123
|
+
return;
|
|
124
|
+
const envelope = encodeRelayEnvelope(this.sessionUuidBuf, innerFrame);
|
|
125
|
+
this.sendBinary(envelope);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
import type { PortalStream } from "./stream.js";
|
|
8
|
+
import type { SynShell } from "./protocol.js";
|
|
9
|
+
import type { PortalSession } from "./session.js";
|
|
10
|
+
/**
|
|
11
|
+
* Open a remote shell from the initiator. Pipes local terminal to remote PTY.
|
|
12
|
+
*/
|
|
13
|
+
export declare function openShell(session: PortalSession, opts?: {
|
|
14
|
+
cols?: number;
|
|
15
|
+
rows?: number;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
command?: string;
|
|
18
|
+
}): Promise<{
|
|
19
|
+
stream: PortalStream;
|
|
20
|
+
cleanup: () => void;
|
|
21
|
+
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Handle an incoming shell stream on the target node.
|
|
24
|
+
* Spawns a PTY and bridges it to the stream.
|
|
25
|
+
*/
|
|
26
|
+
export declare function handleShell(stream: PortalStream, meta: SynShell): Promise<void>;
|