@4xeoz/re-entry 0.2.11 → 0.2.14

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Re-entry Local Connector
2
2
 
3
+ > **Cloud Receiver dependency notice — 2026-09-02:** The former `re-entry-weld.vercel.app` Cloud
4
+ > Receiver is deprecated and must not be used for new pairing, credentials, or production traffic.
5
+ > This Connector package remains a reusable/local preview surface, but a new Receiver origin must
6
+ > be supplied explicitly by an accepted replacement service.
7
+
3
8
  Install it once on the Mac where Codex should open; after one dashboard pairing code, a macOS
4
9
  LaunchAgent keeps the outbound Connector running at login.
5
10
 
@@ -13,7 +18,8 @@ Requirements: macOS, Node.js 24+, Codex installed and signed in, and an absolute
13
18
  Codex may read and write.
14
19
 
15
20
  ```sh
16
- npx @4xeoz/re-entry install
21
+ npx --yes --package=@4xeoz/re-entry re-entry install \
22
+ --receiver https://your-accepted-receiver.example
17
23
  ```
18
24
 
19
25
  `npx` runs a temporary copy, so it does not leave a permanent shell command behind. Install the
@@ -24,25 +30,25 @@ npm install --global @4xeoz/re-entry
24
30
  ```
25
31
 
26
32
  The package installs both `re-entry` and the older `reentry` spelling; `re-entry` is the documented
27
- command.
33
+ command. If you keep using the temporary `npx` form, run later commands with the same executable
34
+ prefix, for example `npx --yes --package=@4xeoz/re-entry re-entry status`.
28
35
 
29
- For the retained hosted preview, pass the deployment alias explicitly:
30
- `https://re-entry-weld.vercel.app`.
31
-
32
- The currently published CLI source still has its previous hosted default. Until a new package
33
- release repoints that default, use `--receiver https://re-entry-weld.vercel.app` for hosted tests.
36
+ The CLI no longer has a default hosted Receiver. For any new or historical test, pass an explicit
37
+ accepted Receiver origin with `--receiver` or `REENTRY_RECEIVER_ORIGIN`; the former
38
+ `https://re-entry-weld.vercel.app` alias is retired.
34
39
 
35
40
  The interactive CLI first offers Desktop, the current folder, or a small folder browser. Choose
36
41
  with ↑/↓ and Enter. If you want to skip the picker, pass the workspace directly:
37
42
 
38
43
  ```sh
39
- npx @4xeoz/re-entry install \
44
+ npx --yes --package=@4xeoz/re-entry re-entry install \
45
+ --receiver https://your-accepted-receiver.example \
40
46
  --codex-cd /absolute/path/to/your/project
41
47
  ```
42
48
 
43
49
  The guided screen stays intentionally small: **Workspace → System check → Connect Re-entry**. It
44
- shows one clear next command when setup finishes; internal Connector IDs, credential paths, and log
45
- paths stay out of the normal success screen.
50
+ shows one executable next command when setup finishes; internal Connector IDs, credential paths, and
51
+ log paths stay out of the normal success screen.
46
52
 
47
53
  Run this from the Host project directory, your home directory, or another normal working
48
54
  directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
@@ -53,11 +59,12 @@ installation instead:
53
59
 
54
60
  ```sh
55
61
  npm install --global @4xeoz/re-entry
56
- re-entry install --codex-cd /absolute/path/to/your/project
62
+ re-entry install \
63
+ --receiver https://your-accepted-receiver.example \
64
+ --codex-cd /absolute/path/to/your/project
57
65
  ```
58
66
 
59
- Use `--receiver` to select the retained hosted preview or a different Receiver, such as the local
60
- preview:
67
+ Use `--receiver` to select an accepted replacement Receiver, such as a local historical preview:
61
68
 
62
69
  ```sh
63
70
  npx --yes --package=@4xeoz/re-entry re-entry install \
@@ -72,7 +79,7 @@ choose Desktop, the current folder, or another Codex workspace
72
79
  -> check Node + find Codex + validate the selected directory
73
80
  -> open the dedicated Re-entry user account page in the default browser
74
81
  -> sign in or create a user account
75
- -> click Pair this Mac in the dashboard
82
+ -> land on the user dashboard and click Pair this Mac
76
83
  -> enter the short pairing code in the CLI
77
84
  -> save the device credential with mode 0600
78
85
  -> install and start a per-user macOS LaunchAgent
@@ -84,29 +91,43 @@ preview.
84
91
 
85
92
  ## Check it
86
93
 
94
+ If you installed the package globally:
95
+
87
96
  ```sh
