@instaclaw/dispatch 0.3.0 → 0.4.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
@@ -62,6 +62,9 @@ async function doClick(params) {
62
62
  const usecomputer = await getUC();
63
63
  const x = Number(params.x);
64
64
  const y = Number(params.y);
65
+ if (isNaN(x) || isNaN(y) || x < 0 || y < 0) {
66
+ return { success: false, error: `Invalid coordinates: (${params.x}, ${params.y}). Must be non-negative numbers.` };
67
+ }
65
68
  const coordMap = params.coordMap ? String(params.coordMap) : lastCoordMap;
66
69
  let point = { x, y };
67
70
  if (coordMap) {
package/dist/index.js CHANGED
@@ -3,8 +3,9 @@
3
3
  * @instaclaw/dispatch — Local relay for InstaClaw agents to control your computer.
4
4
  *
5
5
  * Usage:
6
- * npx @instaclaw/dispatch # Interactive setup
7
- * npx @instaclaw/dispatch --token XXX --vm HOST # Direct connect
6
+ * npx @instaclaw/dispatch # Interactive (pairing code)
7
+ * npx @instaclaw/dispatch --pair ABCD-1234 # Pair with code
8
+ * npx @instaclaw/dispatch --token XXX --vm HOST # Direct connect (advanced)
8
9
  */
9
10
  import { loadConfig, saveConfig, getConfigPath } from "./config.js";
10
11
  import { checkPermissions, printPermissionGuide, runPermissionTest } from "./permissions.js";
@@ -13,6 +14,7 @@ import { closeSupervisor, setMode } from "./supervisor.js";
13
14
  import chalk from "chalk";
14
15
  import readline from "readline";
15
16
  import os from "os";
17
+ const API_BASE = "https://instaclaw.io";
16
18
  async function main() {
17
19
  console.log(`
18
20
  ${chalk.bold("InstaClaw Dispatch")} ${chalk.dim("— Remote Computer Control")}
@@ -25,33 +27,37 @@ async function main() {
25
27
  printPermissionGuide(perms);
26
28
  if (perms.screenRecording === false || perms.accessibility === false) {
27
29
  await waitForEnter();
28
- // Re-check after user grants
29
- const recheck = checkPermissions();
30
- if (recheck.screenRecording === false) {
31
- console.log(" Warning: Screen Recording still not granted. Screenshots may fail.\n");
32
- }
33
- if (recheck.accessibility === false) {
34
- console.log(" Warning: Accessibility still not granted. Click/type may fail.\n");
35
- }
36
30
  }
37
31
  else {
38
- // Run a functional test to be sure
39
32
  console.log(" Running permission test...");
40
33
  const test = await runPermissionTest();
41
34
  if (test.ok) {
42
- console.log(" Permissions: all OK\n");
35
+ console.log(` ${chalk.green("✓")} Permissions: all OK\n`);
43
36
  }
44
37
  else {
45
- console.log(` Warning: ${test.error}`);
38
+ console.log(` ${chalk.yellow("⚠")} ${test.error}`);
46
39
  console.log(" Continuing anyway — some commands may fail.\n");
47
40
  }
48
41
  }
49
42
  }
50
- // 2. Load or create config
43
+ // 2. Resolve config: --pair, --token+--vm, saved config, or interactive
51
44
  let config = loadConfig();
52
45
  const args = parseArgs();
53
- if (args.token && args.vm) {
54
- // CLI args override config
46
+ // Path A: Pairing code (simplest)
47
+ if (args.pair) {
48
+ console.log(` Redeeming pairing code ${chalk.bold(args.pair)}...`);
49
+ const result = await redeemPairingCode(args.pair);
50
+ if (result) {
51
+ config = { gatewayToken: result.token, vmAddress: result.vmAddress, port: 8765, mode: "supervised" };
52
+ saveConfig(config);
53
+ console.log(` ${chalk.green("✓")} Paired successfully\n`);
54
+ }
55
+ else {
56
+ process.exit(1);
57
+ }
58
+ }
59
+ // Path B: Direct token + VM (advanced)
60
+ else if (args.token && args.vm) {
55
61
  config = {
56
62
  gatewayToken: args.token,
57
63
  vmAddress: args.vm,
@@ -60,40 +66,39 @@ async function main() {
60
66
  };
61
67
  saveConfig(config);
62
68
  }
63
- if (!config?.gatewayToken || !config?.vmAddress) {
64
- // Interactive setup
69
+ // Path C: No args — interactive setup
70
+ else if (!config?.gatewayToken || !config?.vmAddress) {
65
71
  config = await interactiveSetup(config);
66
72
  saveConfig(config);
67
73
  console.log(` Config saved to ${getConfigPath()}\n`);
68
74
  }
69
75
  // Set supervisor mode
70
- if (config.mode === "autonomous") {
76
+ if (config.mode === "autonomous" || args.autonomous) {
77
+ config.mode = "autonomous";
71
78
  setMode("autonomous");
72
79
  }
73
- console.log(` VM: ${config.vmAddress}:${config.port}`);
74
- console.log(` Mode: ${config.mode}`);
75
- console.log(` Token: ${config.gatewayToken.substring(0, 8)}...\n`);
80
+ console.log(` ${chalk.dim("VM:")} ${config.vmAddress}:${config.port}`);
81
+ console.log(` ${chalk.dim("Mode:")} ${config.mode}`);
82
+ console.log(` ${chalk.dim("Token:")} ${config.gatewayToken.substring(0, 8)}...\n`);
76
83
  // 3. Connect to VM
77
84
  connect({
78
85
  vmAddress: config.vmAddress,
79
86
  port: config.port,
80
87
  gatewayToken: config.gatewayToken,
81
88
  mode: config.mode,
82
- rejectUnauthorized: false, // Accept self-signed (TOFU)
89
+ rejectUnauthorized: false,
83
90
  certFingerprint: config.certFingerprint,
84
91
  onConnect: () => {
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
92
+ console.log("");
93
+ console.log(` ${chalk.green("✓")} ${chalk.bold("Connected to your agent!")}`);
94
+ console.log(` ${chalk.dim("Mode:")} ${config.mode === "autonomous" ? chalk.yellow("Autonomous") : chalk.green("Supervised")}`);
95
+ console.log(` ${chalk.dim("Your agent can now control this computer.")}`);
96
+ console.log("");
97
+ console.log(` ${chalk.dim("Press 'a' to toggle mode. Ctrl+C to disconnect.")}`);
98
+ console.log("");
99
+ // Runtime mode switching
95
100
  if (process.stdin.isTTY) {
96
- process.stdin.setRawMode?.(false); // Ensure line mode for supervisor readline
101
+ process.stdin.setRawMode?.(false);
97
102
  process.stdin.on("data", (chunk) => {
98
103
  const key = chunk.toString().trim().toLowerCase();
99
104
  if (key === "a") {
@@ -101,50 +106,101 @@ async function main() {
101
106
  config.mode = newMode;
102
107
  setMode(newMode);
103
108
  saveConfig(config);
104
- console.log(`\n Mode switched to: ${newMode.toUpperCase()}\n`);
109
+ console.log(`\n Mode: ${newMode === "autonomous" ? chalk.yellow("AUTONOMOUS") : chalk.green("SUPERVISED")}\n`);
105
110
  }
106
111
  });
107
112
  }
108
113
  },
109
- onDisconnect: () => {
110
- // Will auto-reconnect
111
- },
114
+ onDisconnect: () => { },
112
115
  onCertFingerprint: (fp) => {
113
- // TOFU: save fingerprint on first connection
114
116
  if (config && !config.certFingerprint) {
115
117
  config.certFingerprint = fp;
116
118
  saveConfig(config);
117
- console.log(` TLS fingerprint saved (TOFU): ${fp.substring(0, 20)}...`);
118
119
  }
119
120
  },
120
121
  });
121
- // 4. Handle Ctrl+C
122
+ // 4. Ctrl+C
122
123
  process.on("SIGINT", () => {
123
- console.log("\n Disconnecting...");
124
+ console.log(`\n ${chalk.dim("Disconnecting...")}`);
124
125
  disconnect();
125
126
  closeSupervisor();
126
127
  process.exit(0);
127
128
  });
128
129
  }
130
+ // ── Pairing Code ──
131
+ async function redeemPairingCode(code) {
132
+ try {
133
+ const res = await fetch(`${API_BASE}/api/vm/dispatch-pair/${encodeURIComponent(code)}`);
134
+ const data = await res.json();
135
+ if (data.error) {
136
+ console.log(` ${chalk.red("✗")} ${data.error}`);
137
+ return null;
138
+ }
139
+ return { token: data.token, vmAddress: data.vmAddress };
140
+ }
141
+ catch (err) {
142
+ console.log(` ${chalk.red("✗")} Failed to redeem pairing code: ${err}`);
143
+ return null;
144
+ }
145
+ }
146
+ // ── Interactive Setup ──
129
147
  async function interactiveSetup(existing) {
130
148
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
131
149
  const ask = (q, def) => new Promise((resolve) => {
132
150
  const prompt = def ? `${q} [${def}]: ` : `${q}: `;
133
151
  rl.question(` ${prompt}`, (ans) => resolve(ans.trim() || def || ""));
134
152
  });
135
- console.log(" First-time setup:\n");
136
- const token = await ask("Gateway token", existing?.gatewayToken);
137
- const vm = await ask("VM address (IP or hostname)", existing?.vmAddress);
138
- const portStr = await ask("Port", String(existing?.port || 8765));
153
+ console.log(` ${chalk.bold("Welcome!")} Let's connect to your agent.\n`);
154
+ console.log(` Got a pairing code from ${chalk.underline("instaclaw.io/settings")}?`);
155
+ const input = await ask("Enter pairing code (or 'a' for advanced setup)");
156
+ // Check if it's a pairing code (format: XXXX-YYYY)
157
+ if (input.toLowerCase() === "a") {
158
+ // Advanced: manual token + IP
159
+ console.log("");
160
+ let token = await ask("Gateway token", existing?.gatewayToken);
161
+ let vm = await ask("VM address (IP)", existing?.vmAddress);
162
+ // Swap detection
163
+ const isHexLong = (s) => /^[0-9a-f]{32,}$/i.test(s);
164
+ const isIPish = (s) => /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
165
+ if (isHexLong(vm)) {
166
+ console.log(chalk.yellow("\n That looks like a gateway token, not an IP address."));
167
+ const swap = await ask("Enter your VM IP");
168
+ if (swap) {
169
+ token = vm;
170
+ vm = swap;
171
+ }
172
+ }
173
+ else if (isIPish(token)) {
174
+ console.log(chalk.yellow("\n That looks like an IP, not a gateway token."));
175
+ const swap = await ask("Enter your gateway token");
176
+ if (swap) {
177
+ vm = token;
178
+ token = swap;
179
+ }
180
+ }
181
+ rl.close();
182
+ return { gatewayToken: token, vmAddress: vm, port: 8765, mode: "supervised" };
183
+ }
184
+ // It's a pairing code or something else
185
+ const isHexLong = (s) => /^[0-9a-f]{32,}$/i.test(s);
186
+ if (isHexLong(input)) {
187
+ console.log(chalk.yellow("\n That looks like a gateway token, not a pairing code."));
188
+ console.log(` Get your pairing code at ${chalk.underline("instaclaw.io/settings")}`);
189
+ console.log(` Or press 'a' for advanced setup.\n`);
190
+ rl.close();
191
+ return interactiveSetup(existing); // Retry
192
+ }
193
+ // Try to redeem as pairing code
139
194
  rl.close();
140
- return {
141
- gatewayToken: token,
142
- vmAddress: vm,
143
- port: parseInt(portStr, 10) || 8765,
144
- mode: "supervised",
145
- certFingerprint: existing?.certFingerprint,
146
- };
195
+ const result = await redeemPairingCode(input);
196
+ if (result) {
197
+ return { gatewayToken: result.token, vmAddress: result.vmAddress, port: 8765, mode: "supervised" };
198
+ }
199
+ // Failed — fall back to advanced
200
+ console.log(" Falling back to advanced setup...\n");
201
+ return interactiveSetup(existing);
147
202
  }
203
+ // ── Arg Parsing ──
148
204
  function parseArgs() {
149
205
  const args = process.argv.slice(2);
150
206
  const result = {};
@@ -161,6 +217,9 @@ function parseArgs() {
161
217
  else if (args[i] === "--autonomous" || args[i] === "--auto") {
162
218
  result.autonomous = true;
163
219
  }
220
+ else if ((args[i] === "--pair" || args[i] === "--code") && args[i + 1]) {
221
+ result.pair = args[++i];
222
+ }
164
223
  }
165
224
  return result;
166
225
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instaclaw/dispatch",
3
- "version": "0.3.0",
3
+ "version": "0.4.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"