@4xeoz/re-entry 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/README.md +337 -0
- package/node_modules/@webmcp-challenge/reentry-core/README.md +112 -0
- package/node_modules/@webmcp-challenge/reentry-core/package.json +38 -0
- package/node_modules/@webmcp-challenge/reentry-core/protocol/test-vectors/v0.1.json +47 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/agent-adapter.mjs +438 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +268 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/host-sdk.mjs +278 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/index.mjs +3 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/local-connector-client.mjs +530 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/managed-context-adapter.mjs +275 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/protocol.mjs +845 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-core.mjs +867 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-delivery.mjs +613 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-http-contract.mjs +24 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/receiver-support.mjs +151 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-schema.mjs +184 -0
- package/node_modules/@webmcp-challenge/reentry-core/src/sqlite-receiver-store.mjs +573 -0
- package/package.json +44 -0
- package/src/browser-prompt.mjs +18 -0
- package/src/codex-discovery.mjs +246 -0
- package/src/codex-exec-adapter.mjs +197 -0
- package/src/codex-queue-adapter.mjs +214 -0
- package/src/credentials.mjs +87 -0
- package/src/index.mjs +6 -0
- package/src/local-connector.mjs +82 -0
- package/src/macos-service.mjs +184 -0
- package/src/main.mjs +733 -0
- package/src/pairing-client.mjs +545 -0
- package/src/terminal-ui.mjs +99 -0
package/src/main.mjs
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { homedir, hostname } from "node:os";
|
|
4
|
+
import { createInterface } from "node:readline/promises";
|
|
5
|
+
import { readFileSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { randomBytes } from "node:crypto";
|
|
9
|
+
import process from "node:process";
|
|
10
|
+
|
|
11
|
+
import { LocalConnectorClient } from "@webmcp-challenge/reentry-core/local-connector-client";
|
|
12
|
+
import { createCodexExecAdapter } from "./codex-exec-adapter.mjs";
|
|
13
|
+
import {
|
|
14
|
+
discoverCodexExecutable,
|
|
15
|
+
requireSupportedNode,
|
|
16
|
+
validateCodexWorkingDirectory,
|
|
17
|
+
verifyCodexExecutable,
|
|
18
|
+
} from "./codex-discovery.mjs";
|
|
19
|
+
import { LocalConnector } from "./local-connector.mjs";
|
|
20
|
+
import { LocalConnectorCredentialStore } from "./credentials.mjs";
|
|
21
|
+
import { LocalConnectorPairingClient } from "./pairing-client.mjs";
|
|
22
|
+
import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
|
|
23
|
+
import {
|
|
24
|
+
inspectMacConnectorService,
|
|
25
|
+
installMacConnectorService,
|
|
26
|
+
} from "./macos-service.mjs";
|
|
27
|
+
import { createTerminalUi } from "./terminal-ui.mjs";
|
|
28
|
+
|
|
29
|
+
const DEFAULT_RECEIVER_ORIGIN = process.env.REENTRY_RECEIVER_ORIGIN ?? "https://reentry-cloud.vercel.app";
|
|
30
|
+
const DEFAULT_POLL_INTERVAL_MS = 5_000;
|
|
31
|
+
const DEFAULT_MAX_CONSECUTIVE_ERRORS = 5;
|
|
32
|
+
const CONNECTOR_VERSION = readConnectorVersion();
|
|
33
|
+
|
|
34
|
+
async function main() {
|
|
35
|
+
let ui;
|
|
36
|
+
try {
|
|
37
|
+
const argumentsList = process.argv.slice(2);
|
|
38
|
+
if (argumentsList.includes("--help") || argumentsList[0] === "help") {
|
|
39
|
+
printHelp();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (argumentsList.includes("--version") || argumentsList[0] === "version") {
|
|
43
|
+
process.stdout.write(`${CONNECTOR_VERSION}\n`);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const { command, flags } = parseArguments(argumentsList);
|
|
47
|
+
validateCommandFlags(command, flags);
|
|
48
|
+
ui = createTerminalUi({ interactive: flags.json !== true && process.stdout.isTTY === true });
|
|
49
|
+
requireSupportedNode();
|
|
50
|
+
if (command === "doctor") {
|
|
51
|
+
doctor(flags, ui);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (command === "pair") {
|
|
55
|
+
await pair(flags, ui);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (command === "connect") {
|
|
59
|
+
await connect(flags, ui);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (command === "status") {
|
|
63
|
+
await status(flags, ui);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (command === "install") {
|
|
67
|
+
await install(flags, ui);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (command === "claim-once") {
|
|
71
|
+
await claimOnce(flags, ui);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (command === "start") {
|
|
75
|
+
await start(flags, ui);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
throw cliFailure("connector_command_invalid");
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (ui?.interactive) {
|
|
81
|
+
ui.error("Connector stopped", safeErrorMessage(error), errorHint(error));
|
|
82
|
+
} else {
|
|
83
|
+
process.stderr.write(`${JSON.stringify({
|
|
84
|
+
event: "local_connector_failed",
|
|
85
|
+
code: error?.code ?? "local_connector_failed",
|
|
86
|
+
message: safeErrorMessage(error),
|
|
87
|
+
})}\n`);
|
|
88
|
+
}
|
|
89
|
+
process.exitCode = 1;
|
|
90
|
+
} finally {
|
|
91
|
+
ui?.close();
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function printHelp() {
|
|
96
|
+
process.stdout.write(`Re-entry Local Connector ${CONNECTOR_VERSION}
|
|
97
|
+
|
|
98
|
+
Connect this Mac once, then receive approved work in Codex in the background.
|
|
99
|
+
|
|
100
|
+
Usage:
|
|
101
|
+
re-entry install --receiver <url> --codex-cd <project> Recommended first run
|
|
102
|
+
re-entry status Check the connection
|
|
103
|
+
re-entry start --codex-cd <project> Run in this terminal
|
|
104
|
+
|
|
105
|
+
Commands:
|
|
106
|
+
install Check this Mac, connect the Re-entry account, and start at login
|
|
107
|
+
status Show account, Receiver, service, Node.js, and Codex readiness
|
|
108
|
+
connect Authorize this Mac without installing the background service
|
|
109
|
+
start Poll for approved work until stopped
|
|
110
|
+
doctor Check Node.js, Codex, and the optional project directory
|
|
111
|
+
claim-once Check once for approved work
|
|
112
|
+
pair Legacy Host-code pairing preview
|
|
113
|
+
|
|
114
|
+
Common options:
|
|
115
|
+
--receiver <url> Re-entry Receiver origin
|
|
116
|
+
--codex-cd <path> Project opened for the fresh Codex session
|
|
117
|
+
--codex-binary <path> Explicit Codex executable
|
|
118
|
+
--json Machine-readable output
|
|
119
|
+
--help Show this help
|
|
120
|
+
--version Show the installed version
|
|
121
|
+
|
|
122
|
+
The Connector opens no inbound port. Host keys never belong on this Mac.
|
|
123
|
+
`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readConnectorVersion() {
|
|
127
|
+
const packageJson = JSON.parse(
|
|
128
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
129
|
+
);
|
|
130
|
+
if (typeof packageJson.version !== "string" || !/^\d+\.\d+\.\d+$/.test(packageJson.version)) {
|
|
131
|
+
throw new Error("Local Connector package version is invalid");
|
|
132
|
+
}
|
|
133
|
+
return packageJson.version;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function doctor(flags, ui) {
|
|
137
|
+
if (ui.interactive) ui.begin("Check this Mac", "Read-only readiness check");
|
|
138
|
+
const readiness = inspectReadiness(flags);
|
|
139
|
+
showReadiness(readiness, ui);
|
|
140
|
+
if (!ui.interactive) {
|
|
141
|
+
process.stdout.write(`${JSON.stringify({
|
|
142
|
+
event: "connector_ready",
|
|
143
|
+
node_version: process.versions.node,
|
|
144
|
+
codex_binary: readiness.installation.executable,
|
|
145
|
+
codex_version: readiness.installation.version,
|
|
146
|
+
codex_working_directory: readiness.workingDirectory,
|
|
147
|
+
})}\n`);
|
|
148
|
+
}
|
|
149
|
+
return readiness;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function inspectReadiness(flags) {
|
|
153
|
+
const workingDirectory = flags["codex-cd"] === undefined
|
|
154
|
+
? null
|
|
155
|
+
: validateCodexWorkingDirectory(flags["codex-cd"]);
|
|
156
|
+
const executable = discoverCodexExecutable({
|
|
157
|
+
requested: flags["codex-binary"],
|
|
158
|
+
});
|
|
159
|
+
const installation = verifyCodexExecutable(executable);
|
|
160
|
+
return Object.freeze({ workingDirectory, installation });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function showReadiness(readiness, ui) {
|
|
164
|
+
if (!ui.interactive) return;
|
|
165
|
+
ui.success("Node.js", process.versions.node);
|
|
166
|
+
ui.success("Codex", `${readiness.installation.version} · ${readiness.installation.executable}`);
|
|
167
|
+
if (readiness.workingDirectory) {
|
|
168
|
+
ui.success("Host project", readiness.workingDirectory);
|
|
169
|
+
} else {
|
|
170
|
+
ui.info("Host project", "not selected; pass --codex-cd when starting Codex");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function pair(flags, ui, options = {}) {
|
|
175
|
+
if (!options.quietHeader && ui.interactive) {
|
|
176
|
+
ui.begin("Pair this Mac", "One-time approval connects this Connector to a Host user");
|
|
177
|
+
}
|
|
178
|
+
const receiver = requireFlag(flags, "receiver");
|
|
179
|
+
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
180
|
+
if (ui.interactive) {
|
|
181
|
+
ui.step("Receiver", receiver);
|
|
182
|
+
}
|
|
183
|
+
const userCode = flags.code ?? await askForPairingCode(ui);
|
|
184
|
+
if (ui.interactive) ui.step("Pairing code", "received");
|
|
185
|
+
const client = new LocalConnectorPairingClient({ baseUrl: receiver });
|
|
186
|
+
const credentials = await client.pair({ userCode }, async ({ verificationUri }) => {
|
|
187
|
+
if (ui.interactive) {
|
|
188
|
+
ui.step("Browser", "press Enter to open the Re-entry approval page");
|
|
189
|
+
ui.info("Open this URL", verificationUri);
|
|
190
|
+
await waitForEnterToOpenBrowser();
|
|
191
|
+
ui.step("Browser", "opening the Re-entry approval page");
|
|
192
|
+
ui.wait("Waiting for you to approve this Mac…");
|
|
193
|
+
} else {
|
|
194
|
+
process.stdout.write(`${JSON.stringify({ event: "pairing_waiting", verification_uri: verificationUri })}\n`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
if (ui.interactive) {
|
|
198
|
+
ui.stopWait("Approval received", "the Receiver approved this Connector");
|
|
199
|
+
if (!credentials.browserOpened) {
|
|
200
|
+
ui.warning("Browser", "did not open automatically; use the URL above");
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
204
|
+
await store.save({
|
|
205
|
+
version: 1,
|
|
206
|
+
receiver_origin: receiver,
|
|
207
|
+
connector_id: credentials.connector_id,
|
|
208
|
+
connector_token: credentials.connector_token,
|
|
209
|
+
connector_expires_at: credentials.connector_expires_at,
|
|
210
|
+
});
|
|
211
|
+
if (ui.interactive) {
|
|
212
|
+
ui.success("This Mac is paired", `credential saved at ${credentialFile}`);
|
|
213
|
+
} else {
|
|
214
|
+
process.stdout.write(`${JSON.stringify({ event: "connector_paired", connector_id: credentials.connector_id })}\n`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function connect(flags, ui, options = {}) {
|
|
219
|
+
if (!options.quietHeader && ui.interactive) {
|
|
220
|
+
ui.begin("Connect this Mac", "One browser approval, then Re-entry stays connected");
|
|
221
|
+
}
|
|
222
|
+
const receiver = flags.receiver ?? DEFAULT_RECEIVER_ORIGIN;
|
|
223
|
+
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
224
|
+
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
225
|
+
const current = await store.load();
|
|
226
|
+
if (current && Date.parse(current.connector_expires_at) > Date.now()) {
|
|
227
|
+
if (ui.interactive) {
|
|
228
|
+
ui.success("Already connected", `${current.connector_id} · ${current.receiver_origin}`);
|
|
229
|
+
ui.info("Next", "run `re-entry start` to wait for approved work");
|
|
230
|
+
} else {
|
|
231
|
+
process.stdout.write(`${JSON.stringify({
|
|
232
|
+
event: "connector_already_connected",
|
|
233
|
+
connector_id: current.connector_id,
|
|
234
|
+
receiver_origin: current.receiver_origin,
|
|
235
|
+
})}\n`);
|
|
236
|
+
}
|
|
237
|
+
return current;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
if (ui.interactive) {
|
|
241
|
+
ui.step("Re-entry", receiver);
|
|
242
|
+
ui.step("This Mac", flags["device-name"] ?? defaultDeviceName());
|
|
243
|
+
}
|
|
244
|
+
const client = new LocalConnectorPairingClient({ baseUrl: receiver });
|
|
245
|
+
const credentials = await client.connect(
|
|
246
|
+
{ deviceName: flags["device-name"] ?? defaultDeviceName() },
|
|
247
|
+
async ({ verificationUri }) => {
|
|
248
|
+
if (ui.interactive) {
|
|
249
|
+
ui.step("Browser", "press Enter to open Re-entry sign-in and approval");
|
|
250
|
+
ui.info("If it does not open", verificationUri);
|
|
251
|
+
await waitForEnterToOpenBrowser();
|
|
252
|
+
ui.step("Browser", "opening Re-entry sign-in and approval");
|
|
253
|
+
ui.wait("Waiting for you to connect this Mac…");
|
|
254
|
+
} else {
|
|
255
|
+
process.stdout.write(`${JSON.stringify({
|
|
256
|
+
event: "connector_authorization_waiting",
|
|
257
|
+
verification_uri: verificationUri,
|
|
258
|
+
})}\n`);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
);
|
|
262
|
+
if (ui.interactive) {
|
|
263
|
+
ui.stopWait("Approved", "Re-entry linked this Mac to your account");
|
|
264
|
+
if (!credentials.browserOpened) {
|
|
265
|
+
ui.warning("Browser", "did not open automatically; use the URL shown above");
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
const saved = {
|
|
269
|
+
version: 1,
|
|
270
|
+
receiver_origin: receiver,
|
|
271
|
+
connector_id: credentials.connector_id,
|
|
272
|
+
connector_token: credentials.connector_token,
|
|
273
|
+
connector_expires_at: credentials.connector_expires_at,
|
|
274
|
+
};
|
|
275
|
+
await store.save(saved);
|
|
276
|
+
if (ui.interactive) {
|
|
277
|
+
ui.success("Connected", `credential saved at ${credentialFile}`);
|
|
278
|
+
ui.info("Next", "run `re-entry start` once; it will keep waiting in the background");
|
|
279
|
+
} else {
|
|
280
|
+
process.stdout.write(`${JSON.stringify({
|
|
281
|
+
event: "connector_connected",
|
|
282
|
+
connector_id: credentials.connector_id,
|
|
283
|
+
})}\n`);
|
|
284
|
+
}
|
|
285
|
+
return saved;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async function status(flags, ui) {
|
|
289
|
+
if (ui.interactive) ui.begin("Connector status", "Account, background service, Receiver, and Codex");
|
|
290
|
+
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
291
|
+
const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
|
|
292
|
+
const readiness = inspectReadiness(flags);
|
|
293
|
+
const service = await inspectMacConnectorService();
|
|
294
|
+
const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
|
|
295
|
+
showReadiness(readiness, ui);
|
|
296
|
+
const connected = Boolean(credentials && Date.parse(credentials.connector_expires_at) > Date.now());
|
|
297
|
+
if (ui.interactive) {
|
|
298
|
+
if (connected) ui.success("Account", `${credentials.connector_id} · authorized`);
|
|
299
|
+
else ui.warning("Re-entry", "not connected; run `re-entry connect`");
|
|
300
|
+
if (service.running) ui.success("Background", "running at login");
|
|
301
|
+
else if (service.installed) ui.warning("Background", "installed but not running; run `re-entry install`");
|
|
302
|
+
else ui.warning("Background", "not installed; run `re-entry install`");
|
|
303
|
+
if (connected && receiverReady) ui.success("Receiver", `${credentials.receiver_origin} · reachable`);
|
|
304
|
+
else if (connected) ui.warning("Receiver", `${credentials.receiver_origin} · unavailable`);
|
|
305
|
+
} else {
|
|
306
|
+
process.stdout.write(`${JSON.stringify({
|
|
307
|
+
event: "connector_status",
|
|
308
|
+
connected,
|
|
309
|
+
connector_id: credentials?.connector_id ?? null,
|
|
310
|
+
receiver_origin: credentials?.receiver_origin ?? null,
|
|
311
|
+
receiver_ready: receiverReady,
|
|
312
|
+
service_installed: service.installed,
|
|
313
|
+
service_running: service.running,
|
|
314
|
+
codex_binary: readiness.installation.executable,
|
|
315
|
+
codex_version: readiness.installation.version,
|
|
316
|
+
codex_working_directory: readiness.workingDirectory,
|
|
317
|
+
})}\n`);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
async function install(flags, ui) {
|
|
322
|
+
if (ui.interactive) {
|
|
323
|
+
ui.begin("Install Re-entry", "Check Codex, connect your account, then start at login");
|
|
324
|
+
}
|
|
325
|
+
const runtimeFlags = {
|
|
326
|
+
...flags,
|
|
327
|
+
"codex-cd": flags["codex-cd"] ?? process.cwd(),
|
|
328
|
+
};
|
|
329
|
+
const readiness = inspectReadiness(runtimeFlags);
|
|
330
|
+
showReadiness(readiness, ui);
|
|
331
|
+
const credentials = await connect(runtimeFlags, ui, { quietHeader: true });
|
|
332
|
+
if (ui.interactive) ui.wait("Installing the background Connector…");
|
|
333
|
+
const service = await installMacConnectorService({
|
|
334
|
+
nodeExecutable: process.execPath,
|
|
335
|
+
entrypoint: fileURLToPath(import.meta.url),
|
|
336
|
+
workingDirectory: readiness.workingDirectory,
|
|
337
|
+
credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
|
|
338
|
+
});
|
|
339
|
+
if (ui.interactive) {
|
|
340
|
+
ui.stopWait("Installed", "Re-entry will start automatically when you log in");
|
|
341
|
+
ui.success("Account", credentials.connector_id);
|
|
342
|
+
ui.info("Logs", service.stdoutPath);
|
|
343
|
+
ui.info("You are done", "the Connector now waits in the background");
|
|
344
|
+
} else {
|
|
345
|
+
process.stdout.write(`${JSON.stringify({
|
|
346
|
+
event: "connector_service_installed",
|
|
347
|
+
connector_id: credentials.connector_id,
|
|
348
|
+
service_label: service.label,
|
|
349
|
+
plist_path: service.plistPath,
|
|
350
|
+
log_path: service.stdoutPath,
|
|
351
|
+
})}\n`);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function start(flags, ui) {
|
|
356
|
+
if (ui.interactive) {
|
|
357
|
+
ui.begin("Re-entry is starting", "Connect once, then wait quietly for work you approve");
|
|
358
|
+
}
|
|
359
|
+
const runtimeFlags = {
|
|
360
|
+
...flags,
|
|
361
|
+
"codex-cd": flags["codex-cd"] ?? process.cwd(),
|
|
362
|
+
};
|
|
363
|
+
const readiness = inspectReadiness(runtimeFlags);
|
|
364
|
+
showReadiness(readiness, ui);
|
|
365
|
+
if (!ui.interactive) {
|
|
366
|
+
process.stdout.write(`${JSON.stringify({
|
|
367
|
+
event: "connector_ready",
|
|
368
|
+
node_version: process.versions.node,
|
|
369
|
+
codex_binary: readiness.installation.executable,
|
|
370
|
+
codex_version: readiness.installation.version,
|
|
371
|
+
codex_working_directory: readiness.workingDirectory,
|
|
372
|
+
})}\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
376
|
+
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
377
|
+
let credentials = await store.load();
|
|
378
|
+
if (!credentials) {
|
|
379
|
+
if (ui.interactive) ui.info("Re-entry", "first run — browser approval is required once");
|
|
380
|
+
credentials = await connect(runtimeFlags, ui, { quietHeader: true });
|
|
381
|
+
} else if (ui.interactive) {
|
|
382
|
+
ui.success("Re-entry", "existing account connection loaded");
|
|
383
|
+
}
|
|
384
|
+
if (Date.parse(credentials.connector_expires_at) <= Date.now()) {
|
|
385
|
+
throw cliFailure("connector_credentials_expired");
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const connector = createRuntimeConnector(runtimeFlags, credentials, readiness);
|
|
389
|
+
const pollIntervalMs = readBoundedNumber(
|
|
390
|
+
flags["poll-interval"] ?? DEFAULT_POLL_INTERVAL_MS,
|
|
391
|
+
1_000,
|
|
392
|
+
60_000,
|
|
393
|
+
"connector_poll_interval_invalid",
|
|
394
|
+
);
|
|
395
|
+
const maximumErrors = readBoundedNumber(
|
|
396
|
+
flags["max-errors"] ?? DEFAULT_MAX_CONSECUTIVE_ERRORS,
|
|
397
|
+
1,
|
|
398
|
+
20,
|
|
399
|
+
"connector_max_errors_invalid",
|
|
400
|
+
);
|
|
401
|
+
const stop = createStopSignal();
|
|
402
|
+
let consecutiveErrors = 0;
|
|
403
|
+
if (ui.interactive) ui.wait("Connected. Waiting for approved work…");
|
|
404
|
+
else process.stdout.write('{"event":"connector_waiting"}\n');
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
while (!stop.signal.aborted) {
|
|
408
|
+
try {
|
|
409
|
+
const result = await connector.runOnce();
|
|
410
|
+
consecutiveErrors = 0;
|
|
411
|
+
if (result.status === "activation_result") {
|
|
412
|
+
reportActivation(result, ui);
|
|
413
|
+
if (ui.interactive) ui.wait("Connected. Waiting for approved work…");
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
consecutiveErrors += 1;
|
|
417
|
+
if (ui.interactive) {
|
|
418
|
+
ui.stopWait("Receiver unavailable", `${safeErrorMessage(error)} · ${consecutiveErrors}/${maximumErrors}`, "warning");
|
|
419
|
+
} else {
|
|
420
|
+
process.stderr.write(`${JSON.stringify({
|
|
421
|
+
event: "connector_poll_failed",
|
|
422
|
+
code: error?.code ?? "connector_poll_failed",
|
|
423
|
+
attempt: consecutiveErrors,
|
|
424
|
+
})}\n`);
|
|
425
|
+
}
|
|
426
|
+
if (consecutiveErrors >= maximumErrors) throw error;
|
|
427
|
+
if (ui.interactive) ui.wait("Reconnecting to Re-entry…");
|
|
428
|
+
}
|
|
429
|
+
await waitForNextPoll(pollIntervalMs, stop.signal);
|
|
430
|
+
}
|
|
431
|
+
} finally {
|
|
432
|
+
stop.close();
|
|
433
|
+
if (ui.interactive) ui.stopWait("Connector stopped", "No local credential was removed", "info");
|
|
434
|
+
else process.stdout.write('{"event":"connector_stopped"}\n');
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function inspectReceiver(origin) {
|
|
439
|
+
const controller = new AbortController();
|
|
440
|
+
const timer = setTimeout(() => controller.abort(), 3_000);
|
|
441
|
+
try {
|
|
442
|
+
const response = await fetch(`${origin}/readyz`, {
|
|
443
|
+
method: "GET",
|
|
444
|
+
headers: { Accept: "application/json" },
|
|
445
|
+
redirect: "error",
|
|
446
|
+
signal: controller.signal,
|
|
447
|
+
});
|
|
448
|
+
return response.status === 200;
|
|
449
|
+
} catch {
|
|
450
|
+
return false;
|
|
451
|
+
} finally {
|
|
452
|
+
clearTimeout(timer);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
async function claimOnce(flags, ui, options = {}) {
|
|
457
|
+
if (!options.quietHeader && ui.interactive) {
|
|
458
|
+
ui.begin("Check for approved work", "One-shot delivery check");
|
|
459
|
+
}
|
|
460
|
+
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
461
|
+
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
462
|
+
const credentials = await store.load();
|
|
463
|
+
if (!credentials) throw cliFailure("connector_credentials_missing");
|
|
464
|
+
if (ui.interactive) ui.success("Credentials", "loaded from the local secure file");
|
|
465
|
+
const client = new LocalConnectorClient({
|
|
466
|
+
baseUrl: credentials.receiver_origin,
|
|
467
|
+
connectorToken: credentials.connector_token,
|
|
468
|
+
requestTimeoutMs: Number(flags.timeout ?? 5_000),
|
|
469
|
+
});
|
|
470
|
+
const codexDirectory = flags["codex-cd"];
|
|
471
|
+
if (flags["codex-thread"] !== undefined) {
|
|
472
|
+
throw cliFailure("connector_codex_thread_unsupported");
|
|
473
|
+
}
|
|
474
|
+
if (flags["codex-binary"] !== undefined && codexDirectory === undefined) {
|
|
475
|
+
throw cliFailure("connector_codex_cd_missing");
|
|
476
|
+
}
|
|
477
|
+
const readiness = options.readiness ?? (codexDirectory === undefined ? null : inspectReadiness(flags));
|
|
478
|
+
const workingDirectory = readiness?.workingDirectory;
|
|
479
|
+
if (ui.interactive && !workingDirectory) {
|
|
480
|
+
ui.warning("Codex", "no Host project was selected; this check will report activation as unsupported");
|
|
481
|
+
}
|
|
482
|
+
const connector = createRuntimeConnector(flags, credentials, readiness, client);
|
|
483
|
+
if (ui.interactive) ui.wait("Checking the Receiver and starting Codex if work is ready…");
|
|
484
|
+
const result = await connector.runOnce();
|
|
485
|
+
if (result.status === "idle") {
|
|
486
|
+
if (ui.interactive) {
|
|
487
|
+
ui.stopWait("No pending work", "the Connector is ready and will exit now", "info");
|
|
488
|
+
} else {
|
|
489
|
+
process.stdout.write('{"event":"connector_idle"}\n');
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (ui.interactive) {
|
|
494
|
+
reportActivation(result, ui);
|
|
495
|
+
} else {
|
|
496
|
+
reportActivation(result, ui);
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function createRuntimeConnector(flags, credentials, readiness, existingClient = undefined) {
|
|
501
|
+
const client = existingClient ?? new LocalConnectorClient({
|
|
502
|
+
baseUrl: credentials.receiver_origin,
|
|
503
|
+
connectorToken: credentials.connector_token,
|
|
504
|
+
requestTimeoutMs: Number(flags.timeout ?? 5_000),
|
|
505
|
+
});
|
|
506
|
+
const codexDirectory = flags["codex-cd"];
|
|
507
|
+
const workingDirectory = readiness?.workingDirectory;
|
|
508
|
+
const executable = readiness?.installation.executable;
|
|
509
|
+
const activationTimeoutMs = Number(flags["activation-timeout"] ?? 60_000);
|
|
510
|
+
return new LocalConnector({
|
|
511
|
+
client,
|
|
512
|
+
adapter: codexDirectory === undefined
|
|
513
|
+
? unsupportedAdapter
|
|
514
|
+
: createCodexExecAdapter({
|
|
515
|
+
workingDirectory,
|
|
516
|
+
executable,
|
|
517
|
+
commandTimeoutMs: activationTimeoutMs,
|
|
518
|
+
}),
|
|
519
|
+
clock: () => new Date(),
|
|
520
|
+
activationTimeoutMs,
|
|
521
|
+
createClaimToken: () => randomBytes(32).toString("base64url"),
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function reportActivation(result, ui) {
|
|
526
|
+
if (ui.interactive) {
|
|
527
|
+
const accepted = result.result.outcome === "accepted";
|
|
528
|
+
ui.stopWait(
|
|
529
|
+
accepted ? "Codex session started" : "Activation finished",
|
|
530
|
+
accepted ? "fresh session opened with the approved Re-entry context" : result.result.code,
|
|
531
|
+
accepted ? "success" : "warning",
|
|
532
|
+
);
|
|
533
|
+
ui.info("Delivery", `${result.delivery_id} · ${result.result.outcome}`);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
process.stdout.write(`${JSON.stringify({
|
|
537
|
+
event: "connector_activation_result",
|
|
538
|
+
delivery_id: result.delivery_id,
|
|
539
|
+
event_id: result.event_id,
|
|
540
|
+
outcome: result.result.outcome,
|
|
541
|
+
code: result.result.code,
|
|
542
|
+
})}\n`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const unsupportedAdapter = {
|
|
546
|
+
activate(activation) {
|
|
547
|
+
return {
|
|
548
|
+
type: "webmcp.agent_activation_result",
|
|
549
|
+
protocol_version: "0.1",
|
|
550
|
+
delivery_id: activation.delivery_id,
|
|
551
|
+
event_id: activation.event_id,
|
|
552
|
+
attempt: activation.attempt,
|
|
553
|
+
outcome: "unsupported",
|
|
554
|
+
code: "required_capability_unavailable",
|
|
555
|
+
unavailable_capability: "managed_context_resume",
|
|
556
|
+
};
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
|
|
560
|
+
function parseArguments(argumentsList) {
|
|
561
|
+
const hasLeadingJson = argumentsList[0] === "--json";
|
|
562
|
+
const commandIndex = hasLeadingJson ? 1 : 0;
|
|
563
|
+
const hasExplicitCommand = argumentsList[commandIndex] !== undefined
|
|
564
|
+
&& !argumentsList[commandIndex].startsWith("--");
|
|
565
|
+
const command = hasExplicitCommand ? argumentsList[commandIndex] : "start";
|
|
566
|
+
const rest = [
|
|
567
|
+
...(hasLeadingJson ? ["--json"] : []),
|
|
568
|
+
...(hasExplicitCommand ? argumentsList.slice(commandIndex + 1) : argumentsList.slice(commandIndex)),
|
|
569
|
+
];
|
|
570
|
+
const flags = {};
|
|
571
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
572
|
+
const value = rest[index];
|
|
573
|
+
if (!value.startsWith("--")) throw cliFailure("connector_argument_invalid");
|
|
574
|
+
const name = value.slice(2);
|
|
575
|
+
if (name === "json") {
|
|
576
|
+
if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
|
|
577
|
+
flags[name] = true;
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const next = rest[index + 1];
|
|
581
|
+
if (!name || next === undefined || next.startsWith("--") || Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
|
|
582
|
+
flags[name] = next;
|
|
583
|
+
index += 1;
|
|
584
|
+
}
|
|
585
|
+
return { command, flags };
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function validateCommandFlags(command, flags) {
|
|
589
|
+
const allowedByCommand = {
|
|
590
|
+
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
591
|
+
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
592
|
+
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
593
|
+
status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
|
|
594
|
+
install: new Set([
|
|
595
|
+
"receiver",
|
|
596
|
+
"device-name",
|
|
597
|
+
"credential-file",
|
|
598
|
+
"codex-cd",
|
|
599
|
+
"codex-binary",
|
|
600
|
+
"json",
|
|
601
|
+
]),
|
|
602
|
+
"claim-once": new Set([
|
|
603
|
+
"credential-file",
|
|
604
|
+
"timeout",
|
|
605
|
+
"codex-cd",
|
|
606
|
+
"codex-binary",
|
|
607
|
+
"codex-thread",
|
|
608
|
+
"activation-timeout",
|
|
609
|
+
"json",
|
|
610
|
+
]),
|
|
611
|
+
start: new Set([
|
|
612
|
+
"receiver",
|
|
613
|
+
"device-name",
|
|
614
|
+
"credential-file",
|
|
615
|
+
"timeout",
|
|
616
|
+
"codex-cd",
|
|
617
|
+
"codex-binary",
|
|
618
|
+
"codex-thread",
|
|
619
|
+
"activation-timeout",
|
|
620
|
+
"poll-interval",
|
|
621
|
+
"max-errors",
|
|
622
|
+
"json",
|
|
623
|
+
]),
|
|
624
|
+
};
|
|
625
|
+
const allowed = allowedByCommand[command];
|
|
626
|
+
if (!allowed || Object.keys(flags).some((name) => !allowed.has(name))) {
|
|
627
|
+
throw cliFailure("connector_argument_invalid");
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function requireFlag(flags, name) {
|
|
632
|
+
const value = flags[name];
|
|
633
|
+
if (typeof value !== "string" || value.length === 0) throw cliFailure(`connector_${name.replaceAll("-", "_")}_missing`);
|
|
634
|
+
return value;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function defaultCredentialFile() {
|
|
638
|
+
return join(homedir(), ".webmcp-connector", "credentials.json");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function defaultDeviceName() {
|
|
642
|
+
const value = hostname().trim();
|
|
643
|
+
return value.length >= 2 && Buffer.byteLength(value, "utf8") <= 80
|
|
644
|
+
? value
|
|
645
|
+
: "This Mac";
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function readBoundedNumber(value, minimum, maximum, code) {
|
|
649
|
+
const number = typeof value === "number" ? value : Number(value);
|
|
650
|
+
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
651
|
+
throw cliFailure(code);
|
|
652
|
+
}
|
|
653
|
+
return number;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function createStopSignal() {
|
|
657
|
+
const controller = new AbortController();
|
|
658
|
+
const stop = () => controller.abort();
|
|
659
|
+
process.once("SIGINT", stop);
|
|
660
|
+
process.once("SIGTERM", stop);
|
|
661
|
+
return {
|
|
662
|
+
signal: controller.signal,
|
|
663
|
+
close() {
|
|
664
|
+
process.off("SIGINT", stop);
|
|
665
|
+
process.off("SIGTERM", stop);
|
|
666
|
+
},
|
|
667
|
+
};
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function waitForNextPoll(milliseconds, signal) {
|
|
671
|
+
if (signal.aborted) return Promise.resolve();
|
|
672
|
+
return new Promise((resolve) => {
|
|
673
|
+
const timer = setTimeout(done, milliseconds);
|
|
674
|
+
function done() {
|
|
675
|
+
clearTimeout(timer);
|
|
676
|
+
signal.removeEventListener("abort", done);
|
|
677
|
+
resolve();
|
|
678
|
+
}
|
|
679
|
+
signal.addEventListener("abort", done, { once: true });
|
|
680
|
+
});
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function cliFailure(code) {
|
|
684
|
+
const error = new Error(code);
|
|
685
|
+
error.code = code;
|
|
686
|
+
return error;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function safeErrorMessage(error) {
|
|
690
|
+
if (typeof error?.message !== "string" || error.message.length === 0) {
|
|
691
|
+
return "The Local Connector could not complete the command.";
|
|
692
|
+
}
|
|
693
|
+
return error.message.length > 240
|
|
694
|
+
? `${error.message.slice(0, 237)}...`
|
|
695
|
+
: error.message;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
async function askForPairingCode(ui) {
|
|
699
|
+
if (!ui.interactive || process.stdin.isTTY !== true) {
|
|
700
|
+
throw cliFailure("connector_pairing_code_missing");
|
|
701
|
+
}
|
|
702
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
703
|
+
try {
|
|
704
|
+
const answer = await readline.question(" Enter the pairing code from your Host (XXXX-XXXX-XXXX-XXXX): ");
|
|
705
|
+
return answer.trim();
|
|
706
|
+
} finally {
|
|
707
|
+
readline.close();
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
function errorHint(error) {
|
|
712
|
+
const hints = {
|
|
713
|
+
connector_codex_cd_missing: "pass --codex-cd /absolute/path/to/your/Host-project",
|
|
714
|
+
connector_codex_not_found: "install Codex, add it to PATH, or pass --codex-binary /path/to/codex",
|
|
715
|
+
connector_codex_binary_not_found: "check --codex-binary or remove it to use automatic discovery",
|
|
716
|
+
connector_codex_unusable: "open Codex, complete login if needed, then run `npm run doctor` again",
|
|
717
|
+
connector_node_unsupported: "use Node.js 24 or newer, then run the command again",
|
|
718
|
+
connector_credentials_missing: "connect this Mac once with `re-entry connect`",
|
|
719
|
+
connector_credentials_expired: "run `re-entry connect` to authorize this Mac again",
|
|
720
|
+
connector_pairing_code_missing: "ask the Host backend for a new pairing code, then run pair in a terminal",
|
|
721
|
+
pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
|
|
722
|
+
host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
|
|
723
|
+
pairing_expired: "ask the Host backend for a new pairing code",
|
|
724
|
+
device_authorization_expired: "run `re-entry connect` again and approve within ten minutes",
|
|
725
|
+
device_authorization_denied: "run `re-entry connect` again when you are ready to approve this Mac",
|
|
726
|
+
connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
|
|
727
|
+
connector_max_errors_invalid: "use a maximum error count between 1 and 20",
|
|
728
|
+
connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
|
|
729
|
+
};
|
|
730
|
+
return hints[error?.code] ?? "check the Receiver address and try again";
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
await main();
|