88
97
  re-entry status
89
98
  re-entry listen
90
99
  re-entry --help
100
+ re-entry stop
101
+ re-entry uninstall
102
+ ```
103
+
104
+ If you used the temporary `npx` invocation:
105
+
106
+ ```sh
107
+ npx --yes --package=@4xeoz/re-entry re-entry status
108
+ npx --yes --package=@4xeoz/re-entry re-entry listen
109
+ npx --yes --package=@4xeoz/re-entry re-entry --help
110
+ npx --yes --package=@4xeoz/re-entry re-entry stop
111
+ npx --yes --package=@4xeoz/re-entry re-entry uninstall
91
112
  ```
92
113
 
93
114
  The status view checks the local authorization, background job, Receiver reachability, Node, and
94
115
  Codex. If the Receiver rejects a previously saved device credential, the Connector pauses instead
95
- of retrying forever, and `status`/`listen` tell you to run `re-entry connect` again. `listen` watches
96
- the already-running background Connector and displays new activity until you press Ctrl+C; it does
97
- not start a competing second poller. Useful development commands are:
116
+ of retrying forever, and `status`/`listen` print an executable `connect` command. `listen` watches the
117
+ already-running background Connector and displays new activity until you press Ctrl+C; it does not
118
+ start a competing second poller. Useful development commands are:
98
119
 
99
120
  ```sh
100
- re-entry doctor --codex-cd /absolute/path/to/project
101
- re-entry connect --receiver http://127.0.0.1:43224
102
- re-entry claim-once --codex-cd /absolute/path/to/project
103
- re-entry start --codex-cd /absolute/path/to/project
121
+ npx --yes --package=@4xeoz/re-entry re-entry doctor --codex-cd /absolute/path/to/project
122
+ npx --yes --package=@4xeoz/re-entry re-entry connect --receiver http://127.0.0.1:43224
123
+ npx --yes --package=@4xeoz/re-entry re-entry claim-once --codex-cd /absolute/path/to/project
124
+ npx --yes --package=@4xeoz/re-entry re-entry start --codex-cd /absolute/path/to/project
104
125
  ```
105
126
 
106
127
  Test the local Codex handoff without waiting for Cloud work:
107
128
 
108
129
  ```sh
109
- re-entry test "Reply with: Re-entry is working."
130
+ npx --yes --package=@4xeoz/re-entry re-entry test "Reply with: Re-entry is working."
110
131
  ```
111
132
 
112
133
  This starts one fresh local Codex process through the same adapter seam used by real deliveries. It
@@ -115,8 +136,8 @@ does not create a Grant, claim Receiver work, or prove the browser/WebMCP return
115
136
  To pause or remove the local Connector:
116
137
 
117
138
  ```sh
118
- re-entry stop
119
- re-entry uninstall
139
+ npx --yes --package=@4xeoz/re-entry re-entry stop
140
+ npx --yes --package=@4xeoz/re-entry re-entry uninstall
120
141
  ```
121
142
 
122
143
  `stop` pauses the macOS background service and keeps the account connection. `uninstall` requires
@@ -137,8 +158,8 @@ Install the Re-entry Local Connector on this Mac. First read the package README.
137
158
  or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
138
159
  project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
139
160
  enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
140
- into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
141
- results without claiming Browser/WebMCP or production deployment.
161
+ into chat, logs, source files, or git. Finish by running `npx --yes --package=@4xeoz/re-entry re-entry status`
162
+ and report the bounded results without claiming Browser/WebMCP or production deployment.
142
163
  ```
143
164
 
144
165
  ## Package map
@@ -200,15 +221,22 @@ the npm account that owns the `4xeoz` scope, publish from this directory:
200
221
 
201
222
  ```sh
202
223
  npm login
224
+ npm whoami
225
+ npm version patch --no-git-tag-version
226
+ npm run verify
203
227
  npm publish --access public
204
228
  ```
205
229
 
