@4xeoz/re-entry 0.2.7 → 0.2.9
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 +16 -15
- package/package.json +1 -1
- package/src/main.mjs +30 -30
- package/src/pairing-client.mjs +38 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Re-entry Local Connector
|
|
2
2
|
|
|
3
|
-
Install it once on the Mac where Codex should open; after one
|
|
3
|
+
Install it once on the Mac where Codex should open; after one dashboard pairing code, a macOS
|
|
4
4
|
LaunchAgent keeps the outbound Connector running at login.
|
|
5
5
|
|
|
6
6
|
> Current boundary: this is a verified macOS Connector preview with a publish-ready npm package.
|
|
@@ -67,11 +67,10 @@ npx --yes --package=@4xeoz/re-entry re-entry install \
|
|
|
67
67
|
```text
|
|
68
68
|
choose Desktop, the current folder, or another Codex workspace
|
|
69
69
|
-> check Node + find Codex + validate the selected directory
|
|
70
|
-
->
|
|
71
|
-
->
|
|
72
|
-
->
|
|
73
|
-
->
|
|
74
|
-
-> approve this Mac once
|
|
70
|
+
-> open the dedicated Re-entry user account page in the default browser
|
|
71
|
+
-> sign in or create a user account
|
|
72
|
+
-> click Pair this Mac in the dashboard
|
|
73
|
+
-> enter the short pairing code in the CLI
|
|
75
74
|
-> save the device credential with mode 0600
|
|
76
75
|
-> install and start a per-user macOS LaunchAgent
|
|
77
76
|
```
|
|
@@ -133,8 +132,8 @@ Copy this prompt into a coding-agent task:
|
|
|
133
132
|
```text
|
|
134
133
|
Install the Re-entry Local Connector on this Mac. First read the package README. Verify Node.js 24
|
|
135
134
|
or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
|
|
136
|
-
project directory. Let the human
|
|
137
|
-
|
|
135
|
+
project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
|
|
136
|
+
enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
|
|
138
137
|
into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
|
|
139
138
|
results without claiming Browser/WebMCP or production deployment.
|
|
140
139
|
```
|
|
@@ -142,7 +141,7 @@ results without claiming Browser/WebMCP or production deployment.
|
|
|
142
141
|
## Package map
|
|
143
142
|
|
|
144
143
|
- `src/main.mjs` — `re-entry` CLI and long-running poll loop.
|
|
145
|
-
- `src/pairing-client.mjs` — account-
|
|
144
|
+
- `src/pairing-client.mjs` — account pairing-code redemption and legacy pairing compatibility.
|
|
146
145
|
- `src/credentials.mjs` — atomic local credential storage.
|
|
147
146
|
- `src/macos-service.mjs` — per-user LaunchAgent install, stop, uninstall, and status.
|
|
148
147
|
- `src/workspace-picker.mjs` — interactive Codex workspace selection.
|
|
@@ -251,9 +250,10 @@ npm start -- \
|
|
|
251
250
|
--codex-cd "$HOME/Code/my-host-project"
|
|
252
251
|
```
|
|
253
252
|
|
|
254
|
-
On the first run, the Connector checks the Mac,
|
|
255
|
-
|
|
256
|
-
|
|
253
|
+
On the first run, the Connector checks the Mac, opens the dedicated Re-entry user account page,
|
|
254
|
+
waits for you to create or sign in to an account and click Pair this Mac, then asks for the
|
|
255
|
+
dashboard code and stores the credential. On later runs it reuses the saved credential and skips
|
|
256
|
+
pairing:
|
|
257
257
|
|
|
258
258
|
```sh
|
|
259
259
|
npm start -- --codex-cd "$HOME/Code/my-host-project"
|
|
@@ -271,13 +271,13 @@ events instead so another process can consume it:
|
|
|
271
271
|
npm start -- --json --codex-cd "$HOME/Code/my-host-project"
|
|
272
272
|
```
|
|
273
273
|
|
|
274
|
-
##
|
|
274
|
+
## Compatibility: Host-code pairing (legacy)
|
|
275
275
|
|
|
276
276
|
Pairing connects one local Connector to one already-authenticated Host user. It is separate from
|
|
277
277
|
the Host organization's API key and does not give the Connector permission to create Grants or
|
|
278
278
|
send Host events.
|
|
279
279
|
|
|
280
|
-
The flow is:
|
|
280
|
+
The older compatibility flow is:
|
|
281
281
|
|
|
282
282
|
```text
|
|
283
283
|
Host backend -> Receiver: start pairing for its Host-user reference
|
|
@@ -290,7 +290,8 @@ Receiver -> Connector: one Connector credential
|
|
|
290
290
|
Connector -> local disk: save the credential with mode 0600
|
|
291
291
|
```
|
|
292
292
|
|
|
293
|
-
The
|
|
293
|
+
The current preview does not use this Host-issued code path. Use `re-entry install` above, where
|
|
294
|
+
the authenticated Re-entry dashboard creates the code.
|
|
294
295
|
|
|
295
296
|
```sh
|
|
296
297
|
curl -s -X POST http://127.0.0.1:43218/v0.1/pairing-sessions \
|
package/package.json
CHANGED
package/src/main.mjs
CHANGED
|
@@ -24,8 +24,7 @@ import {
|
|
|
24
24
|
hasConnectorReauthorizationRequired,
|
|
25
25
|
markConnectorReauthorizationRequired,
|
|
26
26
|
} from "./credentials.mjs";
|
|
27
|
-
import { LocalConnectorPairingClient } from "./pairing-client.mjs";
|
|
28
|
-
import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
|
|
27
|
+
import { LocalConnectorPairingClient, openBrowser } from "./pairing-client.mjs";
|
|
29
28
|
import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
|
|
30
29
|
import {
|
|
31
30
|
inspectMacConnectorService,
|
|
@@ -135,7 +134,7 @@ Commands:
|
|
|
135
134
|
test Start one fresh local Codex session with the supplied prompt
|
|
136
135
|
stop Stop the background Connector without removing its credential
|
|
137
136
|
uninstall Stop the Connector and remove its local service data
|
|
138
|
-
connect
|
|
137
|
+
connect Open Re-entry, redeem a dashboard pairing code, and connect this Mac
|
|
139
138
|
start Poll for approved work until stopped
|
|
140
139
|
doctor Check Node.js, Codex, and the optional project directory
|
|
141
140
|
claim-once Check once for approved work
|
|
@@ -272,7 +271,7 @@ async function pair(flags, ui, options = {}) {
|
|
|
272
271
|
|
|
273
272
|
async function connect(flags, ui, options = {}) {
|
|
274
273
|
if (!options.quietHeader && ui.interactive) {
|
|
275
|
-
ui.begin("Connect this Mac", "
|
|
274
|
+
ui.begin("Connect this Mac", "Create a Re-entry account, pair this Mac, then leave it running");
|
|
276
275
|
}
|
|
277
276
|
const receiver = flags.receiver ?? DEFAULT_RECEIVER_ORIGIN;
|
|
278
277
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
@@ -308,28 +307,28 @@ async function connect(flags, ui, options = {}) {
|
|
|
308
307
|
baseUrl: receiver,
|
|
309
308
|
requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
|
|
310
309
|
});
|
|
311
|
-
|
|
312
|
-
const credentials = await client.connect(
|
|
313
|
-
{ deviceName: flags["device-name"] ?? defaultDeviceName() },
|
|
314
|
-
async ({ verificationUri }) => {
|
|
315
|
-
if (ui.interactive) {
|
|
316
|
-
ui.stopWait("Secure link", "ready");
|
|
317
|
-
ui.info("Browser link", verificationUri);
|
|
318
|
-
await waitForEnterToOpenBrowser();
|
|
319
|
-
ui.wait("Waiting for approval in your browser…");
|
|
320
|
-
} else {
|
|
321
|
-
process.stdout.write(`${JSON.stringify({
|
|
322
|
-
event: "connector_authorization_waiting",
|
|
323
|
-
verification_uri: verificationUri,
|
|
324
|
-
})}\n`);
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
);
|
|
310
|
+
const portalUrl = `${receiver}/user-register?next=${encodeURIComponent("/dashboard")}`;
|
|
328
311
|
if (ui.interactive) {
|
|
329
|
-
ui.stopWait("
|
|
330
|
-
|
|
331
|
-
|
|
312
|
+
ui.stopWait("Secure link", "ready");
|
|
313
|
+
ui.info("Re-entry", portalUrl);
|
|
314
|
+
ui.info("Next", "Create or sign in, click Pair this Mac, then return here with the code.");
|
|
315
|
+
if (!(await openBrowser(portalUrl))) {
|
|
316
|
+
ui.warning("Browser", "could not open automatically; use the URL above");
|
|
332
317
|
}
|
|
318
|
+
} else {
|
|
319
|
+
process.stdout.write(`${JSON.stringify({ event: "connector_account_pairing", portal_url: portalUrl })}\n`);
|
|
320
|
+
}
|
|
321
|
+
const pairingCode = flags["pairing-code"] ?? await askForPairingCode(
|
|
322
|
+
ui,
|
|
323
|
+
" Enter the pairing code shown in your Re-entry dashboard (XXXX-XXXX): ",
|
|
324
|
+
);
|
|
325
|
+
if (ui.interactive) ui.step("Pairing code", "received");
|
|
326
|
+
const credentials = await client.connectWithPairingCode({
|
|
327
|
+
pairingCode,
|
|
328
|
+
deviceName: flags["device-name"] ?? defaultDeviceName(),
|
|
329
|
+
});
|
|
330
|
+
if (ui.interactive) {
|
|
331
|
+
ui.success("Account", "connected");
|
|
333
332
|
}
|
|
334
333
|
const saved = {
|
|
335
334
|
version: 1,
|
|
@@ -572,7 +571,7 @@ async function install(flags, ui) {
|
|
|
572
571
|
ui.success("Selected", readiness.workingDirectory);
|
|
573
572
|
ui.section("2 OF 3", "System check");
|
|
574
573
|
showReadiness(readiness, ui, { includeWorkspace: false });
|
|
575
|
-
ui.section("3 OF 3", "Connect Re-entry", "
|
|
574
|
+
ui.section("3 OF 3", "Connect Re-entry", "Create or sign in to your account, then pair this Mac from the dashboard.");
|
|
576
575
|
}
|
|
577
576
|
const credentials = await connect(runtimeFlags, ui, { quietHeader: true, guidedInstall: true });
|
|
578
577
|
if (ui.interactive) ui.wait("Starting Re-entry in the background…");
|
|
@@ -619,12 +618,12 @@ async function start(flags, ui) {
|
|
|
619
618
|
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
620
619
|
let credentials = await store.load();
|
|
621
620
|
if (!credentials) {
|
|
622
|
-
if (ui.interactive) ui.info("Re-entry", "first run —
|
|
621
|
+
if (ui.interactive) ui.info("Re-entry", "first run — create an account and enter a dashboard pairing code");
|
|
623
622
|
credentials = await connect(runtimeFlags, ui, { quietHeader: true });
|
|
624
623
|
} else if (await hasConnectorReauthorizationRequired(credentialFile)) {
|
|
625
624
|
if (ui.interactive) {
|
|
626
625
|
ui.warning("Account", "this Mac needs to be connected again");
|
|
627
|
-
ui.next("re-entry connect", "
|
|
626
|
+
ui.next("re-entry connect", "Create a new dashboard pairing code and connect this Mac again.");
|
|
628
627
|
} else {
|
|
629
628
|
process.stdout.write('{"event":"connector_reauthorization_required"}\n');
|
|
630
629
|
}
|
|
@@ -857,7 +856,7 @@ function validateCommandFlags(command, flags, positionals) {
|
|
|
857
856
|
const allowedByCommand = {
|
|
858
857
|
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
859
858
|
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
860
|
-
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
859
|
+
connect: new Set(["receiver", "device-name", "credential-file", "pairing-code", "json"]),
|
|
861
860
|
status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
|
|
862
861
|
listen: new Set(["json"]),
|
|
863
862
|
test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
|
|
@@ -869,6 +868,7 @@ function validateCommandFlags(command, flags, positionals) {
|
|
|
869
868
|
"credential-file",
|
|
870
869
|
"codex-cd",
|
|
871
870
|
"codex-binary",
|
|
871
|
+
"pairing-code",
|
|
872
872
|
"json",
|
|
873
873
|
]),
|
|
874
874
|
"claim-once": new Set([
|
|
@@ -1004,13 +1004,13 @@ function safeErrorMessage(error) {
|
|
|
1004
1004
|
: error.message;
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
-
async function askForPairingCode(ui) {
|
|
1007
|
+
async function askForPairingCode(ui, prompt = " Enter the pairing code: ") {
|
|
1008
1008
|
if (!ui.interactive || process.stdin.isTTY !== true) {
|
|
1009
1009
|
throw cliFailure("connector_pairing_code_missing");
|
|
1010
1010
|
}
|
|
1011
1011
|
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1012
1012
|
try {
|
|
1013
|
-
const answer = await readline.question(
|
|
1013
|
+
const answer = await readline.question(prompt);
|
|
1014
1014
|
return answer.trim();
|
|
1015
1015
|
} finally {
|
|
1016
1016
|
readline.close();
|
package/src/pairing-client.mjs
CHANGED
|
@@ -2,15 +2,21 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { TextDecoder } from "node:util";
|
|
3
3
|
|
|
4
4
|
export const PAIRING_CLIENT_ROUTES = Object.freeze({
|
|
5
|
+
accountPairingClaim: "/v0.1/account/pairing-sessions/claim",
|
|
5
6
|
claim: "/v0.1/pairing-sessions/claim",
|
|
6
7
|
poll: "/v0.1/pairing-sessions/poll",
|
|
7
8
|
deviceStart: "/v0.1/device-authorizations",
|
|
8
9
|
devicePoll: "/v0.1/device-authorizations/poll",
|
|
9
10
|
});
|
|
10
11
|
|
|
12
|
+
export async function openBrowser(url) {
|
|
13
|
+
return defaultOpenBrowser(url);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
const OPTION_FIELDS = Object.freeze(["baseUrl", "requestTimeoutMs", "openBrowser", "sleep"]);
|
|
12
17
|
const PAIR_INPUT_FIELDS = Object.freeze(["userCode"]);
|
|
13
18
|
const CONNECT_INPUT_FIELDS = Object.freeze(["deviceName"]);
|
|
19
|
+
const ACCOUNT_PAIRING_INPUT_FIELDS = Object.freeze(["pairingCode", "deviceName"]);
|
|
14
20
|
const DEVICE_AUTHORIZATION_FIELDS = Object.freeze([
|
|
15
21
|
"type",
|
|
16
22
|
"protocol_version",
|
|
@@ -68,6 +74,7 @@ const ERROR_BODY_FIELDS = Object.freeze(["code"]);
|
|
|
68
74
|
const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
|
|
69
75
|
const TOKEN_PATTERN = /^[A-Za-z0-9_-]{43}$/;
|
|
70
76
|
const USER_CODE_PATTERN = /^[A-F0-9]{16}$/;
|
|
77
|
+
const ACCOUNT_PAIRING_CODE_PATTERN = /^[A-F0-9]{8}$/;
|
|
71
78
|
const CONTENT_TYPE_PATTERN = /^application\/json(?:\s*;\s*charset=utf-8)?$/i;
|
|
72
79
|
const MIN_REQUEST_TIMEOUT_MS = 100;
|
|
73
80
|
const MAX_REQUEST_TIMEOUT_MS = 60_000;
|
|
@@ -197,6 +204,26 @@ export class LocalConnectorPairingClient {
|
|
|
197
204
|
}
|
|
198
205
|
}
|
|
199
206
|
|
|
207
|
+
async connectWithPairingCode(input) {
|
|
208
|
+
requireExactRecord(
|
|
209
|
+
input,
|
|
210
|
+
ACCOUNT_PAIRING_INPUT_FIELDS,
|
|
211
|
+
ACCOUNT_PAIRING_INPUT_FIELDS,
|
|
212
|
+
"Account pairing input",
|
|
213
|
+
);
|
|
214
|
+
const pairingCode = normalizeAccountPairingCode(input.pairingCode);
|
|
215
|
+
const deviceName = requireDeviceName(input.deviceName);
|
|
216
|
+
const response = await this.#post(PAIRING_CLIENT_ROUTES.accountPairingClaim, {
|
|
217
|
+
pairing_code: pairingCode,
|
|
218
|
+
device_name: deviceName,
|
|
219
|
+
});
|
|
220
|
+
if (response.status !== 200) throw await parseHttpFailure(response);
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
...normalizeCredentials(await parseJsonResponse(response)),
|
|
223
|
+
browserOpened: false,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
200
227
|
async #post(path, body) {
|
|
201
228
|
let response;
|
|
202
229
|
try {
|
|
@@ -331,6 +358,17 @@ function normalizeCredentials(value) {
|
|
|
331
358
|
};
|
|
332
359
|
}
|
|
333
360
|
|
|
361
|
+
function normalizeAccountPairingCode(value) {
|
|
362
|
+
if (typeof value !== "string") {
|
|
363
|
+
throw pairingFailure("account_pairing_code_invalid", "Pairing code is invalid");
|
|
364
|
+
}
|
|
365
|
+
const normalized = value.replaceAll("-", "").trim().toUpperCase();
|
|
366
|
+
if (!ACCOUNT_PAIRING_CODE_PATTERN.test(normalized)) {
|
|
367
|
+
throw pairingFailure("account_pairing_code_invalid", "Pairing code is invalid");
|
|
368
|
+
}
|
|
369
|
+
return normalized;
|
|
370
|
+
}
|
|
371
|
+
|
|
334
372
|
async function parseJsonResponse(response) {
|
|
335
373
|
const declared = response.headers.get("content-length");
|
|
336
374
|
if (declared !== null && (!/^(?:0|[1-9][0-9]*)$/.test(declared) || Number(declared) > MAX_RESPONSE_BYTES)) {
|