@4xeoz/re-entry 0.2.13 → 0.2.15

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
@@ -2,8 +2,9 @@
2
2
 
3
3
  > **Cloud Receiver dependency notice — 2026-09-02:** The former `re-entry-weld.vercel.app` Cloud
4
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.
5
+ > This Connector package remains a reusable/local preview surface and defaults to the accepted
6
+ > preview origin `https://cloud-receiver-delta.vercel.app`; pass `--receiver` for another accepted
7
+ > Receiver. This preview default is not a production deployment.
7
8
 
8
9
  Install it once on the Mac where Codex should open; after one dashboard pairing code, a macOS
9
10
  LaunchAgent keeps the outbound Connector running at login.
@@ -18,7 +19,7 @@ Requirements: macOS, Node.js 24+, Codex installed and signed in, and an absolute
18
19
  Codex may read and write.
19
20
 
20
21
  ```sh
21
- npx @4xeoz/re-entry install
22
+ npx --yes --package=@4xeoz/re-entry re-entry install
22
23
  ```
23
24
 
24
25
  `npx` runs a temporary copy, so it does not leave a permanent shell command behind. Install the
@@ -29,23 +30,25 @@ npm install --global @4xeoz/re-entry
29
30
  ```
30
31
 
31
32
  The package installs both `re-entry` and the older `reentry` spelling; `re-entry` is the documented
32
- 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`.
33
35
 
34
- The CLI no longer has a default hosted Receiver. For any new or historical test, pass an explicit
35
- accepted Receiver origin with `--receiver` or `REENTRY_RECEIVER_ORIGIN`; the former
36
- `https://re-entry-weld.vercel.app` alias is retired.
36
+ The CLI defaults to the current Cloud Receiver v2 preview at
37
+ `https://cloud-receiver-delta.vercel.app`. Override it with `--receiver` or
38
+ `REENTRY_RECEIVER_ORIGIN` when using another accepted Receiver. This preview default is not a
39
+ production deployment.
37
40
 
38
41
  The interactive CLI first offers Desktop, the current folder, or a small folder browser. Choose
39
42
  with ↑/↓ and Enter. If you want to skip the picker, pass the workspace directly:
40
43
 
41
44
  ```sh
42
- npx @4xeoz/re-entry install \
45
+ npx --yes --package=@4xeoz/re-entry re-entry install \
43
46
  --codex-cd /absolute/path/to/your/project
44
47
  ```
45
48
 
46
49
  The guided screen stays intentionally small: **Workspace → System check → Connect Re-entry**. It
47
- shows one clear next command when setup finishes; internal Connector IDs, credential paths, and log
48
- 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.
49
52
 
50
53
  Run this from the Host project directory, your home directory, or another normal working
51
54
  directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
@@ -56,7 +59,8 @@ installation instead:
56
59
 
57
60
  ```sh
58
61
  npm install --global @4xeoz/re-entry
59
- re-entry install --codex-cd /absolute/path/to/your/project
62
+ re-entry install \
63
+ --codex-cd /absolute/path/to/your/project
60
64
  ```
61
65
 
62
66
  Use `--receiver` to select an accepted replacement Receiver, such as a local historical preview:
@@ -86,29 +90,45 @@ preview.
86
90
 
87
91
  ## Check it
88
92
 
93
+ If you installed the package globally:
94
+
89
95
  ```sh
90
96
  re-entry status
91
97
  re-entry listen
92
98
  re-entry --help
99
+ re-entry stop
100
+ re-entry disconnect
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 disconnect
112
+ npx --yes --package=@4xeoz/re-entry re-entry uninstall
93
113
  ```
94
114
 
95
115
  The status view checks the local authorization, background job, Receiver reachability, Node, and
96
116
  Codex. If the Receiver rejects a previously saved device credential, the Connector pauses instead
97
- of retrying forever, and `status`/`listen` tell you to run `re-entry connect` again. `listen` watches
117
+ of retrying forever, and `status`/`listen` tell you to disconnect before reconnecting. `listen` watches
98
118
  the already-running background Connector and displays new activity until you press Ctrl+C; it does
99
119
  not start a competing second poller. Useful development commands are:
100
120
 
