@instaclaw/dispatch 0.3.1 → 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/index.js +109 -71
- package/package.json +1 -1
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
|
|
7
|
-
* npx @instaclaw/dispatch --
|
|
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("
|
|
35
|
+
console.log(` ${chalk.green("✓")} Permissions: all OK\n`);
|
|
43
36
|
}
|
|
44
37
|
else {
|
|
45
|
-
console.log(`
|
|
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.
|
|
43
|
+
// 2. Resolve config: --pair, --token+--vm, saved config, or interactive
|
|
51
44
|
let config = loadConfig();
|
|
52
45
|
const args = parseArgs();
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
|
|
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,
|
|
89
|
+
rejectUnauthorized: false,
|
|
83
90
|
certFingerprint: config.certFingerprint,
|
|
84
91
|
onConnect: () => {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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);
|
|
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,71 +106,101 @@ async function main() {
|
|
|
101
106
|
config.mode = newMode;
|
|
102
107
|
setMode(newMode);
|
|
103
108
|
saveConfig(config);
|
|
104
|
-
console.log(`\n Mode
|
|
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.
|
|
122
|
+
// 4. Ctrl+C
|
|
122
123
|
process.on("SIGINT", () => {
|
|
123
|
-
console.log(
|
|
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("
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
//
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
+
}
|
|
148
172
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
token = swap;
|
|
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
|
+
}
|
|
157
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
|
|
158
192
|
}
|
|
159
|
-
|
|
193
|
+
// Try to redeem as pairing code
|
|
160
194
|
rl.close();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
vmAddress:
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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);
|
|
168
202
|
}
|
|
203
|
+
// ── Arg Parsing ──
|
|
169
204
|
function parseArgs() {
|
|
170
205
|
const args = process.argv.slice(2);
|
|
171
206
|
const result = {};
|
|
@@ -182,6 +217,9 @@ function parseArgs() {
|
|
|
182
217
|
else if (args[i] === "--autonomous" || args[i] === "--auto") {
|
|
183
218
|
result.autonomous = true;
|
|
184
219
|
}
|
|
220
|
+
else if ((args[i] === "--pair" || args[i] === "--code") && args[i + 1]) {
|
|
221
|
+
result.pair = args[++i];
|
|
222
|
+
}
|
|
185
223
|
}
|
|
186
224
|
return result;
|
|
187
225
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@instaclaw/dispatch",
|
|
3
|
-
"version": "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"
|