@instaclaw/dispatch 0.1.0 → 0.2.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/index.js CHANGED
@@ -7,21 +7,44 @@
7
7
  * npx @instaclaw/dispatch --token XXX --vm HOST # Direct connect
8
8
  */
9
9
  import { loadConfig, saveConfig, getConfigPath } from "./config.js";
10
- import { checkPermissions, printPermissionGuide } from "./permissions.js";
10
+ import { checkPermissions, printPermissionGuide, runPermissionTest } from "./permissions.js";
11
11
  import { connect, disconnect } from "./connection.js";
12
- import { closeSupervisor } from "./supervisor.js";
12
+ import { closeSupervisor, setMode } from "./supervisor.js";
13
13
  import readline from "readline";
14
+ import os from "os";
14
15
  async function main() {
15
16
  console.log(`
16
17
  InstaClaw Dispatch — Remote Computer Control
17
18
  ─────────────────────────────────────────────
18
19
  `);
19
- // 1. Check/request permissions
20
- const perms = checkPermissions();
21
- if (!perms.accessibility || !perms.screenRecording) {
20
+ // 1. Check permissions (macOS only)
21
+ if (os.platform() === "darwin") {
22
22
  console.log(" Checking permissions...");
23
- printPermissionGuide();
24
- await waitForEnter();
23
+ const perms = checkPermissions();
24
+ printPermissionGuide(perms);
25
+ if (perms.screenRecording === false || perms.accessibility === false) {
26
+ await waitForEnter();
27
+ // Re-check after user grants
28
+ const recheck = checkPermissions();
29
+ if (recheck.screenRecording === false) {
30
+ console.log(" Warning: Screen Recording still not granted. Screenshots may fail.\n");
31
+ }
32
+ if (recheck.accessibility === false) {
33
+ console.log(" Warning: Accessibility still not granted. Click/type may fail.\n");
34
+ }
35
+ }
36
+ else {
37
+ // Run a functional test to be sure
38
+ console.log(" Running permission test...");
39
+ const test = await runPermissionTest();
40
+ if (test.ok) {
41
+ console.log(" Permissions: all OK\n");
42
+ }
43
+ else {
44
+ console.log(` Warning: ${test.error}`);
45
+ console.log(" Continuing anyway — some commands may fail.\n");
46
+ }
47
+ }
25
48
  }
26
49
  // 2. Load or create config
27
50
  let config = loadConfig();
@@ -32,7 +55,7 @@ async function main() {
32
55
  gatewayToken: args.token,
33
56
  vmAddress: args.vm,
34
57
  port: args.port || 8765,
35
- mode: "supervised",
58
+ mode: args.autonomous ? "autonomous" : "supervised",
36
59
  };
37
60
  saveConfig(config);
38
61
  }
@@ -42,6 +65,10 @@ async function main() {
42
65
  saveConfig(config);
43
66
  console.log(` Config saved to ${getConfigPath()}\n`);
44
67
  }
68
+ // Set supervisor mode
69
+ if (config.mode === "autonomous") {
70
+ setMode("autonomous");
71
+ }
45
72
  console.log(` VM: ${config.vmAddress}:${config.port}`);
46
73
  console.log(` Mode: ${config.mode}`);
47
74
  console.log(` Token: ${config.gatewayToken.substring(0, 8)}...\n`);
@@ -54,8 +81,14 @@ async function main() {
54
81
  rejectUnauthorized: false, // Accept self-signed (TOFU)
55
82
  certFingerprint: config.certFingerprint,
56
83
  onConnect: () => {
57
- console.log(`\n Dispatch mode: SUPERVISED`);
58
- console.log(` Screenshots auto-approved. Actions require [Enter] to confirm.`);
84
+ if (config.mode === "autonomous") {
85
+ console.log(`\n Dispatch mode: AUTONOMOUS`);
86
+ console.log(` All actions auto-approved (dangerous actions still require confirmation).`);
87
+ }
88
+ else {
89
+ console.log(`\n Dispatch mode: SUPERVISED`);
90
+ console.log(` Screenshots auto-approved. Actions require [Enter] to confirm.`);
91
+ }
59
92
  console.log(` Press Ctrl+C to disconnect.\n`);
60
93
  },
61
94
  onDisconnect: () => {
@@ -110,6 +143,9 @@ function parseArgs() {
110
143
  else if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
111
144
  result.port = parseInt(args[++i], 10);
112
145
  }
146
+ else if (args[i] === "--autonomous" || args[i] === "--auto") {
147
+ result.autonomous = true;
148
+ }
113
149
  }
114
150
  return result;
115
151
  }
@@ -1,8 +1,16 @@
1
1
  interface PermissionStatus {
2
- accessibility: boolean;
3
- screenRecording: boolean;
2
+ accessibility: boolean | "unknown";
3
+ screenRecording: boolean | "unknown";
4
4
  platform: string;
5
5
  }
6
6
  export declare function checkPermissions(): PermissionStatus;
7
- export declare function printPermissionGuide(): void;
7
+ /**
8
+ * Run a quick functional test: take a screenshot via usecomputer.
9
+ * If it fails, permissions are likely missing.
10
+ */
11
+ export declare function runPermissionTest(): Promise<{
12
+ ok: boolean;
13
+ error?: string;
14
+ }>;
15
+ export declare function printPermissionGuide(status: PermissionStatus): void;
8
16
  export {};
@@ -1,44 +1,118 @@
1
+ import { execSync } from "child_process";
1
2
  import os from "os";
3
+ import fs from "fs";
4
+ import path from "path";
2
5
  export function checkPermissions() {
3
6
  const platform = os.platform();
4
7
  if (platform === "darwin") {
5
8
  return checkMacPermissions();
6
9
  }
7
- // Linux/Windows — no special permissions needed for X11
10
+ // Linux/Windows — no special permissions needed
8
11
  return { accessibility: true, screenRecording: true, platform };
9
12
  }
10
13
  function checkMacPermissions() {
11
- let accessibility = false;
12
- let screenRecording = false;
13
- // Check accessibility: try to create a CGEvent (will fail without permission)
14
- // We can't check directly from Node — usecomputer will fail at runtime if missing
15
- // For now, assume true and catch errors at execution time
16
- accessibility = true; // Will validate on first command
17
- // Screen recording: same — can't check from Node without native module
18
- screenRecording = true;
14
+ let accessibility = "unknown";
15
+ let screenRecording = "unknown";
16
+ // Check Accessibility via TCC database
17
+ // macOS stores permission grants in ~/Library/Application Support/com.apple.TCC/TCC.db
18
+ // and /Library/Application Support/com.apple.TCC/TCC.db (system)
19
+ // We can try a quick test: usecomputer mouse position (fails without accessibility)
20
+ try {
21
+ const result = execSync('osascript -e \'tell application "System Events" to get name of first process\'', { timeout: 3000, stdio: ["pipe", "pipe", "pipe"] });
22
+ accessibility = true;
23
+ }
24
+ catch {
25
+ // AppleScript fails without Accessibility permission
26
+ accessibility = false;
27
+ }
28
+ // Check Screen Recording by attempting a tiny screenshot
29
+ // CGWindowListCreateImage returns a null/empty image without Screen Recording permission
30
+ try {
31
+ const tmpFile = path.join(os.tmpdir(), `dispatch-perm-check-${Date.now()}.png`);
32
+ execSync(`screencapture -x -t png "${tmpFile}" 2>/dev/null`, { timeout: 5000 });
33
+ if (fs.existsSync(tmpFile)) {
34
+ const size = fs.statSync(tmpFile).size;
35
+ fs.unlinkSync(tmpFile);
36
+ // A valid screenshot is at least a few KB. A blocked capture may produce a tiny or empty file.
37
+ screenRecording = size > 500;
38
+ }
39
+ else {
40
+ screenRecording = false;
41
+ }
42
+ }
43
+ catch {
44
+ screenRecording = false;
45
+ }
19
46
  return { accessibility, screenRecording, platform: "darwin" };
20
47
  }
21
- export function printPermissionGuide() {
22
- const platform = os.platform();
48
+ /**
49
+ * Run a quick functional test: take a screenshot via usecomputer.
50
+ * If it fails, permissions are likely missing.
51
+ */
52
+ export async function runPermissionTest() {
53
+ try {
54
+ const uc = await import("usecomputer");
55
+ const tmpPath = path.join(os.tmpdir(), `dispatch-test-${Date.now()}.png`);
56
+ await uc.screenshot({ path: tmpPath, display: null, window: null, region: null, annotate: null });
57
+ if (fs.existsSync(tmpPath)) {
58
+ const size = fs.statSync(tmpPath).size;
59
+ fs.unlinkSync(tmpPath);
60
+ if (size > 500)
61
+ return { ok: true };
62
+ return { ok: false, error: "Screenshot captured but empty — Screen Recording permission may be missing" };
63
+ }
64
+ return { ok: false, error: "Screenshot file not created" };
65
+ }
66
+ catch (err) {
67
+ const msg = String(err);
68
+ if (msg.includes("EVENT_POST_FAILED") || msg.includes("CGEvent")) {
69
+ return { ok: false, error: "Accessibility permission not granted" };
70
+ }
71
+ if (msg.includes("CAPTURE_FAILED")) {
72
+ return { ok: false, error: "Screen Recording permission not granted" };
73
+ }
74
+ return { ok: false, error: msg.slice(0, 100) };
75
+ }
76
+ }
77
+ export function printPermissionGuide(status) {
78
+ const platform = status.platform;
23
79
  if (platform === "darwin") {
24
- console.log(`
25
- To grant permissions (macOS):
26
- 1. System Settings → Privacy & Security → Accessibility
27
- → Add your terminal app (Terminal, iTerm, Warp, etc.)
28
- 2. System Settings → Privacy & Security → Screen Recording
29
- → Add your terminal app
30
-
31
- Press Enter after granting permissions...`);
80
+ const terminal = process.env.TERM_PROGRAM || "your terminal app";
81
+ const terminalName = {
82
+ "Apple_Terminal": "Terminal",
83
+ "iTerm.app": "iTerm2",
84
+ "vscode": "Visual Studio Code",
85
+ "WarpTerminal": "Warp",
86
+ }[terminal] || terminal;
87
+ console.log("");
88
+ if (status.screenRecording === false) {
89
+ console.log(` Screen Recording: NOT GRANTED`);
90
+ console.log(` → System Settings → Privacy & Security → Screen Recording`);
91
+ console.log(` → Enable "${terminalName}"`);
92
+ console.log(` → You may need to restart ${terminalName} after granting`);
93
+ console.log("");
94
+ }
95
+ else {
96
+ console.log(` Screen Recording: OK`);
97
+ }
98
+ if (status.accessibility === false) {
99
+ console.log(` Accessibility: NOT GRANTED`);
100
+ console.log(` → System Settings → Privacy & Security → Accessibility`);
101
+ console.log(` → Enable "${terminalName}"`);
102
+ console.log("");
103
+ }
104
+ else {
105
+ console.log(` Accessibility: OK`);
106
+ }
107
+ if (status.screenRecording === false || status.accessibility === false) {
108
+ console.log(` After granting permissions, press Enter to continue...`);
109
+ }
32
110
  }
33
111
  else if (platform === "linux") {
34
- console.log(`
35
- Linux: No special permissions needed.
36
- Ensure DISPLAY is set if using X11.`);
112
+ console.log(` Linux: No special permissions needed.`);
37
113
  }
38
114
  else {
39
- console.log(`
40
- Windows: No special permissions needed.
41
- Run as administrator if controlling elevated apps.`);
115
+ console.log(` Windows: No special permissions needed.`);
42
116
  }
43
117
  }
44
118
  //# sourceMappingURL=permissions.js.map
@@ -1,3 +1,4 @@
1
1
  import type { DispatchCommand } from "./types.js";
2
+ export declare function setMode(m: "supervised" | "autonomous"): void;
2
3
  export declare function requestApproval(command: DispatchCommand): Promise<boolean>;
3
4
  export declare function closeSupervisor(): void;
@@ -1,9 +1,25 @@
1
1
  /**
2
- * Supervisor — prompts user for approval before executing commands.
3
- * Screenshots are auto-approved. All other actions require Enter to proceed.
2
+ * Supervisor — controls whether commands need user approval.
3
+ *
4
+ * Modes:
5
+ * supervised (default) — all non-passive commands require Enter to approve
6
+ * autonomous — auto-approves most commands, but ALWAYS prompts for dangerous ones
4
7
  */
5
8
  import readline from "readline";
6
9
  const AUTO_APPROVE_TYPES = new Set(["screenshot", "windows", "status"]);
10
+ // Patterns that indicate dangerous actions — always require confirmation even in autonomous mode
11
+ const DANGEROUS_TEXT_PATTERNS = [
12
+ /password/i, /passwd/i, /secret/i, /credential/i,
13
+ /delete/i, /remove/i, /destroy/i, /format/i, /erase/i,
14
+ /purchase/i, /submit.*order/i, /confirm.*payment/i, /buy\s/i,
15
+ /transfer.*fund/i, /send.*money/i, /withdraw/i,
16
+ /sudo\s/i, /rm\s+-rf/i, /shutdown/i, /reboot/i,
17
+ ];
18
+ const DANGEROUS_KEY_COMBOS = new Set([
19
+ "cmd+delete", "cmd+shift+delete", "ctrl+delete",
20
+ "cmd+q", "alt+f4",
21
+ ]);
22
+ let mode = "supervised";
7
23
  const isTTY = process.stdin.isTTY ?? false;
8
24
  let rl = null;
9
25
  if (isTTY) {
@@ -12,18 +28,36 @@ if (isTTY) {
12
28
  output: process.stdout,
13
29
  });
14
30
  }
31
+ export function setMode(m) {
32
+ mode = m;
33
+ }
15
34
  export async function requestApproval(command) {
16
- // Auto-approve passive commands
35
+ // Always auto-approve passive commands
17
36
  if (AUTO_APPROVE_TYPES.has(command.type)) {
18
37
  return true;
19
38
  }
20
39
  const desc = formatCommand(command);
21
- console.log(`\n Agent wants to: ${desc}`);
22
- // Non-interactive mode (no TTY) — auto-approve with log
40
+ const dangerous = isDangerous(command);
41
+ // Non-interactive (no TTY) — auto-approve everything (can't prompt)
23
42
  if (!rl) {
43
+ if (dangerous) {
44
+ console.log(`\n BLOCKED (dangerous, no TTY): ${desc}`);
45
+ return false;
46
+ }
47
+ console.log(`\n Agent wants to: ${desc}`);
24
48
  console.log(" Auto-approved (non-interactive mode)");
25
49
  return true;
26
50
  }
51
+ // Autonomous mode — auto-approve non-dangerous actions
52
+ if (mode === "autonomous" && !dangerous) {
53
+ console.log(`\n Agent: ${desc}`);
54
+ return true;
55
+ }
56
+ // Supervised mode OR dangerous action in autonomous mode — prompt user
57
+ console.log(`\n Agent wants to: ${desc}`);
58
+ if (dangerous) {
59
+ console.log(" ⚠️ DANGEROUS ACTION — requires confirmation");
60
+ }
27
61
  return new Promise((resolve) => {
28
62
  rl.question(" [Enter] to approve, [n] to deny: ", (answer) => {
29
63
  const denied = answer.trim().toLowerCase() === "n";
@@ -34,6 +68,18 @@ export async function requestApproval(command) {
34
68
  });
35
69
  });
36
70
  }
71
+ function isDangerous(cmd) {
72
+ const p = cmd.params;
73
+ if (cmd.type === "type") {
74
+ const text = String(p.text || "");
75
+ return DANGEROUS_TEXT_PATTERNS.some((pat) => pat.test(text));
76
+ }
77
+ if (cmd.type === "press") {
78
+ const key = String(p.key || "").toLowerCase();
79
+ return DANGEROUS_KEY_COMBOS.has(key);
80
+ }
81
+ return false;
82
+ }
37
83
  function formatCommand(cmd) {
38
84
  const p = cmd.params;
39
85
  switch (cmd.type) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instaclaw/dispatch",
3
- "version": "0.1.0",
3
+ "version": "0.2.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"