@minhspark/codex-mcp-bridge 1.25.3 → 1.26.1

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 (38) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +47 -0
  3. package/README.md +45 -20
  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 +26 -14
  11. package/scripts/install-background-relay.mjs +437 -0
  12. package/scripts/install-native-relay.mjs +1 -1
  13. package/scripts/setup-background-relay.mjs +58 -0
  14. package/scripts/worker-relay.mjs +41 -0
  15. package/src/antigravity-bridge.mjs +18 -3
  16. package/src/antigravity-cli.mjs +13 -2
  17. package/src/antigravity-outbound-bridge.mjs +27 -6
  18. package/src/antigravity-outbound-targets.mjs +59 -4
  19. package/src/background-antigravity-worker.mjs +192 -0
  20. package/src/background-claude-worker.mjs +240 -0
  21. package/src/background-codex-worker.mjs +344 -0
  22. package/src/background-relay-client.mjs +107 -0
  23. package/src/background-relay-config.mjs +98 -0
  24. package/src/background-relay-daemon.mjs +60 -0
  25. package/src/background-relay-mcp.mjs +45 -0
  26. package/src/background-relay-protocol.mjs +54 -0
  27. package/src/background-relay-registration.mjs +198 -0
  28. package/src/background-relay-service.mjs +311 -0
  29. package/src/background-relay-storage.mjs +203 -0
  30. package/src/background-relay-storage.ps1 +64 -0
  31. package/src/background-relay.mjs +373 -0
  32. package/src/claude-bridge.mjs +6 -3
  33. package/src/index.mjs +7 -2
  34. package/src/mcp-supervisor.mjs +17 -11
  35. package/src/native-relay-companion.mjs +1 -1
  36. package/src/reload-control.mjs +2 -2
  37. package/src/setup.mjs +26 -9
  38. package/src/windows-pipe-acl.ps1 +1 -1
@@ -7,6 +7,7 @@ import { applyStdioMcpRegistration, stdioMcpRegistration } from "../src/codex-mc
7
7
  import { isMainModule, stableInstallPath, stableNodeBin } from "../src/install-paths.mjs";
8
8
  import { homeDir, resolveCodexBin, spawnEnv } from "../src/platform.mjs";
9
9
  import { exitForVersionRequest } from "../src/cli-version.mjs";
10
+ import { createReleaseSnapshot, snapshotRoot } from "../src/release-snapshot.mjs";
10
11
 
11
12
  const NAME = "antigravity-bridge";
12
13
  const SIDECAR = "codex-bridge";
