@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.
- package/README.md +320 -230
- package/agent/desktop-manager.mjs +183 -0
- package/agent/file-manager.mjs +369 -265
- package/agent/local-agent.mjs +228 -157
- package/agent/terminal-manager.mjs +507 -229
- package/agent/windows-desktop.ps1 +288 -0
- package/cli/config.mjs +114 -113
- package/cli/device-login.mjs +170 -168
- package/cli/index.mjs +201 -145
- package/cli/remote.mjs +94 -88
- package/package.json +75 -65
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
const MAX_SCREENSHOT_BINARY_BYTES = 32 * 1024;
|
|
6
|
+
const POWERSHELL_TIMEOUT_MS = 15_000;
|
|
7
|
+
const MAX_TEXT_LENGTH = 8192;
|
|
8
|
+
const helperPath = path.join(path.dirname(fileURLToPath(import.meta.url)), "windows-desktop.ps1");
|
|
9
|
+
|
|
10
|
+
const KEY_CODES = new Map([
|
|
11
|
+
["enter", 0x0D],
|
|
12
|
+
["tab", 0x09],
|
|
13
|
+
["escape", 0x1B],
|
|
14
|
+
["backspace", 0x08],
|
|
15
|
+
["delete", 0x2E],
|
|
16
|
+
["arrowleft", 0x25],
|
|
17
|
+
["arrowup", 0x26],
|
|
18
|
+
["arrowright", 0x27],
|
|
19
|
+
["arrowdown", 0x28],
|
|
20
|
+
["home", 0x24],
|
|
21
|
+
["end", 0x23],
|
|
22
|
+
["pageup", 0x21],
|
|
23
|
+
["pagedown", 0x22],
|
|
24
|
+
["space", 0x20],
|
|
25
|
+
...Array.from({ length: 12 }, (_, index) => ["f" + (index + 1), 0x70 + index]),
|
|
26
|
+
...Array.from({ length: 26 }, (_, index) => [String.fromCharCode(97 + index), 0x41 + index]),
|
|
27
|
+
...Array.from({ length: 10 }, (_, index) => [String(index), 0x30 + index]),
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
function normalizedKey(value) {
|
|
31
|
+
return String(value || "").trim().toLowerCase().replace(/[\s_-]+/g, "");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function encodeArgs(args) {
|
|
35
|
+
return Buffer.from(JSON.stringify(args || {}), "utf8").toString("base64");
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function defaultRunner(operation, args) {
|
|
39
|
+
return new Promise((resolve) => {
|
|
40
|
+
execFile(
|
|
41
|
+
"powershell.exe",
|
|
42
|
+
[
|
|
43
|
+
"-NoLogo",
|
|
44
|
+
"-NoProfile",
|
|
45
|
+
"-NonInteractive",
|
|
46
|
+
"-ExecutionPolicy",
|
|
47
|
+
"Bypass",
|
|
48
|
+
"-File",
|
|
49
|
+
helperPath,
|
|
50
|
+
"-Operation",
|
|
51
|
+
operation,
|
|
52
|
+
"-InputBase64",
|
|
53
|
+
encodeArgs(args),
|
|
54
|
+
],
|
|
55
|
+
{
|
|
56
|
+
windowsHide: true,
|
|
57
|
+
timeout: POWERSHELL_TIMEOUT_MS,
|
|
58
|
+
maxBuffer: 512 * 1024,
|
|
59
|
+
},
|
|
60
|
+
(error, stdout) => {
|
|
61
|
+
const line = String(stdout || "").trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
62
|
+
if (!line) {
|
|
63
|
+
resolve({ ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" });
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
resolve(JSON.parse(line));
|
|
68
|
+
} catch {
|
|
69
|
+
resolve({ ok: false, error: operation === "screenshot" ? "capture_failed" : "input_failed" });
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class DesktopManager {
|
|
77
|
+
constructor(options = {}) {
|
|
78
|
+
this.enabled = options.enabled ?? process.env.DESKTOP_ENABLED === "1";
|
|
79
|
+
this.platform = options.platform ?? process.platform;
|
|
80
|
+
this.runner = options.runner ?? defaultRunner;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
getConfig() {
|
|
84
|
+
return {
|
|
85
|
+
enabled: this.enabled,
|
|
86
|
+
supported: this.platform === "win32",
|
|
87
|
+
platform: this.platform,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
gate() {
|
|
92
|
+
if (!this.enabled) return { ok: false, error: "desktop_disabled" };
|
|
93
|
+
if (this.platform !== "win32") return { ok: false, error: "unsupported_platform" };
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async screenshot(input = {}) {
|
|
98
|
+
const gate = this.gate();
|
|
99
|
+
if (gate) return gate;
|
|
100
|
+
|
|
101
|
+
const monitor = input.monitor ?? "primary";
|
|
102
|
+
if (!(monitor === "primary" || monitor === "secondary" || (Number.isInteger(monitor) && monitor >= 0 && monitor <= 15))) {
|
|
103
|
+
return { ok: false, error: "invalid_monitor" };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const result = await this.runner("screenshot", { monitor });
|
|
107
|
+
if (!result?.ok) return result || { ok: false, error: "capture_failed" };
|
|
108
|
+
if (result.mimeType !== "image/jpeg" || typeof result.data !== "string") {
|
|
109
|
+
return { ok: false, error: "capture_failed" };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const binaryBytes = Buffer.from(result.data, "base64").byteLength;
|
|
113
|
+
if (binaryBytes <= 0 || binaryBytes > MAX_SCREENSHOT_BINARY_BYTES) {
|
|
114
|
+
return { ok: false, error: "screenshot_too_large" };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ok: true,
|
|
119
|
+
mimeType: result.mimeType,
|
|
120
|
+
data: result.data,
|
|
121
|
+
width: Number(result.width),
|
|
122
|
+
height: Number(result.height),
|
|
123
|
+
desktopOriginX: Number(result.desktopOriginX),
|
|
124
|
+
desktopOriginY: Number(result.desktopOriginY),
|
|
125
|
+
desktopWidth: Number(result.desktopWidth),
|
|
126
|
+
desktopHeight: Number(result.desktopHeight),
|
|
127
|
+
scaleX: Number(result.scaleX),
|
|
128
|
+
scaleY: Number(result.scaleY),
|
|
129
|
+
monitorIndex: Number(result.monitorIndex),
|
|
130
|
+
isPrimary: result.isPrimary === true,
|
|
131
|
+
deviceName: typeof result.deviceName === "string" ? result.deviceName : undefined,
|
|
132
|
+
byteLength: binaryBytes,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async mouseClick(input = {}) {
|
|
137
|
+
const gate = this.gate();
|
|
138
|
+
if (gate) return gate;
|
|
139
|
+
|
|
140
|
+
const x = Number(input.x);
|
|
141
|
+
const y = Number(input.y);
|
|
142
|
+
const button = String(input.button || "left").toLowerCase();
|
|
143
|
+
const clicks = Number(input.clicks ?? 1);
|
|
144
|
+
|
|
145
|
+
if (!Number.isInteger(x) || !Number.isInteger(y)) return { ok: false, error: "invalid_coordinates" };
|
|
146
|
+
if (!["left", "right", "middle"].includes(button)) return { ok: false, error: "invalid_button" };
|
|
147
|
+
if (clicks !== 1 && clicks !== 2) return { ok: false, error: "invalid_click_count" };
|
|
148
|
+
|
|
149
|
+
return this.runner("mouse_click", { x, y, button, clicks });
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async keyboardInput(input = {}) {
|
|
153
|
+
const gate = this.gate();
|
|
154
|
+
if (gate) return gate;
|
|
155
|
+
|
|
156
|
+
const hasText = typeof input.text === "string" && input.text.length > 0;
|
|
157
|
+
const hasKey = typeof input.key === "string" && input.key.trim().length > 0;
|
|
158
|
+
|
|
159
|
+
if (hasText === hasKey) return { ok: false, error: "invalid_keyboard_input" };
|
|
160
|
+
|
|
161
|
+
if (hasText) {
|
|
162
|
+
if (input.text.length > MAX_TEXT_LENGTH) return { ok: false, error: "text_too_large" };
|
|
163
|
+
if (input.ctrl || input.alt || input.shift || input.win) {
|
|
164
|
+
return { ok: false, error: "invalid_keyboard_input" };
|
|
165
|
+
}
|
|
166
|
+
return this.runner("keyboard_input", { text: input.text });
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const key = normalizedKey(input.key);
|
|
170
|
+
const keyCode = KEY_CODES.get(key);
|
|
171
|
+
if (!keyCode) return { ok: false, error: "unsupported_key" };
|
|
172
|
+
|
|
173
|
+
return this.runner("keyboard_input", {
|
|
174
|
+
keyCode,
|
|
175
|
+
ctrl: input.ctrl === true,
|
|
176
|
+
alt: input.alt === true,
|
|
177
|
+
shift: input.shift === true,
|
|
178
|
+
win: input.win === true,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { KEY_CODES, MAX_SCREENSHOT_BINARY_BYTES };
|