@4xeoz/re-entry 0.2.7 → 0.2.8
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 -14
- 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,11 @@ 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
|
-
-> show the verification URL and wait for the user to press Enter
|
|
70
|
+
-> show the Re-entry account URL and wait for the user to press Enter
|
|
72
71
|
-> open Re-entry in the default browser
|
|
73
|
-
-> sign in or create
|
|
74
|
-
->
|
|
72
|
+
-> sign in or create a user account
|
|
73
|
+
-> click Pair this Mac in the dashboard
|
|
74
|
+
-> enter the short pairing code in the CLI
|
|
75
75
|
-> save the device credential with mode 0600
|
|
76
76
|
-> install and start a per-user macOS LaunchAgent
|
|
77
77
|
```
|
|
@@ -133,8 +133,8 @@ Copy this prompt into a coding-agent task:
|
|
|
133
133
|
```text
|
|
134
134
|
Install the Re-entry Local Connector on this Mac. First read the package README. Verify Node.js 24
|
|
135
135
|
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
|
-
|
|
136
|
+
project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
|
|
137
|
+
enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
|
|
138
138
|
into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
|
|
139
139
|
results without claiming Browser/WebMCP or production deployment.
|
|
140
140
|
```
|
|
@@ -142,7 +142,7 @@ results without claiming Browser/WebMCP or production deployment.
|
|
|
142
142
|
## Package map
|
|
143
143
|
|
|
144
144
|
- `src/main.mjs` — `re-entry` CLI and long-running poll loop.
|
|
145
|
-
- `src/pairing-client.mjs` — account-
|
|
145
|
+
- `src/pairing-client.mjs` — account pairing-code redemption and legacy pairing compatibility.
|
|
146
146
|
- `src/credentials.mjs` — atomic local credential storage.
|
|
147
147
|
- `src/macos-service.mjs` — per-user LaunchAgent install, stop, uninstall, and status.
|
|
148
148
|
- `src/workspace-picker.mjs` — interactive Codex workspace selection.
|
|
@@ -251,9 +251,10 @@ npm start -- \
|
|
|
251
251
|
--codex-cd "$HOME/Code/my-host-project"
|
|
252
252
|
```
|
|
253
253
|
|
|
254
|
-
On the first run, the Connector checks the Mac,
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
On the first run, the Connector checks the Mac, opens the Re-entry account page, waits for you to
|
|
255
|
+
create or sign in to an account and click Pair this Mac, then asks for the
|
|
256
|
+
dashboard code and stores the credential. On later runs it reuses the saved credential and skips
|
|
257
|
+
pairing:
|
|
257
258
|
|
|
258
259
|
```sh
|
|
259
260
|
npm start -- --codex-cd "$HOME/Code/my-host-project"
|
|
@@ -271,13 +272,13 @@ events instead so another process can consume it:
|
|
|
271
272
|
npm start -- --json --codex-cd "$HOME/Code/my-host-project"
|
|
272
273
|
```
|
|
273
274
|
|
|
274
|
-
##
|
|
275
|
+
## Compatibility: Host-code pairing (legacy)
|
|
275
276
|
|
|
276
277
|
Pairing connects one local Connector to one already-authenticated Host user. It is separate from
|
|
277
278
|
the Host organization's API key and does not give the Connector permission to create Grants or
|
|
278
279
|
send Host events.
|
|
279
280
|
|
|
280
|
-
The flow is:
|
|
281
|
+
The older compatibility flow is:
|
|
281
282
|
|
|
282
283
|
```text
|
|
283
284
|
Host backend -> Receiver: start pairing for its Host-user reference
|
|
@@ -290,7 +291,8 @@ Receiver -> Connector: one Connector credential
|
|
|
290
291
|
Connector -> local disk: save the credential with mode 0600
|
|
291
292
|
```
|
|
292
293
|
|
|
293
|
-
The
|
|
294
|
+
The current preview does not use this Host-issued code path. Use `re-entry install` above, where
|
|
295
|
+
the authenticated Re-entry dashboard creates the code.
|
|
294
296
|
|
|
295
297
|
```sh
|
|
296
298
|
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}/register?flow=pair&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)) {
|