@instaclaw/dispatch 0.5.1 → 0.6.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/dist/connection.js +7 -0
- package/dist/executor.js +35 -0
- package/dist/supervisor.js +19 -0
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -16,10 +16,17 @@ export function connect(opts) {
|
|
|
16
16
|
console.log(` Connecting to ${opts.vmAddress}:${opts.port}...`);
|
|
17
17
|
ws = new WebSocket(url, {
|
|
18
18
|
rejectUnauthorized: false, // Accept self-signed certs (TOFU model)
|
|
19
|
+
handshakeTimeout: 10000,
|
|
19
20
|
});
|
|
20
21
|
ws.on("open", () => {
|
|
21
22
|
reconnectDelay = 1000; // Reset backoff
|
|
22
23
|
console.log(` Connected to agent on ${opts.vmAddress}`);
|
|
24
|
+
// Enable TCP keepalive to prevent NAT/firewall/router from killing idle connections
|
|
25
|
+
// Without this, home routers typically drop idle TCP after 2-5 minutes
|
|
26
|
+
const socket = ws?._socket;
|
|
27
|
+
if (socket?.setKeepAlive) {
|
|
28
|
+
socket.setKeepAlive(true, 30000); // Send TCP keepalive every 30s
|
|
29
|
+
}
|
|
23
30
|
opts.onConnect?.();
|
|
24
31
|
});
|
|
25
32
|
ws.on("message", async (data, isBinary) => {
|
package/dist/executor.js
CHANGED
|
@@ -34,6 +34,8 @@ export async function executeCommand(command) {
|
|
|
34
34
|
return await listWindows();
|
|
35
35
|
case "batch":
|
|
36
36
|
return await executeBatch(params);
|
|
37
|
+
case "exec":
|
|
38
|
+
return await doExec(params);
|
|
37
39
|
default:
|
|
38
40
|
return { success: false, error: `Unknown command type: ${type}` };
|
|
39
41
|
}
|
|
@@ -210,4 +212,37 @@ async function listWindows() {
|
|
|
210
212
|
const windows = await usecomputer.windowList();
|
|
211
213
|
return { success: true, data: { windows } };
|
|
212
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Execute a shell command directly on the user's machine.
|
|
217
|
+
* Bypasses the GUI entirely — no Terminal window needed.
|
|
218
|
+
* Returns stdout, stderr, and exit code.
|
|
219
|
+
*/
|
|
220
|
+
async function doExec(params) {
|
|
221
|
+
const command = String(params.command || "");
|
|
222
|
+
if (!command) {
|
|
223
|
+
return { success: false, error: "exec requires a 'command' parameter" };
|
|
224
|
+
}
|
|
225
|
+
// Timeout: default 30s, max 120s
|
|
226
|
+
const timeoutMs = Math.min(Number(params.timeout) || 30000, 120000);
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
const { exec } = require("child_process");
|
|
229
|
+
exec(command, { timeout: timeoutMs, maxBuffer: 1024 * 1024, shell: "/bin/bash" }, (err, stdout, stderr) => {
|
|
230
|
+
const exitCode = err ? err.code ?? 1 : 0;
|
|
231
|
+
// Cap output to prevent context flooding (max 4KB each)
|
|
232
|
+
const cappedStdout = stdout.length > 4096 ? stdout.substring(0, 4096) + "\n[...truncated]" : stdout;
|
|
233
|
+
const cappedStderr = stderr.length > 4096 ? stderr.substring(0, 4096) + "\n[...truncated]" : stderr;
|
|
234
|
+
resolve({
|
|
235
|
+
success: exitCode === 0,
|
|
236
|
+
data: {
|
|
237
|
+
action: "exec",
|
|
238
|
+
command,
|
|
239
|
+
exitCode,
|
|
240
|
+
stdout: cappedStdout,
|
|
241
|
+
stderr: cappedStderr,
|
|
242
|
+
},
|
|
243
|
+
error: exitCode !== 0 ? `Command exited with code ${exitCode}` : undefined,
|
|
244
|
+
});
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
}
|
|
213
248
|
//# sourceMappingURL=executor.js.map
|
package/dist/supervisor.js
CHANGED
|
@@ -85,6 +85,23 @@ function isDangerous(cmd) {
|
|
|
85
85
|
const key = String(p.key || "").toLowerCase();
|
|
86
86
|
return DANGEROUS_KEY_COMBOS.has(key);
|
|
87
87
|
}
|
|
88
|
+
// Check exec commands for dangerous patterns
|
|
89
|
+
if (cmd.type === "exec") {
|
|
90
|
+
const command = String(p.command || "");
|
|
91
|
+
// Check against text patterns (reuses the same dangerous detection)
|
|
92
|
+
if (DANGEROUS_TEXT_PATTERNS.some((pat) => pat.test(command)))
|
|
93
|
+
return true;
|
|
94
|
+
// Additional exec-specific dangerous patterns
|
|
95
|
+
if (/\brm\s+-rf\b/.test(command))
|
|
96
|
+
return true;
|
|
97
|
+
if (/\bsudo\b/.test(command))
|
|
98
|
+
return true;
|
|
99
|
+
if (/\bchmod\s+777\b/.test(command))
|
|
100
|
+
return true;
|
|
101
|
+
if (/\bcurl\b.*\|\s*(bash|sh)\b/.test(command))
|
|
102
|
+
return true;
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
88
105
|
// Check all sub-actions in a batch
|
|
89
106
|
if (cmd.type === "batch") {
|
|
90
107
|
const actions = p.actions;
|
|
@@ -107,6 +124,8 @@ function formatCommand(cmd) {
|
|
|
107
124
|
return `Scroll ${p.direction} by ${p.amount || 3}`;
|
|
108
125
|
case "drag":
|
|
109
126
|
return `Drag from (${p.fromX}, ${p.fromY}) to (${p.toX}, ${p.toY})`;
|
|
127
|
+
case "exec":
|
|
128
|
+
return `Run command: ${String(p.command || "").substring(0, 100)}${String(p.command || "").length > 100 ? "..." : ""}`;
|
|
110
129
|
case "batch": {
|
|
111
130
|
const actions = p.actions;
|
|
112
131
|
if (!Array.isArray(actions))
|
package/dist/types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Command sent from VM agent → dispatch server → local relay */
|
|
2
2
|
export interface DispatchCommand {
|
|
3
3
|
id: string;
|
|
4
|
-
type: "screenshot" | "click" | "type" | "press" | "scroll" | "drag" | "windows" | "status" | "batch";
|
|
4
|
+
type: "screenshot" | "click" | "type" | "press" | "scroll" | "drag" | "windows" | "status" | "batch" | "exec";
|
|
5
5
|
params: Record<string, unknown>;
|
|
6
6
|
description?: string;
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instaclaw/dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Let your InstaClaw AI agent control your computer — screenshots, clicks, typing, and more via a secure WebSocket relay",
|
|
5
5
|
"bin": {
|
|
6
6
|
"instaclaw-dispatch": "./dist/index.js"
|