@4xeoz/re-entry 0.2.5 → 0.2.7
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 +24 -1
- package/node_modules/@webmcp-challenge/reentry-core/src/cloud-receiver-http.mjs +1 -1
- package/package.json +5 -2
- package/src/activity-monitor.mjs +107 -0
- package/src/codex-exec-adapter.mjs +40 -2
- package/src/credentials.mjs +42 -0
- package/src/macos-service.mjs +19 -3
- package/src/main.mjs +222 -18
- 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,15 @@ 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. If the Receiver rejects a previously saved device credential, the Connector pauses instead
|
|
93
|
+
of retrying forever, and `status`/`listen` tell you to run `re-entry connect` again. `listen` watches
|
|
94
|
+
the already-running background Connector and displays new activity until you press Ctrl+C; it does
|
|
95
|
+
not start a competing second poller. Useful development commands are:
|
|
82
96
|
|
|
83
97
|
```sh
|
|
84
98
|
re-entry doctor --codex-cd /absolute/path/to/project
|
|
@@ -87,6 +101,15 @@ re-entry claim-once --codex-cd /absolute/path/to/project
|
|
|
87
101
|
re-entry start --codex-cd /absolute/path/to/project
|
|
88
102
|
```
|
|
89
103
|
|
|
104
|
+
Test the local Codex handoff without waiting for Cloud work:
|
|
105
|
+
|
|
106
|
+
```sh
|
|
107
|
+
re-entry test "Reply with: Re-entry is working."
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
This starts one fresh local Codex process through the same adapter seam used by real deliveries. It
|
|
111
|
+
does not create a Grant, claim Receiver work, or prove the browser/WebMCP return path.
|
|
112
|
+
|
|
90
113
|
To pause or remove the local Connector:
|
|
91
114
|
|
|
92
115
|
```sh
|
|
@@ -42,7 +42,7 @@ export function createCloudReceiverHttpHandler(options) {
|
|
|
42
42
|
requireReceiver(options.receiver);
|
|
43
43
|
|
|
44
44
|
return function cloudReceiverHttpHandler(request, response) {
|
|
45
|
-
handleRequest(options.receiver, request, response).catch((error) => {
|
|
45
|
+
return handleRequest(options.receiver, request, response).catch((error) => {
|
|
46
46
|
if (response.destroyed) return;
|
|
47
47
|
if (response.headersSent) {
|
|
48
48
|
response.destroy();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@4xeoz/re-entry",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
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/credentials.mjs
CHANGED
|
@@ -49,6 +49,48 @@ export class LocalConnectorCredentialStore {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
export function reauthorizationMarkerPath(filename) {
|
|
53
|
+
if (typeof filename !== "string" || filename.length === 0) {
|
|
54
|
+
throw new TypeError("Credential filename is required");
|
|
55
|
+
}
|
|
56
|
+
return `${filename}.reauthorization-required.json`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function markConnectorReauthorizationRequired(filename, value = {}) {
|
|
60
|
+
const marker = reauthorizationMarkerPath(filename);
|
|
61
|
+
await mkdir(dirname(marker), { recursive: true, mode: 0o700 });
|
|
62
|
+
await writeFile(marker, `${JSON.stringify({
|
|
63
|
+
receiver_origin: value.receiver_origin ?? null,
|
|
64
|
+
reason: "connector_identity_invalid",
|
|
65
|
+
observed_at: new Date().toISOString(),
|
|
66
|
+
})}\n`, { encoding: "utf8", mode: 0o600 });
|
|
67
|
+
await chmod(marker, 0o600);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function hasConnectorReauthorizationRequired(filename) {
|
|
71
|
+
try {
|
|
72
|
+
await readFile(reauthorizationMarkerPath(filename), "utf8");
|
|
73
|
+
return true;
|
|
74
|
+
} catch (error) {
|
|
75
|
+
if (error?.code === "ENOENT") return false;
|
|
76
|
+
throw credentialFailure(
|
|
77
|
+
"connector_status_unreadable",
|
|
78
|
+
"Connector authorization status could not be read",
|
|
79
|
+
error,
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export async function clearConnectorReauthorizationRequired(filename) {
|
|
85
|
+
await unlink(reauthorizationMarkerPath(filename)).catch((error) => {
|
|
86
|
+
if (error?.code !== "ENOENT") throw credentialFailure(
|
|
87
|
+
"connector_status_unwritable",
|
|
88
|
+
"Connector authorization status could not be cleared",
|
|
89
|
+
error,
|
|
90
|
+
);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
52
94
|
function normalizeCredentials(value) {
|
|
53
95
|
requireExactRecord(value, CREDENTIAL_FIELDS, CREDENTIAL_FIELDS, "Connector credentials");
|
|
54
96
|
if (value.version !== 1) throw credentialFailure("connector_credentials_invalid", "Connector credential version is unsupported");
|
package/src/macos-service.mjs
CHANGED
|
@@ -91,7 +91,7 @@ export async function inspectMacConnectorService(options = {}) {
|
|
|
91
91
|
return Object.freeze({
|
|
92
92
|
supported: true,
|
|
93
93
|
installed: true,
|
|
94
|
-
running: result.code === 0,
|
|
94
|
+
running: result.code === 0 && isLoadedAndRunning(result.stdout),
|
|
95
95
|
plistPath,
|
|
96
96
|
});
|
|
97
97
|
}
|
|
@@ -127,6 +127,7 @@ export async function uninstallMacConnectorService(options = {}) {
|
|
|
127
127
|
const paths = [...new Set([
|
|
128
128
|
service.plistPath,
|
|
129
129
|
credentialFile,
|
|
130
|
+
`${credentialFile}.reauthorization-required.json`,
|
|
130
131
|
join(stateDirectory, "connector.log"),
|
|
131
132
|
join(stateDirectory, "connector-error.log"),
|
|
132
133
|
])];
|
|
@@ -192,6 +193,7 @@ ${argumentsXml}
|
|
|
192
193
|
function runLaunchctl(argumentsList) {
|
|
193
194
|
return new Promise((resolve) => {
|
|
194
195
|
const child = spawn("launchctl", argumentsList, { stdio: ["ignore", "pipe", "pipe"] });
|
|
196
|
+
let stdout = "";
|
|
195
197
|
let stderr = "";
|
|
196
198
|
let settled = false;
|
|
197
199
|
const finish = (result) => {
|
|
@@ -199,15 +201,29 @@ function runLaunchctl(argumentsList) {
|
|
|
199
201
|
settled = true;
|
|
200
202
|
resolve(result);
|
|
201
203
|
};
|
|
204
|
+
child.stdout.setEncoding("utf8");
|
|
205
|
+
child.stdout.on("data", (chunk) => {
|
|
206
|
+
if (stdout.length < 16_384) stdout += chunk;
|
|
207
|
+
});
|
|
202
208
|
child.stderr.setEncoding("utf8");
|
|
203
209
|
child.stderr.on("data", (chunk) => {
|
|
204
210
|
if (stderr.length < 4_096) stderr += chunk;
|
|
205
211
|
});
|
|
206
|
-
child.once("error", (error) => finish({ code: -1, stderr: error.message }));
|
|
207
|
-
child.once("close", (code) => finish({ code: code ?? -1, stderr }));
|
|
212
|
+
child.once("error", (error) => finish({ code: -1, stdout, stderr: error.message }));
|
|
213
|
+
child.once("close", (code) => finish({ code: code ?? -1, stdout, stderr }));
|
|
208
214
|
});
|
|
209
215
|
}
|
|
210
216
|
|
|
217
|
+
function isLoadedAndRunning(stdout) {
|
|
218
|
+
// Test doubles historically return only { code: 0 }; preserve that contract while
|
|
219
|
+
// treating launchctl's explicit not-running state as stopped in production.
|
|
220
|
+
if (stdout === undefined) return true;
|
|
221
|
+
const state = stdout.match(/^[\t ]*state\s*=\s*([^\r\n]+)$/m)?.[1]?.trim();
|
|
222
|
+
if (state) return state === "running";
|
|
223
|
+
const activeCount = stdout.match(/^[\t ]*active count\s*=\s*(\d+)$/m)?.[1];
|
|
224
|
+
return activeCount !== undefined && Number(activeCount) > 0;
|
|
225
|
+
}
|
|
226
|
+
|
|
211
227
|
function requireServiceConfiguration(options, operation) {
|
|
212
228
|
if (!options || typeof options !== "object" || Array.isArray(options)) {
|
|
213
229
|
throw serviceFailure("connector_service_input_invalid", `Service ${operation} options are invalid`);
|
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,
|
|
@@ -17,7 +18,12 @@ import {
|
|
|
17
18
|
verifyCodexExecutable,
|
|
18
19
|
} from "./codex-discovery.mjs";
|
|
19
20
|
import { LocalConnector } from "./local-connector.mjs";
|
|
20
|
-
import {
|
|
21
|
+
import {
|
|
22
|
+
LocalConnectorCredentialStore,
|
|
23
|
+
clearConnectorReauthorizationRequired,
|
|
24
|
+
hasConnectorReauthorizationRequired,
|
|
25
|
+
markConnectorReauthorizationRequired,
|
|
26
|
+
} from "./credentials.mjs";
|
|
21
27
|
import { LocalConnectorPairingClient } from "./pairing-client.mjs";
|
|
22
28
|
import { waitForEnterToOpenBrowser } from "./browser-prompt.mjs";
|
|
23
29
|
import { chooseWorkspaceDirectory } from "./workspace-picker.mjs";
|
|
@@ -47,8 +53,8 @@ async function main() {
|
|
|
47
53
|
process.stdout.write(`${CONNECTOR_VERSION}\n`);
|
|
48
54
|
return;
|
|
49
55
|
}
|
|
50
|
-
const { command, flags } = parseArguments(argumentsList);
|
|
51
|
-
validateCommandFlags(command, flags);
|
|
56
|
+
const { command, flags, positionals } = parseArguments(argumentsList);
|
|
57
|
+
validateCommandFlags(command, flags, positionals);
|
|
52
58
|
ui = createTerminalUi({ interactive: flags.json !== true && process.stdout.isTTY === true });
|
|
53
59
|
requireSupportedNode();
|
|
54
60
|
if (command === "doctor") {
|
|
@@ -67,6 +73,14 @@ async function main() {
|
|
|
67
73
|
await status(flags, ui);
|
|
68
74
|
return;
|
|
69
75
|
}
|
|
76
|
+
if (command === "listen") {
|
|
77
|
+
await listen(flags, ui);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (command === "test") {
|
|
81
|
+
await testCodex(flags, positionals, ui);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
70
84
|
if (command === "stop") {
|
|
71
85
|
await stop(flags, ui);
|
|
72
86
|
return;
|
|
@@ -111,12 +125,14 @@ Connect this Mac once, then receive approved work in Codex in the background.
|
|
|
111
125
|
|
|
112
126
|
Usage:
|
|
113
127
|
re-entry install Recommended first run
|
|
114
|
-
re-entry
|
|
115
|
-
re-entry
|
|
128
|
+
re-entry listen Watch live activity
|
|
129
|
+
re-entry test "Reply with hello" Test Codex locally
|
|
116
130
|
|
|
117
131
|
Commands:
|
|
118
132
|
install Check this Mac, connect the Re-entry account, and start at login
|
|
119
133
|
status Show account, Receiver, service, Node.js, and Codex readiness
|
|
134
|
+
listen Watch the background Connector until you press Ctrl+C
|
|
135
|
+
test Start one fresh local Codex session with the supplied prompt
|
|
120
136
|
stop Stop the background Connector without removing its credential
|
|
121
137
|
uninstall Stop the Connector and remove its local service data
|
|
122
138
|
connect Authorize this Mac without installing the background service
|
|
@@ -134,6 +150,7 @@ Common options:
|
|
|
134
150
|
--help Show this help
|
|
135
151
|
--version Show the installed version
|
|
136
152
|
|
|
153
|
+
Both commands are installed: re-entry and reentry.
|
|
137
154
|
The Connector opens no inbound port. Host keys never belong on this Mac.
|
|
138
155
|
`);
|
|
139
156
|
}
|
|
@@ -261,8 +278,9 @@ async function connect(flags, ui, options = {}) {
|
|
|
261
278
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
262
279
|
const store = new LocalConnectorCredentialStore({ filename: credentialFile });
|
|
263
280
|
const current = await store.load();
|
|
281
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
|
|
264
282
|
const currentIsValid = current && Date.parse(current.connector_expires_at) > Date.now();
|
|
265
|
-
if (currentIsValid && current.receiver_origin === receiver) {
|
|
283
|
+
if (currentIsValid && current.receiver_origin === receiver && !reauthorizationRequired) {
|
|
266
284
|
if (ui.interactive) {
|
|
267
285
|
ui.success("Account", "already connected");
|
|
268
286
|
if (!options.guidedInstall) {
|
|
@@ -321,6 +339,7 @@ async function connect(flags, ui, options = {}) {
|
|
|
321
339
|
connector_expires_at: credentials.connector_expires_at,
|
|
322
340
|
};
|
|
323
341
|
await store.save(saved);
|
|
342
|
+
await clearConnectorReauthorizationRequired(credentialFile);
|
|
324
343
|
if (ui.interactive) {
|
|
325
344
|
if (!options.guidedInstall) {
|
|
326
345
|
ui.complete("This Mac is connected", "Re-entry can now route approved work here.");
|
|
@@ -339,33 +358,44 @@ async function status(flags, ui) {
|
|
|
339
358
|
if (ui.interactive) ui.begin("Status", "A quick check of this Mac and Re-entry.");
|
|
340
359
|
const credentialFile = flags["credential-file"] ?? defaultCredentialFile();
|
|
341
360
|
const credentials = await new LocalConnectorCredentialStore({ filename: credentialFile }).load();
|
|
361
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(credentialFile);
|
|
342
362
|
const readiness = inspectReadiness(flags);
|
|
343
363
|
const service = await inspectMacConnectorService();
|
|
344
364
|
const receiverReady = credentials ? await inspectReceiver(credentials.receiver_origin) : false;
|
|
345
|
-
const connected = Boolean(
|
|
365
|
+
const connected = Boolean(
|
|
366
|
+
credentials &&
|
|
367
|
+
Date.parse(credentials.connector_expires_at) > Date.now() &&
|
|
368
|
+
!reauthorizationRequired,
|
|
369
|
+
);
|
|
346
370
|
if (ui.interactive) {
|
|
347
371
|
ui.section("SYSTEM", "This Mac");
|
|
348
372
|
showReadiness(readiness, ui);
|
|
349
373
|
ui.section("CONNECTION", "Re-entry");
|
|
350
|
-
if (
|
|
374
|
+
if (reauthorizationRequired) ui.warning("Account", "reconnect required");
|
|
375
|
+
else if (connected) ui.success("Account", "connected");
|
|
351
376
|
else ui.warning("Account", "not connected");
|
|
352
|
-
if (service.running) ui.
|
|
377
|
+
if (reauthorizationRequired && service.running) ui.warning("Background", "paused until this Mac is reconnected");
|
|
378
|
+
else if (service.running) ui.success("Background", "running");
|
|
353
379
|
else if (service.installed) ui.warning("Background", "stopped");
|
|
354
380
|
else ui.warning("Background", "not installed");
|
|
355
|
-
if (
|
|
356
|
-
else if (
|
|
381
|
+
if (receiverReady) ui.success("Cloud", "online");
|
|
382
|
+
else if (credentials) ui.warning("Cloud", "unavailable");
|
|
357
383
|
|
|
358
|
-
if (
|
|
384
|
+
if (reauthorizationRequired) {
|
|
385
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser; the old credential was rejected.");
|
|
386
|
+
} else if (!connected || !service.running) {
|
|
359
387
|
ui.next("re-entry install", "Finish setup and start Re-entry in the background.");
|
|
360
388
|
} else if (!receiverReady) {
|
|
361
389
|
ui.next("re-entry status", "Check again when the Re-entry Cloud service is available.");
|
|
362
390
|
} else {
|
|
363
391
|
ui.complete("Everything looks good", "Re-entry is ready for approved work.");
|
|
392
|
+
ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
|
|
364
393
|
}
|
|
365
394
|
} else {
|
|
366
395
|
process.stdout.write(`${JSON.stringify({
|
|
367
396
|
event: "connector_status",
|
|
368
397
|
connected,
|
|
398
|
+
reauthorization_required: reauthorizationRequired,
|
|
369
399
|
connector_id: credentials?.connector_id ?? null,
|
|
370
400
|
receiver_origin: credentials?.receiver_origin ?? null,
|
|
371
401
|
receiver_ready: receiverReady,
|
|
@@ -378,6 +408,119 @@ async function status(flags, ui) {
|
|
|
378
408
|
}
|
|
379
409
|
}
|
|
380
410
|
|
|
411
|
+
async function listen(flags, ui) {
|
|
412
|
+
if (ui.interactive) {
|
|
413
|
+
ui.begin("Live activity", "Watch the background Connector. Ctrl+C closes this view.");
|
|
414
|
+
}
|
|
415
|
+
const service = await inspectMacConnectorService();
|
|
416
|
+
const credentials = await new LocalConnectorCredentialStore({
|
|
417
|
+
filename: defaultCredentialFile(),
|
|
418
|
+
}).load();
|
|
419
|
+
const reauthorizationRequired = await hasConnectorReauthorizationRequired(defaultCredentialFile());
|
|
420
|
+
if (reauthorizationRequired || !service.running || !credentials) {
|
|
421
|
+
if (ui.interactive) {
|
|
422
|
+
if (reauthorizationRequired) {
|
|
423
|
+
ui.warning("Account", "reconnect required; the Cloud Receiver rejected this Mac");
|
|
424
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
425
|
+
}
|
|
426
|
+
if (!credentials) ui.warning("Account", "not connected");
|
|
427
|
+
if (!service.running) ui.warning("Background", "not running");
|
|
428
|
+
ui.next("re-entry install", "Finish setup and start the background Connector.");
|
|
429
|
+
} else {
|
|
430
|
+
process.stdout.write(`${JSON.stringify({
|
|
431
|
+
event: "connector_listener_unavailable",
|
|
432
|
+
connected: Boolean(credentials),
|
|
433
|
+
reauthorization_required: reauthorizationRequired,
|
|
434
|
+
service_running: service.running,
|
|
435
|
+
})}\n`);
|
|
436
|
+
}
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (ui.interactive) {
|
|
441
|
+
ui.success("Background", "running");
|
|
442
|
+
ui.success("Account", "connected");
|
|
443
|
+
ui.wait("Listening for approved work…");
|
|
444
|
+
} else {
|
|
445
|
+
process.stdout.write('{"event":"connector_listener_started"}\n');
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const stopSignal = createStopSignal();
|
|
449
|
+
try {
|
|
450
|
+
await followConnectorActivity({
|
|
451
|
+
paths: defaultConnectorLogFiles(),
|
|
452
|
+
signal: stopSignal.signal,
|
|
453
|
+
onEvent(event) {
|
|
454
|
+
if (ui.interactive) reportLiveActivity(event, ui);
|
|
455
|
+
else process.stdout.write(`${JSON.stringify(event)}\n`);
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
} finally {
|
|
459
|
+
stopSignal.close();
|
|
460
|
+
if (ui.interactive) {
|
|
461
|
+
ui.stopWait("Live view closed", "the background Connector is still running", "info");
|
|
462
|
+
} else {
|
|
463
|
+
process.stdout.write('{"event":"connector_listener_stopped"}\n');
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
async function testCodex(flags, positionals, ui) {
|
|
469
|
+
const prompt = requireTestPrompt(positionals);
|
|
470
|
+
const runtimeFlags = await withWorkspaceDirectory(flags, ui);
|
|
471
|
+
const readiness = inspectReadiness(runtimeFlags);
|
|
472
|
+
if (ui.interactive) {
|
|
473
|
+
ui.begin("Test Codex", "Run one local prompt through the Re-entry Codex adapter.");
|
|
474
|
+
ui.success("Codex", "ready");
|
|
475
|
+
ui.success("Workspace", readiness.workingDirectory);
|
|
476
|
+
ui.info("Prompt", prompt);
|
|
477
|
+
ui.wait("Starting a fresh Codex session…");
|
|
478
|
+
}
|
|
479
|
+
await runCodexPrompt({
|
|
480
|
+
workingDirectory: readiness.workingDirectory,
|
|
481
|
+
executable: readiness.installation.executable,
|
|
482
|
+
prompt,
|
|
483
|
+
commandTimeoutMs: readBoundedNumber(
|
|
484
|
+
flags["activation-timeout"] ?? 60_000,
|
|
485
|
+
100,
|
|
486
|
+
60_000,
|
|
487
|
+
"connector_activation_timeout_invalid",
|
|
488
|
+
),
|
|
489
|
+
});
|
|
490
|
+
if (ui.interactive) {
|
|
491
|
+
ui.stopWait("Codex", "completed the local test");
|
|
492
|
+
ui.complete("Test passed", "The same fresh-session process seam is ready for Re-entry work.");
|
|
493
|
+
ui.next("re-entry listen", "Watch the background Connector for approved work.");
|
|
494
|
+
} else {
|
|
495
|
+
process.stdout.write('{"event":"connector_codex_test_passed"}\n');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function reportLiveActivity(event, ui) {
|
|
500
|
+
if (event.event === "connector_waiting") return;
|
|
501
|
+
if (event.event === "connector_activation_result") {
|
|
502
|
+
const accepted = event.outcome === "accepted";
|
|
503
|
+
ui.stopWait(
|
|
504
|
+
accepted ? "Work received" : "Work finished",
|
|
505
|
+
accepted ? "a fresh Codex session was started" : String(event.outcome ?? "unknown"),
|
|
506
|
+
accepted ? "success" : "warning",
|
|
507
|
+
);
|
|
508
|
+
} else if (event.event === "connector_reauthorization_required" || event.code === "connector_identity_invalid") {
|
|
509
|
+
ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
|
|
510
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
511
|
+
return;
|
|
512
|
+
} else if (event.event === "connector_poll_failed" || event.event === "local_connector_failed") {
|
|
513
|
+
ui.stopWait("Connection interrupted", "Re-entry is retrying", "warning");
|
|
514
|
+
} else if (event.event === "connector_ready") {
|
|
515
|
+
ui.stopWait("Connector", "ready", "success");
|
|
516
|
+
} else if (event.event === "connector_stopped") {
|
|
517
|
+
ui.stopWait("Background", "stopped", "warning");
|
|
518
|
+
} else {
|
|
519
|
+
ui.stopWait("Activity", event.event.replaceAll("_", " "), "info");
|
|
520
|
+
}
|
|
521
|
+
ui.wait("Listening for approved work…");
|
|
522
|
+
}
|
|
523
|
+
|
|
381
524
|
async function stop(flags, ui) {
|
|
382
525
|
if (ui.interactive) {
|
|
383
526
|
ui.begin("Stop the Connector", "Pause background delivery without removing your account connection");
|
|
@@ -442,7 +585,7 @@ async function install(flags, ui) {
|
|
|
442
585
|
if (ui.interactive) {
|
|
443
586
|
ui.stopWait("Background", "running at login");
|
|
444
587
|
ui.complete("You're all set", "Re-entry is connected and waiting for approved work.");
|
|
445
|
-
ui.next("re-entry
|
|
588
|
+
ui.next("re-entry listen", "Watch live activity. Press Ctrl+C when you are done.");
|
|
446
589
|
} else {
|
|
447
590
|
process.stdout.write(`${JSON.stringify({
|
|
448
591
|
event: "connector_service_installed",
|
|
@@ -478,6 +621,14 @@ async function start(flags, ui) {
|
|
|
478
621
|
if (!credentials) {
|
|
479
622
|
if (ui.interactive) ui.info("Re-entry", "first run — browser approval is required once");
|
|
480
623
|
credentials = await connect(runtimeFlags, ui, { quietHeader: true });
|
|
624
|
+
} else if (await hasConnectorReauthorizationRequired(credentialFile)) {
|
|
625
|
+
if (ui.interactive) {
|
|
626
|
+
ui.warning("Account", "this Mac needs to be connected again");
|
|
627
|
+
ui.next("re-entry connect", "Approve this Mac in your browser, then start the Connector again.");
|
|
628
|
+
} else {
|
|
629
|
+
process.stdout.write('{"event":"connector_reauthorization_required"}\n');
|
|
630
|
+
}
|
|
631
|
+
return;
|
|
481
632
|
} else if (ui.interactive) {
|
|
482
633
|
ui.success("Re-entry", "existing account connection loaded");
|
|
483
634
|
}
|
|
@@ -513,6 +664,18 @@ async function start(flags, ui) {
|
|
|
513
664
|
if (ui.interactive) ui.wait("Connected. Waiting for approved work…");
|
|
514
665
|
}
|
|
515
666
|
} catch (error) {
|
|
667
|
+
if (error?.code === "connector_identity_invalid") {
|
|
668
|
+
await markConnectorReauthorizationRequired(credentialFile, {
|
|
669
|
+
receiver_origin: credentials.receiver_origin,
|
|
670
|
+
});
|
|
671
|
+
if (ui.interactive) {
|
|
672
|
+
ui.stopWait("Reconnect required", "the Cloud Receiver rejected this Mac's saved connection", "warning");
|
|
673
|
+
ui.next("re-entry connect", "Approve this Mac again in your browser.");
|
|
674
|
+
} else {
|
|
675
|
+
process.stdout.write('{"event":"connector_reauthorization_required"}\n');
|
|
676
|
+
}
|
|
677
|
+
return;
|
|
678
|
+
}
|
|
516
679
|
consecutiveErrors += 1;
|
|
517
680
|
if (ui.interactive) {
|
|
518
681
|
ui.stopWait("Receiver unavailable", `${safeErrorMessage(error)} · ${consecutiveErrors}/${maximumErrors}`, "warning");
|
|
@@ -668,9 +831,14 @@ function parseArguments(argumentsList) {
|
|
|
668
831
|
...(hasExplicitCommand ? argumentsList.slice(commandIndex + 1) : argumentsList.slice(commandIndex)),
|
|
669
832
|
];
|
|
670
833
|
const flags = {};
|
|
834
|
+
const positionals = [];
|
|
671
835
|
for (let index = 0; index < rest.length; index += 1) {
|
|
672
836
|
const value = rest[index];
|
|
673
|
-
if (!value.startsWith("--"))
|
|
837
|
+
if (!value.startsWith("--")) {
|
|
838
|
+
if (command !== "test") throw cliFailure("connector_argument_invalid");
|
|
839
|
+
positionals.push(value);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
674
842
|
const name = value.slice(2);
|
|
675
843
|
if (name === "json" || name === "yes") {
|
|
676
844
|
if (Object.hasOwn(flags, name)) throw cliFailure("connector_argument_invalid");
|
|
@@ -682,15 +850,17 @@ function parseArguments(argumentsList) {
|
|
|
682
850
|
flags[name] = next;
|
|
683
851
|
index += 1;
|
|
684
852
|
}
|
|
685
|
-
return { command, flags };
|
|
853
|
+
return { command, flags, positionals };
|
|
686
854
|
}
|
|
687
855
|
|
|
688
|
-
function validateCommandFlags(command, flags) {
|
|
856
|
+
function validateCommandFlags(command, flags, positionals) {
|
|
689
857
|
const allowedByCommand = {
|
|
690
858
|
doctor: new Set(["codex-binary", "codex-cd", "json"]),
|
|
691
859
|
pair: new Set(["receiver", "code", "credential-file", "json"]),
|
|
692
860
|
connect: new Set(["receiver", "device-name", "credential-file", "json"]),
|
|
693
861
|
status: new Set(["credential-file", "codex-cd", "codex-binary", "json"]),
|
|
862
|
+
listen: new Set(["json"]),
|
|
863
|
+
test: new Set(["codex-cd", "codex-binary", "activation-timeout", "json"]),
|
|
694
864
|
stop: new Set(["json"]),
|
|
695
865
|
uninstall: new Set(["credential-file", "yes", "json"]),
|
|
696
866
|
install: new Set([
|
|
@@ -725,7 +895,11 @@ function validateCommandFlags(command, flags) {
|
|
|
725
895
|
]),
|
|
726
896
|
};
|
|
727
897
|
const allowed = allowedByCommand[command];
|
|
728
|
-
if (
|
|
898
|
+
if (
|
|
899
|
+
!allowed ||
|
|
900
|
+
Object.keys(flags).some((name) => !allowed.has(name)) ||
|
|
901
|
+
(command !== "test" && positionals.length > 0)
|
|
902
|
+
) {
|
|
729
903
|
throw cliFailure("connector_argument_invalid");
|
|
730
904
|
}
|
|
731
905
|
}
|
|
@@ -740,6 +914,14 @@ function defaultCredentialFile() {
|
|
|
740
914
|
return join(homedir(), ".webmcp-connector", "credentials.json");
|
|
741
915
|
}
|
|
742
916
|
|
|
917
|
+
function defaultConnectorLogFiles() {
|
|
918
|
+
const stateDirectory = join(homedir(), ".webmcp-connector");
|
|
919
|
+
return [
|
|
920
|
+
join(stateDirectory, "connector.log"),
|
|
921
|
+
join(stateDirectory, "connector-error.log"),
|
|
922
|
+
];
|
|
923
|
+
}
|
|
924
|
+
|
|
743
925
|
function defaultDeviceName() {
|
|
744
926
|
const value = hostname().trim();
|
|
745
927
|
return value.length >= 2 && Buffer.byteLength(value, "utf8") <= 80
|
|
@@ -755,6 +937,23 @@ function displayReceiver(origin) {
|
|
|
755
937
|
}
|
|
756
938
|
}
|
|
757
939
|
|
|
940
|
+
function requireTestPrompt(positionals) {
|
|
941
|
+
if (!Array.isArray(positionals) || positionals.length !== 1) {
|
|
942
|
+
throw cliFailure("connector_test_prompt_missing");
|
|
943
|
+
}
|
|
944
|
+
const value = positionals[0];
|
|
945
|
+
if (
|
|
946
|
+
typeof value !== "string" ||
|
|
947
|
+
value.trim() !== value ||
|
|
948
|
+
value.length === 0 ||
|
|
949
|
+
Buffer.byteLength(value, "utf8") > 4 * 1_024 ||
|
|
950
|
+
/[\u0000-\u001f\u007f]/.test(value)
|
|
951
|
+
) {
|
|
952
|
+
throw cliFailure("connector_test_prompt_invalid");
|
|
953
|
+
}
|
|
954
|
+
return value;
|
|
955
|
+
}
|
|
956
|
+
|
|
758
957
|
function readBoundedNumber(value, minimum, maximum, code) {
|
|
759
958
|
const number = typeof value === "number" ? value : Number(value);
|
|
760
959
|
if (!Number.isSafeInteger(number) || number < minimum || number > maximum) {
|
|
@@ -838,6 +1037,8 @@ function errorHint(error) {
|
|
|
838
1037
|
connector_node_unsupported: "use Node.js 24 or newer, then run the command again",
|
|
839
1038
|
connector_credentials_missing: "connect this Mac once with `re-entry connect`",
|
|
840
1039
|
connector_credentials_expired: "run `re-entry connect` to authorize this Mac again",
|
|
1040
|
+
connector_identity_invalid: "run `re-entry connect` and approve this Mac again",
|
|
1041
|
+
connector_reauthorization_required: "run `re-entry connect` and approve this Mac again",
|
|
841
1042
|
connector_pairing_code_missing: "ask the Host backend for a new pairing code, then run pair in a terminal",
|
|
842
1043
|
pairing_code_invalid: "use the 16-character code returned by the Host, for example ABCD-EFGH-IJKL-MNOP",
|
|
843
1044
|
host_subject_already_paired: "use the existing Connector credential or revoke/reset the preview pairing",
|
|
@@ -853,6 +1054,9 @@ function errorHint(error) {
|
|
|
853
1054
|
connector_service_load_failed: "run `re-entry install` again; if it still fails, inspect the Connector error log",
|
|
854
1055
|
connector_uninstall_confirmation_required: "run uninstall interactively and type DELETE, or pass `--yes` in a deliberate script",
|
|
855
1056
|
connector_service_stop_failed: "check the Connector status and try `re-entry stop` again",
|
|
1057
|
+
connector_test_prompt_missing: "use `re-entry test \"Reply with: Re-entry is working.\"`",
|
|
1058
|
+
connector_test_prompt_invalid: "use one short, single-line prompt inside quotes",
|
|
1059
|
+
connector_activation_timeout_invalid: "use an activation timeout between 100 and 60000 milliseconds",
|
|
856
1060
|
};
|
|
857
1061
|
return hints[error?.code] ?? "check the Receiver address and try again";
|
|
858
1062
|
}
|
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
|
/**
|