230
+ NPM versions are immutable: if the target version has already been published, bump it before
231
+ publishing with `npm version patch --no-git-tag-version`.
232
+
206
233
  After publishing, users run `npx --yes --package=@4xeoz/re-entry re-entry install`. The explicit
207
234
  `--package` form works across npm versions when the package is scoped; the executable itself is
208
235
  still named `re-entry`.
209
236
 
210
- On npm installations affected by the temporary-bin `PATH` bug, use `npm install --global
211
- @4xeoz/re-entry` once and then run `re-entry install` directly.
237
+ A temporary `npx` invocation does not add `re-entry` to your shell `PATH`. If you prefer the shorter
238
+ bare commands, run `npm install --global @4xeoz/re-entry` once and then use `re-entry install`
239
+ directly.
212
240
 
213
241
  ## Check a Mac before pairing
214
242
 
@@ -254,9 +282,9 @@ npm start -- \
254
282
  ```
255
283
 
256
284
  On the first run, the Connector checks the Mac, opens the dedicated Re-entry user account page,
257
- waits for you to create or sign in to an account and click Pair this Mac, then asks for the
258
- dashboard code and stores the credential. On later runs it reuses the saved credential and skips
259
- pairing:
285
+ waits for you to create or sign in to an account, lands on the user dashboard, and asks for the
286
+ pairing code. It then stores the credential. On later runs it reuses the saved credential and
287
+ skips pairing:
260
288
 
261
289
  ```sh
262
290
  npm start -- --codex-cd "$HOME/Code/my-host-project"
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.11",
3
+ "version": "0.2.14",
4
4
  "type": "module",
5
- "description": "Outbound Local Connector process for the Re-entry Core local preview.",
5
+ "description": "Outbound Local Connector process for Re-entry Core; new Receiver origins must be explicit.",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -11,8 +11,8 @@
11
11
  "src"
12
12
  ],
13
13
  "bin": {
14
- "re-entry": "./src/main.mjs",
15
- "reentry": "./src/main.mjs"
14
+ "re-entry": "src/main.mjs",
15
+ "reentry": "src/main.mjs"
16
16
  },
