@instaclaw/dispatch 0.5.0 → 0.6.0

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/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/index.js CHANGED
@@ -8,27 +8,105 @@
8
8
  * npx @instaclaw/dispatch --token XXX --vm HOST # Direct connect (advanced)
9
9
  */
10
10
  import { loadConfig, saveConfig, getConfigPath } from "./config.js";
11
- import { checkPermissions, printPermissionGuide, runPermissionTest } from "./permissions.js";
11
+ import { checkPermissions, runPermissionTest } from "./permissions.js";
12
12
  import { connect, disconnect } from "./connection.js";
13
13
  import { closeSupervisor, setMode } from "./supervisor.js";
14
14
  import chalk from "chalk";
15
15
  import readline from "readline";
16
16
  import os from "os";
17
+ import fs from "fs";
18
+ import path from "path";
17
19
  const API_BASE = "https://instaclaw.io";
20
+ // Temp file to persist pairing code + args across Terminal restarts
21
+ const RETRY_FILE = path.join(os.tmpdir(), "instaclaw-dispatch-retry.json");
22
+ const RETRY_MAX_AGE_MS = 10 * 60 * 1000; // 10 minutes
18
23
  async function main() {
19
24
  console.log(`
20
25
  ${chalk.bold("InstaClaw Dispatch")} ${chalk.dim("— Remote Computer Control")}
21
26
  ${chalk.dim("─────────────────────────────────────────────")}
22
27
  `);
23
- // 1. Check permissions (macOS only)
28
+ const args = parseArgs();
29
+ // ── Auto-retry: check if we're resuming after a Terminal restart ──
30
+ const retry = loadRetryState();
31
+ if (!args.pair && !args.token && retry) {
32
+ console.log(` ${chalk.cyan("↻")} Resuming after Terminal restart...`);
33
+ console.log(` ${chalk.dim("Pairing code:")} ${chalk.bold(retry.pair)}\n`);
34
+ args.pair = retry.pair;
35
+ if (retry.autonomous)
36
+ args.autonomous = true;
37
+ clearRetryState();
38
+ }
39
+ // ── 1. Check permissions FIRST (before consuming pairing code) ──
24
40
  if (os.platform() === "darwin") {
25
41
  console.log(" Checking permissions...");
26
42
  const perms = checkPermissions();
27
- printPermissionGuide(perms);
28
- if (perms.screenRecording === false || perms.accessibility === false) {
29
- await waitForEnter();
43
+ if (perms.accessibility === false || perms.screenRecording === false) {
44
+ const terminal = getTerminalName();
45
+ // Save state so we can auto-resume after Terminal restart
46
+ if (args.pair) {
47
+ saveRetryState({ pair: args.pair, autonomous: args.autonomous });
48
+ }
49
+ console.log("");
50
+ if (perms.accessibility === false) {
51
+ console.log(` ${chalk.red("✗")} Accessibility: ${chalk.bold("NOT GRANTED")}`);
52
+ console.log(` → System Settings → Privacy & Security → Accessibility`);
53
+ console.log(` → Enable "${terminal}"`);
54
+ console.log("");
55
+ console.log(chalk.yellow(` ⚠ Enabling Accessibility will close ${terminal}.`));
56
+ console.log(chalk.yellow(` Don't worry — just reopen ${terminal} and run the same command.`));
57
+ if (args.pair) {
58
+ console.log(chalk.green(` Your pairing code ${chalk.bold(args.pair)} will still work.`));
59
+ }
60
+ console.log("");
61
+ }
62
+ if (perms.screenRecording === false) {
63
+ console.log(` ${chalk.red("✗")} Screen Recording: ${chalk.bold("NOT GRANTED")}`);
64
+ console.log(` → System Settings → Privacy & Security → Screen Recording`);
65
+ console.log(` → Enable "${terminal}"`);
66
+ console.log("");
67
+ }
68
+ if (perms.accessibility === true && perms.screenRecording === false) {
69
+ // Only Screen Recording is missing — Terminal won't be killed
70
+ console.log(` Grant Screen Recording, then press ${chalk.bold("Enter")} to continue...`);
71
+ await waitForEnter();
72
+ // Re-check
73
+ const recheck = checkPermissions();
74
+ if (recheck.screenRecording === false) {
75
+ console.log(` ${chalk.yellow("⚠")} Screen Recording still not granted. Screenshots may not work.\n`);
76
+ }
77
+ else {
78
+ console.log(` ${chalk.green("✓")} Screen Recording: OK\n`);
79
+ }
80
+ }
81
+ else if (perms.accessibility === false) {
82
+ // Accessibility is missing — Terminal WILL be killed
83
+ console.log(` After granting permissions and reopening ${terminal}, run:`);
84
+ console.log("");
85
+ if (args.pair) {
86
+ console.log(` ${chalk.cyan(`npx @instaclaw/dispatch@0.5.0 --pair ${args.pair}`)}`);
87
+ }
88
+ else {
89
+ console.log(` ${chalk.cyan("npx @instaclaw/dispatch")}`);
90
+ }
91
+ console.log("");
92
+ console.log(` ${chalk.dim("Or just reopen Terminal — we'll auto-resume if possible.")}`);
93
+ console.log("");
94
+ console.log(` Press ${chalk.bold("Enter")} when ready (or grant Accessibility and Terminal will restart)...`);
95
+ await waitForEnter();
96
+ // If we get here, user pressed Enter without Terminal being killed
97
+ const recheck = checkPermissions();
98
+ if (recheck.accessibility === false) {
99
+ console.log(`\n ${chalk.yellow("⚠")} Accessibility still not granted. Click/type commands will fail.`);
100
+ console.log(` ${chalk.dim("Continuing anyway — screenshots may still work.\n")}`);
101
+ }
102
+ else {
103
+ console.log(`\n ${chalk.green("✓")} Permissions: all OK\n`);
104
+ clearRetryState();
105
+ }
106
+ }
30
107
  }
31
108
  else {
109
+ // All permissions OK — run functional test
32
110
  console.log(" Running permission test...");
33
111
  const test = await runPermissionTest();
34
112
  if (test.ok) {
@@ -38,11 +116,11 @@ async function main() {
38
116
  console.log(` ${chalk.yellow("⚠")} ${test.error}`);
39
117
  console.log(" Continuing anyway — some commands may fail.\n");
40
118
  }
119
+ clearRetryState();
41
120
  }
42
121
  }
43
- // 2. Resolve config: --pair, --token+--vm, saved config, or interactive
122
+ // ── 2. Resolve config: --pair, --token+--vm, saved config, or interactive ──
44
123
  let config = loadConfig();
45
- const args = parseArgs();
46
124
  // Path A: Pairing code (simplest)
47
125
  if (args.pair) {
48
126
  console.log(` Redeeming pairing code ${chalk.bold(args.pair)}...`);
@@ -51,6 +129,7 @@ async function main() {
51
129
  config = { gatewayToken: result.token, vmAddress: result.vmAddress, port: 8765, mode: "supervised" };
52
130
  saveConfig(config);
53
131
  console.log(` ${chalk.green("✓")} Paired successfully\n`);
132
+ clearRetryState();
54
133
  }
55
134
  else {
56
135
  process.exit(1);
@@ -80,7 +159,7 @@ async function main() {
80
159
  console.log(` ${chalk.dim("VM:")} ${config.vmAddress}:${config.port}`);
81
160
  console.log(` ${chalk.dim("Mode:")} ${config.mode}`);
82
161
  console.log(` ${chalk.dim("Token:")} ${config.gatewayToken.substring(0, 8)}...\n`);
83
- // 3. Connect to VM
162
+ // ── 3. Connect to VM ──
84
163
  connect({
85
164
  vmAddress: config.vmAddress,
86
165
  port: config.port,
@@ -119,7 +198,7 @@ async function main() {
119
198
  }
120
199
  },
121
200
  });
122
- // 4. Ctrl+C
201
+ // ── 4. Ctrl+C ──
123
202
  process.on("SIGINT", () => {
124
203
  console.log(`\n ${chalk.dim("Disconnecting...")}`);
125
204
  disconnect();
@@ -127,6 +206,46 @@ async function main() {
127
206
  process.exit(0);
128
207
  });
129
208
  }
209
+ // ── Retry State (persists across Terminal restarts) ──
210
+ function saveRetryState(state) {
211
+ try {
212
+ fs.writeFileSync(RETRY_FILE, JSON.stringify({ ...state, ts: Date.now() }));
213
+ }
214
+ catch { }
215
+ }
216
+ function loadRetryState() {
217
+ try {
218
+ if (!fs.existsSync(RETRY_FILE))
219
+ return null;
220
+ const data = JSON.parse(fs.readFileSync(RETRY_FILE, "utf-8"));
221
+ // Only use if recent (within 10 min)
222
+ if (Date.now() - data.ts > RETRY_MAX_AGE_MS) {
223
+ clearRetryState();
224
+ return null;
225
+ }
226
+ if (!data.pair)
227
+ return null;
228
+ return { pair: data.pair, autonomous: data.autonomous };
229
+ }
230
+ catch {
231
+ return null;
232
+ }
233
+ }
234
+ function clearRetryState() {
235
+ try {
236
+ fs.unlinkSync(RETRY_FILE);
237
+ }
238
+ catch { }
239
+ }
240
+ function getTerminalName() {
241
+ const terminal = process.env.TERM_PROGRAM || "your terminal app";
242
+ return {
243
+ "Apple_Terminal": "Terminal",
244
+ "iTerm.app": "iTerm2",
245
+ "vscode": "Visual Studio Code",
246
+ "WarpTerminal": "Warp",
247
+ }[terminal] || terminal;
248
+ }
130
249
  // ── Pairing Code ──
131
250
  async function redeemPairingCode(code) {
132
251
  try {
@@ -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.0",
3
+ "version": "0.6.0",
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"