101
121
  ```sh
102
- re-entry doctor --codex-cd /absolute/path/to/project
103
- re-entry connect --receiver http://127.0.0.1:43224
104
- re-entry claim-once --codex-cd /absolute/path/to/project
105
- re-entry start --codex-cd /absolute/path/to/project
122
+ npx --yes --package=@4xeoz/re-entry re-entry doctor --codex-cd /absolute/path/to/project
123
+ npx --yes --package=@4xeoz/re-entry re-entry connect --receiver http://127.0.0.1:43224
124
+ npx --yes --package=@4xeoz/re-entry re-entry claim-once --codex-cd /absolute/path/to/project
125
+ npx --yes --package=@4xeoz/re-entry re-entry start --codex-cd /absolute/path/to/project
106
126
  ```
107
127
 
108
128
  Test the local Codex handoff without waiting for Cloud work:
109
129
 
110
130
  ```sh
111
- re-entry test "Reply with: Re-entry is working."
131
+ npx --yes --package=@4xeoz/re-entry re-entry test "Reply with: Re-entry is working."
112
132
  ```
113
133
 
114
134
  This starts one fresh local Codex process through the same adapter seam used by real deliveries. It
@@ -117,8 +137,9 @@ does not create a Grant, claim Receiver work, or prove the browser/WebMCP return
117
137
  To pause or remove the local Connector:
118
138
 
119
139
  ```sh
120
- re-entry stop
121
- re-entry uninstall
140
+ npx --yes --package=@4xeoz/re-entry re-entry stop
141
+ npx --yes --package=@4xeoz/re-entry re-entry disconnect
142
+ npx --yes --package=@4xeoz/re-entry re-entry uninstall
122
143
  ```
123
144
 
124
145
  `stop` pauses the macOS background service and keeps the account connection. `uninstall` requires
@@ -126,9 +147,23 @@ typing `DELETE`, then removes only the LaunchAgent, saved Connector credential,
126
147
  It does not recursively delete folders or uninstall the npm package. To remove a global npm
127
148
  installation separately, run `npm uninstall --global @4xeoz/re-entry`.
128
149
 
129
- `connect` repeats only account authorization. `install` is the normal product path because it also
130
- installs the background job. `claim-once` is the smallest manual delivery test. The legacy
131
- Host-code `pair` command remains only for compatibility tests.
150
+ `disconnect` is the simple local sign-out: it stops the LaunchAgent, removes the saved Connector
151
+ credential, and leaves the log files in place. It is safe to run again when already disconnected.
152
+ The current CLI does not have a server-side revoke route, so this command does not remove the Mac
153
+ from the account's remote device list; use the account dashboard's revoke/decommission action when
154
+ that Receiver operation is available, or wait for the remote credential to expire. `uninstall` is
155
+ the stronger local cleanup and also removes Connector logs.
156
+
157
+ Each credential file accepts one connection at a time. Repeating `connect` or `install` with the
158
+ same saved connection returns `already connected`; an expired, rejected, or different-Receiver
159
+ connection is not silently replaced. Run `disconnect`, then `install`, to connect this Mac to a
160
+ different account or Receiver. `connect` repeats only account authorization, while `install` is the
161
+ normal product path because it also installs the background job. `claim-once` is the smallest manual
162
+ delivery test. The legacy Host-code `pair` command follows the same one-connection rule and remains
163
+ only for compatibility tests.
164
+
165
+ `Ctrl+C` stops a foreground `start` process or closes the `listen` view. It does not remove the
166
+ saved connection; use `stop` to pause the background service or `disconnect` to clear it.
132
167
 
133
168
  ## Give this to a coding agent
134
169
 
@@ -139,8 +174,8 @@ Install the Re-entry Local Connector on this Mac. First read the package README.
139
174
  or newer and locate the Connector executable. Run `npx --yes --package=@4xeoz/re-entry re-entry install` with an absolute
140
175
  project directory. Let the human create or sign in to a Re-entry account, click Pair this Mac, and
141
176
  enter the code in the CLI; never copy browser cookies, organization keys, Connector tokens, or private keys
142
- into chat, logs, source files, or git. Finish by running `re-entry status` and report the bounded
143
- results without claiming Browser/WebMCP or production deployment.
177
+ into chat, logs, source files, or git. Finish by running `npx --yes --package=@4xeoz/re-entry re-entry status`
178
+ and report the bounded results without claiming Browser/WebMCP or production deployment.
144
179
  ```
145
180
 
146
181
  ## Package map
@@ -203,20 +238,23 @@ the npm account that owns the `4xeoz` scope, publish from this directory:
203
238
  ```sh
204
239
  npm login
205
240
  npm whoami