17
17
  "exports": {
18
18
  ".": "./src/index.mjs",
package/src/main.mjs CHANGED
@@ -34,11 +34,15 @@ import {
34
34
  } from "./macos-service.mjs";
35
35
  import { createTerminalUi } from "./terminal-ui.mjs";
36
36
 
37
- const DEFAULT_RECEIVER_ORIGIN = process.env.REENTRY_RECEIVER_ORIGIN ?? "https://re-entry-weld.vercel.app";
37
+ // The former hosted Cloud Receiver is retired. New pairing must name an accepted replacement
38
+ // explicitly instead of silently sending credentials to the old deployment alias.
39
+ const DEFAULT_RECEIVER_ORIGIN = process.env.REENTRY_RECEIVER_ORIGIN;
38
40
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
39
41
  const DEFAULT_MAX_CONSECUTIVE_ERRORS = 5;
40
42
  const DEFAULT_PAIRING_REQUEST_TIMEOUT_MS = 20_000;
41
43
  const CONNECTOR_VERSION = readConnectorVersion();
44
+ // The npx form is executable even when this CLI was launched from a temporary npx cache.
45
+ const TEMPORARY_CLI_COMMAND = "npx --yes --package=@4xeoz/re-entry re-entry";
42
46
 
43
47
  async function main() {
44
48
  let ui;
@@ -127,6 +131,10 @@ Usage:
127
131
  re-entry listen Watch live activity
128
132
  re-entry test "Reply with hello" Test Codex locally
129
133
 
134
+ Invocation:
135
+ Temporary npx: npx --yes --package=@4xeoz/re-entry re-entry <command>
136
+ Global install: npm install --global @4xeoz/re-entry, then re-entry <command>
137
+
130
138
  Commands:
131
139
  install Check this Mac, connect the Re-entry account, and start at login
132
140
  status Show account, Receiver, service, Node.js, and Codex readiness
@@ -141,7 +149,7 @@ Commands:
141
149
  pair Legacy Host-code pairing preview
142
150
 
143
151
  Common options:
144
- --receiver <url> Re-entry Receiver origin
152
+ --receiver <url> Accepted Receiver origin (required when pairing a new Mac)
145
153
  --codex-cd <path> Workspace for Codex; interactive mode offers a folder picker if omitted
146
154
  --codex-binary <path> Explicit Codex executable
147
155
  --json Machine-readable output
@@ -154,6 +162,10 @@ The Connector opens no inbound port. Host keys never belong on this Mac.
154
162
  `);
155
163
  }
156
164
 
165
+ function cliCommand(command) {
166
+ return `${TEMPORARY_CLI_COMMAND} ${command}`;
167
+ }
168
+
157
169
  function readConnectorVersion() {
158
170
  const packageJson = JSON.parse(
159
171
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
@@ -263,7 +275,7 @@ async function pair(flags, ui, options = {}) {
263
275
  });
264
276
  if (ui.interactive) {
265
277
  ui.complete("Pairing complete", "Re-entry can now deliver approved work to this Mac.");
266
- ui.next("re-entry start", "Wait for approved work in this terminal.");
278
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
267
279
  } else {
268
280
  process.stdout.write(`${JSON.stringify({ event: "connector_paired", connector_id: credentials.connector_id })}\n`);
269
281
  }
@@ -274,6 +286,9 @@ async function connect(flags, ui, options = {}) {
274
286
  ui.begin("Connect this Mac", "Create a Re-entry account, pair this Mac, then leave it running");
275
287
  }
276
288
  const receiver = flags.receiver ?? DEFAULT_RECEIVER_ORIGIN;
289
+ if (typeof receiver !== "string" || receiver.trim().length === 0) {
290
+ throw cliFailure("connector_receiver_missing");
291
+ }
277
292
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
278
293
  const store = new LocalConnectorCredentialStore({ filename: credentialFile });
279
294
  const current = await store.load();
@@ -283,7 +298,7 @@ async function connect(flags, ui, options = {}) {
283
298
  if (ui.interactive) {
284
299
  ui.success("Account", "already connected");
285
300
  if (!options.guidedInstall) {
286
- ui.next("re-entry start", "Wait for approved work in this terminal.");
301
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
287
302
  }
288
303
  } else {
289
304
  process.stdout.write(`${JSON.stringify({
@@ -307,7 +322,7 @@ async function connect(flags, ui, options = {}) {
307
322
  baseUrl: receiver,
308
323
  requestTimeoutMs: DEFAULT_PAIRING_REQUEST_TIMEOUT_MS,
309
324
  });
310
- const portalUrl = `${receiver}/user-register?next=${encodeURIComponent("/dashboard")}`;
325
+ const portalUrl = `${receiver}/user-register?next=${encodeURIComponent("/user-dashboard")}`;
311
326
  if (ui.interactive) {
312
327
  ui.stopWait("Secure link", "ready");
313
328
  ui.info("Re-entry", portalUrl);
@@ -342,7 +357,7 @@ async function connect(flags, ui, options = {}) {
342
357
  if (ui.interactive) {
343
358
  if (!options.guidedInstall) {
344
359
  ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
345
- ui.next("re-entry start", "Wait for approved work in this terminal.");
360
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
346
361
  }
347
362
  } else {
348
363
  process.stdout.write(`${JSON.stringify({
@@ -381,14 +396,14 @@ async function status(flags, ui) {
381
396
  else if (credentials) ui.warning("Cloud", "unavailable");
382
397
 
383
398
  if (reauthorizationRequired) {
384
- ui.next("re-entry connect", "Approve this Mac again in your browser; the old credential was rejected.");
399
+ ui.next(cliCommand("connect"), "Approve this Mac again in your browser; the old credential was rejected.");
385
400
  } else if (!connected || !service.running) {
386
- ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
401
+ ui.next(cliCommand("install"), "Finish setup and start Re-entry in the background.");
387
402
  } else if (!receiverReady) {
388
- ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
403
+ ui.next(cliCommand("status"), "Check again when the Re-entry Cloud service is available.");
389
404
  } else {
390
405
  ui.complete("Everything looks good", "Re-entry is ready for approved work.");
391
- ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
406
+ ui.next(cliCommand("listen"), "Watch live activity. Press Ctrl+C when you are done.");
392
407
  }
393
408
  } else {
394
409
  process.stdout.write(`${JSON.stringify({
@@ -420,11 +435,11 @@ async function listen(flags, ui) {
420
435
  if (ui.interactive) {
421
436
  if (reauthorizationRequired) {
422
437
  ui.warning("Account", "reconnect required; the Cloud Receiver rejected this Mac");
423
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
438
+ ui.next(cliCommand("connect"), "Approve this Mac again in your browser.");
424
439
  }
425
440
  if (!credentials) ui.warning("Account", "not connected");
426
441
  if (!service.running) ui.warning("Background", "not running");
427
- ui.next("re-entry install", "Finish setup and start the background Connector.");
442
+ ui.next(cliCommand("install"), "Finish setup and start the background Connector.");
428
443
  } else {
429
444
  process.stdout.write(`${JSON.stringify({
430
445
  event: "connector_listener_unavailable",
@@ -489,7 +504,7 @@ async function testCodex(flags, positionals, ui) {
489
504
  if (ui.interactive) {
490
505
  ui.stopWait("Codex", "completed the local test");
491
506
  ui.complete("Test passed", "The same fresh-session process seam is ready for Re-entry work.");
492
- ui.next("re-entry listen", "Watch the background Connector for approved work.");
507
+ ui.next(cliCommand("listen"), "Watch the background Connector for approved work.");
493
508
  } else {
494
509
  process.stdout.write('{"event":"connector_codex_test_passed"}\n');
495
510
  }
@@ -506,7 +521,7 @@ function reportLiveActivity(event, ui) {
506
521
  );
507
522
  } else if (event.event === "connector_reauthorization_required" || event.code === "connector_identity_invalid") {
508
523
  ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
509
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
524
+ ui.next(cliCommand("connect"), "Approve this Mac again in your browser.");
510
525
  return;
511
526
  } else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
512
527
  ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
@@ -529,7 +544,7 @@ async function stop(flags, ui) {
529
544
  if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
530
545
  else if (result.stopped) ui.complete("Re-entry is paused", "Your account connection is still saved.");
531
546
  else ui.info("Already stopped", "no running background Connector was found");
532
- if (result.supported) ui.next("re-entry install", "Start Re-entry in the background again.");
547
+ if (result.supported) ui.next(cliCommand("install"), "Start Re-entry in the background again.");
533
548
  } else {
534
549
  process.stdout.write(`${JSON.stringify({
535
550
  event: "connector_stopped",
@@ -552,7 +567,7 @@ async function uninstall(flags, ui) {
552
567
  });
553
568
  if (ui.interactive) {
554
569
  ui.complete("Removed from this Mac", "The local service, connection, and logs are gone.");
555
- ui.next("npx @4xeoz/re-entry install", "Connect this Mac again whenever you are ready.");
570
+ ui.next(cliCommand("install"), "Connect this Mac again whenever you are ready.");
556
571
  } else {
557
572
  process.stdout.write(`${JSON.stringify({
558
573
  event: "connector_uninstalled",
@@ -584,7 +599,7 @@ async function install(flags, ui) {
584
599
  if (ui.interactive) {
585
600
  ui.stopWait("Background", "running at login");
586
601
  ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
587
- ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
602
+ ui.next(cliCommand("listen"), "Watch live activity. Press Ctrl+C when you are done.");
588
603
  } else {
589
604
  process.stdout.write(`${JSON.stringify({
590
605
  event: "connector_service_installed",
@@ -623,7 +638,7 @@ async function start(flags, ui) {
623
638
  } else if (await hasConnectorReauthorizationRequired(credentialFile)) {
624
639
  if (ui.interactive) {
625
640
  ui.warning("Account", "this Mac needs to be connected again");
626
- ui.next("re-entry connect", "Create a new dashboard pairing code and connect this Mac again.");
641
+ ui.next(cliCommand("connect"), "Create a new dashboard pairing code and connect this Mac again.");
627
642
  } else {
628
643
  process.stdout.write('{"event":"connector_reauthorization_required"}\n');
629
644
  }
@@ -669,7 +684,7 @@ async function start(flags, ui) {
669
684
  });
670
685
  if (ui.interactive) {
671
686
  ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
672
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
687
+ ui.next(cliCommand("connect"), "Approve this Mac again in your browser.");
673
688
  } else {
674
689
  process.stdout.write('{"event":"connector_reauthorization_required"}\n');
675
690
  }
@@ -1033,13 +1048,15 @@ function errorHint(error) {
1033
1048
  connector_codex_cd_missing: "pass --codex-cd /absolute/path/to/your/Host-project",
1034
1049
  connector_codex_not_found: "install Codex, add it to PATH, or pass --codex-binary /path/to/codex",
1035
1050
  connector_codex_binary_not_found: "check --codex-binary or remove it to use automatic discovery",
1036
- connector_codex_unusable: "open Codex, complete login if needed, then run `npm run doctor` again",
1051
+ connector_codex_unusable: `open Codex, complete login if needed, then run \`${cliCommand("doctor")}\` again`,
1037
1052
  connector_node_unsupported: "use Node.js 24 or newer, then run the command again",
1038
- connector_credentials_missing: "connect this Mac once with `re-entry connect`",
1039
- connector_credentials_expired: "run `re-entry connect` to authorize this Mac again",
1040
- connector_identity_invalid: "run `re-entry connect` and approve this Mac again",
1041
- connector_reauthorization_required: "run `re-entry connect` and approve this Mac again",
1042
- connector_pairing_code_missing: "ask the Host backend for a new pairing code, then run pair in a terminal",
1053
+ connector_receiver_missing: "pass --receiver <replacement-receiver-origin> or set REENTRY_RECEIVER_ORIGIN",
1054
+ connector_credentials_missing: `connect this Mac once with \`${cliCommand("connect")}\``,
1055
+ connector_credentials_expired: `run \`${cliCommand("connect")}\` to authorize this Mac again`,
1056
+ connector_credentials_already_exists: "use the existing Connector credential or ask for a new pairing code",
1057
+ connector_identity_invalid: `run \`${cliCommand("connect")}\` and approve this Mac again`,
1058
+ connector_reauthorization_required: `run \`${cliCommand("connect")}\` and approve this Mac again`,
1059
+ connector_pairing_code_missing: `ask the Host backend for a new pairing code, then run \`${cliCommand("pair")}\` in a terminal`,
1043
1060
  pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
1044
1061
  host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
1045
1062
  pairing_expired: "ask the Host backend for a new pairing code",
@@ -1047,14 +1064,14 @@ function errorHint(error) {
1047
1064
  pairing_network_error: "check your internet connection and the Receiver address, then try again",
1048
1065
  workspace_directory_unavailable: "choose a readable folder or pass --codex-cd /absolute/path",
1049
1066
  workspace_selection_cancelled: "run the command again when you are ready to choose a workspace",
1050
- device_authorization_expired: "run `re-entry connect` again and approve within ten minutes",
1051
- device_authorization_denied: "run `re-entry connect` again when you are ready to approve this Mac",
1067
+ device_authorization_expired: `run \`${cliCommand("connect")}\` again and approve within ten minutes`,
1068
+ device_authorization_denied: `run \`${cliCommand("connect")}\` again when you are ready to approve this Mac`,
1052
1069
  connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
1053
1070
  connector_max_errors_invalid: "use a maximum error count between 1 and 20",
1054
- connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
1055
- connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
1056
- connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
1057
- connector_test_prompt_missing: "use `re-entry test \"Reply with: Re-entry is working.\"`",
1071
+ connector_service_load_failed: `run \`${cliCommand("install")}\` again; if it still fails, inspect the Connector error log`,
1072
+ connector_uninstall_confirmation_required: `run \`${cliCommand("uninstall")}\` interactively and type DELETE, or pass \`--yes\` in a deliberate script`,
1073
+ connector_service_stop_failed: `check the Connector status and try \`${cliCommand("stop")}\` again`,
1074
+ connector_test_prompt_missing: `use \`${cliCommand('test "Reply with: Re-entry is working."')}\``,
1058
1075
  connector_test_prompt_invalid: "use one short, single-line prompt inside quotes",
1059
1076
  connector_activation_timeout_invalid: "use an activation timeout between 100 and 60000 milliseconds",
1060
1077
  };
@@ -69,6 +69,14 @@ const CREDENTIAL_FIELDS = Object.freeze([
69
69
  "connector_expires_at",
70
70
  "duplicate",
71
71
  ]);
72
+ const TOKENLESS_CREDENTIAL_FIELDS = Object.freeze([
73
+ "type",
74
+ "protocol_version",
75
+ "pairing_id",
76
+ "connector_id",
77
+ "connector_expires_at",
78
+ "duplicate",
79
+ ]);
72
80
  const ERROR_FIELDS = Object.freeze(["error"]);
73
81
  const ERROR_BODY_FIELDS = Object.freeze(["code"]);
74
82
  const IDENTIFIER_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,159}$/;
@@ -218,8 +226,16 @@ export class LocalConnectorPairingClient {
218
226
  device_name: deviceName,
219
227
  });
220
228
  if (response.status !== 200) throw await parseHttpFailure(response);
229
+ const credentials = normalizeAccountCredentials(await parseJsonResponse(response));
230
+ if (credentials.duplicate && !Object.hasOwn(credentials, "connector_token")) {
231
+ throw pairingFailure(
232
+ "connector_credentials_already_exists",
233
+ "Pairing is already complete; use the existing Connector credential or create a new pairing",
234
+ { statusCode: response.status },
235
+ );
236
+ }
221
237
  return Object.freeze({
222
- ...normalizeCredentials(await parseJsonResponse(response)),
238
+ ...credentials,
223
239
  browserOpened: false,
224
240
  });
225
241
  }
@@ -358,6 +374,33 @@ function normalizeCredentials(value) {
358
374
  };
359
375
  }
360
376
 
377
+ function normalizeAccountCredentials(value) {
378
+ const fields = value?.duplicate === true ? TOKENLESS_CREDENTIAL_FIELDS : CREDENTIAL_FIELDS;
379
+ requireExactRecord(
380
+ value,
381
+ fields,
382
+ fields,
383
+ "Account Connector credential response",
384
+ "pairing_response_invalid",
385
+ );
386
+ if (value.type !== "webmcp.connector_credentials" || value.protocol_version !== "0.1") {
387
+ throw pairingFailure("pairing_response_invalid", "Account Connector credential response is unsupported");
388
+ }
389
+ if (typeof value.duplicate !== "boolean") {
390
+ throw pairingFailure("pairing_response_invalid", "Account Connector credential duplicate flag is invalid");
391
+ }
392
+ const normalized = {
393
+ pairing_id: requireIdentifier(value.pairing_id, "pairing_id"),
394
+ connector_id: requireIdentifier(value.connector_id, "connector_id"),
395
+ connector_expires_at: requireTimestamp(value.connector_expires_at, "connector_expires_at"),
396
+ duplicate: value.duplicate,
397
+ };
398
+ if (!value.duplicate) {
399
+ normalized.connector_token = requireToken(value.connector_token, "connector_token");
400
+ }
401
+ return normalized;
402
+ }
403
+
361
404
  function normalizeAccountPairingCode(value) {
362
405
  if (typeof value !== "string") {
363
406
  throw pairingFailure("account_pairing_code_invalid", "Pairing code is invalid");
@@ -547,17 +590,17 @@ function pairingFailure(code, message, options = {}) {
547
590
  return new PairingClientError(code, message, options);
548
591
  }
549
592
 
550
- function requireExactRecord(value, allowedFields, requiredFields, label) {
593
+ function requireExactRecord(value, allowedFields, requiredFields, label, failureCode = "pairing_input_invalid") {
551
594
  if (!value || typeof value !== "object" || Array.isArray(value)) {
552
- throw pairingFailure("pairing_input_invalid", `${label} must be an object`);
595
+ throw pairingFailure(failureCode, `${label} must be an object`);
553
596
  }
554
597
  const prototype = Object.getPrototypeOf(value);
555
598
  if (prototype !== Object.prototype && prototype !== null) {
556
- throw pairingFailure("pairing_input_invalid", `${label} must be a plain object`);
599
+ throw pairingFailure(failureCode, `${label} must be a plain object`);
557
600
  }
558
601
  const fields = Object.keys(value);
559
602
  if (fields.some((field) => !allowedFields.includes(field)) || requiredFields.some((field) => !fields.includes(field))) {
560
- throw pairingFailure("pairing_input_invalid", `${label} fields are invalid`);
603
+ throw pairingFailure(failureCode, `${label} fields are invalid`);
561
604
  }
562
605
  }
563
606