@4xeoz/re-entry 0.2.5 → 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 +22 -1
- package/package.json +5 -2
- package/src/activity-monitor.mjs +107 -0
- package/src/codex-exec-adapter.mjs +40 -2
- package/src/main.mjs +165 -10
- package/src/terminal-ui.mjs +5 -2
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
|
|
|
@@ -74,11 +84,13 @@ preview.
|
|
|
74
84
|
|
|
75
85
|
```sh
|
|
76
86
|
re-entry status
|
|
87
|
+
re-entry listen
|
|
77
88
|
re-entry --help
|
|
78
89
|
```
|
|
79
90
|
|
|
80
91
|
The status view checks the local authorization, background job, Receiver reachability, Node, and
|
|
81
|
-
Codex.
|
|
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:
|
|
82
94
|
|
|
83
95
|
```sh
|
|
84
96
|
re-entry doctor --codex-cd /absolute/path/to/project
|
|
@@ -87,6 +99,15 @@ re-entry claim-once --codex-cd /absolute/path/to/project
|
|
|
87
99
|
re-entry start --codex-cd /absolute/path/to/project
|
|
88
100
|
```
|
|
89
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
|
+
|
|
90
111
|
To pause or remove the local Connector:
|
|
91
112
|
|
|
92
113
|
```sh
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4xeoz/re-entry",
|
|
3
|
-
"version": "0.2.
|
|
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":
|
|
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
|
+
}
|
|
@@ -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
|
|
58
|
+
await runCodexPrompt({
|
|
58
59
|
executable,
|
|
59
60
|
workingDirectory,
|
|
60
61
|
prompt: buildContinuationPrompt(activation),
|
|
61
|
-
|
|
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 {
|
|
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
|
|
115
|
-
re-entry
|
|
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
|
}
|
|
@@ -361,6 +373,7 @@ async function status(flags, ui) {
|
|
|
361
373
|
ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
|
|
362
374
|
} else {
|
|
363
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.");
|
|
364
377
|
}
|
|
365
378
|
} else {
|
|
366
379
|
process.stdout.write(`${JSON.stringify({
|
|
@@ -378,6 +391,109 @@ async function status(flags, ui) {
|
|
|
378
391
|
}
|
|
379
392
|
}
|
|
380
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
|
+
|
|
381
497
|
async function stop(flags, ui) {
|
|
382
498
|
if (ui.interactive) {
|
|
383
499
|
ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
|
|
@@ -442,7 +558,7 @@ async function install(flags, ui) {
|
|
|
442
558
|
if (ui.interactive) {
|
|
443
559
|
ui.stopWait("Background", "running at login");
|
|
444
560
|
ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
|
|
445
|
-
ui.next("re-entry
|
|
561
|
+
ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
|
|
446
562
|
} else {
|
|
447
563
|
process.stdout.write(`${JSON.stringify({
|
|
448
564
|
event: "connector_service_installed",
|
|
@@ -668,9 +784,14 @@ function parseArguments(argumentsList) {
|
|
|
668
784
|
...(hasExplicitCommand ? argumentsList.slice(commandIndex + 1) : argumentsList.slice(commandIndex)),
|
|
669
785
|
];
|
|
670
786
|
const flags = {};
|
|
787
|
+
const positionals = [];
|
|
671
788
|
for (let index = 0; index < rest.length; index += 1) {
|
|
672
789
|
const value = rest[index];
|
|
673
|
-
if (!value.startsWith("--"))
|
|
790
|
+
if (!value.startsWith("--")) {
|
|
791
|
+
if (command !== "test") throw cliFailure("connector_argument_invalid");
|
|
792
|
+
positionals.push(value);
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
674
795
|
const name = value.slice(2);
|
|
675
796
|
if (name === "json" || name === "yes") {
|
|
676
797
|
if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
|
|
@@ -682,15 +803,17 @@ function parseArguments(argumentsList) {
|
|
|
682
803
|
flags[name] = next;
|
|
683
804
|
index += 1;
|
|
684
805
|
}
|
|
685
|
-
return { command, flags };
|
|
806
|
+
return { command, flags, positionals };
|
|
686
807
|
}
|
|
687
808
|
|
|
688
|
-
function validateCommandFlags(command, flags) {
|
|
809
|
+
function validateCommandFlags(command, flags, positionals) {
|
|
689
810
|
const allowedByCommand = {
|
|
690
811
|
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
691
812
|
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
692
813
|
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
693
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"]),
|
|
694
817
|
stop: new Set(["json"]),
|
|
695
818
|
uninstall: new Set(["credential-file", "yes", "json"]),
|
|
696
819
|
install: new Set([
|
|
@@ -725,7 +848,11 @@ function validateCommandFlags(command, flags) {
|
|
|
725
848
|
]),
|
|
726
849
|
};
|
|
727
850
|
const allowed = allowedByCommand[command];
|
|
728
|
-
if (
|
|
851
|
+
if (
|
|
852
|
+
!allowed ||
|
|
853
|
+
Object.keys(flags).some((name) => !allowed.has(name)) ||
|
|
854
|
+
(command !== "test" && positionals.length > 0)
|
|
855
|
+
) {
|
|
729
856
|
throw cliFailure("connector_argument_invalid");
|
|
730
857
|
}
|
|
731
858
|
}
|
|
@@ -740,6 +867,14 @@ function defaultCredentialFile() {
|
|
|
740
867
|
return join(homedir(), ".webmcp-connector", "credentials.json");
|
|
741
868
|
}
|
|
742
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
|
+
|
|
743
878
|
function defaultDeviceName() {
|
|
744
879
|
const value = hostname().trim();
|
|
745
880
|
return value.length >= 2 && Buffer.byteLength(value, "utf8") <= 80
|
|
@@ -755,6 +890,23 @@ function displayReceiver(origin) {
|
|
|
755
890
|
}
|
|
756
891
|
}
|
|
757
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
|
+
|
|
758
910
|
function readBoundedNumber(value, minimum, maximum, code) {
|
|
759
911
|
const number = typeof value === "number" ? value : Number(value);
|
|
760
912
|
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
@@ -853,6 +1005,9 @@ function errorHint(error) {
|
|
|
853
1005
|
connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
|
|
854
1006
|
connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
|
|
855
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",
|
|
856
1011
|
};
|
|
857
1012
|
return hints[error?.code] ?? "check the Receiver address and try again";
|
|
858
1013
|
}
|
package/src/terminal-ui.mjs
CHANGED
|
@@ -11,8 +11,11 @@ const SPINNER_FRAMES = ["·", "✦", "✧", "✦"];
|
|
|
11
11
|
const RULE = " ─────────────────────────────────────────────";
|
|
12
12
|
|
|
13
13
|
export const REENTRY_WORDMARK = Object.freeze([
|
|
14
|
-
"
|
|
15
|
-
"
|
|
14
|
+
" ____ _____ _____ _ _ _____ ______ __",
|
|
15
|
+
" | _ \\| ____| | ____| \\ | |_ _| _ \\ \\ / /",
|
|
16
|
+
" | |_) | _| _____| _| | \\| | | | | |_) | \\ V /",
|
|
17
|
+
" | _ <| |__|_____| |___| |\\ | | | | _ < | |",
|
|
18
|
+
" |_| \\_\\_____| |_____|_| \\_| |_| |_| \\_\\ |_|",
|
|
16
19
|
]);
|
|
17
20
|
|
|
18
21
|
/**
|