241
+ npm view @4xeoz/re-entry version
206
242
  npm version patch --no-git-tag-version
207
243
  npm run verify
208
244
  npm publish --access public
209
245
  ```
210
246
 
211
- NPM versions are immutable: if the current version has already been published, bump it before
212
- publishing (the current published `0.2.11` therefore requires `0.2.12` or another new version).
247
+ NPM versions are immutable: if the target version has already been published, bump it before
248
+ publishing with `npm version patch --no-git-tag-version`.
213
249
 
214
250
  After publishing, users run `npx --yes --package=@4xeoz/re-entry re-entry install`. The explicit
215
251
  `--package` form works across npm versions when the package is scoped; the executable itself is
216
- still named `re-entry`.
252
+ still named `re-entry`. The command uses the built-in preview Receiver unless `--receiver` or
253
+ `REENTRY_RECEIVER_ORIGIN` overrides it.
217
254
 
218
- On npm installations affected by the temporary-bin `PATH` bug, use `npm install --global
219
- @4xeoz/re-entry` once and then run `re-entry install` directly.
255
+ A temporary `npx` invocation does not add `re-entry` to your shell `PATH`. If you prefer the shorter
256
+ bare commands, run `npm install --global @4xeoz/re-entry` once and then use `re-entry install`
257
+ directly.
220
258
 
221
259
  ## Check a Mac before pairing
222
260
 
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.13",
3
+ "version": "0.2.15",
4
4
  "type": "module",
5
- "description": "Outbound Local Connector process for Re-entry Core; new Receiver origins must be explicit.",
5
+ "description": "Outbound Local Connector process for Re-entry Core; uses the Cloud Receiver v2 preview by default with an explicit override for other accepted origins.",
6
6
  "publishConfig": {
7
7
  "access": "public"
8
8
  },
@@ -113,6 +113,30 @@ export async function stopMacConnectorService(options = {}) {
113
113
  return Object.freeze({ ...status, stopped: true, running: false });
114
114
  }
115
115
 
116
+ export async function disconnectMacConnectorService(options = {}) {
117
+ const configuration = requireServiceConfiguration(options, "disconnect");
118
+ if (process.platform !== "darwin" && configuration.allowNonMacForTest !== true) {
119
+ return Object.freeze({ supported: false, disconnected: false, removedPaths: [] });
120
+ }
121
+ const service = await stopMacConnectorService(configuration);
122
+ const stateDirectory = configuration.stateDirectory ?? join(homedir(), ".webmcp-connector");
123
+ const credentialFile = requireAbsolutePath(
124
+ configuration.credentialFile ?? join(stateDirectory, "credentials.json"),
125
+ "Credential file",
126
+ );
127
+ const removedPaths = await removePaths([
128
+ service.plistPath,
129
+ credentialFile,
130
+ `${credentialFile}.reauthorization-required.json`,
131
+ ], "disconnect");
132
+ return Object.freeze({
133
+ supported: true,
134
+ disconnected: removedPaths.length > 0,
135
+ removedPaths,
136
+ plistPath: service.plistPath,
137
+ });
138
+ }
139
+
116
140
  export async function uninstallMacConnectorService(options = {}) {
117
141
  const configuration = requireServiceConfiguration(options, "uninstall");
118
142
  if (process.platform !== "darwin" && configuration.allowNonMacForTest !== true) {
@@ -131,22 +155,28 @@ export async function uninstallMacConnectorService(options = {}) {
131
155
  join(stateDirectory, "connector.log"),
132
156
  join(stateDirectory, "connector-error.log"),
133
157
  ])];
158
+ const removedPaths = await removePaths(paths, "uninstall");
159
+ return Object.freeze({
160
+ supported: true,
161
+ removedPaths,
162
+ plistPath: service.plistPath,
163
+ });
164
+ }
165
+
166
+ async function removePaths(paths, operation) {
134
167
  const removedPaths = [];
135
- for (const path of paths) {
168
+ for (const path of [...new Set(paths)]) {
169
+ if (typeof path !== "string") continue;
136
170
  try {
137
171
  await unlink(path);
138
172
  removedPaths.push(path);
139
173
  } catch (error) {
140
174
  if (error?.code !== "ENOENT") {
141
- throw serviceFailure("connector_service_remove_failed", "Connector data could not be removed", error);
175
+ throw serviceFailure("connector_service_remove_failed", `Connector data could not be removed during ${operation}`, error);
142
176
  }
143
177
  }
144
178
  }
145
- return Object.freeze({
146
- supported: true,
147
- removedPaths,
148
- plistPath: service.plistPath,
149
- });
179
+ return removedPaths;
150
180
  }
151
181
 
152
182
  export function renderLaunchAgent(options) {
package/src/main.mjs CHANGED
@@ -28,19 +28,23 @@ import { LocalConnectorPairingClient, openBrowser } from "./pairing-client.mjs";
28
28
  import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
29
29
  import {
30
30
  inspectMacConnectorService,
31
+ disconnectMacConnectorService,
31
32
  installMacConnectorService,
32
33
  stopMacConnectorService,
33
34
  uninstallMacConnectorService,
34
35
  } from "./macos-service.mjs";
35
36
  import { createTerminalUi } from "./terminal-ui.mjs";
36
37
 
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
+ // This is the current Cloud Receiver v2 preview. A command-line --receiver override remains
39
+ // authoritative, and REENTRY_RECEIVER_ORIGIN can override this built-in preview default.
40
+ const BUILT_IN_RECEIVER_ORIGIN = "https://cloud-receiver-delta.vercel.app";
41
+ const DEFAULT_RECEIVER_ORIGIN = process.env.REENTRY_RECEIVER_ORIGIN?.trim() || BUILT_IN_RECEIVER_ORIGIN;
40
42
  const DEFAULT_POLL_INTERVAL_MS = 5_000;
41
43
  const DEFAULT_MAX_CONSECUTIVE_ERRORS = 5;
42
44
  const DEFAULT_PAIRING_REQUEST_TIMEOUT_MS = 20_000;
43
45
  const CONNECTOR_VERSION = readConnectorVersion();
46
+ // The npx form is executable even when this CLI was launched from a temporary npx cache.
47
+ const TEMPORARY_CLI_COMMAND = "npx --yes --package=@4xeoz/re-entry re-entry";
44
48
 
45
49
  async function main() {
46
50
  let ui;
@@ -86,6 +90,10 @@ async function main() {
86
90
  await stop(flags, ui);
87
91
  return;
88
92
  }
93
+ if (command === "disconnect") {
94
+ await disconnect(flags, ui);
95
+ return;
96
+ }
89
97
  if (command === "uninstall") {
90
98
  await uninstall(flags, ui);
91
99
  return;
@@ -127,14 +135,20 @@ Connect this Mac once, then receive approved work in Codex in the background.
127
135
  Usage:
128
136
  re-entry install Recommended first run
129
137
  re-entry listen Watch live activity
138
+ re-entry disconnect Sign out this Mac locally
130
139
  re-entry test "Reply with hello" Test Codex locally
131
140
 
141
+ Invocation:
142
+ Temporary npx: npx --yes --package=@4xeoz/re-entry re-entry <command>
143
+ Global install: npm install --global @4xeoz/re-entry, then re-entry <command>
144
+
132
145
  Commands:
133
146
  install Check this Mac, connect the Re-entry account, and start at login
134
147
  status Show account, Receiver, service, Node.js, and Codex readiness
135
148
  listen Watch the background Connector until you press Ctrl+C
136
149
  test Start one fresh local Codex session with the supplied prompt
137
150
  stop Stop the background Connector without removing its credential
151
+ disconnect Stop the Connector and clear this Mac's saved connection
138
152
  uninstall Stop the Connector and remove its local service data
139
153
  connect Open Re-entry, redeem a dashboard pairing code, and connect this Mac
140
154
  start Poll for approved work until stopped
@@ -143,7 +157,7 @@ Commands:
143
157
  pair Legacy Host-code pairing preview
144
158
 
145
159
  Common options:
146
- --receiver <url> Accepted Receiver origin (required when pairing a new Mac)
160
+ --receiver <url> Accepted Receiver origin (overrides the preview default)
147
161
  --codex-cd <path> Workspace for Codex; interactive mode offers a folder picker if omitted
148
162
  --codex-binary <path> Explicit Codex executable
149
163
  --json Machine-readable output
@@ -151,11 +165,19 @@ Common options:
151
165
  --help Show this help
152
166
  --version Show the installed version
153
167
 
168
+ Default Receiver:
169
+ https://cloud-receiver-delta.vercel.app
170
+ Override with --receiver or REENTRY_RECEIVER_ORIGIN for another accepted Receiver.
171
+
154
172
  Both commands are installed: re-entry and reentry.
155
173
  The Connector opens no inbound port. Host keys never belong on this Mac.
156
174
  `);
