@instaclaw/dispatch 0.4.0 → 0.5.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.
- package/dist/connection.js +5 -1
- package/dist/executor.js +81 -2
- package/dist/index.js +128 -9
- package/dist/screenshot.d.ts +3 -1
- package/dist/screenshot.js +43 -11
- package/dist/supervisor.js +14 -0
- package/dist/types.d.ts +1 -1
- package/package.json +1 -1
package/dist/connection.js
CHANGED
|
@@ -37,13 +37,17 @@ export function connect(opts) {
|
|
|
37
37
|
const result = await executeCommand(command);
|
|
38
38
|
commandsExecuted++;
|
|
39
39
|
if (result.screenshotBuffer && result.screenshotMeta) {
|
|
40
|
-
// Screenshot: send metadata text frame, then binary frame
|
|
40
|
+
// Screenshot (or batch with screenshot): send metadata text frame, then binary frame
|
|
41
41
|
const meta = {
|
|
42
42
|
id: command.id,
|
|
43
43
|
type: "screenshot_result",
|
|
44
44
|
...result.screenshotMeta,
|
|
45
45
|
size: result.screenshotBuffer.length,
|
|
46
46
|
};
|
|
47
|
+
// For batch commands, include action results in the metadata frame
|
|
48
|
+
if (result.data) {
|
|
49
|
+
meta.data = result.data;
|
|
50
|
+
}
|
|
47
51
|
ws?.send(JSON.stringify(meta));
|
|
48
52
|
ws?.send(result.screenshotBuffer);
|
|
49
53
|
}
|
package/dist/executor.js
CHANGED
|
@@ -32,6 +32,8 @@ export async function executeCommand(command) {
|
|
|
32
32
|
return await doDrag(params);
|
|
33
33
|
case "windows":
|
|
34
34
|
return await listWindows();
|
|
35
|
+
case "batch":
|
|
36
|
+
return await executeBatch(params);
|
|
35
37
|
default:
|
|
36
38
|
return { success: false, error: `Unknown command type: ${type}` };
|
|
37
39
|
}
|
|
@@ -40,10 +42,87 @@ export async function executeCommand(command) {
|
|
|
40
42
|
return { success: false, error: String(err) };
|
|
41
43
|
}
|
|
42
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* Execute multiple actions sequentially with optional per-action wait times.
|
|
47
|
+
* Returns results for all actions; stops on first failure.
|
|
48
|
+
* Optionally takes a screenshot after the batch completes.
|
|
49
|
+
*/
|
|
50
|
+
async function executeBatch(params) {
|
|
51
|
+
const actions = params.actions;
|
|
52
|
+
if (!Array.isArray(actions) || actions.length === 0) {
|
|
53
|
+
return { success: false, error: "batch requires a non-empty 'actions' array" };
|
|
54
|
+
}
|
|
55
|
+
if (actions.length > 20) {
|
|
56
|
+
return { success: false, error: "batch limited to 20 actions max" };
|
|
57
|
+
}
|
|
58
|
+
const results = [];
|
|
59
|
+
for (const action of actions) {
|
|
60
|
+
const cmd = {
|
|
61
|
+
id: `batch_${Date.now()}`,
|
|
62
|
+
type: action.type,
|
|
63
|
+
params: action.params || {},
|
|
64
|
+
};
|
|
65
|
+
// Don't allow nested batches or screenshots inside a batch
|
|
66
|
+
if (cmd.type === "batch") {
|
|
67
|
+
results.push({ type: "batch", success: false, error: "nested batches not allowed" });
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
if (cmd.type === "screenshot") {
|
|
71
|
+
results.push({ type: "screenshot", success: false, error: "use screenshotAfter param instead of screenshot in batch" });
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
const result = await executeCommand(cmd);
|
|
75
|
+
results.push({
|
|
76
|
+
type: action.type,
|
|
77
|
+
success: result.success,
|
|
78
|
+
data: result.data,
|
|
79
|
+
error: result.error,
|
|
80
|
+
});
|
|
81
|
+
if (!result.success)
|
|
82
|
+
break; // Stop batch on first failure
|
|
83
|
+
// Per-action wait time (default: 50ms between actions)
|
|
84
|
+
const waitMs = action.waitAfterMs ?? 50;
|
|
85
|
+
if (waitMs > 0) {
|
|
86
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// Optionally take a screenshot after the batch completes
|
|
90
|
+
const screenshotAfter = params.screenshotAfter !== false; // default true
|
|
91
|
+
let screenshotResult;
|
|
92
|
+
if (screenshotAfter) {
|
|
93
|
+
// Wait for screen to settle before capturing
|
|
94
|
+
const settleMs = typeof params.settleMs === "number" ? params.settleMs : 300;
|
|
95
|
+
if (settleMs > 0) {
|
|
96
|
+
await new Promise((r) => setTimeout(r, settleMs));
|
|
97
|
+
}
|
|
98
|
+
screenshotResult = await takeScreenshot({
|
|
99
|
+
format: params.screenshotFormat || "webp",
|
|
100
|
+
quality: Number(params.screenshotQuality) || 55,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
const batchResult = {
|
|
104
|
+
success: results.every((r) => r.success),
|
|
105
|
+
data: {
|
|
106
|
+
action: "batch",
|
|
107
|
+
actionsExecuted: results.length,
|
|
108
|
+
actionsTotal: actions.length,
|
|
109
|
+
results,
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
// If we took a screenshot, attach it
|
|
113
|
+
if (screenshotResult?.screenshotBuffer && screenshotResult.screenshotMeta) {
|
|
114
|
+
batchResult.screenshotBuffer = screenshotResult.screenshotBuffer;
|
|
115
|
+
batchResult.screenshotMeta = screenshotResult.screenshotMeta;
|
|
116
|
+
// Also include batch results in the screenshot meta so the agent gets both
|
|
117
|
+
batchResult.data.screenshotIncluded = true;
|
|
118
|
+
}
|
|
119
|
+
return batchResult;
|
|
120
|
+
}
|
|
43
121
|
async function takeScreenshot(params) {
|
|
44
122
|
const capture = await captureScreenshot({
|
|
45
|
-
format: params.format || "
|
|
46
|
-
quality: Number(params.quality) ||
|
|
123
|
+
format: params.format || "webp",
|
|
124
|
+
quality: Number(params.quality) || 55,
|
|
125
|
+
maxWidth: Number(params.maxWidth) || 1280,
|
|
47
126
|
});
|
|
48
127
|
// Cache coordMap for subsequent click/hover/drag commands
|
|
49
128
|
lastCoordMap = capture.coordMap;
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 {
|
package/dist/screenshot.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface ScreenshotOptions {
|
|
2
|
-
format?: "jpeg" | "png";
|
|
2
|
+
format?: "jpeg" | "png" | "webp";
|
|
3
3
|
quality?: number;
|
|
4
|
+
/** Max width — downscale if screen is larger (default 1280) */
|
|
5
|
+
maxWidth?: number;
|
|
4
6
|
}
|
|
5
7
|
export interface ScreenshotCapture {
|
|
6
8
|
buffer: Buffer;
|
package/dist/screenshot.js
CHANGED
|
@@ -1,38 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Screenshot — captures screen via usecomputer,
|
|
3
|
-
*
|
|
2
|
+
* Screenshot — captures screen via usecomputer, compresses via sharp.
|
|
3
|
+
* Optimized for speed: WebP format, quality 55, resolution capped at 1280x720.
|
|
4
4
|
*/
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
// Pre-warm sharp at module load (avoid dynamic import per screenshot)
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
let sharpFn = null;
|
|
10
|
+
const sharpReady = import("sharp").then((m) => { sharpFn = m.default || m; }).catch(() => { });
|
|
11
|
+
// Pre-warm usecomputer at module load
|
|
12
|
+
let ucMod = null;
|
|
13
|
+
const ucReady = import("usecomputer").then((m) => { ucMod = m; }).catch(() => { });
|
|
7
14
|
/**
|
|
8
15
|
* Capture a screenshot and return it as a buffer with metadata.
|
|
9
16
|
*/
|
|
10
17
|
export async function captureScreenshot(opts = {}) {
|
|
11
|
-
|
|
18
|
+
// Ensure modules are loaded
|
|
19
|
+
if (!ucMod)
|
|
20
|
+
await ucReady;
|
|
21
|
+
if (!sharpFn)
|
|
22
|
+
await sharpReady;
|
|
23
|
+
const uc = ucMod;
|
|
12
24
|
const tmpPath = path.join("/tmp", `dispatch-ss-${Date.now()}.png`);
|
|
13
|
-
const format = opts.format || "
|
|
14
|
-
const quality = opts.quality ||
|
|
25
|
+
const format = opts.format || "webp";
|
|
26
|
+
const quality = opts.quality || 55;
|
|
27
|
+
const maxWidth = opts.maxWidth || 1280;
|
|
15
28
|
try {
|
|
16
29
|
const result = await uc.screenshot({ path: tmpPath, display: null, window: null, region: null, annotate: null });
|
|
17
30
|
const pngBuffer = fs.readFileSync(tmpPath);
|
|
18
31
|
let outputBuffer;
|
|
19
|
-
|
|
32
|
+
let outWidth = result.imageWidth;
|
|
33
|
+
let outHeight = result.imageHeight;
|
|
34
|
+
if (sharpFn) {
|
|
20
35
|
try {
|
|
21
|
-
|
|
22
|
-
|
|
36
|
+
let pipeline = sharpFn(pngBuffer);
|
|
37
|
+
// Downscale if wider than maxWidth (HiDPI screens capture at 2x+)
|
|
38
|
+
if (result.imageWidth > maxWidth) {
|
|
39
|
+
pipeline = pipeline.resize(maxWidth, null, { fit: "inside", withoutEnlargement: true });
|
|
40
|
+
const ratio = maxWidth / result.imageWidth;
|
|
41
|
+
outWidth = maxWidth;
|
|
42
|
+
outHeight = Math.round(result.imageHeight * ratio);
|
|
43
|
+
}
|
|
44
|
+
// Encode to target format
|
|
45
|
+
if (format === "webp") {
|
|
46
|
+
outputBuffer = await pipeline.webp({ quality, effort: 2 }).toBuffer();
|
|
47
|
+
}
|
|
48
|
+
else if (format === "jpeg") {
|
|
49
|
+
outputBuffer = await pipeline.jpeg({ quality }).toBuffer();
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
outputBuffer = await pipeline.png().toBuffer();
|
|
53
|
+
}
|
|
23
54
|
}
|
|
24
55
|
catch {
|
|
25
|
-
// sharp
|
|
56
|
+
// sharp failed — fall back to raw PNG
|
|
26
57
|
outputBuffer = pngBuffer;
|
|
27
58
|
}
|
|
28
59
|
}
|
|
29
60
|
else {
|
|
61
|
+
// No sharp available — return raw PNG
|
|
30
62
|
outputBuffer = pngBuffer;
|
|
31
63
|
}
|
|
32
64
|
return {
|
|
33
65
|
buffer: outputBuffer,
|
|
34
|
-
width:
|
|
35
|
-
height:
|
|
66
|
+
width: outWidth,
|
|
67
|
+
height: outHeight,
|
|
36
68
|
format,
|
|
37
69
|
coordMap: result.coordMap,
|
|
38
70
|
};
|
package/dist/supervisor.js
CHANGED
|
@@ -85,6 +85,13 @@ function isDangerous(cmd) {
|
|
|
85
85
|
const key = String(p.key || "").toLowerCase();
|
|
86
86
|
return DANGEROUS_KEY_COMBOS.has(key);
|
|
87
87
|
}
|
|
88
|
+
// Check all sub-actions in a batch
|
|
89
|
+
if (cmd.type === "batch") {
|
|
90
|
+
const actions = p.actions;
|
|
91
|
+
if (Array.isArray(actions)) {
|
|
92
|
+
return actions.some((a) => isDangerous({ id: "", type: a.type, params: a.params || {} }));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
88
95
|
return false;
|
|
89
96
|
}
|
|
90
97
|
function formatCommand(cmd) {
|
|
@@ -100,6 +107,13 @@ function formatCommand(cmd) {
|
|
|
100
107
|
return `Scroll ${p.direction} by ${p.amount || 3}`;
|
|
101
108
|
case "drag":
|
|
102
109
|
return `Drag from (${p.fromX}, ${p.fromY}) to (${p.toX}, ${p.toY})`;
|
|
110
|
+
case "batch": {
|
|
111
|
+
const actions = p.actions;
|
|
112
|
+
if (!Array.isArray(actions))
|
|
113
|
+
return "Batch (empty)";
|
|
114
|
+
const summary = actions.map((a) => formatCommand({ id: "", type: a.type, params: a.params || {} })).join(" → ");
|
|
115
|
+
return `Batch (${actions.length} actions): ${summary}`;
|
|
116
|
+
}
|
|
103
117
|
default:
|
|
104
118
|
return `${cmd.type}: ${JSON.stringify(p).substring(0, 80)}`;
|
|
105
119
|
}
|
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";
|
|
4
|
+
type: "screenshot" | "click" | "type" | "press" | "scroll" | "drag" | "windows" | "status" | "batch";
|
|
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.
|
|
3
|
+
"version": "0.5.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"
|