autodev-cli 1.4.0 → 1.4.3
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/autodev.js +0 -0
- package/out/agentBackup/archive.d.ts +44 -0
- package/out/agentBackup/archive.js +131 -0
- package/out/agentBackup/archive.js.map +1 -0
- package/out/agentBackup/export.d.ts +18 -0
- package/out/agentBackup/export.js +92 -0
- package/out/agentBackup/export.js.map +1 -0
- package/out/agentBackup/import.d.ts +21 -0
- package/out/agentBackup/import.js +40 -0
- package/out/agentBackup/import.js.map +1 -0
- package/out/agentBackup/index.d.ts +6 -0
- package/out/agentBackup/index.js +11 -0
- package/out/agentBackup/index.js.map +1 -0
- package/out/agentBackup/layout.d.ts +30 -0
- package/out/agentBackup/layout.js +126 -0
- package/out/agentBackup/layout.js.map +1 -0
- package/out/agentBackup/manifest.d.ts +24 -0
- package/out/agentBackup/manifest.js +70 -0
- package/out/agentBackup/manifest.js.map +1 -0
- package/out/agentBackup/opencodeDb.d.ts +20 -0
- package/out/agentBackup/opencodeDb.js +213 -0
- package/out/agentBackup/opencodeDb.js.map +1 -0
- package/out/agentBackup/sessionProviders.d.ts +35 -0
- package/out/agentBackup/sessionProviders.js +263 -0
- package/out/agentBackup/sessionProviders.js.map +1 -0
- package/out/agentBackup/upload.d.ts +9 -0
- package/out/agentBackup/upload.js +121 -0
- package/out/agentBackup/upload.js.map +1 -0
- package/out/cli.d.ts +1 -0
- package/out/cli.js +8 -0
- package/out/cli.js.map +1 -1
- package/out/cliExit.d.ts +34 -0
- package/out/cliExit.js +159 -0
- package/out/cliExit.js.map +1 -0
- package/out/commands/config.d.ts +2 -0
- package/out/commands/config.js +7 -7
- package/out/commands/config.js.map +1 -1
- package/out/commands/connect.d.ts +2 -0
- package/out/commands/connect.js +11 -0
- package/out/commands/connect.js.map +1 -1
- package/out/commands/export.d.ts +2 -0
- package/out/commands/export.js +79 -0
- package/out/commands/export.js.map +1 -0
- package/out/commands/import.d.ts +2 -0
- package/out/commands/import.js +92 -0
- package/out/commands/import.js.map +1 -0
- package/out/commands/init.d.ts +16 -0
- package/out/commands/init.js +9 -5
- package/out/commands/init.js.map +1 -1
- package/out/commands/resume.d.ts +2 -0
- package/out/commands/resume.js +65 -0
- package/out/commands/resume.js.map +1 -0
- package/out/commands/sessions.d.ts +2 -0
- package/out/commands/sessions.js +64 -0
- package/out/commands/sessions.js.map +1 -0
- package/out/commands/start.d.ts +2 -0
- package/out/commands/start.js +40 -7
- package/out/commands/start.js.map +1 -1
- package/out/commands/status.d.ts +2 -0
- package/out/commands/status.js +3 -3
- package/out/commands/status.js.map +1 -1
- package/out/commands/tailOutput.d.ts +12 -0
- package/out/commands/up.d.ts +3 -0
- package/out/configManager.d.ts +42 -0
- package/out/configManager.js +430 -0
- package/out/configManager.js.map +1 -0
- package/out/connect.d.ts +4 -0
- package/out/connect.js +7 -7
- package/out/connect.js.map +1 -1
- package/out/core/adapters.d.ts +34 -0
- package/out/core/adapters.js +84 -0
- package/out/core/adapters.js.map +1 -0
- package/out/core/commandHelpers.d.ts +12 -0
- package/out/core/commandHelpers.js +96 -0
- package/out/core/commandHelpers.js.map +1 -0
- package/out/core/projectMcp.d.ts +25 -0
- package/out/core/projectMcp.js +144 -0
- package/out/core/projectMcp.js.map +1 -0
- package/out/core/provider/BaseProvider.d.ts +14 -0
- package/out/core/provider/BaseProvider.js +25 -0
- package/out/core/provider/BaseProvider.js.map +1 -0
- package/out/core/provider/ProviderRegistry.d.ts +12 -0
- package/out/core/provider/ProviderRegistry.js +40 -0
- package/out/core/provider/ProviderRegistry.js.map +1 -0
- package/out/core/provider/contract.d.ts +62 -0
- package/out/core/provider/contract.js +9 -0
- package/out/core/provider/contract.js.map +1 -0
- package/out/core/provider/implementations.d.ts +54 -0
- package/out/core/provider/implementations.js +147 -0
- package/out/core/provider/implementations.js.map +1 -0
- package/out/core/settingsLoader.d.ts +221 -0
- package/out/core/settingsLoader.js +176 -0
- package/out/core/settingsLoader.js.map +1 -0
- package/out/discordGateway.d.ts +26 -0
- package/out/discordGateway.js +230 -0
- package/out/discordGateway.js.map +1 -0
- package/out/discordPoller.d.ts +28 -0
- package/out/discordPoller.js +247 -0
- package/out/discordPoller.js.map +1 -0
- package/out/dispatcher.d.ts +12 -0
- package/out/dispatcher.js +214 -0
- package/out/dispatcher.js.map +1 -0
- package/out/emailPoller.d.ts +42 -0
- package/out/emailPoller.js +221 -0
- package/out/emailPoller.js.map +1 -0
- package/out/git/gitService.d.ts +36 -0
- package/out/git/gitService.js +165 -0
- package/out/git/gitService.js.map +1 -0
- package/out/hookEventNormalizer.d.ts +39 -0
- package/out/hookEventNormalizer.js +397 -0
- package/out/hookEventNormalizer.js.map +1 -0
- package/out/hooksManager.d.ts +25 -0
- package/out/hooksManager.js +471 -0
- package/out/hooksManager.js.map +1 -0
- package/out/launchIde.d.ts +14 -0
- package/out/logger.d.ts +12 -0
- package/out/mcpEmailTest.d.ts +29 -0
- package/out/mcpEmailTest.js +245 -0
- package/out/mcpEmailTest.js.map +1 -0
- package/out/mcpInstallCheck.d.ts +23 -0
- package/out/mcpInstallCheck.js +219 -0
- package/out/mcpInstallCheck.js.map +1 -0
- package/out/mcpManager.d.ts +35 -0
- package/out/mcpManager.js +371 -0
- package/out/mcpManager.js.map +1 -0
- package/out/messageBuilder.d.ts +54 -0
- package/out/messageBuilder.js +373 -0
- package/out/messageBuilder.js.map +1 -0
- package/out/openCodeHooksManager.d.ts +23 -0
- package/out/openCodeHooksManager.js +511 -0
- package/out/openCodeHooksManager.js.map +1 -0
- package/out/periodicActions.d.ts +63 -0
- package/out/periodicActions.js +237 -0
- package/out/periodicActions.js.map +1 -0
- package/out/profileBuilder.d.ts +29 -0
- package/out/profileBuilder.js +366 -0
- package/out/profileBuilder.js.map +1 -0
- package/out/prompt.d.ts +12 -0
- package/out/prompt.js +18 -0
- package/out/prompt.js.map +1 -0
- package/out/protocolSections.d.ts +26 -0
- package/out/protocolSections.js +209 -0
- package/out/protocolSections.js.map +1 -0
- package/out/providers/claudeCliProvider.d.ts +71 -0
- package/out/providers/claudeCliProvider.js +425 -0
- package/out/providers/claudeCliProvider.js.map +1 -0
- package/out/providers/claudeTuiProvider.d.ts +23 -0
- package/out/providers/claudeTuiProvider.js +296 -0
- package/out/providers/claudeTuiProvider.js.map +1 -0
- package/out/providers/copilotCliProvider.d.ts +16 -0
- package/out/providers/copilotCliProvider.js +44 -0
- package/out/providers/copilotCliProvider.js.map +1 -0
- package/out/providers/copilotSdkProvider.d.ts +12 -0
- package/out/providers/copilotSdkProvider.js +445 -0
- package/out/providers/copilotSdkProvider.js.map +1 -0
- package/out/providers/grokTuiProvider.d.ts +14 -0
- package/out/providers/grokTuiProvider.js +271 -0
- package/out/providers/grokTuiProvider.js.map +1 -0
- package/out/providers/opencodeCliProvider.d.ts +29 -0
- package/out/providers/opencodeCliProvider.js +199 -0
- package/out/providers/opencodeCliProvider.js.map +1 -0
- package/out/providers/opencodeSdkProvider.d.ts +22 -0
- package/out/providers/opencodeSdkProvider.js +557 -0
- package/out/providers/opencodeSdkProvider.js.map +1 -0
- package/out/providers.d.ts +9 -0
- package/out/providers.js +44 -0
- package/out/providers.js.map +1 -0
- package/out/rateLimit.d.ts +18 -0
- package/out/rateLimit.js +90 -0
- package/out/rateLimit.js.map +1 -0
- package/out/rdp/auth.d.ts +55 -0
- package/out/rdp/auth.js +197 -0
- package/out/rdp/auth.js.map +1 -0
- package/out/rdp/bridge.d.ts +86 -0
- package/out/rdp/bridge.js +1398 -0
- package/out/rdp/bridge.js.map +1 -0
- package/out/rdp/constants.d.ts +86 -0
- package/out/rdp/constants.js +182 -0
- package/out/rdp/constants.js.map +1 -0
- package/out/rdp/index.d.ts +7 -0
- package/out/rdp/index.js +14 -0
- package/out/rdp/index.js.map +1 -0
- package/out/rdp/session.d.ts +30 -0
- package/out/rdp/session.js +196 -0
- package/out/rdp/session.js.map +1 -0
- package/out/rdp/types.d.ts +27 -0
- package/out/rdp/types.js +6 -0
- package/out/rdp/types.js.map +1 -0
- package/out/sdk/index.d.ts +22 -0
- package/out/sdk/index.js +81 -0
- package/out/sdk/index.js.map +1 -0
- package/out/sessionState.d.ts +54 -0
- package/out/sessionState.js +284 -0
- package/out/sessionState.js.map +1 -0
- package/out/sessions.d.ts +11 -0
- package/out/sessions.js +32 -0
- package/out/sessions.js.map +1 -0
- package/out/taskLoop.d.ts +152 -0
- package/out/taskLoop.js +2505 -0
- package/out/taskLoop.js.map +1 -0
- package/out/todo.d.ts +42 -0
- package/out/todo.js +311 -0
- package/out/todo.js.map +1 -0
- package/out/todoWriteManager.d.ts +26 -0
- package/out/todoWriteManager.js +44 -0
- package/out/todoWriteManager.js.map +1 -0
- package/out/vnc/auth.d.ts +52 -0
- package/out/vnc/auth.js +181 -0
- package/out/vnc/auth.js.map +1 -0
- package/out/vnc/bridge.d.ts +40 -0
- package/out/vnc/bridge.js +540 -0
- package/out/vnc/bridge.js.map +1 -0
- package/out/vnc/constants.d.ts +8 -0
- package/out/vnc/constants.js +34 -0
- package/out/vnc/constants.js.map +1 -0
- package/out/vnc/des.d.ts +6 -0
- package/out/vnc/des.js +93 -0
- package/out/vnc/des.js.map +1 -0
- package/out/vnc/index.d.ts +7 -0
- package/out/vnc/index.js +13 -0
- package/out/vnc/index.js.map +1 -0
- package/out/vnc/session.d.ts +18 -0
- package/out/vnc/session.js +193 -0
- package/out/vnc/session.js.map +1 -0
- package/out/vnc/types.d.ts +16 -0
- package/out/vnc/types.js +6 -0
- package/out/vnc/types.js.map +1 -0
- package/out/webSocketPoller.d.ts +95 -0
- package/out/webSocketPoller.js +986 -0
- package/out/webSocketPoller.js.map +1 -0
- package/out/webhook.d.ts +37 -0
- package/out/webhook.js +265 -0
- package/out/webhook.js.map +1 -0
- package/out/webhookPoller.d.ts +40 -0
- package/out/webhookPoller.js +378 -0
- package/out/webhookPoller.js.map +1 -0
- package/package.json +54 -41
|
@@ -0,0 +1,986 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.WebSocketPoller = void 0;
|
|
37
|
+
const net = __importStar(require("net"));
|
|
38
|
+
const tls = __importStar(require("tls"));
|
|
39
|
+
const crypto = __importStar(require("crypto"));
|
|
40
|
+
const fs = __importStar(require("fs"));
|
|
41
|
+
const path = __importStar(require("path"));
|
|
42
|
+
const vnc_1 = require("./vnc");
|
|
43
|
+
const rdp_1 = require("./rdp");
|
|
44
|
+
const messageBuilder_1 = require("./messageBuilder");
|
|
45
|
+
const todo_1 = require("./todo");
|
|
46
|
+
const todoWriteManager_1 = require("./todoWriteManager");
|
|
47
|
+
const gitService = __importStar(require("./git/gitService"));
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// WebSocketPoller — persistent WS connection for ws:// / wss:// endpoints
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
class WebSocketPoller {
|
|
52
|
+
wsUrl;
|
|
53
|
+
apiKey;
|
|
54
|
+
slug;
|
|
55
|
+
_socket = null;
|
|
56
|
+
_connected = false;
|
|
57
|
+
_buffer = Buffer.alloc(0);
|
|
58
|
+
_todoPath = '';
|
|
59
|
+
_workspaceRoot;
|
|
60
|
+
_destroyed = false;
|
|
61
|
+
_reconnectTimer = null;
|
|
62
|
+
_log = () => { };
|
|
63
|
+
static RECONNECT_DELAY_MS = 5_000;
|
|
64
|
+
_vncPassword;
|
|
65
|
+
_vncSessions = new Map();
|
|
66
|
+
_rdpSessions = new Map();
|
|
67
|
+
_rdpSettings = {};
|
|
68
|
+
_gitEnabled = false;
|
|
69
|
+
_onConnect = null;
|
|
70
|
+
_onTaskAppend = null;
|
|
71
|
+
_onCommand = null;
|
|
72
|
+
_onMcpUpdate = null;
|
|
73
|
+
_onExportRequest = null;
|
|
74
|
+
_onRestoreRequest = null;
|
|
75
|
+
_onExportConfig = null;
|
|
76
|
+
_pendingFrames = [];
|
|
77
|
+
_seenTaskIds = new Set();
|
|
78
|
+
// Heartbeat: send a WS Ping every 25 s and expect a Pong. If 2 pings in a
|
|
79
|
+
// row come back without a pong (≈55 s), force-reconnect. Without this the
|
|
80
|
+
// server's 10-min stale-connection sweep silently drops idle agents and the
|
|
81
|
+
// client doesn't notice until the next manual restart.
|
|
82
|
+
static PING_INTERVAL_MS = 25_000;
|
|
83
|
+
static PONG_GRACE_MS = 55_000;
|
|
84
|
+
_pingTimer = null;
|
|
85
|
+
_lastPongAt = 0;
|
|
86
|
+
constructor(wsUrl, apiKey, slug) {
|
|
87
|
+
this.wsUrl = wsUrl;
|
|
88
|
+
this.apiKey = apiKey;
|
|
89
|
+
this.slug = slug;
|
|
90
|
+
}
|
|
91
|
+
/** Called once when the WS connection is first established (and on each reconnect). */
|
|
92
|
+
setOnConnect(cb) { this._onConnect = cb; }
|
|
93
|
+
/** Called whenever a task is successfully appended to TODO.md via a WS push. */
|
|
94
|
+
setOnTaskAppend(cb) { this._onTaskAppend = cb; }
|
|
95
|
+
/** Called when a slash command (e.g. /restart) is received via WS push. */
|
|
96
|
+
setOnCommand(cb) { this._onCommand = cb; }
|
|
97
|
+
/** Called when a mcp_update frame arrives — receives the new mcpServers map. */
|
|
98
|
+
setOnMcpUpdate(cb) { this._onMcpUpdate = cb; }
|
|
99
|
+
/** Called when an export_request frame arrives — agent should create + upload a backup. */
|
|
100
|
+
setOnExportRequest(cb) { this._onExportRequest = cb; }
|
|
101
|
+
/** Called when a restore_request frame arrives — agent should download + restore a backup. */
|
|
102
|
+
setOnRestoreRequest(cb) { this._onRestoreRequest = cb; }
|
|
103
|
+
/** Called when an export_config frame arrives — sync exportEnabled/exportDailyBackup settings. */
|
|
104
|
+
setOnExportConfig(cb) { this._onExportConfig = cb; }
|
|
105
|
+
/** Start the WebSocket connection (call once). */
|
|
106
|
+
start(todoPath, log, workspaceRoot) {
|
|
107
|
+
this._todoPath = todoPath;
|
|
108
|
+
this._workspaceRoot = workspaceRoot;
|
|
109
|
+
if (log) {
|
|
110
|
+
this._log = log;
|
|
111
|
+
}
|
|
112
|
+
this._log(`WS connecting → ${this.wsUrl} (slug: ${this.slug})`);
|
|
113
|
+
this._connect();
|
|
114
|
+
}
|
|
115
|
+
/** Tear down the connection permanently. */
|
|
116
|
+
destroy() {
|
|
117
|
+
this._destroyed = true;
|
|
118
|
+
this._stopHeartbeat();
|
|
119
|
+
this._stopAllVncSessions();
|
|
120
|
+
this._stopAllRdpSessions();
|
|
121
|
+
if (this._reconnectTimer) {
|
|
122
|
+
clearTimeout(this._reconnectTimer);
|
|
123
|
+
this._reconnectTimer = null;
|
|
124
|
+
}
|
|
125
|
+
this._closeSocket();
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Called by the poller loop — always returns false because the WebSocket
|
|
129
|
+
* connection is event-driven; tasks are appended directly in _onFrame().
|
|
130
|
+
*/
|
|
131
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
132
|
+
pollAndAppend(_todoPath, _workspaceRoot) {
|
|
133
|
+
return Promise.resolve(false);
|
|
134
|
+
}
|
|
135
|
+
// ─── private ─────────────────────────────────────────────────────────────
|
|
136
|
+
_connect() {
|
|
137
|
+
if (this._destroyed) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const parsed = new URL(this.wsUrl);
|
|
141
|
+
const isSecure = parsed.protocol === 'wss:';
|
|
142
|
+
// On Windows, Node.js may resolve 'localhost' to ::1 (IPv6) but the WS server
|
|
143
|
+
// only binds to 0.0.0.0 (IPv4). Force 127.0.0.1 to avoid the mismatch.
|
|
144
|
+
const rawHost = parsed.hostname;
|
|
145
|
+
const host = (rawHost === 'localhost' || rawHost === '::1') ? '127.0.0.1' : rawHost;
|
|
146
|
+
const port = parsed.port ? parseInt(parsed.port, 10) : (isSecure ? 443 : 80);
|
|
147
|
+
// Build WebSocket upgrade path: preserve any existing path, append query params
|
|
148
|
+
const basePath = parsed.pathname || '/';
|
|
149
|
+
const qs = new URLSearchParams({ token: this.apiKey, endpoint: this.slug }).toString();
|
|
150
|
+
const upgradePath = `${basePath}?${qs}`;
|
|
151
|
+
const key = crypto.randomBytes(16).toString('base64');
|
|
152
|
+
const handshake = [
|
|
153
|
+
`GET ${upgradePath} HTTP/1.1`,
|
|
154
|
+
`Host: ${host}:${port}`,
|
|
155
|
+
'Upgrade: websocket',
|
|
156
|
+
'Connection: Upgrade',
|
|
157
|
+
`Sec-WebSocket-Key: ${key}`,
|
|
158
|
+
'Sec-WebSocket-Version: 13',
|
|
159
|
+
'',
|
|
160
|
+
'',
|
|
161
|
+
].join('\r\n');
|
|
162
|
+
const sock = isSecure
|
|
163
|
+
? tls.connect({ host, port, servername: host })
|
|
164
|
+
: net.createConnection({ host, port });
|
|
165
|
+
// For plain TCP, 'connect' is the ready signal.
|
|
166
|
+
// For TLS, 'secureConnect' fires after the TLS handshake; we skip the
|
|
167
|
+
// plain 'connect' event to avoid writing the HTTP upgrade too early.
|
|
168
|
+
if (isSecure) {
|
|
169
|
+
sock.once('secureConnect', () => {
|
|
170
|
+
sock.write(handshake);
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
sock.once('connect', () => {
|
|
175
|
+
sock.write(handshake);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
let headersDone = false;
|
|
179
|
+
let headerBuf = '';
|
|
180
|
+
sock.on('data', (chunk) => {
|
|
181
|
+
if (!headersDone) {
|
|
182
|
+
headerBuf += chunk.toString('binary');
|
|
183
|
+
const sep = headerBuf.indexOf('\r\n\r\n');
|
|
184
|
+
if (sep === -1) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (!headerBuf.includes('101 Switching Protocols')) {
|
|
188
|
+
const statusLine = headerBuf.split('\r\n')[0] ?? '(no response)';
|
|
189
|
+
this._log(`WS upgrade rejected by ${host}:${port}: "${statusLine}" — reconnecting in ${WebSocketPoller.RECONNECT_DELAY_MS}ms`);
|
|
190
|
+
sock.destroy();
|
|
191
|
+
this._scheduleReconnect();
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
headersDone = true;
|
|
195
|
+
this._connected = true;
|
|
196
|
+
this._log(`WS connected → ${host}:${port} (slug: ${this.slug})`);
|
|
197
|
+
// Flush any frames queued before the connection was established
|
|
198
|
+
const pending = this._pendingFrames.splice(0);
|
|
199
|
+
for (const frame of pending) {
|
|
200
|
+
this._sendTextFrame(JSON.stringify(frame));
|
|
201
|
+
}
|
|
202
|
+
// Notify listener so caller can resend agent_online on reconnect
|
|
203
|
+
if (this._onConnect) {
|
|
204
|
+
this._onConnect();
|
|
205
|
+
}
|
|
206
|
+
// Subscribe to the deliveries channel so the server pushes webhook events
|
|
207
|
+
this._sendTextFrame(JSON.stringify({ type: 'subscribe', data: { channels: ['deliveries'] } }));
|
|
208
|
+
// Start the WS-level keepalive (server drops idle conns after ~10 min)
|
|
209
|
+
this._startHeartbeat();
|
|
210
|
+
// Any bytes after the headers belong to the first WS frame
|
|
211
|
+
const remaining = Buffer.from(headerBuf.slice(sep + 4), 'binary');
|
|
212
|
+
if (remaining.length > 0) {
|
|
213
|
+
this._buffer = remaining;
|
|
214
|
+
this._processBuffer();
|
|
215
|
+
}
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this._buffer = Buffer.concat([this._buffer, chunk]);
|
|
219
|
+
this._processBuffer();
|
|
220
|
+
});
|
|
221
|
+
sock.on('error', (err) => {
|
|
222
|
+
this._log(`WS error (${host}:${port}): ${err.message} — reconnecting in ${WebSocketPoller.RECONNECT_DELAY_MS}ms`);
|
|
223
|
+
this._connected = false;
|
|
224
|
+
this._scheduleReconnect();
|
|
225
|
+
});
|
|
226
|
+
sock.on('close', () => {
|
|
227
|
+
if (this._connected) {
|
|
228
|
+
this._log(`WS disconnected from ${host}:${port} — reconnecting`);
|
|
229
|
+
}
|
|
230
|
+
this._connected = false;
|
|
231
|
+
this._scheduleReconnect();
|
|
232
|
+
});
|
|
233
|
+
this._socket = sock;
|
|
234
|
+
}
|
|
235
|
+
_closeSocket() {
|
|
236
|
+
if (this._socket) {
|
|
237
|
+
try {
|
|
238
|
+
// Send WebSocket close frame (opcode 0x8, masked, zero-length payload)
|
|
239
|
+
const mask = crypto.randomBytes(4);
|
|
240
|
+
this._socket.write(Buffer.from([0x88, 0x80, mask[0], mask[1], mask[2], mask[3]]));
|
|
241
|
+
}
|
|
242
|
+
catch { /* ignore */ }
|
|
243
|
+
this._socket.destroy();
|
|
244
|
+
this._socket = null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
_scheduleReconnect() {
|
|
248
|
+
this._stopHeartbeat();
|
|
249
|
+
this._stopAllVncSessions();
|
|
250
|
+
this._stopAllRdpSessions();
|
|
251
|
+
if (this._destroyed) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// If a reconnect is already scheduled, don't schedule another.
|
|
255
|
+
if (this._reconnectTimer) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
// Detach and destroy the old socket so its stale event listeners (close/error)
|
|
259
|
+
// can't trigger another _scheduleReconnect() call after we've already queued one.
|
|
260
|
+
const oldSocket = this._socket;
|
|
261
|
+
this._socket = null;
|
|
262
|
+
this._connected = false;
|
|
263
|
+
this._buffer = Buffer.alloc(0);
|
|
264
|
+
if (oldSocket) {
|
|
265
|
+
oldSocket.removeAllListeners('data');
|
|
266
|
+
oldSocket.removeAllListeners('close');
|
|
267
|
+
oldSocket.removeAllListeners('error');
|
|
268
|
+
oldSocket.destroy();
|
|
269
|
+
}
|
|
270
|
+
this._reconnectTimer = setTimeout(() => {
|
|
271
|
+
this._reconnectTimer = null;
|
|
272
|
+
this._connect();
|
|
273
|
+
}, WebSocketPoller.RECONNECT_DELAY_MS);
|
|
274
|
+
}
|
|
275
|
+
/** Parse and consume complete WebSocket frames from _buffer. */
|
|
276
|
+
_processBuffer() {
|
|
277
|
+
while (true) {
|
|
278
|
+
const frame = this._parseFrame();
|
|
279
|
+
if (!frame) {
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
this._onFrame(frame.opcode, frame.payload);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
_parseFrame() {
|
|
286
|
+
if (this._buffer.length < 2) {
|
|
287
|
+
return null;
|
|
288
|
+
}
|
|
289
|
+
const byte1 = this._buffer[0];
|
|
290
|
+
const byte2 = this._buffer[1];
|
|
291
|
+
const opcode = byte1 & 0x0f;
|
|
292
|
+
const isMasked = (byte2 & 0x80) !== 0;
|
|
293
|
+
let payloadLen = byte2 & 0x7f;
|
|
294
|
+
let offset = 2;
|
|
295
|
+
if (payloadLen === 126) {
|
|
296
|
+
if (this._buffer.length < offset + 2) {
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
payloadLen = this._buffer.readUInt16BE(offset);
|
|
300
|
+
offset += 2;
|
|
301
|
+
}
|
|
302
|
+
else if (payloadLen === 127) {
|
|
303
|
+
if (this._buffer.length < offset + 8) {
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
// Use only the lower 32 bits (messages won't be >4 GB)
|
|
307
|
+
payloadLen = this._buffer.readUInt32BE(offset + 4);
|
|
308
|
+
offset += 8;
|
|
309
|
+
}
|
|
310
|
+
const maskBytes = isMasked ? 4 : 0;
|
|
311
|
+
if (this._buffer.length < offset + maskBytes + payloadLen) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
const mask = isMasked ? this._buffer.slice(offset, offset + 4) : null;
|
|
315
|
+
offset += maskBytes;
|
|
316
|
+
let payload = this._buffer.slice(offset, offset + payloadLen);
|
|
317
|
+
if (mask) {
|
|
318
|
+
payload = Buffer.from(payload);
|
|
319
|
+
for (let i = 0; i < payload.length; i++) {
|
|
320
|
+
payload[i] ^= mask[i % 4];
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
// Consume frame from buffer
|
|
324
|
+
this._buffer = this._buffer.slice(offset + payloadLen);
|
|
325
|
+
return { opcode, payload };
|
|
326
|
+
}
|
|
327
|
+
_onFrame(opcode, payload) {
|
|
328
|
+
if (opcode === 0x9) {
|
|
329
|
+
// Ping from server — reply with pong + treat as proof of life
|
|
330
|
+
this._sendPong(payload);
|
|
331
|
+
this._lastPongAt = Date.now();
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
if (opcode === 0xa) {
|
|
335
|
+
// Pong from server (reply to one of our pings) — heartbeat alive
|
|
336
|
+
this._lastPongAt = Date.now();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (opcode === 0x8) {
|
|
340
|
+
// Close — reconnect
|
|
341
|
+
this._connected = false;
|
|
342
|
+
this._scheduleReconnect();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (opcode !== 0x1) {
|
|
346
|
+
return;
|
|
347
|
+
} // only handle text frames
|
|
348
|
+
let msg;
|
|
349
|
+
try {
|
|
350
|
+
msg = JSON.parse(payload.toString('utf8'));
|
|
351
|
+
}
|
|
352
|
+
catch {
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const msgType = msg['type'];
|
|
356
|
+
// ── VNC frames from pixel-office ─────────────────────────────────────────
|
|
357
|
+
// ── MCP update from pixel-office ─────────────────────────────────────────
|
|
358
|
+
if (msgType === 'mcp_update') {
|
|
359
|
+
const entries = msg['mcpServers'];
|
|
360
|
+
if (entries && typeof entries === 'object') {
|
|
361
|
+
this._onMcpUpdate?.(entries);
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// ── Export / restore requests from pixel-office ───────────────────────────
|
|
366
|
+
if (msgType === 'export_request') {
|
|
367
|
+
const agentId = msg['agentId'];
|
|
368
|
+
if (agentId) {
|
|
369
|
+
this._onExportRequest?.(agentId);
|
|
370
|
+
}
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
if (msgType === 'restore_request') {
|
|
374
|
+
const agentId = msg['agentId'];
|
|
375
|
+
const downloadUrl = msg['downloadUrl'];
|
|
376
|
+
if (agentId && downloadUrl) {
|
|
377
|
+
this._onRestoreRequest?.(agentId, downloadUrl);
|
|
378
|
+
}
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (msgType === 'export_config') {
|
|
382
|
+
const exportEnabled = !!msg['exportEnabled'];
|
|
383
|
+
const exportDailyBackup = !!msg['exportDailyBackup'];
|
|
384
|
+
const agentId = msg['agentId'] ?? '';
|
|
385
|
+
this._onExportConfig?.(exportEnabled, exportDailyBackup, agentId);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
// ── File browser requests from server ─────────────────────────────────────
|
|
389
|
+
if (msgType === 'fb_request') {
|
|
390
|
+
const requestId = msg['requestId'];
|
|
391
|
+
const action = msg['action'];
|
|
392
|
+
const relPath = msg['path'] ?? '';
|
|
393
|
+
const content = msg['content'];
|
|
394
|
+
const newPath = msg['newPath'];
|
|
395
|
+
const query = msg['query'];
|
|
396
|
+
if (requestId && action) {
|
|
397
|
+
this._handleFbRequest(requestId, action, relPath, content, newPath, query);
|
|
398
|
+
}
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (msgType === 'git_request') {
|
|
402
|
+
const requestId = msg['requestId'];
|
|
403
|
+
const action = msg['action'];
|
|
404
|
+
if (requestId && action) {
|
|
405
|
+
this._handleGitRequest(requestId, action, msg['path'], msg['staged'], msg['message'], msg['branch'], msg['hash']);
|
|
406
|
+
}
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
if (msgType === 'vnc_session') {
|
|
410
|
+
const action = msg['action'];
|
|
411
|
+
if (action === 'start') {
|
|
412
|
+
const sessionId = msg['sessionId'];
|
|
413
|
+
const port = Number(msg['port'] ?? 5900);
|
|
414
|
+
// Prefer password from server frame; fall back to locally-configured password
|
|
415
|
+
const password = msg['password'] || this._vncPassword;
|
|
416
|
+
this._log(`VNC session start: ${sessionId} → port ${port}`);
|
|
417
|
+
const session = new vnc_1.VncSession(sessionId, (frame) => this.sendFrame(frame));
|
|
418
|
+
this._vncSessions.set(sessionId, session);
|
|
419
|
+
session.start(port, password).catch((err) => {
|
|
420
|
+
this._log(`VNC session ${sessionId} failed to start: ${err.message}`);
|
|
421
|
+
this._vncSessions.delete(sessionId);
|
|
422
|
+
this.sendFrame({ type: 'vnc_close', sessionId, reason: err.message });
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (msgType === 'vnc_input') {
|
|
428
|
+
const sessionId = msg['sessionId'];
|
|
429
|
+
const event = msg['event'];
|
|
430
|
+
if (sessionId && event) {
|
|
431
|
+
this._vncSessions.get(sessionId)?.handleInput(event);
|
|
432
|
+
}
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (msgType === 'vnc_close') {
|
|
436
|
+
const sessionId = msg['sessionId'];
|
|
437
|
+
if (sessionId) {
|
|
438
|
+
this._log(`VNC session closed: ${sessionId}`);
|
|
439
|
+
this._vncSessions.get(sessionId)?.stop();
|
|
440
|
+
this._vncSessions.delete(sessionId);
|
|
441
|
+
}
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
// ── RDP frames from pixel-office ─────────────────────────────────────────
|
|
445
|
+
if (msgType === 'rdp_session') {
|
|
446
|
+
const action = msg['action'];
|
|
447
|
+
if (action === 'start') {
|
|
448
|
+
const sessionId = msg['sessionId'];
|
|
449
|
+
const opts = {
|
|
450
|
+
// Local settings take priority: the server-sent host is pixel-office's own IP,
|
|
451
|
+
// not the target RDP server. Use local rdpHost setting, or default to 127.0.0.1
|
|
452
|
+
// (xrdp always runs on the same machine as the extension).
|
|
453
|
+
host: this._rdpSettings.host || msg['host'] || '127.0.0.1',
|
|
454
|
+
port: msg['port'] ? Number(msg['port']) : (this._rdpSettings.port ?? 3389),
|
|
455
|
+
// credentials never sent from server — always use local settings
|
|
456
|
+
username: this._rdpSettings.username || msg['username'],
|
|
457
|
+
password: this._rdpSettings.password || msg['password'],
|
|
458
|
+
domain: this._rdpSettings.domain || msg['domain'],
|
|
459
|
+
width: msg['width'] ? Number(msg['width']) : undefined,
|
|
460
|
+
height: msg['height'] ? Number(msg['height']) : undefined,
|
|
461
|
+
colorDepth: msg['colorDepth'] ? Number(msg['colorDepth']) : undefined,
|
|
462
|
+
};
|
|
463
|
+
this._log(`RDP session start: ${sessionId} → ${opts.host}:${opts.port ?? 3389}`);
|
|
464
|
+
// Send Guacamole token to browser so it can connect via guacamole-lite
|
|
465
|
+
// (guacd + guacamole-lite must be running on the same host as xrdp, port 4567)
|
|
466
|
+
if (opts.username || opts.password) {
|
|
467
|
+
const guacSettings = {
|
|
468
|
+
hostname: opts.host,
|
|
469
|
+
port: String(opts.port ?? 3389),
|
|
470
|
+
'ignore-cert': true,
|
|
471
|
+
};
|
|
472
|
+
if (opts.username) {
|
|
473
|
+
guacSettings['username'] = opts.username;
|
|
474
|
+
}
|
|
475
|
+
if (opts.password) {
|
|
476
|
+
guacSettings['password'] = opts.password;
|
|
477
|
+
}
|
|
478
|
+
if (opts.domain) {
|
|
479
|
+
guacSettings['domain'] = opts.domain;
|
|
480
|
+
}
|
|
481
|
+
if (opts.width) {
|
|
482
|
+
guacSettings['width'] = opts.width;
|
|
483
|
+
}
|
|
484
|
+
if (opts.height) {
|
|
485
|
+
guacSettings['height'] = opts.height;
|
|
486
|
+
}
|
|
487
|
+
guacSettings['color-depth'] = opts.colorDepth ?? 24;
|
|
488
|
+
const tokenPayload = JSON.stringify({ connection: { type: 'rdp', settings: guacSettings } });
|
|
489
|
+
const token = Buffer.from(tokenPayload).toString('base64');
|
|
490
|
+
// Use configured WSS URL (for HTTPS frontends), else fall back to plain WS on port 4567
|
|
491
|
+
const guacWsUrl = this._rdpSettings.guacWsUrl || `ws://${opts.host}:4567`;
|
|
492
|
+
this.sendFrame({
|
|
493
|
+
type: 'rdp_guac_token',
|
|
494
|
+
sessionId,
|
|
495
|
+
wsUrl: guacWsUrl,
|
|
496
|
+
token,
|
|
497
|
+
width: opts.width ?? 1280,
|
|
498
|
+
height: opts.height ?? 800,
|
|
499
|
+
});
|
|
500
|
+
this._log(`RDP guac token sent for session ${sessionId} → ${guacWsUrl}`);
|
|
501
|
+
}
|
|
502
|
+
const session = new rdp_1.RdpSession(sessionId, (frame) => this.sendFrame(frame), (msg) => this._log(msg));
|
|
503
|
+
this._rdpSessions.set(sessionId, session);
|
|
504
|
+
session.start(opts).catch((err) => {
|
|
505
|
+
this._log(`RDP session ${sessionId} failed to start: ${err.message}`);
|
|
506
|
+
this._rdpSessions.delete(sessionId);
|
|
507
|
+
this.sendFrame({ type: 'rdp_close', sessionId, reason: err.message });
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
if (msgType === 'rdp_input') {
|
|
513
|
+
const sessionId = msg['sessionId'];
|
|
514
|
+
const event = msg['event'];
|
|
515
|
+
if (sessionId && event) {
|
|
516
|
+
this._rdpSessions.get(sessionId)?.handleInput(event);
|
|
517
|
+
}
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
if (msgType === 'rdp_close') {
|
|
521
|
+
const sessionId = msg['sessionId'];
|
|
522
|
+
if (sessionId) {
|
|
523
|
+
this._log(`RDP session closed: ${sessionId}`);
|
|
524
|
+
this._rdpSessions.get(sessionId)?.stop();
|
|
525
|
+
this._rdpSessions.delete(sessionId);
|
|
526
|
+
}
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
// ── A2A task frame ────────────────────────────────────────────────────────
|
|
530
|
+
// A2A task frame: { task: { id, contextId, status: { state }, metadata: { event, task, parts } } }
|
|
531
|
+
if (msg['task']) {
|
|
532
|
+
const t = msg['task'];
|
|
533
|
+
const state = t['status']?.['state'];
|
|
534
|
+
if (state !== 'TASK_STATE_SUBMITTED') {
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
// Deduplicate by task ID so the same delivery isn't re-processed on reconnect,
|
|
538
|
+
// but a new task with identical text is still accepted.
|
|
539
|
+
const taskId = t['id'];
|
|
540
|
+
if (taskId) {
|
|
541
|
+
if (this._seenTaskIds.has(taskId)) {
|
|
542
|
+
this._log(`WS task already processed (id=${taskId}), skipping`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
this._seenTaskIds.add(taskId);
|
|
546
|
+
// Bound the set to avoid unbounded growth over long-running sessions
|
|
547
|
+
if (this._seenTaskIds.size > 1000) {
|
|
548
|
+
const oldest = this._seenTaskIds.values().next().value;
|
|
549
|
+
if (oldest) {
|
|
550
|
+
this._seenTaskIds.delete(oldest);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
const meta = t['metadata'] ?? {};
|
|
555
|
+
if (meta['event'] !== 'user_message') {
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
const taskObj = meta['task'];
|
|
559
|
+
let taskText = typeof taskObj?.['text'] === 'string' ? taskObj['text'] : '';
|
|
560
|
+
// Handle A2A parts — extract text parts and save file parts as attachments
|
|
561
|
+
// Pre-generate task ID so all attachments share the same prefix
|
|
562
|
+
const wsTaskId = (0, todo_1.shortId)();
|
|
563
|
+
const rawParts = meta['parts'];
|
|
564
|
+
const textParts = [];
|
|
565
|
+
const attRefs = [];
|
|
566
|
+
if (rawParts) {
|
|
567
|
+
for (const part of rawParts) {
|
|
568
|
+
if (part['kind'] === 'text') {
|
|
569
|
+
const t = part['text'] ?? '';
|
|
570
|
+
if (t) {
|
|
571
|
+
textParts.push(t);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
else if (part['kind'] === 'file' && this._workspaceRoot) {
|
|
575
|
+
const file = part['file'];
|
|
576
|
+
if (file) {
|
|
577
|
+
const name = file['name'] ?? 'attachment';
|
|
578
|
+
const bytesB64 = file['bytes'];
|
|
579
|
+
if (bytesB64) {
|
|
580
|
+
const buf = Buffer.from(bytesB64, 'base64');
|
|
581
|
+
const rel = (0, messageBuilder_1.saveAttachment)(this._workspaceRoot, name, buf, wsTaskId);
|
|
582
|
+
attRefs.push(rel);
|
|
583
|
+
}
|
|
584
|
+
else if (typeof file['uri'] === 'string') {
|
|
585
|
+
attRefs.push(file['uri']);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Use parts text only as fallback when task.text is absent
|
|
592
|
+
if (!taskText && textParts.length > 0) {
|
|
593
|
+
taskText = textParts.join(' ');
|
|
594
|
+
}
|
|
595
|
+
if (!taskText) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
// Collapse newlines so the entire message becomes a single TODO.md line
|
|
599
|
+
taskText = taskText.replace(/\r\n|\r|\n/g, ' ').trim();
|
|
600
|
+
const fullText = attRefs.length > 0
|
|
601
|
+
? taskText + ' ' + attRefs.map(p => `[attachment: ${p}]`).join(' ')
|
|
602
|
+
: taskText;
|
|
603
|
+
this._log(`WS task received: "${taskText}"${attRefs.length > 0 ? ` (+${attRefs.length} attachment(s))` : ''}`);
|
|
604
|
+
// Handle slash commands — don't append as tasks
|
|
605
|
+
if (fullText.startsWith('/')) {
|
|
606
|
+
this._onCommand?.(fullText);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
if (!this._todoPath) {
|
|
610
|
+
this._log('WS failed to append task to TODO.md: todoPath is empty');
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
todoWriteManager_1.todoWriter.append(this._todoPath, fullText, wsTaskId)
|
|
614
|
+
.then(() => { this._onTaskAppend?.(); })
|
|
615
|
+
.catch(err => { this._log(`WS failed to append task to TODO.md: ${err}`); });
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
/** Handle a file-browser request from the server (originated by the browser UI). */
|
|
619
|
+
_handleFbRequest(requestId, action, relPath, content, newPath, query) {
|
|
620
|
+
const respond = (ok, extra) => {
|
|
621
|
+
this.sendFrame({ type: 'fb_response', requestId, ok, ...extra });
|
|
622
|
+
};
|
|
623
|
+
const root = this._workspaceRoot;
|
|
624
|
+
if (!root) {
|
|
625
|
+
respond(false, { error: 'No workspace root configured' });
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
// Resolve and validate path is within workspace root
|
|
629
|
+
const resolveSafe = (rel) => {
|
|
630
|
+
const resolved = path.resolve(root, rel);
|
|
631
|
+
return resolved.startsWith(root + path.sep) || resolved === root ? resolved : null;
|
|
632
|
+
};
|
|
633
|
+
const absPath = resolveSafe(relPath);
|
|
634
|
+
if (!absPath) {
|
|
635
|
+
respond(false, { error: 'Path outside workspace' });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
switch (action) {
|
|
640
|
+
case 'list': {
|
|
641
|
+
const entries = fs.readdirSync(absPath, { withFileTypes: true }).map(e => {
|
|
642
|
+
const stat = (() => { try {
|
|
643
|
+
return fs.statSync(path.join(absPath, e.name));
|
|
644
|
+
}
|
|
645
|
+
catch {
|
|
646
|
+
return null;
|
|
647
|
+
} })();
|
|
648
|
+
return {
|
|
649
|
+
name: e.name,
|
|
650
|
+
type: e.isDirectory() ? 'dir' : 'file',
|
|
651
|
+
size: stat?.size ?? 0,
|
|
652
|
+
mtime: stat?.mtimeMs ?? 0,
|
|
653
|
+
};
|
|
654
|
+
});
|
|
655
|
+
// Dirs first, then files; both alphabetical
|
|
656
|
+
entries.sort((a, b) => {
|
|
657
|
+
if (a.type !== b.type) {
|
|
658
|
+
return a.type === 'dir' ? -1 : 1;
|
|
659
|
+
}
|
|
660
|
+
return a.name.localeCompare(b.name);
|
|
661
|
+
});
|
|
662
|
+
respond(true, { entries });
|
|
663
|
+
break;
|
|
664
|
+
}
|
|
665
|
+
case 'read': {
|
|
666
|
+
const stat = fs.statSync(absPath);
|
|
667
|
+
const MAX_BYTES = 1_048_576; // 1 MB
|
|
668
|
+
if (stat.size > MAX_BYTES) {
|
|
669
|
+
respond(false, { error: `File too large (${stat.size} bytes, limit 1 MB)` });
|
|
670
|
+
break;
|
|
671
|
+
}
|
|
672
|
+
// Binary detection: read first 512 bytes and check for null bytes
|
|
673
|
+
const sample = Buffer.allocUnsafe(Math.min(512, stat.size));
|
|
674
|
+
const fd = fs.openSync(absPath, 'r');
|
|
675
|
+
fs.readSync(fd, sample, 0, sample.length, 0);
|
|
676
|
+
fs.closeSync(fd);
|
|
677
|
+
const isBinary = sample.includes(0x00);
|
|
678
|
+
if (isBinary) {
|
|
679
|
+
respond(false, { error: 'Binary file — cannot display' });
|
|
680
|
+
break;
|
|
681
|
+
}
|
|
682
|
+
const fileContent = fs.readFileSync(absPath, 'utf8');
|
|
683
|
+
respond(true, { content: fileContent });
|
|
684
|
+
break;
|
|
685
|
+
}
|
|
686
|
+
case 'write': {
|
|
687
|
+
if (content === undefined) {
|
|
688
|
+
respond(false, { error: 'No content provided' });
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
fs.writeFileSync(absPath, content, 'utf8');
|
|
692
|
+
respond(true);
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
case 'delete': {
|
|
696
|
+
fs.rmSync(absPath, { recursive: true, force: true });
|
|
697
|
+
respond(true);
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
case 'rename': {
|
|
701
|
+
if (!newPath) {
|
|
702
|
+
respond(false, { error: 'No newPath provided' });
|
|
703
|
+
break;
|
|
704
|
+
}
|
|
705
|
+
const absNewPath = resolveSafe(newPath);
|
|
706
|
+
if (!absNewPath) {
|
|
707
|
+
respond(false, { error: 'newPath outside workspace' });
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
fs.renameSync(absPath, absNewPath);
|
|
711
|
+
respond(true);
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case 'download': {
|
|
715
|
+
const buf = fs.readFileSync(absPath);
|
|
716
|
+
respond(true, { base64: buf.toString('base64') });
|
|
717
|
+
break;
|
|
718
|
+
}
|
|
719
|
+
case 'mkdir': {
|
|
720
|
+
fs.mkdirSync(absPath, { recursive: true });
|
|
721
|
+
respond(true);
|
|
722
|
+
break;
|
|
723
|
+
}
|
|
724
|
+
case 'search': {
|
|
725
|
+
const rawQuery = (query ?? '').toLowerCase().trim();
|
|
726
|
+
if (!rawQuery) {
|
|
727
|
+
respond(true, { results: [] });
|
|
728
|
+
break;
|
|
729
|
+
}
|
|
730
|
+
const results = [];
|
|
731
|
+
const walk = (dir, relDir, depth) => {
|
|
732
|
+
if (depth > 8 || results.length >= 300)
|
|
733
|
+
return;
|
|
734
|
+
let dirents;
|
|
735
|
+
try {
|
|
736
|
+
dirents = fs.readdirSync(dir, { withFileTypes: true });
|
|
737
|
+
}
|
|
738
|
+
catch {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
for (const e of dirents) {
|
|
742
|
+
if (results.length >= 300)
|
|
743
|
+
break;
|
|
744
|
+
const rel = relDir ? `${relDir}/${e.name}` : e.name;
|
|
745
|
+
if (e.name.toLowerCase().includes(rawQuery)) {
|
|
746
|
+
results.push({ path: rel, name: e.name, type: e.isDirectory() ? 'dir' : 'file' });
|
|
747
|
+
}
|
|
748
|
+
if (e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules' && e.name !== 'vendor') {
|
|
749
|
+
walk(path.join(dir, e.name), rel, depth + 1);
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
};
|
|
753
|
+
walk(absPath, '', 0);
|
|
754
|
+
respond(true, { results });
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
default:
|
|
758
|
+
respond(false, { error: `Unknown action: ${action}` });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
catch (err) {
|
|
762
|
+
respond(false, { error: String(err) });
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
_handleGitRequest(requestId, action, filePath, staged, message, branch, hash) {
|
|
766
|
+
const respond = (ok, data, error) => {
|
|
767
|
+
this.sendFrame({ type: 'git_response', requestId, ok, ...(data ?? {}), ...(error ? { error } : {}) });
|
|
768
|
+
};
|
|
769
|
+
const root = this._workspaceRoot;
|
|
770
|
+
if (!root) {
|
|
771
|
+
respond(false, undefined, 'No workspace root');
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
if (!this._gitEnabled) {
|
|
775
|
+
respond(false, undefined, 'Git not enabled');
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
(async () => {
|
|
779
|
+
try {
|
|
780
|
+
switch (action) {
|
|
781
|
+
case 'status': {
|
|
782
|
+
const status = await gitService.getStatus(root);
|
|
783
|
+
respond(true, { status });
|
|
784
|
+
break;
|
|
785
|
+
}
|
|
786
|
+
case 'log': {
|
|
787
|
+
const commits = await gitService.getLog(root);
|
|
788
|
+
respond(true, { commits });
|
|
789
|
+
break;
|
|
790
|
+
}
|
|
791
|
+
case 'diff': {
|
|
792
|
+
const diff = await gitService.getDiff(root, filePath ?? '', staged ?? false);
|
|
793
|
+
respond(true, { diff });
|
|
794
|
+
break;
|
|
795
|
+
}
|
|
796
|
+
case 'commit_diff': {
|
|
797
|
+
const diff = await gitService.getCommitDiff(root, hash ?? '', filePath);
|
|
798
|
+
respond(true, { diff });
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
case 'stage': {
|
|
802
|
+
if (filePath)
|
|
803
|
+
await gitService.stageFile(root, filePath);
|
|
804
|
+
else
|
|
805
|
+
await gitService.stageAll(root);
|
|
806
|
+
respond(true);
|
|
807
|
+
break;
|
|
808
|
+
}
|
|
809
|
+
case 'unstage': {
|
|
810
|
+
if (!filePath) {
|
|
811
|
+
respond(false, undefined, 'path required');
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
await gitService.unstageFile(root, filePath);
|
|
815
|
+
respond(true);
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
case 'commit': {
|
|
819
|
+
if (!message) {
|
|
820
|
+
respond(false, undefined, 'message required');
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
const commitHash = await gitService.commit(root, message);
|
|
824
|
+
respond(true, { hash: commitHash });
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
case 'fetch': {
|
|
828
|
+
await gitService.fetchOrigin(root);
|
|
829
|
+
respond(true);
|
|
830
|
+
break;
|
|
831
|
+
}
|
|
832
|
+
case 'branches': {
|
|
833
|
+
const branches = await gitService.getBranches(root);
|
|
834
|
+
respond(true, { branches });
|
|
835
|
+
break;
|
|
836
|
+
}
|
|
837
|
+
case 'checkout': {
|
|
838
|
+
if (!branch) {
|
|
839
|
+
respond(false, undefined, 'branch required');
|
|
840
|
+
break;
|
|
841
|
+
}
|
|
842
|
+
await gitService.checkoutBranch(root, branch);
|
|
843
|
+
respond(true);
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
default:
|
|
847
|
+
respond(false, undefined, `Unknown git action: ${action}`);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
catch (err) {
|
|
851
|
+
respond(false, undefined, String(err));
|
|
852
|
+
}
|
|
853
|
+
})();
|
|
854
|
+
}
|
|
855
|
+
/** Stop all active VNC sessions (called on destroy/reconnect). */
|
|
856
|
+
_stopAllVncSessions() {
|
|
857
|
+
for (const [id, session] of this._vncSessions) {
|
|
858
|
+
this._log(`VNC session terminated (disconnect): ${id}`);
|
|
859
|
+
session.stop();
|
|
860
|
+
}
|
|
861
|
+
this._vncSessions.clear();
|
|
862
|
+
}
|
|
863
|
+
/** Stop all active RDP sessions (called on destroy/reconnect). */
|
|
864
|
+
_stopAllRdpSessions() {
|
|
865
|
+
for (const [id, session] of this._rdpSessions) {
|
|
866
|
+
this._log(`RDP session terminated (disconnect): ${id}`);
|
|
867
|
+
session.stop();
|
|
868
|
+
}
|
|
869
|
+
this._rdpSessions.clear();
|
|
870
|
+
}
|
|
871
|
+
/** Update the VNC password used for incoming vnc_session requests. */
|
|
872
|
+
setVncPassword(password) {
|
|
873
|
+
this._vncPassword = password;
|
|
874
|
+
}
|
|
875
|
+
setGitEnabled(enabled) {
|
|
876
|
+
this._gitEnabled = enabled;
|
|
877
|
+
}
|
|
878
|
+
setRdpSettings(s) {
|
|
879
|
+
this._rdpSettings = s;
|
|
880
|
+
}
|
|
881
|
+
/**
|
|
882
|
+
* Send a JSON payload to the server over the WebSocket connection.
|
|
883
|
+
* Queues the frame if not yet connected — always returns true (accepted).
|
|
884
|
+
*/
|
|
885
|
+
static MAX_PENDING_FRAMES = 200;
|
|
886
|
+
sendFrame(payload) {
|
|
887
|
+
if (!this._connected || !this._socket) {
|
|
888
|
+
// Cap the queue to prevent unbounded growth during a long reconnect loop.
|
|
889
|
+
if (this._pendingFrames.length >= WebSocketPoller.MAX_PENDING_FRAMES) {
|
|
890
|
+
this._pendingFrames.shift(); // drop oldest frame
|
|
891
|
+
}
|
|
892
|
+
this._pendingFrames.push(payload);
|
|
893
|
+
return true; // accepted into queue
|
|
894
|
+
}
|
|
895
|
+
this._sendTextFrame(JSON.stringify(payload));
|
|
896
|
+
return true;
|
|
897
|
+
}
|
|
898
|
+
/** Send a masked WebSocket text frame. */
|
|
899
|
+
_sendTextFrame(text) {
|
|
900
|
+
if (!this._socket) {
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
const data = Buffer.from(text, 'utf8');
|
|
904
|
+
const len = data.length;
|
|
905
|
+
const mask = crypto.randomBytes(4);
|
|
906
|
+
let header;
|
|
907
|
+
if (len <= 125) {
|
|
908
|
+
header = Buffer.alloc(6);
|
|
909
|
+
header[0] = 0x81;
|
|
910
|
+
header[1] = len | 0x80;
|
|
911
|
+
mask.copy(header, 2);
|
|
912
|
+
}
|
|
913
|
+
else if (len <= 65535) {
|
|
914
|
+
header = Buffer.alloc(8);
|
|
915
|
+
header[0] = 0x81;
|
|
916
|
+
header[1] = 126 | 0x80;
|
|
917
|
+
header.writeUInt16BE(len, 2);
|
|
918
|
+
mask.copy(header, 4);
|
|
919
|
+
}
|
|
920
|
+
else {
|
|
921
|
+
header = Buffer.alloc(14);
|
|
922
|
+
header[0] = 0x81;
|
|
923
|
+
header[1] = 127 | 0x80;
|
|
924
|
+
header.writeBigUInt64BE(BigInt(len), 2);
|
|
925
|
+
mask.copy(header, 10);
|
|
926
|
+
}
|
|
927
|
+
const masked = Buffer.from(data);
|
|
928
|
+
for (let i = 0; i < masked.length; i++) {
|
|
929
|
+
masked[i] ^= mask[i % 4];
|
|
930
|
+
}
|
|
931
|
+
this._socket.write(Buffer.concat([header, masked]));
|
|
932
|
+
}
|
|
933
|
+
_sendPong(payload) {
|
|
934
|
+
if (!this._socket) {
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
const mask = crypto.randomBytes(4);
|
|
938
|
+
const len = payload.length;
|
|
939
|
+
const header = Buffer.alloc(2 + 4);
|
|
940
|
+
header[0] = 0x8a; // FIN + pong opcode
|
|
941
|
+
header[1] = (len & 0x7f) | 0x80; // masked, length (assumes len <= 125)
|
|
942
|
+
mask.copy(header, 2);
|
|
943
|
+
const maskedPayload = Buffer.from(payload);
|
|
944
|
+
for (let i = 0; i < maskedPayload.length; i++) {
|
|
945
|
+
maskedPayload[i] ^= mask[i % 4];
|
|
946
|
+
}
|
|
947
|
+
this._socket.write(Buffer.concat([header, maskedPayload]));
|
|
948
|
+
}
|
|
949
|
+
/** Send a zero-payload Ping frame. Server's WS server replies with Pong. */
|
|
950
|
+
_sendPing() {
|
|
951
|
+
if (!this._socket || !this._connected) {
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
try {
|
|
955
|
+
// FIN + opcode 0x9 (ping), masked, zero-length payload
|
|
956
|
+
const mask = crypto.randomBytes(4);
|
|
957
|
+
this._socket.write(Buffer.from([0x89, 0x80, mask[0], mask[1], mask[2], mask[3]]));
|
|
958
|
+
}
|
|
959
|
+
catch { /* socket may be dead — close handler will reconnect */ }
|
|
960
|
+
}
|
|
961
|
+
/** Start the heartbeat. Cleared automatically on close/destroy/reconnect. */
|
|
962
|
+
_startHeartbeat() {
|
|
963
|
+
this._stopHeartbeat();
|
|
964
|
+
this._lastPongAt = Date.now();
|
|
965
|
+
this._pingTimer = setInterval(() => {
|
|
966
|
+
// If the server hasn't responded in PONG_GRACE_MS, the link is dead
|
|
967
|
+
// even though TCP still thinks we're connected (proxy/NAT timeout).
|
|
968
|
+
const silence = Date.now() - this._lastPongAt;
|
|
969
|
+
if (silence > WebSocketPoller.PONG_GRACE_MS) {
|
|
970
|
+
this._log(`WS heartbeat timeout (no pong for ${Math.round(silence / 1000)}s) — forcing reconnect`);
|
|
971
|
+
this._connected = false;
|
|
972
|
+
this._scheduleReconnect();
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
this._sendPing();
|
|
976
|
+
}, WebSocketPoller.PING_INTERVAL_MS);
|
|
977
|
+
}
|
|
978
|
+
_stopHeartbeat() {
|
|
979
|
+
if (this._pingTimer) {
|
|
980
|
+
clearInterval(this._pingTimer);
|
|
981
|
+
this._pingTimer = null;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
exports.WebSocketPoller = WebSocketPoller;
|
|
986
|
+
//# sourceMappingURL=webSocketPoller.js.map
|