157
175
  }
158
176
 
177
+ function cliCommand(command) {
178
+ return `${TEMPORARY_CLI_COMMAND} ${command}`;
179
+ }
180
+
159
181
  function readConnectorVersion() {
160
182
  const packageJson = JSON.parse(
161
183
  readFileSync(new URL("../package.json", import.meta.url), "utf8"),
@@ -229,6 +251,8 @@ async function pair(flags, ui, options = {}) {
229
251
  }
230
252
  const receiver = requireFlag(flags, "receiver");
231
253
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
254
+ const store = new LocalConnectorCredentialStore({ filename: credentialFile });
255
+ if (await store.load()) throw cliFailure("connector_account_already_connected");
232
256
  if (ui.interactive) {
233
257
  ui.step("Receiver", receiver);
234
258
  }
@@ -255,7 +279,6 @@ async function pair(flags, ui, options = {}) {
255
279
  ui.warning("Browser", "did not open automatically; use the URL above");
256
280
  }
257
281
  }
258
- const store = new LocalConnectorCredentialStore({ filename: credentialFile });
259
282
  await store.save({
260
283
  version: 1,
261
284
  receiver_origin: receiver,
@@ -265,7 +288,7 @@ async function pair(flags, ui, options = {}) {
265
288
  });
266
289
  if (ui.interactive) {
267
290
  ui.complete("Pairing complete", "Re-entry can now deliver approved work to this Mac.");
268
- ui.next("re-entry start", "Wait for approved work in this terminal.");
291
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
269
292
  } else {
270
293
  process.stdout.write(`${JSON.stringify({ event: "connector_paired", connector_id: credentials.connector_id })}\n`);
271
294
  }
@@ -288,7 +311,7 @@ async function connect(flags, ui, options = {}) {
288
311
  if (ui.interactive) {
289
312
  ui.success("Account", "already connected");
290
313
  if (!options.guidedInstall) {
291
- ui.next("re-entry start", "Wait for approved work in this terminal.");
314
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
292
315
  }
293
316
  } else {
294
317
  process.stdout.write(`${JSON.stringify({
@@ -300,9 +323,7 @@ async function connect(flags, ui, options = {}) {
300
323
  return current;
301
324
  }
302
325
 
303
- if (currentIsValid && ui.interactive) {
304
- ui.warning("Receiver changed", `${current.receiver_origin} → ${receiver}; approve this Mac again`);
305
- }
326
+ if (current) throw cliFailure("connector_account_already_connected");
306
327
 
307
328
  if (ui.interactive) {
308
329
  if (!options.guidedInstall) ui.info("Re-entry", displayReceiver(receiver));
@@ -347,7 +368,7 @@ async function connect(flags, ui, options = {}) {
347
368
  if (ui.interactive) {
348
369
  if (!options.guidedInstall) {
349
370
  ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
350
- ui.next("re-entry start", "Wait for approved work in this terminal.");
371
+ ui.next(cliCommand("start"), "Wait for approved work in this terminal.");
351
372
  }
352
373
  } else {
353
374
  process.stdout.write(`${JSON.stringify({
@@ -371,11 +392,15 @@ async function status(flags, ui) {
371
392
  Date.parse(credentials.connector_expires_at) > Date.now() &&
372
393
  !reauthorizationRequired,
373
394
  );
395
+ const credentialExpired = Boolean(
396
+ credentials && Date.parse(credentials.connector_expires_at) <= Date.now(),
397
+ );
374
398
  if (ui.interactive) {
375
399
  ui.section("SYSTEM", "This Mac");
376
400
  showReadiness(readiness, ui);
377
401
  ui.section("CONNECTION", "Re-entry");
378
- if (reauthorizationRequired) ui.warning("Account", "reconnect required");
402
+ if (reauthorizationRequired) ui.warning("Account", "disconnect required before reconnecting");
403
+ else if (credentialExpired) ui.warning("Account", "connection expired; disconnect before reconnecting");
379
404
  else if (connected) ui.success("Account", "connected");
380
405
  else ui.warning("Account", "not connected");
381
406
  if (reauthorizationRequired && service.running) ui.warning("Background", "paused until this Mac is reconnected");
@@ -385,15 +410,15 @@ async function status(flags, ui) {
385
410
  if (receiverReady) ui.success("Cloud", "online");
386
411
  else if (credentials) ui.warning("Cloud", "unavailable");
387
412
 
388
- if (reauthorizationRequired) {
389
- ui.next("re-entry connect", "Approve this Mac again in your browser; the old credential was rejected.");
413
+ if (reauthorizationRequired || credentialExpired) {
414
+ ui.next(cliCommand("disconnect"), "Clear this Mac's saved connection, then run install to connect again.");
390
415
  } else if (!connected || !service.running) {
391
- ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
416
+ ui.next(cliCommand("install"), "Finish setup and start Re-entry in the background.");
392
417
  } else if (!receiverReady) {
393
- ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
418
+ ui.next(cliCommand("status"), "Check again when the Re-entry Cloud service is available.");
394
419
  } else {
395
420
  ui.complete("Everything looks good", "Re-entry is ready for approved work.");
396
- ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
421
+ ui.next(cliCommand("listen"), "Watch live activity. Press Ctrl+C when you are done.");
397
422
  }
398
423
  } else {
399
424
  process.stdout.write(`${JSON.stringify({
@@ -403,6 +428,7 @@ async function status(flags, ui) {
403
428
  connector_id: credentials?.connector_id ?? null,
404
429
  receiver_origin: credentials?.receiver_origin ?? null,
405
430
  receiver_ready: receiverReady,
431
+ credential_expired: credentialExpired,
406
432
  service_installed: service.installed,
407
433
  service_running: service.running,
408
434
  codex_binary: readiness.installation.executable,
@@ -421,20 +447,28 @@ async function listen(flags, ui) {
421
447
  filename: defaultCredentialFile(),
422
448
  }).load();
423
449
  const reauthorizationRequired = await hasConnectorReauthorizationRequired(defaultCredentialFile());
424
- if (reauthorizationRequired || !service.running || !credentials) {
450
+ const credentialExpired = Boolean(
451
+ credentials && Date.parse(credentials.connector_expires_at) <= Date.now(),
452
+ );
453
+ if (reauthorizationRequired || credentialExpired || !service.running || !credentials) {
425
454
  if (ui.interactive) {
426
455
  if (reauthorizationRequired) {
427
- ui.warning("Account", "reconnect required; the Cloud Receiver rejected this Mac");
428
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
456
+ ui.warning("Account", "disconnect required; the Cloud Receiver rejected this Mac");
457
+ ui.next(cliCommand("disconnect"), "Clear this Mac's saved connection, then run install again.");
458
+ }
459
+ if (credentialExpired) {
460
+ ui.warning("Account", "connection expired");
461
+ ui.next(cliCommand("disconnect"), "Clear this Mac's saved connection, then run install again.");
429
462
  }
430
463
  if (!credentials) ui.warning("Account", "not connected");
431
464
  if (!service.running) ui.warning("Background", "not running");
432
- ui.next("re-entry install", "Finish setup and start the background Connector.");
465
+ ui.next(cliCommand("install"), "Finish setup and start the background Connector.");
433
466
  } else {
434
467
  process.stdout.write(`${JSON.stringify({
435
468
  event: "connector_listener_unavailable",
436
469
  connected: Boolean(credentials),
437
470
  reauthorization_required: reauthorizationRequired,
471
+ credential_expired: credentialExpired,
438
472
  service_running: service.running,
439
473
  })}\n`);
440
474
  }
@@ -494,7 +528,7 @@ async function testCodex(flags, positionals, ui) {
494
528
  if (ui.interactive) {
495
529
  ui.stopWait("Codex", "completed the local test");
496
530
  ui.complete("Test passed", "The same fresh-session process seam is ready for Re-entry work.");
497
- ui.next("re-entry listen", "Watch the background Connector for approved work.");
531
+ ui.next(cliCommand("listen"), "Watch the background Connector for approved work.");
498
532
  } else {
499
533
  process.stdout.write('{"event":"connector_codex_test_passed"}\n');
500
534
  }
@@ -511,7 +545,7 @@ function reportLiveActivity(event, ui) {
511
545
  );
512
546
  } else if (event.event === "connector_reauthorization_required" || event.code === "connector_identity_invalid") {
513
547
  ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
514
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
548
+ ui.next(cliCommand("disconnect"), "Clear this Mac's saved connection, then run install again.");
515
549
  return;
516
550
  } else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
517
551
  ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
@@ -534,7 +568,7 @@ async function stop(flags, ui) {
534
568
  if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
535
569
  else if (result.stopped) ui.complete("Re-entry is paused", "Your account connection is still saved.");
536
570
  else ui.info("Already stopped", "no running background Connector was found");
537
- if (result.supported) ui.next("re-entry install", "Start Re-entry in the background again.");
571
+ if (result.supported) ui.next(cliCommand("install"), "Start Re-entry in the background again.");
538
572
  } else {
539
573
  process.stdout.write(`${JSON.stringify({
540
574
  event: "connector_stopped",
@@ -544,6 +578,32 @@ async function stop(flags, ui) {
544
578
  }
545
579
  }
546
580
 
581
+ async function disconnect(flags, ui) {
582
+ if (ui.interactive) {
583
+ ui.begin("Disconnect Re-entry", "Stop background delivery and clear this Mac's saved connection");
584
+ }
585
+ const result = await disconnectMacConnectorService({
586
+ credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
587
+ });
588
+ if (ui.interactive) {
589
+ if (!result.supported) {
590
+ ui.warning("Platform", "connection lifecycle control currently supports macOS only");
591
+ } else if (result.disconnected) {
592
+ ui.complete("This Mac is disconnected", "The LaunchAgent and local Connector credential are cleared.");
593
+ } else {
594
+ ui.info("Already disconnected", "No saved local connection or background service was found.");
595
+ }
596
+ if (result.supported) ui.next(cliCommand("install"), "Connect this Mac to one Re-entry account again.");
597
+ } else {
598
+ process.stdout.write(`${JSON.stringify({
599
+ event: "connector_disconnected",
600
+ supported: result.supported,
601
+ disconnected: result.disconnected,
602
+ removed_paths: result.removedPaths,
603
+ })}\n`);
604
+ }
605
+ }
606
+
547
607
  async function uninstall(flags, ui) {
548
608
  if (ui.interactive) {
549
609
  ui.begin("Uninstall Re-entry", "Stop the Connector and remove only its local service data");
@@ -557,7 +617,7 @@ async function uninstall(flags, ui) {
557
617
  });
558
618
  if (ui.interactive) {
559
619
  ui.complete("Removed from this Mac", "The local service, connection, and logs are gone.");
560
- ui.next("npx @4xeoz/re-entry install", "Connect this Mac again whenever you are ready.");
620
+ ui.next(cliCommand("install"), "Connect this Mac again whenever you are ready.");
561
621
  } else {
562
622
  process.stdout.write(`${JSON.stringify({
563
623
  event: "connector_uninstalled",
@@ -589,7 +649,7 @@ async function install(flags, ui) {
589
649
  if (ui.interactive) {
590
650
  ui.stopWait("Background", "running at login");
591
651
  ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
592
- ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
652
+ ui.next(cliCommand("listen"), "Watch live activity. Press Ctrl+C when you are done.");
593
653
  } else {
594
654
  process.stdout.write(`${JSON.stringify({
595
655
  event: "connector_service_installed",
@@ -627,8 +687,8 @@ async function start(flags, ui) {
627
687
  credentials = await connect(runtimeFlags, ui, { quietHeader: true });
628
688
  } else if (await hasConnectorReauthorizationRequired(credentialFile)) {
629
689
  if (ui.interactive) {
630
- ui.warning("Account", "this Mac needs to be connected again");
631
- ui.next("re-entry connect", "Create a new dashboard pairing code and connect this Mac again.");
690
+ ui.warning("Account", "this Mac must be disconnected before reconnecting");
691
+ ui.next(cliCommand("disconnect"), "Clear the saved connection, then run install again.");
632
692
  } else {
633
693
  process.stdout.write('{"event":"connector_reauthorization_required"}\n');
634
694
  }
@@ -674,7 +734,7 @@ async function start(flags, ui) {
674
734
  });
675
735
  if (ui.interactive) {
676
736
  ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
677
- ui.next("re-entry connect", "Approve this Mac again in your browser.");
737
+ ui.next(cliCommand("disconnect"), "Clear this Mac's saved connection, then run install again.");
678
738
  } else {
679
739
  process.stdout.write('{"event":"connector_reauthorization_required"}\n');
680
740
  }
@@ -866,6 +926,7 @@ function validateCommandFlags(command, flags, positionals) {
866
926
  listen: new Set(["json"]),
867
927
  test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
868
928
  stop: new Set(["json"]),
929
+ disconnect: new Set(["credential-file", "json"]),
869
930
  uninstall: new Set(["credential-file", "yes", "json"]),
870
931
  install: new Set([
871
932
  "receiver",
@@ -1038,15 +1099,16 @@ function errorHint(error) {
1038
1099
  connector_codex_cd_missing: "pass --codex-cd /absolute/path/to/your/Host-project",
1039
1100
  connector_codex_not_found: "install Codex, add it to PATH, or pass --codex-binary /path/to/codex",
1040
1101
  connector_codex_binary_not_found: "check --codex-binary or remove it to use automatic discovery",
1041
- connector_codex_unusable: "open Codex, complete login if needed, then run `npm run doctor` again",
1102
+ connector_codex_unusable: `open Codex, complete login if needed, then run \`${cliCommand("doctor")}\` again`,
1042
1103
  connector_node_unsupported: "use Node.js 24 or newer, then run the command again",
1043
1104
  connector_receiver_missing: "pass --receiver <replacement-receiver-origin> or set REENTRY_RECEIVER_ORIGIN",
1044
- connector_credentials_missing: "connect this Mac once with `re-entry connect`",
1045
- connector_credentials_expired: "run `re-entry connect` to authorize this Mac again",
1105
+ connector_credentials_missing: `connect this Mac once with \`${cliCommand("connect")}\``,
1106
+ connector_credentials_expired: `run \`${cliCommand("disconnect")}\`, then \`${cliCommand("install")}\` to authorize this Mac again`,
1046
1107
  connector_credentials_already_exists: "use the existing Connector credential or ask for a new pairing code",
1047
- connector_identity_invalid: "run `re-entry connect` and approve this Mac again",
1048
- connector_reauthorization_required: "run `re-entry connect` and approve this Mac again",
1049
- connector_pairing_code_missing: "ask the Host backend for a new pairing code, then run pair in a terminal",
1108
+ connector_account_already_connected: `run \`${cliCommand("disconnect")}\` before connecting a different account or Receiver`,
1109
+ connector_identity_invalid: `run \`${cliCommand("disconnect")}\`, then \`${cliCommand("install")}\` and approve this Mac again`,
1110
+ connector_reauthorization_required: `run \`${cliCommand("disconnect")}\`, then \`${cliCommand("install")}\` and approve this Mac again`,
1111
+ connector_pairing_code_missing: `ask the Host backend for a new pairing code, then run \`${cliCommand("pair")}\` in a terminal`,
1050
1112
  pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
1051
1113
  host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
1052
1114
  pairing_expired: "ask the Host backend for a new pairing code",
@@ -1054,14 +1116,14 @@ function errorHint(error) {
1054
1116
  pairing_network_error: "check your internet connection and the Receiver address, then try again",
1055
1117
  workspace_directory_unavailable: "choose a readable folder or pass --codex-cd /absolute/path",
1056
1118
  workspace_selection_cancelled: "run the command again when you are ready to choose a workspace",
1057
- device_authorization_expired: "run `re-entry connect` again and approve within ten minutes",
1058
- device_authorization_denied: "run `re-entry connect` again when you are ready to approve this Mac",
1119
+ device_authorization_expired: `run \`${cliCommand("connect")}\` again and approve within ten minutes`,
1120
+ device_authorization_denied: `run \`${cliCommand("connect")}\` again when you are ready to approve this Mac`,
1059
1121
  connector_poll_interval_invalid: "use a polling interval between 1000 and 60000 milliseconds",
1060
1122
  connector_max_errors_invalid: "use a maximum error count between 1 and 20",
1061
- connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
1062
- connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
1063
- connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
1064
- connector_test_prompt_missing: "use `re-entry test \"Reply with: Re-entry is working.\"`",
1123
+ connector_service_load_failed: `run \`${cliCommand("install")}\` again; if it still fails, inspect the Connector error log`,
1124
+ connector_uninstall_confirmation_required: `run \`${cliCommand("uninstall")}\` interactively and type DELETE, or pass \`--yes\` in a deliberate script`,
1125
+ connector_service_stop_failed: `check the Connector status and try \`${cliCommand("stop")}\` again`,
1126
+ connector_test_prompt_missing: `use \`${cliCommand('test "Reply with: Re-entry is working."')}\``,
1065
1127
  connector_test_prompt_invalid: "use one short, single-line prompt inside quotes",
1066
1128
  connector_activation_timeout_invalid: "use an activation timeout between 100 and 60000 milliseconds",
1067
1129
  };