@instaclaw/dispatch 0.1.1 → 0.3.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/auth.d.ts +21 -0
- package/dist/auth.js +64 -0
- package/dist/connection.js +8 -9
- package/dist/executor.js +50 -35
- package/dist/index.js +35 -7
- package/dist/screenshot.d.ts +15 -0
- package/dist/screenshot.js +47 -0
- package/dist/supervisor.d.ts +1 -0
- package/dist/supervisor.js +58 -5
- package/dist/types.d.ts +6 -0
- package/package.json +6 -4
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate auth params for WebSocket handshake (client-side).
|
|
3
|
+
*/
|
|
4
|
+
export declare function generateHandshake(gatewayToken: string): {
|
|
5
|
+
hmac: string;
|
|
6
|
+
ts: string;
|
|
7
|
+
nonce: string;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Build the WebSocket URL with auth query params.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildAuthUrl(vmAddress: string, port: number, gatewayToken: string): string;
|
|
13
|
+
/**
|
|
14
|
+
* Verify auth params from WebSocket handshake (server-side).
|
|
15
|
+
* Returns null on success, error string on failure.
|
|
16
|
+
*/
|
|
17
|
+
export declare function verifyHandshake(hmac: string, ts: string, nonce: string, gatewayToken: string, seenNonces: Map<string, number>): string | null;
|
|
18
|
+
/**
|
|
19
|
+
* Clean up expired nonces (call periodically).
|
|
20
|
+
*/
|
|
21
|
+
export declare function cleanupNonces(seenNonces: Map<string, number>, maxAgeMs?: number): void;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth — HMAC-based authentication for the dispatch WebSocket handshake.
|
|
3
|
+
*
|
|
4
|
+
* Protocol:
|
|
5
|
+
* Client sends: ?hmac=<hex>&ts=<unix_ms>&nonce=<random_hex>
|
|
6
|
+
* Server computes: HMAC-SHA256(gateway_token, ts + ":" + nonce)
|
|
7
|
+
* Server verifies: hmac matches, ts within 30s, nonce not seen before
|
|
8
|
+
*/
|
|
9
|
+
import crypto from "crypto";
|
|
10
|
+
const HANDSHAKE_MAX_AGE_MS = 30_000; // 30 seconds
|
|
11
|
+
/**
|
|
12
|
+
* Generate auth params for WebSocket handshake (client-side).
|
|
13
|
+
*/
|
|
14
|
+
export function generateHandshake(gatewayToken) {
|
|
15
|
+
const ts = String(Date.now());
|
|
16
|
+
const nonce = crypto.randomBytes(16).toString("hex");
|
|
17
|
+
const hmac = crypto.createHmac("sha256", gatewayToken)
|
|
18
|
+
.update(ts + ":" + nonce)
|
|
19
|
+
.digest("hex");
|
|
20
|
+
return { hmac, ts, nonce };
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the WebSocket URL with auth query params.
|
|
24
|
+
*/
|
|
25
|
+
export function buildAuthUrl(vmAddress, port, gatewayToken) {
|
|
26
|
+
const { hmac, ts, nonce } = generateHandshake(gatewayToken);
|
|
27
|
+
return `wss://${vmAddress}:${port}?hmac=${hmac}&ts=${ts}&nonce=${nonce}`;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Verify auth params from WebSocket handshake (server-side).
|
|
31
|
+
* Returns null on success, error string on failure.
|
|
32
|
+
*/
|
|
33
|
+
export function verifyHandshake(hmac, ts, nonce, gatewayToken, seenNonces) {
|
|
34
|
+
// Timestamp check
|
|
35
|
+
const tsMs = parseInt(ts, 10);
|
|
36
|
+
if (isNaN(tsMs) || Math.abs(Date.now() - tsMs) > HANDSHAKE_MAX_AGE_MS) {
|
|
37
|
+
return "Handshake expired (timestamp > 30s old)";
|
|
38
|
+
}
|
|
39
|
+
// Nonce replay check
|
|
40
|
+
if (seenNonces.has(nonce)) {
|
|
41
|
+
return "Nonce already used (replay attack)";
|
|
42
|
+
}
|
|
43
|
+
// HMAC verification
|
|
44
|
+
const expected = crypto.createHmac("sha256", gatewayToken)
|
|
45
|
+
.update(ts + ":" + nonce)
|
|
46
|
+
.digest("hex");
|
|
47
|
+
if (!crypto.timingSafeEqual(Buffer.from(hmac, "hex"), Buffer.from(expected, "hex"))) {
|
|
48
|
+
return "HMAC verification failed";
|
|
49
|
+
}
|
|
50
|
+
// Record nonce (clean up old ones)
|
|
51
|
+
seenNonces.set(nonce, Date.now());
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Clean up expired nonces (call periodically).
|
|
56
|
+
*/
|
|
57
|
+
export function cleanupNonces(seenNonces, maxAgeMs = 60_000) {
|
|
58
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
59
|
+
for (const [nonce, ts] of seenNonces) {
|
|
60
|
+
if (ts < cutoff)
|
|
61
|
+
seenNonces.delete(nonce);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/connection.js
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
import WebSocket from "ws";
|
|
6
6
|
import { executeCommand } from "./executor.js";
|
|
7
7
|
import { requestApproval } from "./supervisor.js";
|
|
8
|
+
import { buildAuthUrl } from "./auth.js";
|
|
8
9
|
let ws = null;
|
|
9
10
|
let reconnectDelay = 1000;
|
|
10
11
|
let shouldReconnect = true;
|
|
11
12
|
let commandsExecuted = 0;
|
|
12
13
|
export function connect(opts) {
|
|
13
|
-
|
|
14
|
-
const url =
|
|
14
|
+
// HMAC-authenticated URL (token + timestamp + nonce)
|
|
15
|
+
const url = buildAuthUrl(opts.vmAddress, opts.port, opts.gatewayToken);
|
|
15
16
|
console.log(` Connecting to ${opts.vmAddress}:${opts.port}...`);
|
|
16
17
|
ws = new WebSocket(url, {
|
|
17
18
|
rejectUnauthorized: false, // Accept self-signed certs (TOFU model)
|
|
@@ -26,13 +27,11 @@ export function connect(opts) {
|
|
|
26
27
|
return; // We don't expect binary from the server
|
|
27
28
|
try {
|
|
28
29
|
const command = JSON.parse(data.toString());
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
return;
|
|
35
|
-
}
|
|
30
|
+
// Check approval (supervisor handles mode logic — prompts in supervised, auto-approves in autonomous, blocks dangerous)
|
|
31
|
+
const approved = await requestApproval(command);
|
|
32
|
+
if (!approved) {
|
|
33
|
+
sendResult(command.id, { success: false, error: "User denied the action" });
|
|
34
|
+
return;
|
|
36
35
|
}
|
|
37
36
|
// Execute the command
|
|
38
37
|
const result = await executeCommand(command);
|
package/dist/executor.js
CHANGED
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
* Returns results (action confirmations or screenshot data).
|
|
4
4
|
*/
|
|
5
5
|
import { execSync } from "child_process";
|
|
6
|
-
import
|
|
7
|
-
import path from "path";
|
|
6
|
+
import { captureScreenshot } from "./screenshot.js";
|
|
8
7
|
// Dynamic import for usecomputer (ESM)
|
|
9
8
|
let uc = null;
|
|
9
|
+
// Track the most recent coordMap from screenshots for click mapping
|
|
10
|
+
let lastCoordMap = null;
|
|
10
11
|
async function getUC() {
|
|
11
12
|
if (!uc) {
|
|
12
13
|
uc = await import("usecomputer");
|
|
@@ -27,6 +28,8 @@ export async function executeCommand(command) {
|
|
|
27
28
|
return await doPress(params);
|
|
28
29
|
case "scroll":
|
|
29
30
|
return await doScroll(params);
|
|
31
|
+
case "drag":
|
|
32
|
+
return await doDrag(params);
|
|
30
33
|
case "windows":
|
|
31
34
|
return await listWindows();
|
|
32
35
|
default:
|
|
@@ -38,45 +41,41 @@ export async function executeCommand(command) {
|
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
async function takeScreenshot(params) {
|
|
44
|
+
const capture = await captureScreenshot({
|
|
45
|
+
format: params.format || "jpeg",
|
|
46
|
+
quality: Number(params.quality) || 80,
|
|
47
|
+
});
|
|
48
|
+
// Cache coordMap for subsequent click/hover/drag commands
|
|
49
|
+
lastCoordMap = capture.coordMap;
|
|
50
|
+
return {
|
|
51
|
+
success: true,
|
|
52
|
+
screenshotBuffer: capture.buffer,
|
|
53
|
+
screenshotMeta: {
|
|
54
|
+
width: capture.width,
|
|
55
|
+
height: capture.height,
|
|
56
|
+
format: capture.format,
|
|
57
|
+
coordMap: capture.coordMap,
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
async function doClick(params) {
|
|
41
62
|
const usecomputer = await getUC();
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
63
|
+
const x = Number(params.x);
|
|
64
|
+
const y = Number(params.y);
|
|
65
|
+
const coordMap = params.coordMap ? String(params.coordMap) : lastCoordMap;
|
|
66
|
+
let point = { x, y };
|
|
67
|
+
if (coordMap) {
|
|
68
|
+
// Map screenshot-space coordinates to desktop-space (critical for Retina/HiDPI)
|
|
48
69
|
try {
|
|
49
|
-
const
|
|
50
|
-
|
|
70
|
+
const parsed = usecomputer.parseCoordMapOrThrow(coordMap);
|
|
71
|
+
point = usecomputer.mapPointFromCoordMap({ point: { x, y }, coordMap: parsed });
|
|
51
72
|
}
|
|
52
73
|
catch {
|
|
53
|
-
//
|
|
54
|
-
jpegBuffer = pngBuffer;
|
|
74
|
+
// If mapping fails, fall back to raw coords
|
|
55
75
|
}
|
|
56
|
-
return {
|
|
57
|
-
success: true,
|
|
58
|
-
screenshotBuffer: jpegBuffer,
|
|
59
|
-
screenshotMeta: {
|
|
60
|
-
width: result.imageWidth,
|
|
61
|
-
height: result.imageHeight,
|
|
62
|
-
format: "jpeg",
|
|
63
|
-
coordMap: result.coordMap,
|
|
64
|
-
},
|
|
65
|
-
};
|
|
66
76
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
fs.unlinkSync(tmpPath);
|
|
70
|
-
}
|
|
71
|
-
catch { }
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
async function doClick(params) {
|
|
75
|
-
const usecomputer = await getUC();
|
|
76
|
-
const x = Number(params.x);
|
|
77
|
-
const y = Number(params.y);
|
|
78
|
-
await usecomputer.click({ point: { x, y }, button: "left", count: 1 });
|
|
79
|
-
return { success: true, data: { action: "click", x, y } };
|
|
77
|
+
await usecomputer.click({ point, button: "left", count: 1 });
|
|
78
|
+
return { success: true, data: { action: "click", x, y, mappedX: point.x, mappedY: point.y, coordMap: coordMap || "none" } };
|
|
80
79
|
}
|
|
81
80
|
async function doType(params) {
|
|
82
81
|
const usecomputer = await getUC();
|
|
@@ -108,6 +107,22 @@ async function doScroll(params) {
|
|
|
108
107
|
await usecomputer.scroll({ direction, amount, at: null });
|
|
109
108
|
return { success: true, data: { action: "scroll", direction, amount } };
|
|
110
109
|
}
|
|
110
|
+
async function doDrag(params) {
|
|
111
|
+
const usecomputer = await getUC();
|
|
112
|
+
const coordMap = params.coordMap ? String(params.coordMap) : lastCoordMap;
|
|
113
|
+
let from = { x: Number(params.fromX), y: Number(params.fromY) };
|
|
114
|
+
let to = { x: Number(params.toX), y: Number(params.toY) };
|
|
115
|
+
if (coordMap) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = usecomputer.parseCoordMapOrThrow(coordMap);
|
|
118
|
+
from = usecomputer.mapPointFromCoordMap({ point: from, coordMap: parsed });
|
|
119
|
+
to = usecomputer.mapPointFromCoordMap({ point: to, coordMap: parsed });
|
|
120
|
+
}
|
|
121
|
+
catch { }
|
|
122
|
+
}
|
|
123
|
+
await usecomputer.drag({ from, to, durationMs: null, button: null });
|
|
124
|
+
return { success: true, data: { action: "drag" } };
|
|
125
|
+
}
|
|
111
126
|
async function listWindows() {
|
|
112
127
|
const usecomputer = await getUC();
|
|
113
128
|
const windows = await usecomputer.windowList();
|
package/dist/index.js
CHANGED
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
import { loadConfig, saveConfig, getConfigPath } from "./config.js";
|
|
10
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
|
+
import chalk from "chalk";
|
|
13
14
|
import readline from "readline";
|
|
14
15
|
import os from "os";
|
|
15
16
|
async function main() {
|
|
16
17
|
console.log(`
|
|
17
|
-
InstaClaw Dispatch — Remote Computer Control
|
|
18
|
-
─────────────────────────────────────────────
|
|
18
|
+
${chalk.bold("InstaClaw Dispatch")} ${chalk.dim("— Remote Computer Control")}
|
|
19
|
+
${chalk.dim("─────────────────────────────────────────────")}
|
|
19
20
|
`);
|
|
20
21
|
// 1. Check permissions (macOS only)
|
|
21
22
|
if (os.platform() === "darwin") {
|
|
@@ -55,7 +56,7 @@ async function main() {
|
|
|
55
56
|
gatewayToken: args.token,
|
|
56
57
|
vmAddress: args.vm,
|
|
57
58
|
port: args.port || 8765,
|
|
58
|
-
mode: "supervised",
|
|
59
|
+
mode: args.autonomous ? "autonomous" : "supervised",
|
|
59
60
|
};
|
|
60
61
|
saveConfig(config);
|
|
61
62
|
}
|
|
@@ -65,6 +66,10 @@ async function main() {
|
|
|
65
66
|
saveConfig(config);
|
|
66
67
|
console.log(` Config saved to ${getConfigPath()}\n`);
|
|
67
68
|
}
|
|
69
|
+
// Set supervisor mode
|
|
70
|
+
if (config.mode === "autonomous") {
|
|
71
|
+
setMode("autonomous");
|
|
72
|
+
}
|
|
68
73
|
console.log(` VM: ${config.vmAddress}:${config.port}`);
|
|
69
74
|
console.log(` Mode: ${config.mode}`);
|
|
70
75
|
console.log(` Token: ${config.gatewayToken.substring(0, 8)}...\n`);
|
|
@@ -77,9 +82,29 @@ async function main() {
|
|
|
77
82
|
rejectUnauthorized: false, // Accept self-signed (TOFU)
|
|
78
83
|
certFingerprint: config.certFingerprint,
|
|
79
84
|
onConnect: () => {
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
if (config.mode === "autonomous") {
|
|
86
|
+
console.log(`\n Dispatch mode: AUTONOMOUS`);
|
|
87
|
+
console.log(` All actions auto-approved (dangerous actions still require confirmation).`);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
console.log(`\n Dispatch mode: SUPERVISED`);
|
|
91
|
+
console.log(` Screenshots auto-approved. Actions require [Enter] to confirm.`);
|
|
92
|
+
}
|
|
93
|
+
console.log(` Press 'a' to toggle autonomous mode. Press Ctrl+C to disconnect.\n`);
|
|
94
|
+
// Runtime mode switching via keypress
|
|
95
|
+
if (process.stdin.isTTY) {
|
|
96
|
+
process.stdin.setRawMode?.(false); // Ensure line mode for supervisor readline
|
|
97
|
+
process.stdin.on("data", (chunk) => {
|
|
98
|
+
const key = chunk.toString().trim().toLowerCase();
|
|
99
|
+
if (key === "a") {
|
|
100
|
+
const newMode = config.mode === "supervised" ? "autonomous" : "supervised";
|
|
101
|
+
config.mode = newMode;
|
|
102
|
+
setMode(newMode);
|
|
103
|
+
saveConfig(config);
|
|
104
|
+
console.log(`\n Mode switched to: ${newMode.toUpperCase()}\n`);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
83
108
|
},
|
|
84
109
|
onDisconnect: () => {
|
|
85
110
|
// Will auto-reconnect
|
|
@@ -133,6 +158,9 @@ function parseArgs() {
|
|
|
133
158
|
else if ((args[i] === "--port" || args[i] === "-p") && args[i + 1]) {
|
|
134
159
|
result.port = parseInt(args[++i], 10);
|
|
135
160
|
}
|
|
161
|
+
else if (args[i] === "--autonomous" || args[i] === "--auto") {
|
|
162
|
+
result.autonomous = true;
|
|
163
|
+
}
|
|
136
164
|
}
|
|
137
165
|
return result;
|
|
138
166
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface ScreenshotOptions {
|
|
2
|
+
format?: "jpeg" | "png";
|
|
3
|
+
quality?: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ScreenshotCapture {
|
|
6
|
+
buffer: Buffer;
|
|
7
|
+
width: number;
|
|
8
|
+
height: number;
|
|
9
|
+
format: string;
|
|
10
|
+
coordMap: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Capture a screenshot and return it as a buffer with metadata.
|
|
14
|
+
*/
|
|
15
|
+
export declare function captureScreenshot(opts?: ScreenshotOptions): Promise<ScreenshotCapture>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Screenshot — captures screen via usecomputer, converts PNG→JPEG via sharp.
|
|
3
|
+
* Separated per PRD Section 5.1 package structure.
|
|
4
|
+
*/
|
|
5
|
+
import fs from "fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
/**
|
|
8
|
+
* Capture a screenshot and return it as a buffer with metadata.
|
|
9
|
+
*/
|
|
10
|
+
export async function captureScreenshot(opts = {}) {
|
|
11
|
+
const uc = await import("usecomputer");
|
|
12
|
+
const tmpPath = path.join("/tmp", `dispatch-ss-${Date.now()}.png`);
|
|
13
|
+
const format = opts.format || "jpeg";
|
|
14
|
+
const quality = opts.quality || 80;
|
|
15
|
+
try {
|
|
16
|
+
const result = await uc.screenshot({ path: tmpPath, display: null, window: null, region: null, annotate: null });
|
|
17
|
+
const pngBuffer = fs.readFileSync(tmpPath);
|
|
18
|
+
let outputBuffer;
|
|
19
|
+
if (format === "jpeg") {
|
|
20
|
+
try {
|
|
21
|
+
const sharp = (await import("sharp")).default;
|
|
22
|
+
outputBuffer = await sharp(pngBuffer).jpeg({ quality }).toBuffer();
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// sharp not available — fall back to PNG
|
|
26
|
+
outputBuffer = pngBuffer;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
outputBuffer = pngBuffer;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
buffer: outputBuffer,
|
|
34
|
+
width: result.imageWidth,
|
|
35
|
+
height: result.imageHeight,
|
|
36
|
+
format,
|
|
37
|
+
coordMap: result.coordMap,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
finally {
|
|
41
|
+
try {
|
|
42
|
+
fs.unlinkSync(tmpPath);
|
|
43
|
+
}
|
|
44
|
+
catch { }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=screenshot.js.map
|
package/dist/supervisor.d.ts
CHANGED
package/dist/supervisor.js
CHANGED
|
@@ -1,9 +1,32 @@
|
|
|
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
|
+
// Authentication
|
|
13
|
+
/password/i, /passwd/i, /secret/i, /credential/i,
|
|
14
|
+
/\b2fa\b/i, /\bmfa\b/i, /authenticat/i, /verification.?code/i, /one.?time.?pass/i,
|
|
15
|
+
// Destructive
|
|
16
|
+
/delete/i, /remove/i, /destroy/i, /format/i, /erase/i, /empty.?trash/i,
|
|
17
|
+
// Financial
|
|
18
|
+
/purchase/i, /submit.*order/i, /confirm.*payment/i, /buy\s/i,
|
|
19
|
+
/transfer.*fund/i, /send.*money/i, /withdraw/i,
|
|
20
|
+
// System
|
|
21
|
+
/sudo\s/i, /rm\s+-rf/i, /shutdown/i, /reboot/i, /restart/i,
|
|
22
|
+
/\binstall\b/i, /\bapt\b/i, /\bbrew\b/i, /\bnpm\s+i/i, /pip\s+install/i,
|
|
23
|
+
];
|
|
24
|
+
const DANGEROUS_KEY_COMBOS = new Set([
|
|
25
|
+
"cmd+delete", "cmd+shift+delete", "ctrl+delete",
|
|
26
|
+
"cmd+q", "alt+f4",
|
|
27
|
+
"cmd+shift+backspace", // Empty Trash on macOS
|
|
28
|
+
]);
|
|
29
|
+
let mode = "supervised";
|
|
7
30
|
const isTTY = process.stdin.isTTY ?? false;
|
|
8
31
|
let rl = null;
|
|
9
32
|
if (isTTY) {
|
|
@@ -12,18 +35,36 @@ if (isTTY) {
|
|
|
12
35
|
output: process.stdout,
|
|
13
36
|
});
|
|
14
37
|
}
|
|
38
|
+
export function setMode(m) {
|
|
39
|
+
mode = m;
|
|
40
|
+
}
|
|
15
41
|
export async function requestApproval(command) {
|
|
16
|
-
//
|
|
42
|
+
// Always auto-approve passive commands
|
|
17
43
|
if (AUTO_APPROVE_TYPES.has(command.type)) {
|
|
18
44
|
return true;
|
|
19
45
|
}
|
|
20
46
|
const desc = formatCommand(command);
|
|
21
|
-
|
|
22
|
-
// Non-interactive
|
|
47
|
+
const dangerous = isDangerous(command);
|
|
48
|
+
// Non-interactive (no TTY) — auto-approve everything (can't prompt)
|
|
23
49
|
if (!rl) {
|
|
50
|
+
if (dangerous) {
|
|
51
|
+
console.log(`\n BLOCKED (dangerous, no TTY): ${desc}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
console.log(`\n Agent wants to: ${desc}`);
|
|
24
55
|
console.log(" Auto-approved (non-interactive mode)");
|
|
25
56
|
return true;
|
|
26
57
|
}
|
|
58
|
+
// Autonomous mode — auto-approve non-dangerous actions
|
|
59
|
+
if (mode === "autonomous" && !dangerous) {
|
|
60
|
+
console.log(`\n Agent: ${desc}`);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
// Supervised mode OR dangerous action in autonomous mode — prompt user
|
|
64
|
+
console.log(`\n Agent wants to: ${desc}`);
|
|
65
|
+
if (dangerous) {
|
|
66
|
+
console.log(" ⚠️ DANGEROUS ACTION — requires confirmation");
|
|
67
|
+
}
|
|
27
68
|
return new Promise((resolve) => {
|
|
28
69
|
rl.question(" [Enter] to approve, [n] to deny: ", (answer) => {
|
|
29
70
|
const denied = answer.trim().toLowerCase() === "n";
|
|
@@ -34,6 +75,18 @@ export async function requestApproval(command) {
|
|
|
34
75
|
});
|
|
35
76
|
});
|
|
36
77
|
}
|
|
78
|
+
function isDangerous(cmd) {
|
|
79
|
+
const p = cmd.params;
|
|
80
|
+
if (cmd.type === "type") {
|
|
81
|
+
const text = String(p.text || "");
|
|
82
|
+
return DANGEROUS_TEXT_PATTERNS.some((pat) => pat.test(text));
|
|
83
|
+
}
|
|
84
|
+
if (cmd.type === "press") {
|
|
85
|
+
const key = String(p.key || "").toLowerCase();
|
|
86
|
+
return DANGEROUS_KEY_COMBOS.has(key);
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
37
90
|
function formatCommand(cmd) {
|
|
38
91
|
const p = cmd.params;
|
|
39
92
|
switch (cmd.type) {
|
package/dist/types.d.ts
CHANGED
|
@@ -31,4 +31,10 @@ export interface DispatchConfig {
|
|
|
31
31
|
mode: "supervised" | "autonomous";
|
|
32
32
|
/** TLS cert fingerprint for TOFU (trust on first use) */
|
|
33
33
|
certFingerprint?: string;
|
|
34
|
+
/** Screenshot format: "jpeg" or "png" */
|
|
35
|
+
screenshotFormat?: "jpeg" | "png";
|
|
36
|
+
/** Screenshot quality (1-100, JPEG only) */
|
|
37
|
+
screenshotQuality?: number;
|
|
38
|
+
/** Max actions per minute before rate limiting */
|
|
39
|
+
maxActionsPerMinute?: number;
|
|
34
40
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instaclaw/dispatch",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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"
|
|
@@ -30,13 +30,15 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://instaclaw.io",
|
|
32
32
|
"dependencies": {
|
|
33
|
+
"chalk": "^5.6.2",
|
|
34
|
+
"inquirer": "^9.3.8",
|
|
35
|
+
"sharp": "^0.33.0",
|
|
33
36
|
"usecomputer": "^0.1.4",
|
|
34
|
-
"ws": "^8.18.0"
|
|
35
|
-
"sharp": "^0.33.0"
|
|
37
|
+
"ws": "^8.18.0"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
38
|
-
"@types/ws": "^8.5.0",
|
|
39
40
|
"@types/node": "^22.0.0",
|
|
41
|
+
"@types/ws": "^8.5.0",
|
|
40
42
|
"typescript": "^5.7.0"
|
|
41
43
|
},
|
|
42
44
|
"engines": {
|