@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.
@@ -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
@@ -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.5.1",
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"