@anusornneal/chat-relay 0.6.0 → 0.7.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.
@@ -1,157 +1,228 @@
1
- import WebSocket from "ws";
2
- import { FileManager } from "./file-manager.mjs";
3
- import { ProcessManager } from "./process-manager.mjs";
4
- import { TerminalManager } from "./terminal-manager.mjs";
5
-
6
- const relayUrl = process.env.RELAY_URL;
7
- const agentToken = process.env.AGENT_TOKEN;
8
- const agentId = process.env.AGENT_ID || "default";
9
- const agentName = process.env.AGENT_NAME || agentId;
10
- const reconnectMs = Number(process.env.RECONNECT_MS ?? 2000);
11
- const terminalEnabled = process.env.TERMINAL_ENABLED === "1";
12
- const terminals = new TerminalManager();
13
- const files = new FileManager(process.env.ALLOWED_ROOTS);
14
- const processes = new ProcessManager();
15
- const recentCalls = [];
16
-
17
- if (!relayUrl || !agentToken) {
18
- console.error("RELAY_URL and AGENT_TOKEN are required");
19
- process.exit(1);
20
- }
21
-
22
- const wsUrl = relayUrl.replace(/^http/, "ws").replace(/\/$/, "") +
23
- `/agent?agentId=${encodeURIComponent(agentId)}`;
24
-
25
- async function handlePayload(payload) {
26
- if (!payload || typeof payload !== "object") return payload;
27
-
28
- switch (payload.action) {
29
- case "ping":
30
- return { ok: true, action: "pong", at: new Date().toISOString(), agentId, agentName };
31
-
32
- case "agent.config":
33
- return processes.info({
34
- agentId,
35
- agentName,
36
- terminalEnabled,
37
- allowedRoots: files.getRoots(),
38
- reconnectMs,
39
- });
40
- case "agent.recentCalls":
41
- return recentCalls.slice(-(Math.min(Math.max(Number(payload.limit) || 50, 1), 100)));
42
-
43
- case "fs.stat":
44
- return files.stat(payload.path);
45
- case "fs.list":
46
- return files.list(payload.path, payload.depth);
47
- case "fs.read":
48
- return files.read(payload.path, payload.offset, payload.length);
49
- case "fs.readMany":
50
- return files.readMany(payload.paths);
51
- case "fs.write":
52
- return files.write(payload.path, payload.content, payload.mode);
53
- case "fs.edit":
54
- return files.edit(payload.path, payload.oldText, payload.newText, payload.expectedReplacements);
55
- case "fs.mkdir":
56
- return files.mkdir(payload.path);
57
- case "fs.move":
58
- return files.move(payload.source, payload.destination);
59
- case "fs.delete":
60
- return files.remove(payload.path, payload.recursive);
61
- case "fs.search.start":
62
- return files.startSearch(payload.path, payload.pattern, payload.searchType, payload.maxResults);
63
- case "fs.search.results":
64
- return files.getSearchResults(payload.sessionId, payload.offset, payload.length);
65
-
66
- case "process.list":
67
- return processes.list(payload.filter);
68
- case "process.kill":
69
- return processes.kill(payload.pid);
70
-
71
- case "terminal.exec":
72
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
73
- return terminals.exec(payload.command, payload.cwd, payload.timeoutMs);
74
- case "terminal.start":
75
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
76
- return terminals.start(payload.command, payload.cwd);
77
- case "terminal.shell.start":
78
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
79
- return terminals.startShell(payload.cwd);
80
- case "terminal.read":
81
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
82
- return terminals.read(payload.sessionId, payload.afterSeq, payload.maxChars);
83
- case "terminal.write":
84
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
85
- return terminals.write(payload.sessionId, payload.input, payload.appendNewline !== false);
86
- case "terminal.kill":
87
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
88
- return terminals.kill(payload.sessionId);
89
- case "terminal.list":
90
- if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
91
- return terminals.list();
92
-
93
- default:
94
- return { ok: false, error: "unknown_action" };
95
- }
96
- }
97
-
98
- function connect() {
99
- console.log(`Connecting to ${wsUrl}`);
100
- console.log(`Terminal access: ${terminalEnabled ? "enabled" : "disabled"}`);
101
- const socket = new WebSocket(wsUrl, {
102
- headers: { Authorization: `Bearer ${agentToken}` },
103
- });
104
-
105
- socket.on("open", () => console.log("Agent connected"));
106
-
107
- socket.on("message", async (raw) => {
108
- let message;
109
- try {
110
- message = JSON.parse(raw.toString());
111
- } catch {
112
- return;
113
- }
114
-
115
- if (!message || typeof message.requestId !== "string") return;
116
-
117
- const startedAt = Date.now();
118
- const action = message.payload && typeof message.payload === "object"
119
- ? String(message.payload.action ?? "unknown")
120
- : "unknown";
121
- try {
122
- const result = await handlePayload(message.payload);
123
- recentCalls.push({
124
- at: new Date().toISOString(),
125
- action,
126
- durationMs: Date.now() - startedAt,
127
- ok: !(result && typeof result === "object" && result.ok === false),
128
- });
129
- if (recentCalls.length > 100) recentCalls.shift();
130
- socket.send(JSON.stringify({ requestId: message.requestId, payload: result }));
131
- } catch (error) {
132
- recentCalls.push({
133
- at: new Date().toISOString(),
134
- action,
135
- durationMs: Date.now() - startedAt,
136
- ok: false,
137
- error: error instanceof Error ? error.message : "agent_error",
138
- });
139
- if (recentCalls.length > 100) recentCalls.shift();
140
- socket.send(JSON.stringify({
141
- requestId: message.requestId,
142
- payload: { ok: false, error: error instanceof Error ? error.message : "agent_error" },
143
- }));
144
- }
145
- });
146
-
147
- socket.on("close", (code, reason) => {
148
- console.log(`Agent disconnected (${code}) ${reason.toString()}`);
149
- setTimeout(connect, reconnectMs);
150
- });
151
-
152
- socket.on("error", (error) => {
153
- console.error("WebSocket error:", error.message);
154
- });
155
- }
156
-
157
- connect();
1
+ import WebSocket from "ws";
2
+ import { DesktopManager } from "./desktop-manager.mjs";
3
+ import { FileManager } from "./file-manager.mjs";
4
+ import { ProcessManager } from "./process-manager.mjs";
5
+ import { TerminalManager } from "./terminal-manager.mjs";
6
+
7
+ const relayUrl = process.env.RELAY_URL;
8
+ const agentToken = process.env.AGENT_TOKEN;
9
+ const agentId = process.env.AGENT_ID || "default";
10
+ const agentName = process.env.AGENT_NAME || agentId;
11
+ const reconnectMs = Number(process.env.RECONNECT_MS ?? 2000);
12
+ const terminalEnabled = process.env.TERMINAL_ENABLED === "1";
13
+ const desktopEnabled = process.env.DESKTOP_ENABLED === "1";
14
+ const MAX_RESPONSE_BYTES = 60 * 1024;
15
+ const terminals = new TerminalManager({
16
+ batchConcurrency: process.env.TERMINAL_BATCH_CONCURRENCY,
17
+ maxQueuedJobs: process.env.TERMINAL_BATCH_MAX_QUEUED,
18
+ });
19
+ const files = new FileManager(process.env.ALLOWED_ROOTS);
20
+ const processes = new ProcessManager();
21
+ const desktop = new DesktopManager({ enabled: desktopEnabled });
22
+ const recentCalls = [];
23
+ let reauthorizationRequired = false;
24
+
25
+ if (!relayUrl || !agentToken) {
26
+ console.error("RELAY_URL and AGENT_TOKEN are required");
27
+ process.exit(1);
28
+ }
29
+
30
+ const wsUrl = relayUrl.replace(/^http/, "ws").replace(/\/$/, "") +
31
+ `/agent?agentId=${encodeURIComponent(agentId)}`;
32
+
33
+ function serializeResponse(requestId, payload) {
34
+ const response = JSON.stringify({ requestId, payload });
35
+ if (Buffer.byteLength(response, "utf8") <= MAX_RESPONSE_BYTES) return response;
36
+ return JSON.stringify({
37
+ requestId,
38
+ payload: {
39
+ ok: false,
40
+ error: "response_too_large",
41
+ maxBytes: MAX_RESPONSE_BYTES,
42
+ },
43
+ });
44
+ }
45
+
46
+ async function handlePayload(payload) {
47
+ if (!payload || typeof payload !== "object") return payload;
48
+
49
+ switch (payload.action) {
50
+ case "ping":
51
+ return { ok: true, action: "pong", at: new Date().toISOString(), agentId, agentName };
52
+
53
+ case "agent.config":
54
+ return processes.info({
55
+ agentId,
56
+ agentName,
57
+ terminalEnabled,
58
+ terminalBatch: terminals.getBatchConfig(),
59
+ desktop: desktop.getConfig(),
60
+ allowedRoots: files.getRoots(),
61
+ reconnectMs,
62
+ });
63
+ case "agent.recentCalls":
64
+ return recentCalls.slice(-(Math.min(Math.max(Number(payload.limit) || 50, 1), 100)));
65
+
66
+ case "fs.stat":
67
+ return files.stat(payload.path);
68
+ case "fs.list":
69
+ return files.list(payload.path, payload.depth);
70
+ case "fs.read":
71
+ return files.read(payload.path, payload.offset, payload.length, payload.maxBytes);
72
+ case "fs.readMany":
73
+ return files.readMany(payload.paths, payload.maxTotalBytes);
74
+ case "fs.write":
75
+ return files.write(payload.path, payload.content, payload.mode);
76
+ case "fs.edit":
77
+ return files.edit(payload.path, payload.oldText, payload.newText, payload.expectedReplacements);
78
+ case "fs.mkdir":
79
+ return files.mkdir(payload.path);
80
+ case "fs.move":
81
+ return files.move(payload.source, payload.destination);
82
+ case "fs.delete":
83
+ return files.remove(payload.path, payload.recursive);
84
+ case "fs.search.start":
85
+ return files.startSearch(payload.path, payload.pattern, payload.searchType, payload.maxResults);
86
+ case "fs.search.results":
87
+ return files.getSearchResults(payload.sessionId, payload.offset, payload.length);
88
+
89
+ case "process.list":
90
+ return processes.list(payload.filter);
91
+ case "process.kill":
92
+ return processes.kill(payload.pid);
93
+
94
+ case "desktop.screenshot":
95
+ return desktop.screenshot(payload);
96
+ case "desktop.mouse.click":
97
+ return desktop.mouseClick(payload);
98
+ case "desktop.keyboard.input":
99
+ return desktop.keyboardInput(payload);
100
+
101
+ case "terminal.exec":
102
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
103
+ return terminals.exec(payload.command, payload.cwd, payload.timeoutMs);
104
+ case "terminal.batch.start":
105
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
106
+ return terminals.batchStart(payload.jobs, { cwd: payload.cwd, timeoutMs: payload.timeoutMs, concurrency: payload.concurrency });
107
+ case "terminal.batch.status":
108
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
109
+ return terminals.batchStatus(payload.batchId);
110
+ case "terminal.batch.read":
111
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
112
+ return terminals.batchRead(payload.batchId, payload.maxChars);
113
+ case "terminal.batch.cancel":
114
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
115
+ return terminals.batchCancel(payload.batchId);
116
+ case "terminal.start":
117
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
118
+ return terminals.start(payload.command, payload.cwd);
119
+ case "terminal.shell.start":
120
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
121
+ return terminals.startShell(payload.cwd);
122
+ case "terminal.read":
123
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
124
+ return terminals.read(payload.sessionId, payload.afterSeq, payload.maxChars);
125
+ case "terminal.write":
126
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
127
+ return terminals.write(payload.sessionId, payload.input, payload.appendNewline !== false);
128
+ case "terminal.kill":
129
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
130
+ return terminals.kill(payload.sessionId);
131
+ case "terminal.list":
132
+ if (!terminalEnabled) return { ok: false, error: "terminal_disabled" };
133
+ return terminals.list();
134
+
135
+ default:
136
+ return { ok: false, error: "unknown_action" };
137
+ }
138
+ }
139
+
140
+ function requireReauthorization() {
141
+ if (reauthorizationRequired) return;
142
+ reauthorizationRequired = true;
143
+ process.exitCode = 2;
144
+ console.error("Agent credential was revoked or rejected.");
145
+ console.error('Recovery: run "chat-relay login --force", then "chat-relay remote".');
146
+ }
147
+
148
+ function connect() {
149
+ if (reauthorizationRequired) return;
150
+ console.log(`Connecting to ${wsUrl}`);
151
+ console.log(`Terminal access: ${terminalEnabled ? "enabled" : "disabled"}`);
152
+ console.log("Desktop access: " + (desktopEnabled ? "enabled" : "disabled"));
153
+ const socket = new WebSocket(wsUrl, {
154
+ headers: { Authorization: `Bearer ${agentToken}` },
155
+ });
156
+
157
+ socket.on("open", () => console.log("Agent connected"));
158
+
159
+ socket.on("message", async (raw) => {
160
+ let message;
161
+ try {
162
+ message = JSON.parse(raw.toString());
163
+ } catch {
164
+ return;
165
+ }
166
+
167
+ if (message?.control === "credential_revoked") {
168
+ requireReauthorization();
169
+ try { socket.close(4001, "credential_revoked"); } catch {}
170
+ return;
171
+ }
172
+
173
+ if (!message || typeof message.requestId !== "string") return;
174
+
175
+ const startedAt = Date.now();
176
+ const action = message.payload && typeof message.payload === "object"
177
+ ? String(message.payload.action ?? "unknown")
178
+ : "unknown";
179
+ try {
180
+ const result = await handlePayload(message.payload);
181
+ recentCalls.push({
182
+ at: new Date().toISOString(),
183
+ action,
184
+ durationMs: Date.now() - startedAt,
185
+ ok: !(result && typeof result === "object" && result.ok === false),
186
+ });
187
+ if (recentCalls.length > 100) recentCalls.shift();
188
+ socket.send(serializeResponse(message.requestId, result));
189
+ } catch (error) {
190
+ recentCalls.push({
191
+ at: new Date().toISOString(),
192
+ action,
193
+ durationMs: Date.now() - startedAt,
194
+ ok: false,
195
+ error: error instanceof Error ? error.message : "agent_error",
196
+ });
197
+ if (recentCalls.length > 100) recentCalls.shift();
198
+ socket.send(serializeResponse(message.requestId, {
199
+ ok: false,
200
+ error: error instanceof Error ? error.message : "agent_error",
201
+ }));
202
+ }
203
+ });
204
+
205
+ socket.on("unexpected-response", (_request, response) => {
206
+ if (response.statusCode === 401 || response.statusCode === 403) {
207
+ response.resume();
208
+ requireReauthorization();
209
+ socket.terminate();
210
+ }
211
+ });
212
+
213
+ socket.on("close", (code, reason) => {
214
+ const reasonText = reason.toString();
215
+ console.log("Agent disconnected (" + code + ") " + reasonText);
216
+ if (code === 4001 || reasonText === "credential_revoked") {
217
+ requireReauthorization();
218
+ return;
219
+ }
220
+ if (!reauthorizationRequired) setTimeout(connect, reconnectMs);
221
+ });
222
+
223
+ socket.on("error", (error) => {
224
+ if (!reauthorizationRequired) console.error("WebSocket error:", error.message);
225
+ });
226
+ }
227
+
228
+ connect();