@minhspark/codex-mcp-bridge 1.25.2 → 1.26.0

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.
Files changed (50) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +81 -32
  4. package/docs/new-macos-setup.md +47 -21
  5. package/docs/new-windows-setup.md +34 -9
  6. package/docs/project-onboarding.md +21 -8
  7. package/package.json +2 -1
  8. package/plugin.json +1 -1
  9. package/scripts/background-relay-hidden.vbs +11 -0
  10. package/scripts/install-antigravity-bridge.mjs +65 -23
  11. package/scripts/install-background-relay.mjs +428 -0
  12. package/scripts/install-claude-code.mjs +2 -2
  13. package/scripts/install-claude-desktop.mjs +12 -7
  14. package/scripts/install-native-relay.mjs +2 -2
  15. package/scripts/setup-background-relay.mjs +58 -0
  16. package/scripts/setup.mjs +3 -3
  17. package/scripts/worker-relay.mjs +41 -0
  18. package/src/antigravity-bridge.mjs +96 -27
  19. package/src/antigravity-cli.mjs +13 -2
  20. package/src/antigravity-deliveries.mjs +32 -8
  21. package/src/antigravity-ipc.mjs +18 -9
  22. package/src/antigravity-outbound-bridge.mjs +71 -23
  23. package/src/antigravity-outbound-targets.mjs +100 -10
  24. package/src/background-antigravity-worker.mjs +192 -0
  25. package/src/background-claude-worker.mjs +240 -0
  26. package/src/background-codex-worker.mjs +344 -0
  27. package/src/background-relay-client.mjs +107 -0
  28. package/src/background-relay-config.mjs +98 -0
  29. package/src/background-relay-daemon.mjs +60 -0
  30. package/src/background-relay-mcp.mjs +45 -0
  31. package/src/background-relay-protocol.mjs +54 -0
  32. package/src/background-relay-registration.mjs +198 -0
  33. package/src/background-relay-service.mjs +311 -0
  34. package/src/background-relay-storage.mjs +203 -0
  35. package/src/background-relay-storage.ps1 +64 -0
  36. package/src/background-relay.mjs +373 -0
  37. package/src/bridge-readiness.mjs +2 -2
  38. package/src/claude-bridge.mjs +43 -25
  39. package/src/claude-code-registration.mjs +2 -1
  40. package/src/claude-session-router.mjs +8 -15
  41. package/src/index.mjs +39 -22
  42. package/src/mcp-supervisor.mjs +26 -16
  43. package/src/native-relay-companion.mjs +4 -1
  44. package/src/native-relay.mjs +5 -1
  45. package/src/peer-protocol.mjs +9 -6
  46. package/src/recipient-preflight.mjs +81 -0
  47. package/src/reload-control.mjs +2 -2
  48. package/src/setup.mjs +62 -25
  49. package/src/thread-delivery.mjs +10 -5
  50. package/src/windows-pipe-acl.ps1 +1 -1
