@4xeoz/re-entry 0.2.4 → 0.2.6

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
@@ -16,6 +16,16 @@ Codex may read and write.
16
16
  npx @4xeoz/re-entry install
17
17
  ```
18
18
 
19
+ `npx` runs a temporary copy, so it does not leave a permanent shell command behind. Install the
20
+ small CLI globally once if you want to use `re-entry` from any folder afterward:
21
+
22
+ ```sh
23
+ npm install --global @4xeoz/re-entry
24
+ ```
25
+
26
+ The package installs both `re-entry` and the older `reentry` spelling; `re-entry` is the documented
27
+ command.
28
+
19
29
  This uses the deployed Re-entry Cloud Receiver by default:
20
30
  `https://reentry-cloud.vercel.app`.
21
31
 
@@ -27,6 +37,10 @@ npx @4xeoz/re-entry install \
27
37
  --codex-cd /absolute/path/to/your/project
28
38
  ```
29
39
 
40
+ The guided screen stays intentionally small: **Workspace → System check → Connect Re-entry**. It
41
+ shows one clear next command when setup finishes; internal Connector IDs, credential paths, and log
42
+ paths stay out of the normal success screen.
43
+
30
44
  Run this from the Host project directory, your home directory, or another normal working
31
45
  directory—not from a checked-out `runtime/local-connector` package directory. npm can treat that
32
46
  source directory as the package itself and fail to create the temporary executable link.
@@ -70,11 +84,13 @@ preview.
70
84
 
71
85
  ```sh
72
86
  re-entry status
87
+ re-entry listen
73
88
  re-entry --help
74
89
  ```
75
90
 
76
91
  The status view checks the local authorization, background job, Receiver reachability, Node, and
77
- Codex. Useful development commands are:
92
+ Codex. `listen` watches the already-running background Connector and displays new activity until
93
+ you press Ctrl+C; it does not start a competing second poller. Useful development commands are:
78
94
 
79
95
  ```sh
80
96
  re-entry doctor --codex-cd /absolute/path/to/project
@@ -83,6 +99,15 @@ re-entry claim-once --codex-cd /absolute/path/to/project
83
99
  re-entry start --codex-cd /absolute/path/to/project
84
100
  ```
85
101
 
102
+ Test the local Codex handoff without waiting for Cloud work:
103
+
104
+ ```sh
105
+ re-entry test "Reply with: Re-entry is working."
106
+ ```
107
+
108
+ This starts one fresh local Codex process through the same adapter seam used by real deliveries. It
109
+ does not create a Grant, claim Receiver work, or prove the browser/WebMCP return path.
110
+
86
111
  To pause or remove the local Connector:
87
112
 
88
113
  ```sh
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@4xeoz/re-entry",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
4
4
  "type": "module",
5
5
  "description": "Outbound Local Connector process for the Re-entry Core local preview.",
6
6
  "publishConfig": {
@@ -10,7 +10,10 @@
10
10
  "README.md",
11
11
  "src"
12
12
  ],