@@ -59,17 +60,21 @@ export function validateCodexExecutable(executable, { platform = process.platfor
59
60
  return resolved;
60
61
  }
61
62
 
62
- function ownEntry(entry, expected, label, retarget = false) {
63
+ function ownEntry(entry, expected, label, retarget = false, supervised = false) {
63
64
  if (entry === undefined || entry === null) return;
64
65
  object(entry, label);
65
66
  const script = entry.args?.[0];
66
- if (!Array.isArray(entry.args) || entry.args.length !== 1 || typeof script !== "string") {
67
+ const managed = supervised && Array.isArray(entry.args) && entry.args.length === 2
68
+ && typeof script === "string" && path.basename(script) === "mcp-supervisor.mjs" && entry.args[1] === path.basename(expected);
69
+ if (!Array.isArray(entry.args) || !managed && entry.args.length !== 1 || typeof script !== "string") {
67
70
  throw new Error(`${label} is already configured with a different entrypoint; it was not changed`);
68
71
  }
69
- if (path.resolve(script) === path.resolve(expected)) return;
72
+ const target = managed ? path.join(path.dirname(expected), "mcp-supervisor.mjs") : expected;
73
+ if (path.resolve(script) === path.resolve(target) && !managed) return;
70
74
  const source = path.dirname(script);
71
- if (!retarget || !path.isAbsolute(script) || script.includes("\0") || path.basename(script) !== path.basename(expected)
72
- || path.basename(source) !== "src" || path.basename(path.dirname(source)).toLowerCase() !== "codex-mcp-bridge") {
75
+ if (!path.isAbsolute(script) || script.includes("\0") || path.resolve(script) !== path.resolve(target)
76
+ && (!retarget || path.basename(script) !== path.basename(target)
77
+ || path.basename(source) !== "src" || path.basename(path.dirname(source)).toLowerCase() !== "codex-mcp-bridge")) {
73
78
  throw new Error(`${label} is already configured with a different entrypoint; it was not changed`);
74
79
  }
75
80
  const command = entry.command;
@@ -91,31 +96,35 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
91
96
  const entry = path.join(root, "src", "antigravity-bridge.mjs");
92
97
  const sidecarEntry = path.join(root, "src", "antigravity-sidecar.mjs");
93
98
  const outboundEntry = path.join(root, "src", "antigravity-outbound-bridge.mjs");
99
+ const supervisorEntry = path.join(root, "src", "mcp-supervisor.mjs");
94
100
  const edits = [];
95
101
  const notices = [];
102
+ const runtimeCaches = new Set();
96
103
  let backgroundPreference = null;
97
104
  const retarget = setup && !remove;
98
105
  let codex = null;
99
106
  if (targets.includes("codex")) {
100
107
  if (codexExisting && codexExisting.transport?.type !== "stdio") throw new Error("The existing Antigravity MCP transport is not stdio");
101
- ownEntry(codexExisting?.transport, entry, "Codex Antigravity bridge", retarget);
108
+ ownEntry(codexExisting?.transport, entry, "Codex Antigravity bridge", retarget, true);
102
109
  const existingCaller = codexExisting?.transport?.env?.ANTIGRAVITY_BRIDGE_CALLER;
103
110
  if (existingCaller !== undefined && existingCaller !== "codex") throw new Error("The existing Codex bridge has a conflicting caller identity");
104
- codex = remove ? { remove: Boolean(codexExisting) } : stdioMcpRegistration({ name: NAME, existing: codexExisting, node, entry, envOverrides: { ANTIGRAVITY_BRIDGE_CALLER: "codex" } });
111
+ codex = remove ? { remove: Boolean(codexExisting) } : stdioMcpRegistration({ name: NAME, existing: codexExisting, node, entry: supervisorEntry, entryArgs: [path.basename(entry)], envOverrides: { ANTIGRAVITY_BRIDGE_CALLER: "codex" } });
112
+ if (!remove) runtimeCaches.add(snapshotRoot({ ...process.env, ...codex.environment }));
105
113
  }
106
114
  if (targets.includes("claude")) {
107
115
  const original = snapshot(path.join(codeConfigDir, ".claude.json"));
108
116
  const value = structuredClone(original.value);
109
117
  if (value.mcpServers !== undefined) object(value.mcpServers, "Claude mcpServers");
110
118
  const existing = value.mcpServers?.[NAME];
111
- ownEntry(existing, entry, "Claude Antigravity bridge", retarget);
119
+ ownEntry(existing, entry, "Claude Antigravity bridge", retarget, true);
112
120
  if (existing?.type !== undefined && existing.type !== "stdio") throw new Error("The existing Claude bridge transport is not stdio");
113
121
  if (existing?.env !== undefined) object(existing.env, "Claude bridge environment");
114
122
  if (existing?.env?.ANTIGRAVITY_BRIDGE_CALLER !== undefined && existing.env.ANTIGRAVITY_BRIDGE_CALLER !== "claude") throw new Error("The existing Claude bridge has a conflicting caller identity");
115
123
  if (remove) { if (existing) delete value.mcpServers[NAME]; }
116
124
  else {
117
125
  value.mcpServers ??= {};
118
- value.mcpServers[NAME] = { ...existing, type: "stdio", command: node, args: [entry], env: { ...existing?.env, ANTIGRAVITY_BRIDGE_CALLER: "claude" } };
126
+ value.mcpServers[NAME] = { ...existing, type: "stdio", command: node, args: [supervisorEntry, path.basename(entry)], env: { ...existing?.env, ANTIGRAVITY_BRIDGE_CALLER: "claude" } };
127
+ runtimeCaches.add(snapshotRoot({ ...process.env, ...value.mcpServers[NAME].env }));
119
128
  }
120
129
  const edit = changed(original, value);
121
130
  if (edit) edits.push(edit);
@@ -126,7 +135,7 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
126
135
  const mcp = structuredClone(originalMcp.value);
127
136
  if (mcp.mcpServers !== undefined) object(mcp.mcpServers, "Antigravity mcpServers");
128
137
  const outbound = mcp.mcpServers?.[OUTBOUND];
129
- ownEntry(outbound, outboundEntry, "Antigravity outbound bridge", retarget);
138
+ ownEntry(outbound, outboundEntry, "Antigravity outbound bridge", retarget, true);
130
139
  if (outbound?.type !== undefined && outbound.type !== "stdio" || outbound?.url !== undefined || outbound?.serverUrl !== undefined || outbound?.httpUrl !== undefined) throw new Error("The existing Antigravity outbound bridge transport is not stdio");
131
140
  if (outbound?.env !== undefined) object(outbound.env, "Antigravity outbound bridge environment");
132
141
  if (Object.keys(outbound?.env ?? {}).some((name) => /^(?:CLAUDE|CODEX_|ANTIGRAVITY_BRIDGE_CALLER$)/i.test(name))) throw new Error("The existing Antigravity outbound bridge contains a foreign caller environment; review it before installation");
@@ -136,7 +145,8 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
136
145
  if (remove) { if (outbound) delete mcp.mcpServers[OUTBOUND]; }
137
146
  else {
138
147
  mcp.mcpServers ??= {};
139
- mcp.mcpServers[OUTBOUND] = { ...outbound, command: node, args: [outboundEntry] };
148
+ mcp.mcpServers[OUTBOUND] = { ...outbound, command: node, args: [supervisorEntry, path.basename(outboundEntry)] };
149
+ runtimeCaches.add(snapshotRoot({ ...process.env, ...outbound?.env }));
140
150
  }
141
151
  const mcpEdit = changed(originalMcp, mcp);
142
152
  if (mcpEdit) edits.push(mcpEdit);
@@ -180,7 +190,8 @@ export function planAntigravityInstallation({ home = homeDir(), codeConfigDir =
180
190
  if (backgroundPreference.detail) notices.push(backgroundPreference.detail);
181
191
  }
182
192
  }
183
- return { name: NAME, targets: [...new Set(targets)], remove, edits, notices, codex, entry, sidecarEntry, outboundEntry, backgroundPreference };
193
+ if (!remove) notices.push("Running direct Antigravity MCP processes retain their current version until the host reconnects normally. Future supervised workers update automatically when quiescent.");
194
+ return { name: NAME, targets: [...new Set(targets)], remove, edits, notices, codex, entry, sidecarEntry, outboundEntry, supervisorEntry, runtimeCaches: [...runtimeCaches], backgroundPreference };
184
195
  }
185
196
 
186
197
  function assertCurrent(edit) {
@@ -224,7 +235,7 @@ export function installerArguments(args) {
224
235
  return { targets: args.includes("--all") || !selected.length ? ["codex", "claude", "desktop"] : selected, remove: args.includes("--remove"), dryRun: args.includes("--dry-run"), setup: args.includes("--setup"), help: args.includes("--help") };
225
236
  }
226
237
 
227
- export function runAntigravityInstaller(args = process.argv.slice(2)) {
238
+ export function runAntigravityInstaller(args = process.argv.slice(2), { prepareRuntime = createReleaseSnapshot } = {}) {
228
239
  const options = installerArguments(args);
229
240
  if (options.help) {
230
241
  console.log("Usage: antigravity-mcp-bridge-install [--all|--codex|--claude|--desktop] [--remove] [--dry-run] [--setup]");
@@ -244,7 +255,7 @@ export function runAntigravityInstaller(args = process.argv.slice(2)) {
244
255
  }
245
256
  const plan = planAntigravityInstallation({ ...options, home, root, node, codexExisting });
246
257
  if (!options.remove) {
247
- for (const entry of [plan.entry, ...(options.targets.includes("desktop") ? [plan.sidecarEntry, plan.outboundEntry] : [])]) {
258
+ for (const entry of [plan.supervisorEntry, plan.entry, ...(options.targets.includes("desktop") ? [plan.sidecarEntry, plan.outboundEntry] : [])]) {
248
259
  if (!fs.existsSync(entry)) throw new Error(`Missing bridge entrypoint: ${entry}`);
249
260
  }
250
261
  }
@@ -252,6 +263,7 @@ export function runAntigravityInstaller(args = process.argv.slice(2)) {
252
263
  console.log(JSON.stringify({ dryRun: true, remove: plan.remove, targets: plan.targets, files: plan.edits.map((edit) => ({ path: edit.file, action: edit.next === null ? "remove" : "write" })), notices: plan.notices, backgroundPreference: plan.backgroundPreference, codex: plan.codex ? { name: NAME, action: options.remove ? "remove" : "register" } : null }, null, 2));
253
264
  return plan;
254
265
  }
266
+ for (const cache of plan.runtimeCaches) prepareRuntime(root, { cache });
255
267
  for (const edit of plan.edits) assertCurrent(edit);
256
268
  if (plan.codex) {
257
269
  if (options.remove) { if (plan.codex.remove) run(["mcp", "remove", NAME]); }
@@ -0,0 +1,437 @@
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 RUNTIME_ENVIRONMENT_KEYS = ["HOME", "PATH", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME"];
16
+ const TASK_SCRIPT = String.raw`$ErrorActionPreference = 'Stop'
17
+ $relayRequest = [Console]::In.ReadToEnd() | ConvertFrom-Json
18
+ try {
19
+ $relayScheduler = New-Object -ComObject Schedule.Service
20
+ $relayScheduler.Connect()
21
+ $relayFolder = $relayScheduler.GetFolder('\')
22
+ if ($relayRequest.action -eq 'register') {
23
+ $null = $relayFolder.RegisterTask($relayRequest.name, $relayRequest.xml, 6, $null, $null, 3, $null)
24
+ }
25
+ $relayTask = $null
26
+ try { $relayTask = $relayFolder.GetTask($relayRequest.name) }
27
+ catch {
28
+ $relayException = $_.Exception
29
+ $relayMissing = $false
30
+ while ($null -ne $relayException) {
31
+ if ($relayException.HResult -eq -2147024894) { $relayMissing = $true }
32
+ $relayException = $relayException.InnerException
33
+ }
34
+ if (-not $relayMissing) { throw }
35
+ }
36
+ if ($relayRequest.action -eq 'start') {
37
+ if ($null -eq $relayTask) { throw 'Missing task' }
38
+ $null = $relayTask.Run($null)
39
+ }
40
+ $relayUser = [Security.Principal.WindowsIdentity]::GetCurrent().Name
41
+ if ($null -eq $relayTask) {
42
+ @{ exists = $false; user = $relayUser } | ConvertTo-Json -Compress
43
+ } else {
44
+ $relayDefinition = $relayTask.Definition
45
+ $relayAction = $relayDefinition.Actions.Item(1)
46
+ $relayPrincipalId = [string]$relayDefinition.Principal.UserId
47
+ $relayPrincipalSid = $null
48
+ try {
49
+ if ($relayPrincipalId -match '^S-\d+(?:-\d+)+$') {
50
+ $relayPrincipalSid = [Security.Principal.SecurityIdentifier]::new($relayPrincipalId).Value
51
+ } else {
52
+ $relayPrincipalSid = [Security.Principal.NTAccount]::new($relayPrincipalId).Translate([Security.Principal.SecurityIdentifier]).Value
53
+ }
54
+ } catch {}
55
+ @{ exists = $true; user = $relayUser; enabled = [bool]$relayTask.Enabled; state = [int]$relayTask.State;
56
+ description = [string]$relayDefinition.RegistrationInfo.Description;
57
+ command = [string]$relayAction.Path; arguments = [string]$relayAction.Arguments;
58
+ actions = [int]$relayDefinition.Actions.Count; logonType = [int]$relayDefinition.Principal.LogonType;
59
+ principalId = $relayPrincipalId; principalSid = $relayPrincipalSid; userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value;
60
+ runLevel = [int]$relayDefinition.Principal.RunLevel; hidden = [bool]$relayDefinition.Settings.Hidden;
61
+ executionTimeLimit = [string]$relayDefinition.Settings.ExecutionTimeLimit;
62
+ multipleInstances = [int]$relayDefinition.Settings.MultipleInstances } | ConvertTo-Json -Compress
63
+ }
64
+ } catch {
65
+ @{ error = 'TASK_SCHEDULER_UNAVAILABLE' } | ConvertTo-Json -Compress
66
+ exit 1
67
+ }`;
68
+
69
+ function execute(command, args, options) {
70
+ try { return { status: 0, stdout: execFileSync(command, args, options), stderr: "" }; }
71
+ catch (error) { return { status: Number.isInteger(error.status) ? error.status : 1, stdout: error.stdout ?? "", stderr: "" }; }
72
+ }
73
+
74
+ function installFailure(code) { return Object.assign(new Error(code), { code }); }
75
+ const escapeXml = (value) => String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
76
+ const quoteWindows = (value) => `"${String(value).replace(/(\\*)"/g, "$1$1\\\"").replace(/(\\+)$/g, "$1$1")}"`;
77
+ const quoteSystemd = (value, expandDollar = true) => `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, () => expandDollar ? "$$" : "$")}"`;
78
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
79
+
80
+ function startupLocation(input, segments) {
81
+ if (typeof input !== "string" || !path.isAbsolute(input) || /[\u0000-\u001f\u007f]/.test(input)) throw installFailure("SERVICE_STORAGE_UNSAFE");
82
+ let ancestor = path.resolve(input);
83
+ const suffix = [];
84
+ while (true) {
85
+ try { fs.lstatSync(ancestor); break; }
86
+ catch (error) {
87
+ if (error.code !== "ENOENT" || ancestor === path.dirname(ancestor)) throw installFailure("SERVICE_STORAGE_UNSAFE");
88
+ suffix.unshift(path.basename(ancestor));
89
+ ancestor = path.dirname(ancestor);
90
+ }
91
+ }
92
+ ancestor = fs.realpathSync.native(ancestor);
93
+ const root = path.resolve(ancestor, ...suffix);
94
+ const file = path.resolve(root, ...segments);
95
+ if (!file.startsWith(root.endsWith(path.sep) ? root : `${root}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
96
+ const location = { root, file, ancestor, identities: new Map() };
97
+ startupDirectories(location);
98
+ return location;
99
+ }
100
+
101
+ function startupDirectories(location, create = false) {
102
+ const relative = path.relative(location.ancestor, path.dirname(location.file));
103
+ if (path.isAbsolute(relative) || relative === ".." || relative.startsWith(`..${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
104
+ let current = location.ancestor;
105
+ for (const segment of ["", ...relative.split(path.sep).filter(Boolean)]) {
106
+ if (segment) current = path.join(current, segment);
107
+ let entry;
108
+ try { entry = fs.lstatSync(current, { bigint: true }); }
109
+ catch (error) {
110
+ if (error.code !== "ENOENT" || location.identities.has(current)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
111
+ if (!create) return;
112
+ fs.mkdirSync(current, { mode: 0o700 });
113
+ entry = fs.lstatSync(current, { bigint: true });
114
+ }
115
+ const identity = `${entry.dev}:${entry.ino}:${entry.birthtimeNs}`;
116
+ if (!entry.isDirectory() || entry.isSymbolicLink() || path.relative(current, fs.realpathSync.native(current))) throw installFailure("SERVICE_STORAGE_UNSAFE");
117
+ if (location.identities.has(current) && location.identities.get(current) !== identity) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
118
+ location.identities.set(current, identity);
119
+ }
120
+ }
121
+
122
+ function startupText(location) {
123
+ startupDirectories(location);
124
+ return readRegularConfigFile(location.file, { missing: true, maxBytes: 128 * 1024 });
125
+ }
126
+
127
+ function sameStartupFile(location, candidate) {
128
+ if (typeof candidate !== "string" || !path.isAbsolute(candidate) || path.basename(candidate) !== path.basename(location.file)) return false;
129
+ try {
130
+ const entry = fs.lstatSync(candidate);
131
+ return entry.isFile() && !entry.isSymbolicLink() && path.relative(location.file, fs.realpathSync.native(candidate)) === "";
132
+ } catch { return false; }
133
+ }
134
+
135
+ function atomicText(location, content, previous) {
136
+ startupDirectories(location, true);
137
+ const file = location.file;
138
+ if (startupText(location) !== previous) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
139
+ const directory = path.dirname(file);
140
+ const temporary = path.resolve(directory, `.${path.basename(file)}.${randomUUID()}.tmp`);
141
+ if (!temporary.startsWith(directory.endsWith(path.sep) ? directory : `${directory}${path.sep}`)) throw installFailure("SERVICE_STORAGE_UNSAFE");
142
+ let descriptor;
143
+ let identity;
144
+ const sameTemporary = (entry) => entry.isFile() && !entry.isSymbolicLink() && entry.nlink === 1n
145
+ && ["dev", "ino", "birthtimeNs"].every((field) => entry[field] === identity[field]);
146
+ try {
147
+ descriptor = fs.openSync(temporary, "wx", 0o600);
148
+ identity = fs.fstatSync(descriptor, { bigint: true });
149
+ fs.writeFileSync(descriptor, content);
150
+ fs.fsyncSync(descriptor);
151
+ fs.closeSync(descriptor);
152
+ descriptor = undefined;
153
+ if (startupText(location) !== previous || !sameTemporary(fs.lstatSync(temporary, { bigint: true }))) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
154
+ fs.renameSync(temporary, file);
155
+ } finally {
156
+ if (descriptor !== undefined) fs.closeSync(descriptor);
157
+ if (identity) {
158
+ startupDirectories(location);
159
+ let remaining;
160
+ try { remaining = fs.lstatSync(temporary, { bigint: true }); } catch (error) { if (error.code !== "ENOENT") throw error; }
161
+ if (remaining) {
162
+ if (!sameTemporary(remaining)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
163
+ fs.unlinkSync(temporary);
164
+ }
165
+ }
166
+ }
167
+ }
168
+
169
+ function taskXml({ command, args, user, enabled }) {
170
+ return `<?xml version="1.0" encoding="UTF-16"?>
171
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
172
+ <RegistrationInfo><Description>${escapeXml(RELAY_SERVICE_OWNER)}</Description></RegistrationInfo>
173
+ <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${escapeXml(user)}</UserId></LogonTrigger></Triggers>
174
+ <Principals><Principal id="Author"><UserId>${escapeXml(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
175
+ <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>
176
+ <Actions Context="Author"><Exec><Command>${escapeXml(command)}</Command><Arguments>${escapeXml(args)}</Arguments></Exec></Actions>
177
+ </Task>\n`;
178
+ }
179
+
180
+ function savedEnvironment(env, config) {
181
+ const result = { AGENT_RELAY_DIRECTORY: config.paths.root, AGENT_RELAY_SERVICE_OWNER: RELAY_SERVICE_OWNER };
182
+ for (const name of RUNTIME_ENVIRONMENT_KEYS) {
183
+ if (typeof env[name] === "string" && env[name] && !/[\u0000-\u001f\u007f]/.test(env[name])) result[name] = env[name];
184
+ }
185
+ return result;
186
+ }
187
+
188
+ function plist(label, node, service, environment, enabled) {
189
+ return `<?xml version="1.0" encoding="UTF-8"?>
190
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
191
+ <plist version="1.0"><dict>
192
+ <key>Label</key><string>${escapeXml(label)}</string>
193
+ <key>ProgramArguments</key><array><string>${escapeXml(node)}</string><string>${escapeXml(service)}</string></array>
194
+ <key>EnvironmentVariables</key><dict>${Object.entries(environment).map(([key, value]) => `<key>${escapeXml(key)}</key><string>${escapeXml(value)}</string>`).join("")}</dict>
195
+ <key>RunAtLoad</key><${enabled ? "true" : "false"}/><key>Disabled</key><${enabled ? "false" : "true"}/>
196
+ <key>KeepAlive</key><dict><key>SuccessfulExit</key><false/></dict>
197
+ <key>ThrottleInterval</key><integer>10</integer><key>ProcessType</key><string>Background</string>
198
+ <key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
199
+ </dict></plist>\n`;
200
+ }
201
+
202
+ function unit(node, service, environment) {
203
+ return `[Unit]
204
+ Description=${RELAY_SERVICE_OWNER}
205
+ StartLimitIntervalSec=0
206
+
207
+ [Service]
208
+ Type=simple
209
+ ExecStart=${quoteSystemd(node)} ${quoteSystemd(service)}
210
+ ${Object.entries(environment).map(([name, value]) => `Environment=${quoteSystemd(`${name}=${value}`, false)}`).join("\n")}
211
+ Restart=on-failure
212
+ RestartSec=10
213
+ TimeoutStopSec=infinity
214
+ StandardOutput=null
215
+ StandardError=null
216
+
217
+ [Install]
218
+ WantedBy=default.target
219
+ `;
220
+ }
221
+
222
+ async function installWindows(context) {
223
+ const { env, config, root, node, service, label, invoke, mutate } = context;
224
+ const command = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "wscript.exe");
225
+ const launcher = path.join(root, "scripts", "background-relay-hidden.vbs");
226
+ if (![node, service, launcher].every((value) => !/["%\r\n\0]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
227
+ if (!fs.existsSync(launcher)) throw installFailure("SERVICE_RUNTIME_MISSING");
228
+ if (context.nativeRun && !fs.existsSync(command)) throw installFailure("SERVICE_HIDDEN_LAUNCHER_UNAVAILABLE");
229
+ const args = ["//B", "//Nologo", quoteWindows(launcher), quoteWindows(node), quoteWindows(service), quoteWindows(config.paths.root)].join(" ");
230
+ const powershell = path.win32.join(env.SystemRoot || env.WINDIR || "C:\\Windows", "System32", "WindowsPowerShell", "v1.0", "powershell.exe");
231
+ const task = async (action, extra = {}) => {
232
+ const response = await invoke(powershell, ["-NoProfile", "-NonInteractive", "-WindowStyle", "Hidden", "-EncodedCommand", Buffer.from(TASK_SCRIPT, "utf16le").toString("base64")], {
233
+ input: JSON.stringify({ action, name: label, ...extra }),
234
+ });
235
+ let value;
236
+ try { value = JSON.parse(response.stdout); } catch { throw installFailure("TASK_SCHEDULER_UNAVAILABLE"); }
237
+ if (response.status !== 0 || value.error || typeof value.exists !== "boolean") throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
238
+ return value;
239
+ };
240
+ let current = await task("query");
241
+ if (current.exists && current.description !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
242
+ const sameUser = (value) => typeof value.principalSid === "string" && /^S-\d+(?:-\d+)+$/i.test(value.principalSid)
243
+ && typeof value.userSid === "string" && value.principalSid.toLowerCase() === value.userSid.toLowerCase();
244
+ if (current.exists && !sameUser(current)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
245
+ const enabled = config.enabled && (!current.exists || current.enabled === true);
246
+ const matches = (value) => value.exists && value.description === RELAY_SERVICE_OWNER && value.command.toLowerCase() === command.toLowerCase()
247
+ && value.arguments === args && value.actions === 1 && value.logonType === 3 && value.runLevel === 0
248
+ && value.hidden === true && value.executionTimeLimit === "PT0S" && value.multipleInstances === 2 && value.enabled === enabled && sameUser(value);
249
+ const changed = !matches(current);
250
+ if (changed && mutate) {
251
+ if (typeof current.user !== "string" || !current.user || /[\r\n\0]/.test(current.user)) throw installFailure("TASK_SCHEDULER_UNAVAILABLE");
252
+ await task("register", { xml: taskXml({ command, args, user: current.user, enabled }) });
253
+ current = await task("query");
254
+ }
255
+ if (mutate && !matches(current)) throw installFailure("SERVICE_READBACK_FAILED");
256
+ if (mutate) await context.prepareRuntime();
257
+ let started = false;
258
+ if (enabled && mutate && current.state !== 4) { await task("start"); started = true; }
259
+ return { configured: matches(current), enabled, changed, started, registration: label, activationPending: changed && current.state === 4 };
260
+ }
261
+
262
+ async function installMac(context) {
263
+ const { env, config, node, service, label, invoke, mutate, uid } = context;
264
+ if (!Number.isSafeInteger(uid) || uid < 0) throw installFailure("SERVICE_USER_UNAVAILABLE");
265
+ const home = env.HOME || env.USERPROFILE || os.homedir();
266
+ const location = startupLocation(home, ["Library", "LaunchAgents", `${label}.plist`]);
267
+ const file = location.file;
268
+ const previous = startupText(location);
269
+ if (previous !== null && !previous.includes(escapeXml(RELAY_SERVICE_OWNER))) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
270
+ const domain = `gui/${uid}`;
271
+ const disabled = await invoke("/bin/launchctl", ["print-disabled", domain]);
272
+ if (disabled.status !== 0) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
273
+ const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
274
+ const userDisabled = new RegExp(`"${escaped}"\\s*=>\\s*true`).test(disabled.stdout)
275
+ || /<key>Disabled<\/key>\s*<true\s*\/>/.test(previous ?? "");
276
+ const enabled = config.enabled && !userDisabled;
277
+ const wanted = plist(label, node, service, savedEnvironment(env, config), enabled);
278
+ const loaded = await invoke("/bin/launchctl", ["print", `${domain}/${label}`]);
279
+ if (loaded.status === 0 && previous === null) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
280
+ if (loaded.status !== 0 && loaded.status !== 113) throw installFailure("LAUNCH_AGENT_UNAVAILABLE");
281
+ const changed = previous !== wanted;
282
+ if (changed && mutate) atomicText(location, wanted, previous);
283
+ const configured = startupText(location) === wanted;
284
+ if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
285
+ if (mutate) await context.prepareRuntime();
286
+ let started = false;
287
+ if (enabled && mutate) {
288
+ const args = loaded.status === 0 ? ["kickstart", `${domain}/${label}`] : ["bootstrap", domain, file];
289
+ if (loaded.status !== 0 || !/\bstate = running\b/.test(loaded.stdout)) {
290
+ if ((await invoke("/bin/launchctl", args)).status !== 0) throw installFailure("SERVICE_START_FAILED");
291
+ started = true;
292
+ }
293
+ }
294
+ return { configured, enabled, changed, started, registration: label, activationPending: changed && loaded.status === 0 };
295
+ }
296
+
297
+ async function installLinux(context) {
298
+ const { env, config, node, service, label, invoke, mutate } = context;
299
+ const home = env.HOME || env.USERPROFILE || os.homedir();
300
+ const location = startupLocation(env.XDG_CONFIG_HOME || path.join(home, ".config"), ["systemd", "user", `${label}.service`]);
301
+ const file = location.file;
302
+ const query = async () => {
303
+ const response = await invoke("systemctl", ["--user", "show", `${label}.service`, "--property=LoadState,UnitFileState,ActiveState,FragmentPath", "--no-pager"]);
304
+ 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)]));
305
+ if (!values.LoadState || response.status !== 0 && values.LoadState !== "not-found") throw installFailure("USER_SYSTEMD_UNAVAILABLE");
306
+ return values;
307
+ };
308
+ const values = await query();
309
+ if (values.UnitFileState === "masked" || values.UnitFileState === "masked-runtime") return { configured: false, enabled: false, changed: false, started: false, registration: label, code: "SERVICE_MASKED" };
310
+ const previous = startupText(location);
311
+ if (previous !== null && !previous.includes(`Description=${RELAY_SERVICE_OWNER}\n`)
312
+ || values.FragmentPath && !sameStartupFile(location, values.FragmentPath)) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
313
+ const userDisabled = previous !== null && ["disabled", "disabled-runtime"].includes(values.UnitFileState);
314
+ const enabled = config.enabled && !userDisabled;
315
+ const wanted = unit(node, service, savedEnvironment(env, config));
316
+ const changed = previous !== wanted;
317
+ if (changed && mutate) {
318
+ atomicText(location, wanted, previous);
319
+ if ((await invoke("systemctl", ["--user", "daemon-reload"])).status !== 0) throw installFailure("USER_SYSTEMD_UNAVAILABLE");
320
+ }
321
+ if (enabled && mutate && !["enabled", "enabled-runtime"].includes(values.UnitFileState)) {
322
+ if ((await invoke("systemctl", ["--user", "enable", `${label}.service`])).status !== 0) throw installFailure("SERVICE_REGISTRATION_FAILED");
323
+ }
324
+ if (mutate) await context.prepareRuntime();
325
+ let started = false;
326
+ if (enabled && mutate && values.ActiveState !== "active") {
327
+ if ((await invoke("systemctl", ["--user", "start", `${label}.service`])).status !== 0) throw installFailure("SERVICE_START_FAILED");
328
+ started = true;
329
+ }
330
+ const verified = mutate ? await query() : values;
331
+ const configured = startupText(location) === wanted
332
+ && verified.LoadState === "loaded" && sameStartupFile(location, verified.FragmentPath)
333
+ && (!enabled || ["enabled", "enabled-runtime"].includes(verified.UnitFileState));
334
+ if (mutate && !configured) throw installFailure("SERVICE_READBACK_FAILED");
335
+ return { configured, enabled, changed, started, registration: label };
336
+ }
337
+
338
+ export async function installBackgroundRelay({ env = process.env, platform = process.platform, root = ROOT, run = execute,
339
+ dryRun = false, check = false, health = callBackgroundRelay, sleep = pause, pollAttempts = 15, uid = process.getuid?.() } = {}) {
340
+ const base = { configured: false, enabled: false, started: false, liveVerified: false, ready: false };
341
+ let config;
342
+ try {
343
+ config = readRelayConfiguration({ env, platform, missing: true });
344
+ if (!config) return { ...base, code: "WORKER_RELAY_NOT_CONFIGURED", reason: "Configure the background relay before installing its user service" };
345
+ if (!["win32", "darwin", "linux"].includes(platform)) return { ...base, code: "SERVICE_PLATFORM_UNSUPPORTED", reason: "Background relay user startup is unsupported on this platform" };
346
+ if (!path.isAbsolute(root)) throw installFailure("SERVICE_RUNTIME_MISSING");
347
+ root = fs.realpathSync.native(root);
348
+ const node = stableNodeBin(env);
349
+ const service = path.join(root, "src", "background-relay-service.mjs");
350
+ if (!path.isAbsolute(root) || !fs.existsSync(service) || !fs.existsSync(path.join(root, "src", "background-relay-daemon.mjs"))) throw installFailure("SERVICE_RUNTIME_MISSING");
351
+ let packageVersion;
352
+ try { packageVersion = JSON.parse(readRegularConfigFile(path.join(root, "package.json"), { maxBytes: 128 * 1024 })).version; }
353
+ catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
354
+ if (typeof packageVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.+-]+)?$/.test(packageVersion) || packageVersion.length > 96) throw installFailure("SERVICE_RUNTIME_INVALID");
355
+ if ([root, config.paths.root, node].some((value) => /[\u0000-\u001f\u007f]/.test(value))) throw installFailure("SERVICE_LAUNCH_PATH_UNSUPPORTED");
356
+ const hash = createHash("sha256").update(platform === "win32" ? config.paths.root.toLowerCase() : config.paths.root).digest("hex").slice(0, 12);
357
+ const label = platform === "win32" ? `MinhSpark Agent Relay ${hash}` : `com.minhspark.agent-relay.${hash}`;
358
+ const invoke = async (command, args, extra = {}) => {
359
+ const result = await run(command, args, { env, encoding: "utf8", windowsHide: true, shell: false, timeout: 15000, maxBuffer: 256 * 1024, stdio: ["pipe", "pipe", "pipe"], ...extra });
360
+ return { status: result?.status ?? result?.exitCode ?? 0, stdout: String(result?.stdout ?? "") };
361
+ };
362
+ const runtimeFile = path.join(config.paths.root, "service-runtime.json");
363
+ const readRuntime = () => {
364
+ const raw = readRegularConfigFile(runtimeFile, { missing: true, maxBytes: 64 * 1024 });
365
+ if (raw === null) return null;
366
+ let value;
367
+ try { value = JSON.parse(raw); } catch { throw installFailure("SERVICE_RUNTIME_INVALID"); }
368
+ if (value?.schemaVersion !== 1 || value.owner !== RELAY_SERVICE_OWNER) throw installFailure("SERVICE_CONFIGURATION_CONFLICT");
369
+ if (value.environment !== undefined && (!value.environment || typeof value.environment !== "object" || Array.isArray(value.environment)
370
+ || Object.entries(value.environment).some(([name, entry]) => !RUNTIME_ENVIRONMENT_KEYS.includes(name)
371
+ || typeof entry !== "string" || !entry || /[\u0000-\u001f\u007f]/.test(entry)))) throw installFailure("SERVICE_RUNTIME_INVALID");
372
+ return value;
373
+ };
374
+ const previousRuntime = readRuntime();
375
+ const installedEnv = check && previousRuntime ? {
376
+ ...Object.fromEntries(Object.entries(env).filter(([name]) => !RUNTIME_ENVIRONMENT_KEYS.includes(name))),
377
+ ...previousRuntime.environment,
378
+ } : env;
379
+ const desiredRuntime = { schemaVersion: 1, owner: RELAY_SERVICE_OWNER, packageRoot: root, nodeExecutable: node, packageVersion,
380
+ environment: check && previousRuntime ? previousRuntime.environment ?? {}
381
+ : Object.fromEntries(Object.entries(savedEnvironment(env, config)).filter(([name]) => !name.startsWith("AGENT_RELAY_"))) };
382
+ const expectedRuntimeId = relayServiceRuntimeId(desiredRuntime);
383
+ const runtimeChanged = JSON.stringify(previousRuntime) !== JSON.stringify(desiredRuntime);
384
+ const prepareRuntime = async () => {
385
+ if (JSON.stringify(readRuntime()) !== JSON.stringify(previousRuntime)) throw installFailure("SERVICE_CONFIGURATION_CHANGED");
386
+ if (runtimeChanged) writeRelayJson(runtimeFile, desiredRuntime);
387
+ if (JSON.stringify(readRuntime()) !== JSON.stringify(desiredRuntime)) throw installFailure("SERVICE_READBACK_FAILED");
388
+ };
389
+ const context = { env: installedEnv, config, root, node, service, label, invoke, mutate: !dryRun && !check, uid, nativeRun: run === execute, prepareRuntime };
390
+ const installed = await ({ win32: installWindows, darwin: installMac, linux: installLinux })[platform](context);
391
+ installed.configured = installed.configured && JSON.stringify(readRuntime()) === JSON.stringify(desiredRuntime);
392
+ const result = { ...base, ...installed, dryRun, deliveryEnabled: installed.enabled, liveVerified: false,
393
+ expectedVersion: packageVersion, runtimeChanged, activationPending: installed.activationPending === true || runtimeChanged && previousRuntime !== null };
394
+ if (!installed.enabled) {
395
+ result.notice = config.enabled ? "User startup is intentionally disabled; the installer preserved that choice" : "Background relay is intentionally disabled in its configuration";
396
+ return result;
397
+ }
398
+ if (!installed.configured || dryRun) return result;
399
+ const attempts = check ? 1 : Math.max(1, Math.min(30, pollAttempts));
400
+ for (let attempt = 0; attempt < attempts; attempt++) {
401
+ try {
402
+ const status = await health("status", {}, { env, connection: { endpoint: config.endpoint, token: config.token, workerId: null }, timeoutMs: 1000 });
403
+ const serviceState = JSON.parse(readRegularConfigFile(path.join(config.paths.root, "service-status.json"), { maxBytes: 64 * 1024 }));
404
+ const owned = status?.schemaVersion === 1 && status.transportReady === true && status.desktopRequired === false
405
+ && Number.isSafeInteger(status.pid) && status.pid > 0 && serviceState?.owner === RELAY_SERVICE_OWNER
406
+ && serviceState.status === "running" && serviceState.daemonPid === status.pid;
407
+ if (owned && typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version)) result.version = status.version;
408
+ if (owned && (serviceState.runtimeId !== expectedRuntimeId || status.runtimeId !== expectedRuntimeId || status.version !== packageVersion)) result.activationPending = true;
409
+ if (owned && serviceState.runtimeId === expectedRuntimeId && status.runtimeId === expectedRuntimeId && status.version === packageVersion) {
410
+ return { ...result, activationPending: false, liveVerified: true, ready: status.accepting === true, pid: status.pid,
411
+ version: typeof status.version === "string" && /^[A-Za-z0-9.+_-]{1,96}$/.test(status.version) ? status.version : null,
412
+ ...(status.accepting === true ? {} : { code: "SERVICE_DRAINING", notice: "The relay is finishing existing work before accepting new jobs" }) };
413
+ }
414
+ } catch {}
415
+ if (attempt + 1 < attempts) await sleep(250);
416
+ }
417
+ return { ...result, code: result.activationPending ? "SERVICE_UPDATE_PENDING" : "SERVICE_NOT_READY",
418
+ reason: result.activationPending ? "User startup is configured; the running service has not yet verified the desired idle runtime update"
419
+ : "User startup is configured, but its owned daemon did not report healthy transport" };
420
+ } catch (error) {
421
+ const allowed = /^(?:SERVICE_[A-Z_]+|TASK_SCHEDULER_UNAVAILABLE|LAUNCH_AGENT_UNAVAILABLE|USER_SYSTEMD_UNAVAILABLE|WORKER_CONFIG_INVALID|WORKER_STORAGE_UNSAFE)$/;
422
+ const code = typeof error.code === "string" && allowed.test(error.code) ? error.code : "SERVICE_INSTALL_FAILED";
423
+ return { ...base, code, reason: "The background relay user service could not be safely configured or verified" };
424
+ }
425
+ }
426
+
427
+ if (isMainModule(process.argv[1], import.meta.url)) {
428
+ const args = process.argv.slice(2);
429
+ if (args.some((arg) => !["--check", "--dry-run", "--json"].includes(arg)) || args.includes("--check") && args.includes("--dry-run")) {
430
+ process.stderr.write("Usage: install-background-relay.mjs [--check|--dry-run] [--json]\n");
431
+ process.exitCode = 2;
432
+ } else {
433
+ const result = await installBackgroundRelay({ check: args.includes("--check"), dryRun: args.includes("--dry-run") });
434
+ process.stdout.write(`${JSON.stringify(result)}\n`);
435
+ if (!result.configured && !args.includes("--dry-run") && result.code !== "SERVICE_MASKED") process.exitCode = 1;
436
+ }
437
+ }
@@ -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.3";
28
+ const VERSION = "1.26.1";
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"];
@@ -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
+ }