@@ -0,0 +1,428 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { createHash, randomUUID } from "node:crypto";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ import { callBackgroundRelay } from "../src/background-relay-client.mjs";
9
+ import { readRelayConfiguration, writeRelayJson } from "../src/background-relay-config.mjs";
10
+ import { RELAY_SERVICE_OWNER, relayServiceRuntimeId } from "../src/background-relay-service.mjs";
11
+ import { isMainModule, stableNodeBin } from "../src/install-paths.mjs";
12
+ import { readRegularConfigFile } from "../src/project-policy.mjs";
13
+
14
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
15
+ const TASK_SCRIPT = String.raw`$ErrorActionPreference = 'Stop'
16
+ $relayRequest = [Console]::In.ReadToEnd() | ConvertFrom-Json
17
+ try {
18
+ $relayScheduler = New-Object -ComObject Schedule.Service
19
+ $relayScheduler.Connect()
20
+ $relayFolder = $relayScheduler.GetFolder('\')
21
+ if ($relayRequest.action -eq 'register') {
22
+ $null = $relayFolder.RegisterTask($relayRequest.name, $relayRequest.xml, 6, $null, $null, 3, $null)
23
+ }
24
+ $relayTask = $null
25
+ try { $relayTask = $relayFolder.GetTask($relayRequest.name) }
26
+ catch {
27
+ $relayException = $_.Exception
28
+ $relayMissing = $false
29
+ while ($null -ne $relayException) {
30
+ if ($relayException.HResult -eq -2147024894) { $relayMissing = $true }
31
+ $relayException = $relayException.InnerException
32
+ }
33
+ if (-not $relayMissing) { throw }
34
+ }
35
+ if ($relayRequest.action -eq 'start') {
36
+ if ($null -eq $relayTask) { throw 'Missing task' }
37
+ $null = $relayTask.Run($null)
38
+ }
39
+ $relayUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
40
+ if ($null -eq $relayTask) {
41
+ @{ exists = $false; user = $relayUser } | ConvertTo-Json -Compress
42
+ } else {
43
+ $relayDefinition = $relayTask.Definition
44
+ $relayAction = $relayDefinition.Actions.Item(1)
45
+ $relayPrincipalId = [string]$relayDefinition.Principal.UserId
46
+ $relayPrincipalSid = $null
47
+ try {
48
+ if ($relayPrincipalId -match '^S-\d+(?:-\d+)+$') {
49
+ $relayPrincipalSid = [Security.Principal.SecurityIdentifier]::new($relayPrincipalId).Value
50
+ } else {
51
+ $relayPrincipalSid = [Security.Principal.NTAccount]::new($relayPrincipalId).Translate([Security.Principal.SecurityIdentifier]).Value
52
+ }
53
+ } catch {}
54
+ @{ exists = $true; user = $relayUser; enabled = [bool]$relayTask.Enabled; state = [int]$relayTask.State;
55
+ description = [string]$relayDefinition.RegistrationInfo.Description;
56
+ command = [string]$relayAction.Path; arguments = [string]$relayAction.Arguments;
57
+ actions = [int]$relayDefinition.Actions.Count; logonType = [int]$relayDefinition.Principal.LogonType;
58
+ principalId = $relayPrincipalId; principalSid = $relayPrincipalSid; userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
59
+ runLevel = [int]$relayDefinition.Principal.RunLevel; hidden = [bool]$relayDefinition.Settings.Hidden;
60
+ executionTimeLimit = [string]$relayDefinition.Settings.ExecutionTimeLimit;
61
+ multipleInstances = [int]$relayDefinition.Settings.MultipleInstances } | ConvertTo-Json -Compress
62
+ }
63
+ } catch {
64
+ @{ error = 'TASK_SCHEDULER_UNAVAILABLE' } | ConvertTo-Json -Compress
65
+ exit 1
66
+ }`;
67
+
68
+ function execute(command, args, options) {
69
+ try { return { status: 0, stdout: execFileSync(command, args, options), stderr: "" }; }
70
+ catch (error) { return { status: Number.isInteger(error.status) ? error.status : 1, stdout: error.stdout ?? "", stderr: "" }; }
71
+ }
72
+
73
+ function installFailure(code) { return Object.assign(new Error(code), { code }); }
74
+ const escapeXml = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
75
+ const quoteWindows = (value) => `"${String(value).replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1")}"`;
76
+ const quoteSystemd = (value, expandDollar = true) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, () => expandDollar ? "$$" : "$")}"`;
77
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
78
+
79
+ function startupLocation(input, segments) {
80
+ if (typeof input !== "string" || !path.isAbsolute(input) || /[\u0000-\u001f\u007f]/.test(input)) throw installFailure("SERVICE_STORAGE_UNSAFE");
81
+ let ancestor = path.resolve(input);
82
+ const suffix = [];
83
+ while (true) {
84
+ try { fs.lstatSync(ancestor); break; }
85
+ catch (error) {
86
+ if (error.code !== "ENOENT" || ancestor === path.dirname(ancestor)) throw installFailure("SERVICE_STORAGE_UNSAFE");
87
+ suffix.unshift(path.basename(ancestor));
88
+ ancestor = path.dirname(ancestor);
89
+ }
90
+ }
91
+ ancestor = fs.realpathSync.native(ancestor);
92
+ const root = path.resolve(ancestor, ...suffix);
93
+ const file = path.resolve(root, ...segments);
94
+ if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
95
+ const location = { root, file, ancestor, identities: new Map() };
96
+ startupDirectories(location);
97
+ return location;
98
+ }
99
+
100
+ function startupDirectories(location, create = false) {
101
+ const relative = path.relative(location.ancestor, path.dirname(location.file));
102
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
103
+ let current = location.ancestor;
104
+ for (const segment of ["", ...relative.split(path.sep).filter(Boolean)]) {
105
+ if (segment) current = path.join(current, segment);
106
+ let entry;
107
+ try { entry = fs.lstatSync(current, { bigint: true }); }
108
+ catch (error) {
109
+ if (error.code !== "ENOENT" || location.identities.has(current)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
110
+ if (!create) return;
111
+ fs.mkdirSync(current, { mode: 0o700 });
112
+ entry = fs.lstatSync(current, { bigint: true });
113
+ }
114
+ const identity = `${entry.dev}:${entry.ino}:${entry.birthtimeNs}`;
115
+ if (!entry.isDirectory() || entry.isSymbolicLink() || path.relative(current, fs.realpathSync.native(current))) throw installFailure("SERVICE_STORAGE_UNSAFE");
116
+ if (location.identities.has(current) && location.identities.get(current) !== identity) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
117
+ location.identities.set(current, identity);
118
+ }
119
+ }
120
+
121
+ function startupText(location) {
122
+ startupDirectories(location);
123
+ return readRegularConfigFile(location.file, { missing: true, maxBytes: 128 * 1024 });
124
+ }
125
+
126
+ function sameStartupFile(location, candidate) {
127
+ if (typeof candidate !== "string" || !path.isAbsolute(candidate) || path.basename(candidate) !== path.basename(location.file)) return false;
128
+ try {
129
+ const entry = fs.lstatSync(candidate);
130
+ return entry.isFile() && !entry.isSymbolicLink() && path.relative(location.file, fs.realpathSync.native(candidate)) === "";
131
+ } catch { return false; }
132
+ }
133
+
134
+ function atomicText(location, content, previous) {
135
+ startupDirectories(location, true);
136
+ const file = location.file;
137
+ if (startupText(location) !== previous) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
138
+ const directory = path.dirname(file);
139
+ const temporary = path.resolve(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
140
+ if (!temporary.startsWith(directory.endsWith(path.sep) ? directory : `${directory}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
141
+ let descriptor;
142
+ let identity;
143
+ const sameTemporary = (entry) => entry.isFile() && !entry.isSymbolicLink() && entry.nlink === 1n
144
+ && ["dev", "ino", "birthtimeNs"].every((field) => entry[field] === identity[field]);
145
+ try {
146
+ descriptor = fs.openSync(temporary, "wx", 0o600);
147
+ identity = fs.fstatSync(descriptor, { bigint: true });
148
+ fs.writeFileSync(descriptor, content);
149
+ fs.fsyncSync(descriptor);
150
+ fs.closeSync(descriptor);
151
+ descriptor = undefined;
152
+ if (startupText(location) !== previous || !sameTemporary(fs.lstatSync(temporary, { bigint: true }))) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
153
+ fs.renameSync(temporary, file);
154
+ } finally {
155
+ if (descriptor !== undefined) fs.closeSync(descriptor);
156
+ if (identity) {
157
+ startupDirectories(location);
158
+ let remaining;
159
+ try { remaining = fs.lstatSync(temporary, { bigint: true }); } catch (error) { if (error.code !== "ENOENT") throw error; }
160
+ if (remaining) {
161
+ if (!sameTemporary(remaining)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
162
+ fs.unlinkSync(temporary);
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ function taskXml({ command, args, user, enabled }) {
169
+ return `<?xml version="1.0" encoding="UTF-16"?>
170
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
171
+ <RegistrationInfo><Description>${escapeXml(RELAY_SERVICE_OWNER)}</Description></RegistrationInfo>
172
+ <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${escapeXml(user)}</UserId></LogonTrigger></Triggers>
173
+ <Principals><Principal id="Author"><UserId>${escapeXml(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
174
+ <Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><AllowHardTerminate>false</AllowHardTerminate><StartWhenAvailable>true</StartWhenAvailable><AllowStartOnDemand>true</AllowStartOnDemand><Enabled>${enabled}</Enabled><Hidden>true</Hidden><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure></Settings>
175
+ <Actions Context="Author"><Exec><Command>${escapeXml(command)}</Command><Arguments>${escapeXml(args)}</Arguments></Exec></Actions>
176
+ </Task>\n`;
177
+ }
178
+
179
+ function savedEnvironment(env, config) {
180
+ const result = { AGENT_RELAY_DIRECTORY: config.paths.root, AGENT_RELAY_SERVICE_OWNER: RELAY_SERVICE_OWNER };
181
+ for (const name of ["HOME", "PATH", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME"]) {
182
+ if (typeof env[name] === "string" && env[name] && !/[\u0000-\u001f\u007f]/.test(env[name])) result[name] = env[name];
183
+ }
184
+ return result;
185
+ }
186
+
187
+ function plist(label, node, service, environment, enabled) {
188
+ return `<?xml version="1.0" encoding="UTF-8"?>
189
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
190
+ <plist version="1.0"><dict>
191
+ <key>Label</key><string>${escapeXml(label)}</string>
192
+ <key>ProgramArguments</key><array><string>${escapeXml(node)}</string><string>${escapeXml(service)}</string></array>
193
+ <key>EnvironmentVariables</key><dict>${Object.entries(environment).map(([key, value]) => `<key>${escapeXml(key)}</key><string>${escapeXml(value)}</string>`).join("")}</dict>
194
+ <key>RunAtLoad</key><${enabled ? "true" : "false"}/><key>Disabled</key><${enabled ? "false" : "true"}/>
195
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
196
+ <key>ThrottleInterval</key><integer>10</integer><key>ProcessType</key><string>Background</string>
197
+ <key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
198
+ </dict></plist>\n`;
199
+ }
200
+
201
+ function unit(node, service, environment) {
202
+ return `[Unit]
203
+ Description=${RELAY_SERVICE_OWNER}
204
+ StartLimitIntervalSec=0
205
+
206
+ [Service]
207
+ Type=simple
208
+ ExecStart=${quoteSystemd(node)} ${quoteSystemd(service)}
209
+ ${Object.entries(environment).map(([name, value]) => `Environment=${quoteSystemd(`${name}=${value}`, false)}`).join("\n")}
210
+ Restart=on-failure
211
+ RestartSec=10
212
+ TimeoutStopSec=infinity
213
+ StandardOutput=null
214
+ StandardError=null
215
+
216
+ [Install]
217
+ WantedBy=default.target
218
+ `;
219
+ }
220
+
221
+ async function installWindows(context) {
222
+ const { env, config, root, node, service, label, invoke, mutate } = context;
223
+ const command = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "wscript.exe");
224
+ const launcher = path.join(root, "scripts", "background-relay-hidden.vbs");
225
+ if (![node, service, launcher].every((value) => !/["%\r\n\0]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
226
+ if (!fs.existsSync(launcher)) throw installFailure("SERVICE_RUNTIME_MISSING");
227
+ if (context.nativeRun && !fs.existsSync(command)) throw installFailure("SERVICE_HIDDEN_LAUNCHER_UNAVAILABLE");
228
+ const args = ["//B", "//Nologo", quoteWindows(launcher), quoteWindows(node), quoteWindows(service), quoteWindows(config.paths.root)].join(" ");
229
+ const powershell = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
230
+ const task = async (action, extra = {}) => {
231
+ const response = await invoke(powershell, ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-EncodedCommand", Buffer.from(TASK_SCRIPT, "utf16le").toString("base64")], {
232
+ input: JSON.stringify({ action, name: label, ...extra }),
233
+ });
234
+ let value;
235
+ try { value = JSON.parse(response.stdout); } catch { throw installFailure("TASK_SCHEDULER_UNAVAILABLE"); }
236
+ if (response.status !== 0 || value.error || typeof value.exists !== "boolean") throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
237
+ return value;
238
+ };
239
+ let current = await task("query");
240
+ if (current.exists && current.description !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
241
+ const sameUser = (value) => typeof value.principalSid === "string" && /^S-\d+(?:-\d+)+$/i.test(value.principalSid)
242
+ && typeof value.userSid === "string" && value.principalSid.toLowerCase() === value.userSid.toLowerCase();
243
+ if (current.exists && !sameUser(current)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
244
+ const enabled = config.enabled && (!current.exists || current.enabled === true);
245
+ const matches = (value) => value.exists && value.description === RELAY_SERVICE_OWNER && value.command.toLowerCase() === command.toLowerCase()
246
+ && value.arguments === args && value.actions === 1 && value.logonType === 3 && value.runLevel === 0
247
+ && value.hidden === true && value.executionTimeLimit === "PT0S" && value.multipleInstances === 2 && value.enabled === enabled && sameUser(value);
248
+ const changed = !matches(current);
249
+ if (changed && mutate) {
250
+ if (typeof current.user !== "string" || !current.user || /[\r\n\0]/.test(current.user)) throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
251
+ await task("register", { xml: taskXml({ command, args, user: current.user, enabled }) });
252
+ current = await task("query");
253
+ }
254
+ if (mutate && !matches(current)) throw installFailure("SERVICE_READBACK_FAILED");
255
+ if (mutate) await context.prepareRuntime();
256
+ let started = false;
257
+ if (enabled && mutate && current.state !== 4) { await task("start"); started = true; }
258
+ return { configured: matches(current), enabled, changed, started, registration: label, activationPending: changed && current.state === 4 };
259
+ }
260
+
261
+ async function installMac(context) {
262
+ const { env, config, node, service, label, invoke, mutate, uid } = context;
263
+ if (!Number.isSafeInteger(uid) || uid < 0) throw installFailure("SERVICE_USER_UNAVAILABLE");
264
+ const home = env.HOME || env.USERPROFILE || os.homedir();
265
+ const location = startupLocation(home, ["Library", "LaunchAgents", `${label}.plist`]);
266
+ const file = location.file;
267
+ const previous = startupText(location);
268
+ if (previous !== null && !previous.includes(escapeXml(RELAY_SERVICE_OWNER))) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
269
+ const domain = `gui/${uid}`;
270
+ const disabled = await invoke("/bin/launchctl", ["print-disabled", domain]);
271
+ if (disabled.status !== 0) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
272
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
273
+ const userDisabled = new RegExp(`"${escaped}"\\s*=>\\s*true`).test(disabled.stdout)
274
+ || /<key>Disabled<\/key>\s*<true\s*\/>/.test(previous ?? "");
275
+ const enabled = config.enabled && !userDisabled;
276
+ const wanted = plist(label, node, service, savedEnvironment(env, config), enabled);
277
+ const loaded = await invoke("/bin/launchctl", ["print", `${domain}/${label}`]);
278
+ if (loaded.status === 0 && previous === null) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
279
+ if (loaded.status !== 0 && loaded.status !== 113) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
280
+ const changed = previous !== wanted;
281
+ if (changed && mutate) atomicText(location, wanted, previous);
282
+ const configured = startupText(location) === wanted;
283
+ if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
284
+ if (mutate) await context.prepareRuntime();
285
+ let started = false;
286
+ if (enabled && mutate) {
287
+ const args = loaded.status === 0 ? ["kickstart", `${domain}/${label}`] : ["bootstrap", domain, file];
288
+ if (loaded.status !== 0 || !/\bstate = running\b/.test(loaded.stdout)) {
289
+ if ((await invoke("/bin/launchctl", args)).status !== 0) throw installFailure("SERVICE_START_FAILED");
290
+ started = true;
291
+ }
292
+ }
293
+ return { configured, enabled, changed, started, registration: label, activationPending: changed && loaded.status === 0 };
294
+ }
295
+
296
+ async function installLinux(context) {
297
+ const { env, config, node, service, label, invoke, mutate } = context;
298
+ const home = env.HOME || env.USERPROFILE || os.homedir();
299
+ const location = startupLocation(env.XDG_CONFIG_HOME || path.join(home, ".config"), ["systemd", "user", `${label}.service`]);
300
+ const file = location.file;
301
+ const query = async () => {
302
+ const response = await invoke("systemctl", ["--user", "show", `${label}.service`, "--property=LoadState,UnitFileState,ActiveState,FragmentPath", "--no-pager"]);
303
+ const values = Object.fromEntries(response.stdout.split(/\r?\n/).filter((line) => line.includes("=")).map((line) => [line.slice(0, line.indexOf("=")), line.slice(line.indexOf("=") + 1)]));
304
+ if (!values.LoadState || response.status !== 0 && values.LoadState !== "not-found") throw installFailure("USER_SYSTEMD_UNAVAILABLE");
305
+ return values;
306
+ };
307
+ const values = await query();
308
+ if (values.UnitFileState === "masked" || values.UnitFileState === "masked-runtime") return { configured: false, enabled: false, changed: false, started: false, registration: label, code: "SERVICE_MASKED" };
309
+ const previous = startupText(location);
310
+ if (previous !== null && !previous.includes(`Description=${RELAY_SERVICE_OWNER}\n`)
311
+ || values.FragmentPath && !sameStartupFile(location, values.FragmentPath)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
312
+ const userDisabled = previous !== null && ["disabled", "disabled-runtime"].includes(values.UnitFileState);
313
+ const enabled = config.enabled && !userDisabled;
314
+ const wanted = unit(node, service, savedEnvironment(env, config));
315
+ const changed = previous !== wanted;
316
+ if (changed && mutate) {
317
+ atomicText(location, wanted, previous);
318
+ if ((await invoke("systemctl", ["--user", "daemon-reload"])).status !== 0) throw installFailure("USER_SYSTEMD_UNAVAILABLE");
319
+ }
320
+ if (enabled && mutate && !["enabled", "enabled-runtime"].includes(values.UnitFileState)) {
321
+ if ((await invoke("systemctl", ["--user", "enable", `${label}.service`])).status !== 0) throw installFailure("SERVICE_REGISTRATION_FAILED");
322
+ }
323
+ if (mutate) await context.prepareRuntime();
324
+ let started = false;
325
+ if (enabled && mutate && values.ActiveState !== "active") {
326
+ if ((await invoke("systemctl", ["--user", "start", `${label}.service`])).status !== 0) throw installFailure("SERVICE_START_FAILED");
327
+ started = true;
328
+ }
329
+ const verified = mutate ? await query() : values;
330
+ const configured = startupText(location) === wanted
331
+ && verified.LoadState === "loaded" && sameStartupFile(location, verified.FragmentPath)
332
+ && (!enabled || ["enabled", "enabled-runtime"].includes(verified.UnitFileState));
333
+ if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
334
+ return { configured, enabled, changed, started, registration: label };
335
+ }
336
+
337
+ export async function installBackgroundRelay({ env = process.env, platform = process.platform, root = ROOT, run = execute,
338
+ dryRun = false, check = false, health = callBackgroundRelay, sleep = pause, pollAttempts = 15, uid = process.getuid?.() } = {}) {
339
+ const base = { configured: false, enabled: false, started: false, liveVerified: false, ready: false };
340
+ let config;
341
+ try {
342
+ config = readRelayConfiguration({ env, platform, missing: true });
343
+ if (!config) return { ...base, code: "WORKER_RELAY_NOT_CONFIGURED", reason: "Configure the background relay before installing its user service" };
344
+ if (!["win32", "darwin", "linux"].includes(platform)) return { ...base, code: "SERVICE_PLATFORM_UNSUPPORTED", reason: "Background relay user startup is unsupported on this platform" };
345
+ if (!path.isAbsolute(root)) throw installFailure("SERVICE_RUNTIME_MISSING");
346
+ root = fs.realpathSync.native(root);
347
+ const node = stableNodeBin(env);
348
+ const service = path.join(root, "src", "background-relay-service.mjs");
349
+ if (!path.isAbsolute(root) || !fs.existsSync(service) || !fs.existsSync(path.join(root, "src", "background-relay-daemon.mjs"))) throw installFailure("SERVICE_RUNTIME_MISSING");
350
+ let packageVersion;
351
+ try { packageVersion = JSON.parse(readRegularConfigFile(path.join(root, "package.json"), { maxBytes: 128 * 1024 })).version; }
352
+ catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
353
+ if (typeof packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.+-]+)?$/.test(packageVersion) || packageVersion.length > 96) throw installFailure("SERVICE_RUNTIME_INVALID");
354
+ if ([root, config.paths.root, node].some((value) => /[\u0000-\u001f\u007f]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
355
+ const hash = createHash("sha256").update(platform === "win32" ? config.paths.root.toLowerCase() : config.paths.root).digest("hex").slice(0, 12);
356
+ const label = platform === "win32" ? `MinhSpark Agent Relay ${hash}` : `com.minhspark.agent-relay.${hash}`;
357
+ const invoke = async (command, args, extra = {}) => {
358
+ const result = await run(command, args, { env, encoding: "utf8", windowsHide: true, shell: false, timeout: 15000, maxBuffer: 256 * 1024, stdio: ["pipe", "pipe", "pipe"], ...extra });
359
+ return { status: result?.status ?? result?.exitCode ?? 0, stdout: String(result?.stdout ?? "") };
360
+ };
361
+ const runtimeFile = path.join(config.paths.root, "service-runtime.json");
362
+ const desiredRuntime = { schemaVersion: 1, owner: RELAY_SERVICE_OWNER, packageRoot: root, nodeExecutable: node, packageVersion,
363
+ environment: Object.fromEntries(Object.entries(savedEnvironment(env, config)).filter(([name]) => !name.startsWith("AGENT_RELAY_"))) };
364
+ const expectedRuntimeId = relayServiceRuntimeId(desiredRuntime);
365
+ const readRuntime = () => {
366
+ const raw = readRegularConfigFile(runtimeFile, { missing: true, maxBytes: 64 * 1024 });
367
+ if (raw === null) return null;
368
+ let value;
369
+ try { value = JSON.parse(raw); } catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
370
+ if (value?.schemaVersion !== 1 || value.owner !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
371
+ return value;
372
+ };
373
+ const previousRuntime = readRuntime();
374
+ const runtimeChanged = JSON.stringify(previousRuntime) !== JSON.stringify(desiredRuntime);
375
+ const prepareRuntime = async () => {
376
+ if (JSON.stringify(readRuntime()) !== JSON.stringify(previousRuntime)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
377
+ if (runtimeChanged) writeRelayJson(runtimeFile, desiredRuntime);
378
+ if (JSON.stringify(readRuntime()) !== JSON.stringify(desiredRuntime)) throw installFailure("SERVICE_READBACK_FAILED");
379
+ };
380
+ const context = { env, config, root, node, service, label, invoke, mutate: !dryRun && !check, uid, nativeRun: run === execute, prepareRuntime };
381
+ const installed = await ({ win32: installWindows, darwin: installMac, linux: installLinux })[platform](context);
382
+ installed.configured = installed.configured && JSON.stringify(readRuntime()) === JSON.stringify(desiredRuntime);
383
+ const result = { ...base, ...installed, dryRun, deliveryEnabled: installed.enabled, liveVerified: false,
384
+ expectedVersion: packageVersion, runtimeChanged, activationPending: installed.activationPending === true || runtimeChanged && previousRuntime !== null };
385
+ if (!installed.enabled) {
386
+ result.notice = config.enabled ? "User startup is intentionally disabled; the installer preserved that choice" : "Background relay is intentionally disabled in its configuration";
387
+ return result;
388
+ }
389
+ if (!installed.configured || dryRun) return result;
390
+ const attempts = check ? 1 : Math.max(1, Math.min(30, pollAttempts));
391
+ for (let attempt = 0; attempt < attempts; attempt++) {
392
+ try {
393
+ const status = await health("status", {}, { env, connection: { endpoint: config.endpoint, token: config.token, workerId: null }, timeoutMs: 1000 });
394
+ const serviceState = JSON.parse(readRegularConfigFile(path.join(config.paths.root, "service-status.json"), { maxBytes: 64 * 1024 }));
395
+ const owned = status?.schemaVersion === 1 && status.transportReady === true && status.desktopRequired === false
396
+ && Number.isSafeInteger(status.pid) && status.pid > 0 && serviceState?.owner === RELAY_SERVICE_OWNER
397
+ && serviceState.status === "running" && serviceState.daemonPid === status.pid;
398
+ if (owned && typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version)) result.version = status.version;
399
+ if (owned && (serviceState.runtimeId !== expectedRuntimeId || status.runtimeId !== expectedRuntimeId || status.version !== packageVersion)) result.activationPending = true;
400
+ if (owned && serviceState.runtimeId === expectedRuntimeId && status.runtimeId === expectedRuntimeId && status.version === packageVersion) {
401
+ return { ...result, activationPending: false, liveVerified: true, ready: status.accepting === true, pid: status.pid,
402
+ version: typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version) ? status.version : null,
403
+ ...(status.accepting === true ? {} : { code: "SERVICE_DRAINING", notice: "The relay is finishing existing work before accepting new jobs" }) };
404
+ }
405
+ } catch {}
406
+ if (attempt + 1 < attempts) await sleep(250);
407
+ }
408
+ return { ...result, code: result.activationPending ? "SERVICE_UPDATE_PENDING" : "SERVICE_NOT_READY",
409
+ reason: result.activationPending ? "User startup is configured; the running service has not yet verified the desired idle runtime update"
410
+ : "User startup is configured, but its owned daemon did not report healthy transport" };
411
+ } catch (error) {
412
+ const allowed = /^(?:SERVICE_[A-Z_]+|TASK_SCHEDULER_UNAVAILABLE|LAUNCH_AGENT_UNAVAILABLE|USER_SYSTEMD_UNAVAILABLE|WORKER_CONFIG_INVALID|WORKER_STORAGE_UNSAFE)$/;
413
+ const code = typeof error.code === "string" && allowed.test(error.code) ? error.code : "SERVICE_INSTALL_FAILED";
414
+ return { ...base, code, reason: "The background relay user service could not be safely configured or verified" };
415
+ }
416
+ }
417
+
418
+ if (isMainModule(process.argv[1], import.meta.url)) {
419
+ const args = process.argv.slice(2);
420
+ if (args.some((arg) => !["--check", "--dry-run", "--json"].includes(arg)) || args.includes("--check") && args.includes("--dry-run")) {
421
+ process.stderr.write("Usage: install-background-relay.mjs [--check|--dry-run] [--json]\n");
422
+ process.exitCode = 2;
423
+ } else {
424
+ const result = await installBackgroundRelay({ check: args.includes("--check"), dryRun: args.includes("--dry-run") });
425
+ process.stdout.write(`${JSON.stringify(result)}\n`);
426
+ if (!result.configured && !args.includes("--dry-run") && result.code !== "SERVICE_MASKED") process.exitCode = 1;
427
+ }
428
+ }
@@ -68,7 +68,7 @@ try {
68
68
  if (args.includes("--install")) {
69
69
  if (args.includes("--check") || args.includes("--apply")) throw new Error("Use --install separately from --check or --apply");
70
70
  const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
71
- const plan = planInitialCodeRegistration({ code, desktop, root, node: stableNodeBin(), environment: process.env });
71
+ const plan = planInitialCodeRegistration({ code, desktop, root, node: stableNodeBin(), environment: process.env, setup: args.includes("--setup") });
72
72
  const dryRun = args.includes("--dry-run");
73
73
  const writtenFiles = dryRun ? 0 : apply(plan);
74
74
  console.log(JSON.stringify({ action: dryRun ? "plan" : "install", configured: !dryRun, liveVerified: false,
@@ -86,7 +86,7 @@ try {
86
86
  if (args.includes("--apply")) {
87
87
  if (!args.includes("--clients-stopped")) throw new Error("Exit Claude and the configuration manager first, then pass --clients-stopped to avoid their overwriting the migration");
88
88
  apply(plan);
89
- console.log("Reopen the original Code task and call codex_bridge_status with the intended project cwd. Update the configuration manager's matching entry to the same policy file. No new per-project MCP entry is needed.");
89
+ console.log("Shared policy migration applied. Loaded Code registrations have not been verified; codex_bridge_status reports the active configuration. The configuration manager's matching entry must use the same policy file.");
90
90
  }
91
91
  }
92
92
  } catch (error) { console.error(error.message); process.exitCode = 1; }
@@ -20,10 +20,11 @@ const entryOption = process.argv.indexOf("--entry-name");
20
20
  const requestedEntryName = entryOption < 0 ? undefined : process.argv[entryOption + 1];
21
21
  if (entryOption >= 0 && !["codex-bridge", "codex-bridge-desktop"].includes(requestedEntryName)) throw new Error("--entry-name must be codex-bridge or codex-bridge-desktop");
22
22
  const quiet = process.argv.includes("--quiet");
23
+ const setup = process.argv.includes("--setup");
23
24
  const log = (...values) => { if (!quiet) console.log(...values); };
24
25
  const reset = process.argv.includes("--reset");
25
26
  const forceDesktopTasks = process.argv.includes("--desktop-tasks");
26
- const fullAccess = process.argv.includes("--full-access") || requestedEntryName === undefined && fullAccessEnabled();
27
+ const fullAccess = process.argv.includes("--full-access") || !setup && requestedEntryName === undefined && fullAccessEnabled();
27
28
 
28
29
  /**
29
30
  * This bridge is intentionally the local hand-off point between Claude and
@@ -80,6 +81,7 @@ const original = readConfig();
80
81
  const cfg = original ? JSON.parse(original.data.toString("utf8")) : {};
81
82
  if (!cfg || typeof cfg !== "object" || Array.isArray(cfg)) throw new Error("Desktop configuration must be a JSON object");
82
83
  if (cfg.mcpServers !== undefined && (!cfg.mcpServers || typeof cfg.mcpServers !== "object" || Array.isArray(cfg.mcpServers))) throw new Error("Desktop mcpServers must be a JSON object");
84
+ if (setup && cfg.preferences !== undefined && (!cfg.preferences || typeof cfg.preferences !== "object" || Array.isArray(cfg.preferences))) throw new Error("Desktop preferences must be a JSON object");
83
85
  cfg.mcpServers = cfg.mcpServers ?? {};
84
86
 
85
87
  /**
@@ -108,7 +110,7 @@ if (bridgeEntries.length > 1 || requestedEntryName && bridgeEntries.some((name)
108
110
  throw new Error(`Claude Desktop already has several bridge entries (${bridgeEntries.join(", ")}); remove all but one and re-run. No configuration was changed.`);
109
111
  }
110
112
  const entryName = requestedEntryName ?? bridgeEntries[0] ?? "codex-bridge";
111
- if (requestedEntryName && cfg.mcpServers[entryName] !== undefined) {
113
+ if ((requestedEntryName || setup) && cfg.mcpServers[entryName] !== undefined) {
112
114
  const existing = cfg.mcpServers[entryName];
113
115
  const script = existing?.args?.[0];
114
116
  const source = typeof script === "string" && path.isAbsolute(script) ? path.dirname(script) : null;
@@ -133,6 +135,7 @@ const desktopEnabled = desktopMode === undefined ? desktopTasksConfigured() : de
133
135
 
134
136
  const kept = Object.keys(previousEnv).filter(
135
137
  (name) => process.env[name] === undefined && name !== "CODEX_BIN" &&
138
+ !(setup && name === "CODEX_BRIDGE_OPEN_IN_APP") &&
136
139
  !(forceDesktopTasks && name === "CODEX_BRIDGE_DESKTOP_TASKS") &&
137
140
  !(desktopEnabled && name === "CODEX_BRIDGE_AUTOSTART"),
138
141
  );
@@ -168,7 +171,7 @@ cfg.mcpServers[entryName] = {
168
171
  */
169
172
  CODEX_BRIDGE_THREAD_POLICY: settled("CODEX_BRIDGE_THREAD_POLICY", "roots"),
170
173
  CODEX_BRIDGE_SANDBOX: fullAccess ? "danger-full-access" : settled("CODEX_BRIDGE_SANDBOX", "workspace-write"),
171
- CODEX_BRIDGE_OPEN_IN_APP: settled("CODEX_BRIDGE_OPEN_IN_APP", IS_WINDOWS ? "1" : "0"),
174
+ CODEX_BRIDGE_OPEN_IN_APP: setup ? "0" : settled("CODEX_BRIDGE_OPEN_IN_APP", "0"),
172
175
  CODEX_BRIDGE_RELEASE_AFTER_TURN: settled("CODEX_BRIDGE_RELEASE_AFTER_TURN", IS_WINDOWS ? "1" : "0"),
173
176
  ...(desktopMode !== undefined ? { CODEX_BRIDGE_DESKTOP_TASKS: desktopMode } : {}),
174
177
  CODEX_BRIDGE_AUTOSTART: desktopEnabled ? "0" : settled("CODEX_BRIDGE_AUTOSTART", "1"),
@@ -185,6 +188,7 @@ cfg.mcpServers[entryName] = {
185
188
  : {}),
186
189
  },
187
190
  };
191
+ if (setup) cfg.preferences = { ...cfg.preferences, menuBarEnabled: true };
188
192
 
189
193
  createReleaseSnapshot(root, { cache: snapshotRoot({ ...process.env, ...cfg.mcpServers[entryName].env }) });
190
194
 
@@ -255,7 +259,8 @@ if (entryName === "codex-bridge") {
255
259
  }
256
260
  }
257
261
 
258
- log(`\nReconnect ${entryName} once in the existing Claude task to load the supervisor. Subsequent compatible installed-source updates reload automatically when the worker is safely idle.`);
259
- log("Verify codex_bridge_status from that task: autoReload must be enabled and runtime state must be current. A separate diagnostic process does not verify the app's loaded MCP process.");
260
- if (desktopEnabled) log("Verify Desktop tasks are enabled and the native relay is available. Configuration changes require MCP reconnect even when source auto-reload is enabled; keep the destination task open in Codex Desktop.");
261
- if (quiet) console.log(JSON.stringify({ action: "install", entry: entryName, configured: true, liveVerified: false, changed, restartRequired: changed, desktopTasks: desktopEnabled }));
262
+ log(`\nConfiguration saved for ${entryName}. Running MCP processes were not restarted and may still hold earlier settings.`);
263
+ log("An existing compatible supervisor reloads installed source when the worker is safely idle. codex_bridge_status reports the loaded process; a separate diagnostic process cannot verify it.");
264
+ if (desktopEnabled) log("Native delivery requires a loaded Desktop-owned relay. This installer does not start apps or open conversations, and saved configuration does not prove live readiness.");
265
+ if (quiet) console.log(JSON.stringify({ action: "install", entry: entryName, configured: true, liveVerified: false, changed, restartRequired: changed, desktopTasks: desktopEnabled,
266
+ ...(setup ? { backgroundPreference: { setting: "preferences.menuBarEnabled", status: "configured", liveVerified: false } } : {}) }));
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
25
25
 
26
26
  exitForVersionRequest(import.meta.url);
27
27
 
28
- const VERSION = "1.25.2";
28
+ const VERSION = "1.26.0";
29
29
  const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
30
30
  const entry = path.join(root, "src", "mcp-supervisor.mjs");
31
31
  const entryArgs = ["native-relay-companion.mjs"];
@@ -136,5 +136,5 @@ if (process.argv.includes("--desktop-tasks")) {
136
136
  writeRelayConfig({ ...readRelayConfig(), desktopTasks: true });
137
137
  console.log("Desktop task creation enabled: exact saved local projects, immediate visibility, Codex Desktop permissions.");
138
138
  }
139
- console.log("One-time step: after active Codex work finishes, quit and reopen Codex Desktop once so it starts the supervisor. Then call native_relay_status in an existing task: it must report \"automatic reload: current\" with a supervisor pid. Later compatible installed updates reload automatically when the relay is idle.");
139
+ console.log("Native relay configuration saved. Running MCP processes were not restarted; native_relay_status reports whether a Desktop-owned relay is loaded and current. Existing compatible supervisors reload installed source when idle, but saved configuration does not prove the running process has loaded new settings. This installer does not start Codex Desktop or open conversations.");
140
140
  console.log("remove: node scripts/install-native-relay.mjs --remove");
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { isMainModule } from "../src/install-paths.mjs";
6
+ import { backgroundAdapters } from "../src/background-relay.mjs";
7
+ import { BACKGROUND_AGENTS, canStartBackgroundWorker, configureRelay, readRelayConfiguration, relayFailure } from "../src/background-relay-config.mjs";
8
+ import { configureBackgroundWorkerMcp } from "../src/background-relay-registration.mjs";
9
+ import { installBackgroundRelay } from "./install-background-relay.mjs";
10
+ import { diagnosticText, withBridgeDiagnostic } from "../src/recipient-preflight.mjs";
11
+
12
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
13
+
14
+ export async function setupBackgroundRelay({ env = process.env, platform = process.platform, root = ROOT, check = false, dryRun = false,
15
+ adapters, readConfig = readRelayConfiguration, configure = configureRelay, registration = configureBackgroundWorkerMcp, service = installBackgroundRelay } = {}) {
16
+ if (dryRun) return { configured: false, dryRun: true, liveVerified: false,
17
+ planned: ["Independent relay configuration", "Scoped worker MCP communication", "Per-user background service"], opensApps: false, opensChats: false };
18
+ const workers = adapters ?? await backgroundAdapters();
19
+ const saved = readConfig({ env, platform, missing: true });
20
+ const readiness = await Promise.all(BACKGROUND_AGENTS.map(async agent => {
21
+ try { return { agent, ...await workers[agent].inspect({ env, platform, executable: saved?.providers[agent]?.executable ?? undefined }) }; }
22
+ catch (error) { return { agent, available: false, authenticated: null, ready: false, code: error.code ?? "WORKER_INSPECTION_FAILED" }; }
23
+ }));
24
+ const config = check ? saved
25
+ : configure({ env, platform });
26
+ if (!config) throw relayFailure("WORKER_RELAY_NOT_CONFIGURED", "The independent relay has not been configured; rerun the package setup command");
27
+ const mcp = registration({ env, platform, root, check });
28
+ const installed = await service({ env, platform, root, check });
29
+ const notices = [
30
+ "Background tasks have separate CLI identities and receipts; they never resume Desktop chats.",
31
+ ...(!fs.existsSync(config.projectPolicy) ? ["No shared project policy is saved yet. Explicitly authorize a selected project with bridge-projects allow-project before sending background work."] : []),
32
+ ...(mcp.blocked ? [mcp.reason ?? "An existing permission or disabled-server choice blocks worker MCP communication; that choice was preserved."] : []),
33
+ ...readiness.filter(item => !item.available || item.authenticated === false).map(item => `${item.agent}: ${item.code ?? "WORKER_NOT_READY"}; CLI authentication and installation remain separate from Desktop sign-in.`),
34
+ ...(installed.code ? [diagnosticText(`${installed.code}: ${installed.notice ?? installed.reason ?? "Inspect worker-relay status for service readiness."}`)]
35
+ : installed.notice ? [diagnosticText(installed.notice)] : []),
36
+ ];
37
+ return { configured: mcp.configured === true && installed.configured === true, enabled: config.enabled,
38
+ liveVerified: installed.liveVerified === true, transportReady: installed.ready === true,
39
+ desktopRequired: false, opensApps: false, opensChats: false, notices,
40
+ workerReadiness: readiness.map(({ agent, available, authenticated, ready, version, code }) => ({ agent, available, authenticated, ready, version, code,
41
+ canAcceptWork: config.enabled && config.providers[agent].enabled && canStartBackgroundWorker(agent, { available, authenticated, ready, code }) })),
42
+ permissions: { blocked: mcp.blocked === true, ...(mcp.code ? { code: mcp.code } : {}) },
43
+ service: { configured: installed.configured, started: installed.started, activationPending: installed.activationPending, code: installed.code, version: installed.version } };
44
+ }
45
+
46
+ if (isMainModule(process.argv[1], import.meta.url)) {
47
+ const args = process.argv.slice(2);
48
+ try {
49
+ if (args.some(arg => !["--check", "--dry-run", "--json"].includes(arg))) throw relayFailure("WORKER_SETUP_ARGUMENT_INVALID", "Unsupported background relay setup argument");
50
+ const result = await setupBackgroundRelay({ check: args.includes("--check"), dryRun: args.includes("--dry-run") });
51
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
52
+ if (!result.configured && !result.dryRun) process.exitCode = 1;
53
+ } catch (error) {
54
+ const result = withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "background-relay", operation: "setup" });
55
+ process.stderr.write(result.content.map(item => item.text ?? "").join("\n") + "\n");
56
+ process.exitCode = 1;
57
+ }
58
+ }
package/scripts/setup.mjs CHANGED
@@ -6,9 +6,9 @@ exitForVersionRequest(import.meta.url);
6
6
  try {
7
7
  const result = runSetup({ args: process.argv.slice(2) });
8
8
  printSetup(result, { json: process.argv.includes("--json") });
9
- if (result.configured === false && !result.dryRun && !process.argv.includes("--postinstall")) process.exitCode = 1;
9
+ if (result.configured === false && !result.dryRun) process.exitCode = 1;
10
10
  } catch (error) {
11
11
  process.stderr.write(`Bridge setup: ${error.message}\n`);
12
- process.stderr.write("Run codex-bridge-setup to retry configuration; the installed package is retained.\n");
13
- if (!process.argv.includes("--postinstall")) process.exitCode = 1;
12
+ process.stderr.write("Bridge configuration was not completed. Run codex-bridge-setup to retry after resolving the reported error.\n");
13
+ process.exitCode = 1;
14
14
  }