@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 +46 -10
- package/dist/permissions.d.ts +11 -3
- package/dist/permissions.js +99 -25
- package/dist/supervisor.d.ts +1 -0
- package/dist/supervisor.js +51 -5
- package/package.json +1 -1
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
|
|
20
|
-
|
|
21
|
-
if (!perms.accessibility || !perms.screenRecording) {
|
|
20
|
+
// 1. Check permissions (macOS only)
|
|
21
|
+
if (os.platform() === "darwin") {
|
|
22
22
|
console.log(" Checking permissions...");
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
}
|
package/dist/permissions.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
package/dist/permissions.js
CHANGED
|
@@ -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
|
|
10
|
+
// Linux/Windows — no special permissions needed
|
|
8
11
|
return { accessibility: true, screenRecording: true, platform };
|
|
9
12
|
}
|
|
10
13
|
function checkMacPermissions() {
|
|
11
|
-
let accessibility =
|
|
12
|
-
let screenRecording =
|
|
13
|
-
// Check
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
package/dist/supervisor.d.ts
CHANGED
package/dist/supervisor.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Supervisor —
|
|
3
|
-
*
|
|
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
|
-
//
|
|
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
|
-
|
|
22
|
-
// Non-interactive
|
|
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.
|
|
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"
|