13
- "bin": "./src/main.mjs",
13
+ "bin": {
14
+ "re-entry": "./src/main.mjs",
15
+ "reentry": "./src/main.mjs"
16
+ },
14
17
  "exports": {
15
18
  ".": "./src/index.mjs",
16
19
  "./connector": "./src/local-connector.mjs",
@@ -0,0 +1,107 @@
1
+ import { open, stat } from "node:fs/promises";
2
+
3
+ const MAX_LINE_BYTES = 16 * 1_024;
4
+ const MAX_READ_BYTES = 64 * 1_024;
5
+
6
+ /**
7
+ * Follow new JSON events written by the installed background Connector.
8
+ * The monitor starts at the end of each file and never starts another Connector poller.
9
+ */
10
+ export async function followConnectorActivity(options) {
11
+ requireOptions(options);
12
+ const cursors = await Promise.all(options.paths.map(initializeCursor));
13
+ const pollIntervalMs = options.pollIntervalMs ?? 350;
14
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 10 || pollIntervalMs > 5_000) {
15
+ throw new TypeError("Activity monitor poll interval is invalid");
16
+ }
17
+
18
+ while (!options.signal.aborted) {
19
+ for (const cursor of cursors) {
20
+ await readNewEvents(cursor, options.onEvent);
21
+ }
22
+ await waitForNextRead(pollIntervalMs, options.signal);
23
+ }
24
+ }
25
+
26
+ async function initializeCursor(path) {
27
+ try {
28
+ const metadata = await stat(path);
29
+ return { path, offset: metadata.size, pending: "" };
30
+ } catch (error) {
31
+ if (error?.code === "ENOENT") return { path, offset: 0, pending: "" };
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ async function readNewEvents(cursor, onEvent) {
37
+ let handle;
38
+ try {
39
+ handle = await open(cursor.path, "r");
40
+ const metadata = await handle.stat();
41
+ if (metadata.size < cursor.offset) {
42
+ cursor.offset = 0;
43
+ cursor.pending = "";
44
+ }
45
+ if (metadata.size === cursor.offset) return;
46
+ const length = Math.min(metadata.size - cursor.offset, MAX_READ_BYTES);
47
+ const buffer = Buffer.alloc(length);
48
+ const { bytesRead } = await handle.read(buffer, 0, length, cursor.offset);
49
+ cursor.offset += bytesRead;
50
+ const lines = `${cursor.pending}${buffer.subarray(0, bytesRead).toString("utf8")}`.split("\n");
51
+ cursor.pending = lines.pop() ?? "";
52
+ if (Buffer.byteLength(cursor.pending, "utf8") > MAX_LINE_BYTES) cursor.pending = "";
53
+ for (const line of lines) {
54
+ const event = parseEvent(line);
55
+ if (event) await onEvent(event);
56
+ }
57
+ } catch (error) {
58
+ if (error?.code !== "ENOENT") throw error;
59
+ } finally {
60
+ await handle?.close();
61
+ }
62
+ }
63
+
64
+ function parseEvent(line) {
65
+ if (line.length === 0 || Buffer.byteLength(line, "utf8") > MAX_LINE_BYTES) return null;
66
+ try {
67
+ const value = JSON.parse(line);
68
+ if (!value || typeof value !== "object" || Array.isArray(value) || typeof value.event !== "string") {
69
+ return null;
70
+ }
71
+ return value;
72
+ } catch {
73
+ return null;
74
+ }
75
+ }
76
+
77
+ function waitForNextRead(milliseconds, signal) {
78
+ if (signal.aborted) return Promise.resolve();
79
+ return new Promise((resolve) => {
80
+ const timer = setTimeout(done, milliseconds);
81
+ function done() {
82
+ clearTimeout(timer);
83
+ signal.removeEventListener("abort", done);
84
+ resolve();
85
+ }
86
+ signal.addEventListener("abort", done, { once: true });
87
+ });
88
+ }
89
+
90
+ function requireOptions(options) {
91
+ if (!options || typeof options !== "object" || Array.isArray(options)) {
92
+ throw new TypeError("Activity monitor options are required");
93
+ }
94
+ const fields = Object.keys(options);
95
+ if (fields.some((field) => !["paths", "signal", "onEvent", "pollIntervalMs"].includes(field))) {
96
+ throw new TypeError("Activity monitor options contain an unsupported field");
97
+ }
98
+ if (
99
+ !Array.isArray(options.paths) ||
100
+ options.paths.length !== 2 ||
101
+ options.paths.some((path) => typeof path !== "string" || path.length === 0) ||
102
+ !(options.signal instanceof AbortSignal) ||
103
+ typeof options.onEvent !== "function"
104
+ ) {
105
+ throw new TypeError("Activity monitor options are invalid");
106
+ }
107
+ }
@@ -8,7 +8,7 @@ import process from "node:process";
8
8
  export async function waitForEnterToOpenBrowser(options = {}) {
9
9
  const input = options.input ?? process.stdin;
10
10
  const output = options.output ?? process.stdout;
11
- const prompt = options.prompt ?? " Press Enter to open Re-entry in your browser: ";
11
+ const prompt = options.prompt ?? "\n Press Enter to open Re-entry ";
12
12
  const readline = createInterface({ input, output });
13
13
  try {
14
14
  await readline.question(prompt);
@@ -22,6 +22,7 @@ const MIN_COMMAND_TIMEOUT_MS = 100;
22
22
  const MAX_COMMAND_TIMEOUT_MS = 60_000;
23
23
  const MAX_REFERENCE_BYTES = 4 * 1_024;
24
24
  const CONTROL_CHARACTER_PATTERN = /[\u0000-\u001f\u007f]/;
25
+ const PROMPT_CONTROL_CHARACTER_PATTERN = /[\u0000-\u0009\u000b-\u001f\u007f]/;
25
26
 
26
27
  /**
27
28
  * Create the fresh-session Codex adapter that lives inside the Local Connector process.
@@ -54,11 +55,11 @@ export function createCodexExecAdapter(options) {
54
55
  return activationResult(activation, "rejected", "activation_rejected", null);
55
56
  }
56
57
 
57
- await runCodexExec({
58
+ await runCodexPrompt({
58
59
  executable,
59
60
  workingDirectory,
60
61
  prompt: buildContinuationPrompt(activation),
61
- timeoutMs: commandTimeoutMs,
62
+ commandTimeoutMs,
62
63
  spawnCommand,
63
64
  });
64
65
  return activationResult(
@@ -71,6 +72,30 @@ export function createCodexExecAdapter(options) {
71
72
  });
72
73
  }
73
74
 
75
+ /**
76
+ * Run one local prompt through the same fresh Codex process seam used by real activations.
77
+ * This helper carries no Receiver authority and is intended only for an explicit local smoke test.
78
+ */
79
+ export async function runCodexPrompt(options) {
80
+ requireExactRecord(
81
+ options,
82
+ ["workingDirectory", "prompt", "executable", "commandTimeoutMs", "spawnCommand"],
83
+ ["workingDirectory", "prompt"],
84
+ "Codex prompt options",
85
+ );
86
+ const workingDirectory = validateCodexWorkingDirectory(options.workingDirectory);
87
+ const executable = options.executable === undefined
88
+ ? discoverCodexExecutable()
89
+ : requireReference(options.executable, "Codex executable");
90
+ const prompt = requirePrompt(options.prompt);
91
+ const timeoutMs = requireTimeout(options.commandTimeoutMs ?? 60_000);
92
+ const spawnCommand = options.spawnCommand ?? spawn;
93
+ if (typeof spawnCommand !== "function") {
94
+ throw new TypeError("Codex prompt spawnCommand must be a function");
95
+ }
96
+ await runCodexExec({ executable, workingDirectory, prompt, timeoutMs, spawnCommand });
97
+ }
98
+
74
99
  function buildContinuationPrompt(activation) {
75
100
  return [
76
101
  "You are a Re-entry continuation agent.",
@@ -183,6 +208,19 @@ function requireReference(value, label) {
183
208
  return value;
184
209
  }
185
210
 
211
+ function requirePrompt(value) {
212
+ if (
213
+ typeof value !== "string" ||
214
+ value.length === 0 ||
215
+ value.trim() !== value ||
216
+ Buffer.byteLength(value, "utf8") > MAX_REFERENCE_BYTES ||
217
+ PROMPT_CONTROL_CHARACTER_PATTERN.test(value)
218
+ ) {
219
+ throw new TypeError("Codex prompt is invalid");
220
+ }
221
+ return value;
222
+ }
223
+
186
224
  function requireExactRecord(value, allowedFields, requiredFields, label) {
187
225
  if (!value || typeof value !== "object" || Array.isArray(value)) {
188
226
  throw new TypeError(`${label} must be an object`);
package/src/main.mjs CHANGED
@@ -9,7 +9,8 @@ import { randomBytes } from "node:crypto";
9
9
  import process from "node:process";
10
10
 
11
11
  import { LocalConnectorClient } from "@webmcp-challenge/reentry-core/local-connector-client";
12
- import { createCodexExecAdapter } from "./codex-exec-adapter.mjs";
12
+ import { followConnectorActivity } from "./activity-monitor.mjs";
13
+ import { createCodexExecAdapter, runCodexPrompt } from "./codex-exec-adapter.mjs";
13
14
  import {
14
15
  discoverCodexExecutable,
15
16
  requireSupportedNode,
@@ -47,8 +48,8 @@ async function main() {
47
48
  process.stdout.write(`${CONNECTOR_VERSION}\n`);
48
49
  return;
49
50
  }
50
- const { command, flags } = parseArguments(argumentsList);
51
- validateCommandFlags(command, flags);
51
+ const { command, flags, positionals } = parseArguments(argumentsList);
52
+ validateCommandFlags(command, flags, positionals);
52
53
  ui = createTerminalUi({ interactive: flags.json !== true && process.stdout.isTTY === true });
53
54
  requireSupportedNode();
54
55
  if (command === "doctor") {
@@ -67,6 +68,14 @@ async function main() {
67
68
  await status(flags, ui);
68
69
  return;
69
70
  }
71
+ if (command === "listen") {
72
+ await listen(flags, ui);
73
+ return;
74
+ }
75
+ if (command === "test") {
76
+ await testCodex(flags, positionals, ui);
77
+ return;
78
+ }
70
79
  if (command === "stop") {
71
80
  await stop(flags, ui);
72
81
  return;
@@ -111,12 +120,14 @@ Connect this Mac once, then receive approved work in Codex in the background.
111
120
 
112
121
  Usage:
113
122
  re-entry install Recommended first run
114
- re-entry status Check the connection
115
- re-entry start Run in this terminal
123
+ re-entry listen Watch live activity
124
+ re-entry test "Reply with hello" Test Codex locally
116
125
 
117
126
  Commands:
118
127
  install Check this Mac, connect the Re-entry account, and start at login
119
128
  status Show account, Receiver, service, Node.js, and Codex readiness
129
+ listen Watch the background Connector until you press Ctrl+C
130
+ test Start one fresh local Codex session with the supplied prompt
120
131
  stop Stop the background Connector without removing its credential
121
132
  uninstall Stop the Connector and remove its local service data
122
133
  connect Authorize this Mac without installing the background service
@@ -134,6 +145,7 @@ Common options:
134
145
  --help Show this help
135
146
  --version Show the installed version
136
147
 
148
+ Both commands are installed: re-entry and reentry.
137
149
  The Connector opens no inbound port. Host keys never belong on this Mac.
138
150
  `);
139
151
  }
@@ -149,9 +161,12 @@ function readConnectorVersion() {
149
161
  }
150
162
 
151
163
  function doctor(flags, ui) {
152
- if (ui.interactive) ui.begin("Check this Mac", "Read-only readiness check");
164
+ if (ui.interactive) {
165
+ ui.begin("System check", "Confirm that this Mac is ready for Re-entry.");
166
+ ui.section("CHECK", "Requirements");
167
+ }
153
168
  const readiness = inspectReadiness(flags);
154
- showReadiness(readiness, ui);
169
+ showReadiness(readiness, ui, { detailed: true });
155
170
  if (!ui.interactive) {
156
171
  process.stdout.write(`${JSON.stringify({
157
172
  event: "connector_ready",
@@ -179,21 +194,26 @@ async function withWorkspaceDirectory(flags, ui) {
179
194
  if (flags["codex-cd"] !== undefined || !ui.interactive) {
180
195
  return { ...flags, "codex-cd": flags["codex-cd"] ?? process.cwd() };
181
196
  }
182
- ui.info("Workspace", "choose the folder Codex will open");
183
197
  const selected = await chooseWorkspaceDirectory({ startDirectory: process.cwd() });
184
198
  if (!selected) throw cliFailure("connector_codex_cd_missing");
185
- ui.success("Workspace", selected);
186
199
  return { ...flags, "codex-cd": selected };
187
200
  }
188
201
 
189
- function showReadiness(readiness, ui) {
202
+ function showReadiness(readiness, ui, options = {}) {
190
203
  if (!ui.interactive) return;
191
- ui.success("Node.js", process.versions.node);
192
- ui.success("Codex", `${readiness.installation.version} · ${readiness.installation.executable}`);
193
- if (readiness.workingDirectory) {
194
- ui.success("Host project", readiness.workingDirectory);
195
- } else {
196
- ui.info("Host project", "not selected; pass --codex-cd when starting Codex");
204
+ ui.success("Node.js", `v${process.versions.node}`);
205
+ ui.success(
206
+ "Codex",
207
+ options.detailed
208
+ ? `${readiness.installation.version} · ${readiness.installation.executable}`
209
+ : "ready",
210
+ );
211
+ if (options.includeWorkspace !== false) {
212
+ if (readiness.workingDirectory) {
213
+ ui.success("Workspace", readiness.workingDirectory);
214
+ } else {
215
+ ui.info("Workspace", "selected automatically when Re-entry starts");
216
+ }
197
217
  }
198
218
  }
199
219
 
@@ -215,18 +235,16 @@ async function pair(flags, ui, options = {}) {
215
235
  if (ui.interactive) ui.wait("Contacting Re-entry…");
216
236
  const credentials = await client.pair({ userCode }, async ({ verificationUri }) => {
217
237
  if (ui.interactive) {
218
- ui.stopWait("Re-entry ready", "approval link created");
219
- ui.step("Browser", "press Enter to open the Re-entry approval page");
220
- ui.info("Open this URL", verificationUri);
238
+ ui.stopWait("Secure link", "ready");
239
+ ui.info("Browser link", verificationUri);
221
240
  await waitForEnterToOpenBrowser();
222
- ui.step("Browser", "opening the Re-entry approval page");
223
- ui.wait("Waiting for you to approve this Mac…");
241
+ ui.wait("Waiting for approval in your browser…");
224
242
  } else {
225
243
  process.stdout.write(`${JSON.stringify({ event: "pairing_waiting", verification_uri: verificationUri })}\n`);
226
244
  }
227
245
  });
228
246
  if (ui.interactive) {
229
- ui.stopWait("Approval received", "the Receiver approved this Connector");
247
+ ui.stopWait("Approved", "this Mac is connected");
230
248
  if (!credentials.browserOpened) {
231
249
  ui.warning("Browser", "did not open automatically; use the URL above");
232
250
  }
@@ -240,7 +258,8 @@ async function pair(flags, ui, options = {}) {
240
258
  connector_expires_at: credentials.connector_expires_at,
241
259
  });
242
260
  if (ui.interactive) {
243
- ui.success("This Mac is paired", `credential saved at ${credentialFile}`);
261
+ ui.complete("Pairing complete", "Re-entry can now deliver approved work to this Mac.");
262
+ ui.next("re-entry start", "Wait for approved work in this terminal.");
244
263
  } else {
245
264
  process.stdout.write(`${JSON.stringify({ event: "connector_paired", connector_id: credentials.connector_id })}\n`);
246
265
  }
@@ -257,8 +276,10 @@ async function connect(flags, ui, options = {}) {
257
276
  const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
258
277
  if (currentIsValid && current.receiver_origin === receiver) {
259
278
  if (ui.interactive) {
260
- ui.success("Already connected", `${current.connector_id} · ${current.receiver_origin}`);
261
- ui.info("Next", "run `re-entry start` to wait for approved work");
279
+ ui.success("Account", "already connected");
280
+ if (!options.guidedInstall) {
281
+ ui.next("re-entry start", "Wait for approved work in this terminal.");
282
+ }
262
283
  } else {
263
284
  process.stdout.write(`${JSON.stringify({
264
285
  event: "connector_already_connected",
@@ -274,8 +295,8 @@ async function connect(flags, ui, options = {}) {
274
295
  }
275
296
 
276
297
  if (ui.interactive) {
277
- ui.step("Re-entry", receiver);
278
- ui.step("This Mac", flags["device-name"] ?? defaultDeviceName());
298
+ if (!options.guidedInstall) ui.info("Re-entry", displayReceiver(receiver));
299
+ ui.info("This Mac", flags["device-name"] ?? defaultDeviceName());
279
300
  }
280
301
  const client = new LocalConnectorPairingClient({
281
302
  baseUrl: receiver,
@@ -286,12 +307,10 @@ async function connect(flags, ui, options = {}) {
286
307
  { deviceName: flags["device-name"] ?? defaultDeviceName() },
287
308
  async ({ verificationUri }) => {
288
309
  if (ui.interactive) {
289
- ui.stopWait("Re-entry ready", "secure approval link created");
290
- ui.step("Browser", "press Enter to open Re-entry sign-in and approval");
291
- ui.info("If it does not open", verificationUri);
310
+ ui.stopWait("Secure link", "ready");
311
+ ui.info("Browser link", verificationUri);
292
312
  await waitForEnterToOpenBrowser();
293
- ui.step("Browser", "opening Re-entry sign-in and approval");
294
- ui.wait("Waiting for you to connect this Mac…");
313
+ ui.wait("Waiting for approval in your browser…");
295
314
  } else {
296
315
  process.stdout.write(`${JSON.stringify({
297
316
  event: "connector_authorization_waiting",
@@ -301,7 +320,7 @@ async function connect(flags, ui, options = {}) {
301
320
  },
302
321
  );
303
322
  if (ui.interactive) {
304
- ui.stopWait("Approved", "Re-entry linked this Mac to your account");
323
+ ui.stopWait("Account", "connected");
305
324
  if (!credentials.browserOpened) {
306
325
  ui.warning("Browser", "did not open automatically; use the URL shown above");
307
326
  }
@@ -315,8 +334,10 @@ async function connect(flags, ui, options = {}) {
315
334
  };
316
335
  await store.save(saved);
317
336
  if (ui.interactive) {
318
- ui.success("Connected", `credential saved at ${credentialFile}`);
319
- ui.info("Next", "run `re-entry start` once; it will keep waiting in the background");
337
+ if (!options.guidedInstall) {
338
+ ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
339
+ ui.next("re-entry start", "Wait for approved work in this terminal.");
340
+ }
320
341
  } else {
321
342
  process.stdout.write(`${JSON.stringify({
322
343
  event: "connector_connected",
@@ -327,22 +348,33 @@ async function connect(flags, ui, options = {}) {
327
348
  }
328
349
 
329
350
  async function status(flags, ui) {
330
- if (ui.interactive) ui.begin("Connector status", "Account, background service, Receiver, and Codex");
351
+ if (ui.interactive) ui.begin("Status", "A quick check of this Mac and Re-entry.");
331
352
  const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
332
353
  const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
333
354
  const readiness = inspectReadiness(flags);
334
355
  const service = await inspectMacConnectorService();
335
356
  const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
336
- showReadiness(readiness, ui);
337
357
  const connected = Boolean(credentials && Date.parse(credentials.connector_expires_at) > Date.now());
338
358
  if (ui.interactive) {
339
- if (connected) ui.success("Account", `${credentials.connector_id} · authorized`);
340
- else ui.warning("Re-entry", "not connected; run `re-entry connect`");
341
- if (service.running) ui.success("Background", "running at login");
342
- else if (service.installed) ui.warning("Background", "installed but not running; run `re-entry install`");
343
- else ui.warning("Background", "not installed; run `re-entry install`");
344
- if (connected && receiverReady) ui.success("Receiver", `${credentials.receiver_origin} · reachable`);
345
- else if (connected) ui.warning("Receiver", `${credentials.receiver_origin} · unavailable`);
359
+ ui.section("SYSTEM", "This Mac");
360
+ showReadiness(readiness, ui);
361
+ ui.section("CONNECTION", "Re-entry");
362
+ if (connected) ui.success("Account", "connected");
363
+ else ui.warning("Account", "not connected");
364
+ if (service.running) ui.success("Background", "running");
365
+ else if (service.installed) ui.warning("Background", "stopped");
366
+ else ui.warning("Background", "not installed");
367
+ if (connected && receiverReady) ui.success("Cloud", "online");
368
+ else if (connected) ui.warning("Cloud", "unavailable");
369
+
370
+ if (!connected || !service.running) {
371
+ ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
372
+ } else if (!receiverReady) {
373
+ ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
374
+ } else {
375
+ ui.complete("Everything looks good", "Re-entry is ready for approved work.");
376
+ ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
377
+ }
346
378
  } else {
347
379
  process.stdout.write(`${JSON.stringify({
348
380
  event: "connector_status",
@@ -359,6 +391,109 @@ async function status(flags, ui) {
359
391
  }
360
392
  }
361
393
 
394
+ async function listen(flags, ui) {
395
+ if (ui.interactive) {
396
+ ui.begin("Live activity", "Watch the background Connector. Ctrl+C closes this view.");
397
+ }
398
+ const service = await inspectMacConnectorService();
399
+ const credentials = await new LocalConnectorCredentialStore({
400
+ filename: defaultCredentialFile(),
401
+ }).load();
402
+ if (!service.running || !credentials) {
403
+ if (ui.interactive) {
404
+ if (!credentials) ui.warning("Account", "not connected");
405
+ if (!service.running) ui.warning("Background", "not running");
406
+ ui.next("re-entry install", "Finish setup and start the background Connector.");
407
+ } else {
408
+ process.stdout.write(`${JSON.stringify({
409
+ event: "connector_listener_unavailable",
410
+ connected: Boolean(credentials),
411
+ service_running: service.running,
412
+ })}\n`);
413
+ }
414
+ return;
415
+ }
416
+
417
+ if (ui.interactive) {
418
+ ui.success("Background", "running");
419
+ ui.success("Account", "connected");
420
+ ui.wait("Listening for approved work…");
421
+ } else {
422
+ process.stdout.write('{"event":"connector_listener_started"}\n');
423
+ }
424
+
425
+ const stopSignal = createStopSignal();
426
+ try {
427
+ await followConnectorActivity({
428
+ paths: defaultConnectorLogFiles(),
429
+ signal: stopSignal.signal,
430
+ onEvent(event) {
431
+ if (ui.interactive) reportLiveActivity(event, ui);
432
+ else process.stdout.write(`${JSON.stringify(event)}\n`);
433
+ },
434
+ });
435
+ } finally {
436
+ stopSignal.close();
437
+ if (ui.interactive) {
438
+ ui.stopWait("Live view closed", "the background Connector is still running", "info");
439
+ } else {
440
+ process.stdout.write('{"event":"connector_listener_stopped"}\n');
441
+ }
442
+ }
443
+ }
444
+
445
+ async function testCodex(flags, positionals, ui) {
446
+ const prompt = requireTestPrompt(positionals);
447
+ const runtimeFlags = await withWorkspaceDirectory(flags, ui);
448
+ const readiness = inspectReadiness(runtimeFlags);
449
+ if (ui.interactive) {
450
+ ui.begin("Test Codex", "Run one local prompt through the Re-entry Codex adapter.");
451
+ ui.success("Codex", "ready");
452
+ ui.success("Workspace", readiness.workingDirectory);
453
+ ui.info("Prompt", prompt);
454
+ ui.wait("Starting a fresh Codex session…");
455
+ }
456
+ await runCodexPrompt({
457
+ workingDirectory: readiness.workingDirectory,
458
+ executable: readiness.installation.executable,
459
+ prompt,
460
+ commandTimeoutMs: readBoundedNumber(
461
+ flags["activation-timeout"] ?? 60_000,
462
+ 100,
463
+ 60_000,
464
+ "connector_activation_timeout_invalid",
465
+ ),
466
+ });
467
+ if (ui.interactive) {
468
+ ui.stopWait("Codex", "completed the local test");
469
+ ui.complete("Test passed", "The same fresh-session process seam is ready for Re-entry work.");
470
+ ui.next("re-entry listen", "Watch the background Connector for approved work.");
471
+ } else {
472
+ process.stdout.write('{"event":"connector_codex_test_passed"}\n');
473
+ }
474
+ }
475
+
476
+ function reportLiveActivity(event, ui) {
477
+ if (event.event === "connector_waiting") return;
478
+ if (event.event === "connector_activation_result") {
479
+ const accepted = event.outcome === "accepted";
480
+ ui.stopWait(
481
+ accepted ? "Work received" : "Work finished",
482
+ accepted ? "a fresh Codex session was started" : String(event.outcome ?? "unknown"),
483
+ accepted ? "success" : "warning",
484
+ );
485
+ } else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
486
+ ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
487
+ } else if (event.event === "connector_ready") {
488
+ ui.stopWait("Connector", "ready", "success");
489
+ } else if (event.event === "connector_stopped") {
490
+ ui.stopWait("Background", "stopped", "warning");
491
+ } else {
492
+ ui.stopWait("Activity", event.event.replaceAll("_", " "), "info");
493
+ }
494
+ ui.wait("Listening for approved work…");
495
+ }
496
+
362
497
  async function stop(flags, ui) {
363
498
  if (ui.interactive) {
364
499
  ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
@@ -366,8 +501,9 @@ async function stop(flags, ui) {
366
501
  const result = await stopMacConnectorService();
367
502
  if (ui.interactive) {
368
503
  if (!result.supported) ui.warning("Platform", "background service control currently supports macOS only");
369
- else if (result.stopped) ui.success("Stopped", "the background Connector is no longer running");
504
+ else if (result.stopped) ui.complete("Re-entry is paused", "Your account connection is still saved.");
370
505
  else ui.info("Already stopped", "no running background Connector was found");
506
+ if (result.supported) ui.next("re-entry install", "Start Re-entry in the background again.");
371
507
  } else {
372
508
  process.stdout.write(`${JSON.stringify({
373
509
  event: "connector_stopped",
@@ -389,8 +525,8 @@ async function uninstall(flags, ui) {
389
525
  credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
390
526
  });
391
527
  if (ui.interactive) {
392
- ui.success("Uninstalled", "local Connector service data was removed");
393
- ui.info("Package", "remove the npm package separately with `npm uninstall --global @4xeoz/re-entry`");
528
+ ui.complete("Removed from this Mac", "The local service, connection, and logs are gone.");
529
+ ui.next("npx @4xeoz/re-entry install", "Connect this Mac again whenever you are ready.");
394
530
  } else {
395
531
  process.stdout.write(`${JSON.stringify({
396
532
  event: "connector_uninstalled",
@@ -401,16 +537,18 @@ async function uninstall(flags, ui) {
401
537
  }
402
538
 
403
539
  async function install(flags, ui) {
404
- if (ui.interactive) {
405
- ui.begin("Install Re-entry", "Check Codex, connect your account, then start at login");
406
- ui.info("Setup", "choose a workspace, approve this Mac, then leave the Connector running");
407
- }
408
540
  const runtimeFlags = await withWorkspaceDirectory(flags, ui);
409
- if (ui.interactive) ui.info("Receiver", runtimeFlags.receiver ?? DEFAULT_RECEIVER_ORIGIN);
410
541
  const readiness = inspectReadiness(runtimeFlags);
411
- showReadiness(readiness, ui);
412
- const credentials = await connect(runtimeFlags, ui, { quietHeader: true });
413
- if (ui.interactive) ui.wait("Installing the background Connector…");
542
+ if (ui.interactive) {
543
+ ui.begin("Set up this Mac", "Three quick steps. You only do this once.");
544
+ ui.section("1 OF 3", "Workspace");
545
+ ui.success("Selected", readiness.workingDirectory);
546
+ ui.section("2 OF 3", "System check");
547
+ showReadiness(readiness, ui, { includeWorkspace: false });
548
+ ui.section("3 OF 3", "Connect Re-entry", "Approve this Mac in your browser.");
549
+ }
550
+ const credentials = await connect(runtimeFlags, ui, { quietHeader: true, guidedInstall: true });
551
+ if (ui.interactive) ui.wait("Starting Re-entry in the background…");
414
552
  const service = await installMacConnectorService({
415
553
  nodeExecutable: process.execPath,
416
554
  entrypoint: fileURLToPath(import.meta.url),
@@ -418,11 +556,9 @@ async function install(flags, ui) {
418
556
  credentialFile: flags["credential-file"] ?? defaultCredentialFile(),
419
557
  });
420
558
  if (ui.interactive) {
421
- ui.stopWait("Installed", "Re-entry will start automatically when you log in");
422
- ui.success("Account", credentials.connector_id);
423
- ui.info("Logs", service.stdoutPath);
424
- ui.info("Commands", "status to inspect · stop to pause · uninstall to remove local setup");
425
- ui.info("You are done", "the Connector now waits in the background");
559
+ ui.stopWait("Background", "running at login");
560
+ ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
561
+ ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
426
562
  } else {
427
563
  process.stdout.write(`${JSON.stringify({
428
564
  event: "connector_service_installed",
@@ -435,12 +571,13 @@ async function install(flags, ui) {
435
571
  }
436
572
 
437
573
  async function start(flags, ui) {
438
- if (ui.interactive) {
439
- ui.begin("Re-entry is starting", "Connect once, then wait quietly for work you approve");
440
- }
441
574
  const runtimeFlags = await withWorkspaceDirectory(flags, ui);
442
575
  const readiness = inspectReadiness(runtimeFlags);
443
- showReadiness(readiness, ui);
576
+ if (ui.interactive) {
577
+ ui.begin("Start Re-entry", "Wait for work you have approved.");
578
+ ui.section("READY", "This Mac");
579
+ showReadiness(readiness, ui);
580
+ }
444
581
  if (!ui.interactive) {
445
582
  process.stdout.write(`${JSON.stringify({
446
583
  event: "connector_ready",
@@ -647,9 +784,14 @@ function parseArguments(argumentsList) {
647
784
  ...(hasExplicitCommand ? argumentsList.slice(commandIndex + 1) : argumentsList.slice(commandIndex)),
648
785
  ];
649
786
  const flags = {};
787
+ const positionals = [];
650
788
  for (let index = 0; index < rest.length; index += 1) {
651
789
  const value = rest[index];
652
- if (!value.startsWith("--")) throw cliFailure("connector_argument_invalid");
790
+ if (!value.startsWith("--")) {
791
+ if (command !== "test") throw cliFailure("connector_argument_invalid");
792
+ positionals.push(value);
793
+ continue;
794
+ }
653
795
  const name = value.slice(2);
654
796
  if (name === "json" || name === "yes") {
655
797
  if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
@@ -661,15 +803,17 @@ function parseArguments(argumentsList) {
661
803
  flags[name] = next;
662
804
  index += 1;
663
805
  }
664
- return { command, flags };
806
+ return { command, flags, positionals };
665
807
  }
666
808
 
667
- function validateCommandFlags(command, flags) {
809
+ function validateCommandFlags(command, flags, positionals) {
668
810
  const allowedByCommand = {
669
811
  doctor: new Set(["codex-binary", "codex-cd", "json"]),
670
812
  pair: new Set(["receiver", "code", "credential-file", "json"]),
671
813
  connect: new Set(["receiver", "device-name", "credential-file", "json"]),
672
814
  status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
815
+ listen: new Set(["json"]),
816
+ test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
673
817
  stop: new Set(["json"]),
674
818
  uninstall: new Set(["credential-file", "yes", "json"]),
675
819
  install: new Set([
@@ -704,7 +848,11 @@ function validateCommandFlags(command, flags) {
704
848
  ]),
705
849
  };
706
850
  const allowed = allowedByCommand[command];
707
- if (!allowed || Object.keys(flags).some((name) => !allowed.has(name))) {
851
+ if (
852
+ !allowed ||
853
+ Object.keys(flags).some((name) => !allowed.has(name)) ||
854
+ (command !== "test" && positionals.length > 0)
855
+ ) {
708
856
  throw cliFailure("connector_argument_invalid");
709
857
  }
710
858
  }
@@ -719,6 +867,14 @@ function defaultCredentialFile() {
719
867
  return join(homedir(), ".webmcp-connector", "credentials.json");
720
868
  }
721
869
 
870
+ function defaultConnectorLogFiles() {
871
+ const stateDirectory = join(homedir(), ".webmcp-connector");
872
+ return [
873
+ join(stateDirectory, "connector.log"),
874
+ join(stateDirectory, "connector-error.log"),
875
+ ];
876
+ }
877
+
722
878
  function defaultDeviceName() {
723
879
  const value = hostname().trim();
724
880
  return value.length >= 2 && Buffer.byteLength(value, "utf8") <= 80
@@ -726,6 +882,31 @@ function defaultDeviceName() {
726
882
  : "This Mac";
727
883
  }
728
884
 
885
+ function displayReceiver(origin) {
886
+ try {
887
+ return new URL(origin).host;
888
+ } catch {
889
+ return origin;
890
+ }
891
+ }
892
+
893
+ function requireTestPrompt(positionals) {
894
+ if (!Array.isArray(positionals) || positionals.length !== 1) {
895
+ throw cliFailure("connector_test_prompt_missing");
896
+ }
897
+ const value = positionals[0];
898
+ if (
899
+ typeof value !== "string" ||
900
+ value.trim() !== value ||
901
+ value.length === 0 ||
902
+ Buffer.byteLength(value, "utf8") > 4 * 1_024 ||
903
+ /[\u0000-\u001f\u007f]/.test(value)
904
+ ) {
905
+ throw cliFailure("connector_test_prompt_invalid");
906
+ }
907
+ return value;
908
+ }
909
+
729
910
  function readBoundedNumber(value, minimum, maximum, code) {
730
911
  const number = typeof value === "number" ? value : Number(value);
731
912
  if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
@@ -824,6 +1005,9 @@ function errorHint(error) {
824
1005
  connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
825
1006
  connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
826
1007
  connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
1008
+ connector_test_prompt_missing: "use `re-entry test \"Reply with: Re-entry is working.\"`",
1009
+ connector_test_prompt_invalid: "use one short, single-line prompt inside quotes",
1010
+ connector_activation_timeout_invalid: "use an activation timeout between 100 and 60000 milliseconds",
827
1011
  };
828
1012
  return hints[error?.code] ?? "check the Receiver address and try again";
829
1013
  }
@@ -8,19 +8,15 @@ const YELLOW = "\u001b[33m";
8
8
  const RED = "\u001b[31m";
9
9
  const CYAN = "\u001b[36m";
10
10
  const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
11
- const REENTRY_ASCII = [
12
- " ____ _____ _____ _ _ _____ ____ __ __",
13
- " | _ \\| ____| | ____| \\ | | ____| _ \\ \\ \\ / /",
14
- " | |_) | _| _____ | _| | \\| | _| | |_) | \\ V /",
15
- " | _ <| |__|_____| | |___| |\\ | |___| _ < | |",
16
- " |_| \\_\\_____| |_____|_| \\_|_____|_| \\_\\ |_|",
17
- ];
18
- const REENTRY_PANEL = [
19
- " +-----------------------------------------------+",
20
- " | RE-ENTRY |",
21
- " | LOCAL CONNECTOR |",
22
- " +-----------------------------------------------+",
23
- ];
11
+ const RULE = " ─────────────────────────────────────────────";
12
+
13
+ export const REENTRY_WORDMARK = Object.freeze([
14
+ " ____ _____ _____ _ _ _____ ______ __",
15
+ " | _ \\| ____| | ____| \\ | |_ _| _ \\ \\ / /",
16
+ " | |_) | _| _____| _| | \\| | | | | |_) | \\ V /",
17
+ " | _ <| |__|_____| |___| |\\ | | | | _ < | |",
18
+ " |_| \\_\\_____| |_____|_| \\_| |_| |_| \\_\\ |_|",
19
+ ]);
24
20
 
25
21
  /**
26
22
  * Small dependency-free terminal presentation for the Local Connector CLI.
@@ -36,7 +32,7 @@ export function createTerminalUi(options = {}) {
36
32
  let spinnerFrame = 0;
37
33
 
38
34
  const style = (value, code) => color ? `${code}${value}${RESET}` : value;
39
- const write = (value) => output.write(`${value}\n`);
35
+ const write = (value = "") => output.write(`${value}\n`);
40
36
  const clearSpinnerLine = () => {
41
37
  if (spinnerTimer === null) return;
42
38
  output.write("\r\u001b[2K");
@@ -44,44 +40,56 @@ export function createTerminalUi(options = {}) {
44
40
  spinnerTimer = null;
45
41
  };
46
42
  const renderSpinner = () => {
47
- output.write(`\r\u001b[2K ${style(SPINNER_FRAMES[spinnerFrame], CYAN)} ${spinnerMessage}`);
43
+ output.write(`\r\u001b[2K ${style(SPINNER_FRAMES[spinnerFrame], CYAN)} ${spinnerMessage}`);
48
44
  spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
49
45
  };
46
+ const renderState = (symbol, symbolColor, label, detail) => {
47
+ clearSpinnerLine();
48
+ write(` ${style(symbol, symbolColor)} ${style(label, BOLD)}${detail ? ` ${style(detail, DIM)}` : ""}`);
49
+ };
50
50
 
51
51
  return Object.freeze({
52
52
  interactive,
53
53
 
54
54
  begin(title, subtitle) {
55
55
  if (!interactive) return;
56
- write("");
57
- for (const line of REENTRY_ASCII) write(style(line, BOLD));
58
- write("");
59
- for (const line of REENTRY_PANEL) write(style(line, BOLD));
60
- write("");
61
- write(` ${style("RE-ENTRY", BOLD)} ${style("LOCAL CONNECTOR", DIM)}`);
56
+ clearSpinnerLine();
57
+ write();
58
+ for (const line of REENTRY_WORDMARK) write(style(line, `${BOLD}${CYAN}`));
59
+ write(` ${style("LOCAL CONNECTOR", DIM)}`);
60
+ write();
62
61
  write(` ${style(title, BOLD)}`);
63
62
  if (subtitle) write(` ${style(subtitle, DIM)}`);
64
- write("");
63
+ write(RULE);
64
+ },
65
+
66
+ section(step, title, detail) {
67
+ if (!interactive) return;
68
+ clearSpinnerLine();
69
+ write();
70
+ write(` ${style(step, CYAN)} ${style(title, BOLD)}`);
71
+ if (detail) write(` ${style(detail, DIM)}`);
72
+ write();
65
73
  },
66
74
 
67
75
  step(label, detail) {
68
76
  if (!interactive) return;
69
- write(` ${style("→", CYAN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
77
+ renderState("→", CYAN, label, detail);
70
78
  },
71
79
 
72
80
  success(label, detail) {
73
81
  if (!interactive) return;
74
- write(` ${style("✓", GREEN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
82
+ renderState("✓", GREEN, label, detail);
75
83
  },
76
84
 
77
85
  info(label, detail) {
78
86
  if (!interactive) return;
79
- write(` ${style("·", CYAN)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
87
+ renderState("·", CYAN, label, detail);
80
88
  },
81
89
 
82
90
  warning(label, detail) {
83
91
  if (!interactive) return;
84
- write(` ${style("!", YELLOW)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}`);
92
+ renderState("!", YELLOW, label, detail);
85
93
  },
86
94
 
87
95
  wait(message) {
@@ -97,16 +105,38 @@ export function createTerminalUi(options = {}) {
97
105
  stopWait(label, detail, outcome = "success") {
98
106
  if (!interactive) return;
99
107
  clearSpinnerLine();
100
- if (outcome === "warning") this.warning(label, detail);
101
- else if (outcome === "info") this.info(label, detail);
102
- else this.success(label, detail);
108
+ if (outcome === "warning") renderState("!", YELLOW, label, detail);
109
+ else if (outcome === "info") renderState("·", CYAN, label, detail);
110
+ else renderState("✓", GREEN, label, detail);
111
+ },
112
+
113
+ complete(title, detail) {
114
+ if (!interactive) return;
115
+ clearSpinnerLine();
116
+ write();
117
+ write(` ${style("✓", GREEN)} ${style(title, BOLD)}`);
118
+ if (detail) write(` ${style(detail, DIM)}`);
119
+ },
120
+
121
+ next(command, detail) {
122
+ if (!interactive) return;
123
+ clearSpinnerLine();
124
+ write();
125
+ write(` ${style("NEXT", CYAN)}`);
126
+ write(` ${style("$", CYAN)} ${style(command, BOLD)}`);
127
+ if (detail) write(` ${style(detail, DIM)}`);
128
+ write();
103
129
  },
104
130
 
105
131
  error(label, detail, hint) {
106
132
  if (!interactive) return;
107
133
  clearSpinnerLine();
108
- errorOutput.write(` ${style("✕", RED)} ${style(label, BOLD)}${detail ? ` ${detail}` : ""}\n`);
109
- if (hint) errorOutput.write(` ${style("Next:", DIM)} ${hint}\n`);
134
+ errorOutput.write(`\n ${style("✕", RED)} ${style(label, BOLD)}\n`);
135
+ if (detail) errorOutput.write(` ${style(detail, DIM)}\n`);
136
+ if (hint) {
137
+ errorOutput.write(`\n ${style("NEXT", CYAN)}\n`);
138
+ errorOutput.write(` ${style("→", CYAN)} ${hint}\n\n`);
139
+ }
110
140
  },
111
141
 
112
142
  close() {
@@ -4,6 +4,8 @@ import { dirname, join, resolve } from "node:path";
4
4
  import { emitKeypressEvents } from "node:readline";
5
5
  import process from "node:process";
6
6
 
7
+ import { REENTRY_WORDMARK } from "./terminal-ui.mjs";
8
+
7
9
  const MAX_VISIBLE_DIRECTORIES = 12;
8
10
 
9
11
  /**
@@ -77,7 +79,7 @@ export async function chooseWorkspaceDirectory(options = {}) {
77
79
  }
78
80
  } finally {
79
81
  input.setRawMode(wasRaw);
80
- output.write("\n");
82
+ output.write("\u001b[2J\u001b[H");
81
83
  }
82
84
  }
83
85
 
@@ -158,19 +160,22 @@ async function readableDirectories(directory) {
158
160
 
159
161
  function renderPicker(output, { title, current, choices, selected, truncated = false }) {
160
162
  output.write("\u001b[2J\u001b[H");
161
- output.write("RE-ENTRY WORKSPACE\n\n");
162
- output.write(`${title}\n`);
163
- if (current) output.write(`Current folder: ${current}\n`);
163
+ for (const line of REENTRY_WORDMARK) output.write(`${line}\n`);
164
+ output.write(" LOCAL CONNECTOR\n\n");
165
+ output.write(" 1 OF 3 WORKSPACE\n");
166
+ output.write(` ${title}\n`);
167
+ if (current) output.write(` Current: ${current}\n`);
168
+ output.write(" ─────────────────────────────────────────────\n");
164
169
  output.write("\n");
165
170
  for (let index = 0; index < choices.length; index += 1) {
166
171
  const choice = choices[index];
167
- output.write(`${index === selected ? "❯" : " "} ${choice.label}\n`);
168
- if (choice.detail) output.write(` ${choice.detail}\n`);
172
+ output.write(` ${index === selected ? "❯" : " "} ${choice.label}\n`);
173
+ if (choice.detail) output.write(` ${choice.detail}\n`);
169
174
  }
170
175
  if (truncated) {
171
- output.write(`\nShowing the first ${MAX_VISIBLE_DIRECTORIES} folders. Open a folder to continue.\n`);
176
+ output.write(`\n Showing the first ${MAX_VISIBLE_DIRECTORIES} folders.\n`);
172
177
  }
173
- output.write("\n↑/↓ Move Enter Select Esc Cancel");
178
+ output.write("\n ↑↓ Move Enter Select Esc Cancel");
174
179
  }
175
180
 
176
181
  function readKey(input) {