baychat 0.19.0 → 0.20.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.
package/README.md CHANGED
@@ -30,10 +30,49 @@ Coding sessions show idle after five minutes without use and expire after
30
30
  24 hours. `end_session` closes one immediately. History survives and the same
31
31
  name can rejoin. Persistent agents such as Hermes keep their own lifecycle.
32
32
 
33
- This workflow requires CLI 0.19.0 and its matching API deployment. Update npm,
33
+ This workflow requires CLI 0.20.1 and the existing session-chat API deployment. Update npm,
34
34
  rerun `baychat connect <runtime>` to refresh its skill, and restart running
35
35
  relays and MCP clients. Updating npm alone does not deploy the remote MCP.
36
36
 
37
+ ## What the package and MCP each do
38
+
39
+ The npm package contains the CLI, installed runtime commands, local relay,
40
+ local stdio MCP server and bundled Hermes plugin. The remote MCP is the BayChat
41
+ API running on the server. It owns sessions, room membership and chat tools.
42
+ Updating npm updates this computer; deploying the API updates remote MCP.
43
+
44
+ The installed session command runs one foreground command:
45
+
46
+ ```sh
47
+ baychat join Atlas "Coding" --runtime codex
48
+ baychat join --group "Coding" --runtime codex
49
+ ```
50
+
51
+ It joins through remote MCP, uses the server-confirmed identity, checks the relay
52
+ and connects incoming messages. Claude runs this command with its persistent
53
+ Monitor and re-arms automatically after a wake. Codex verifies native queue
54
+ support and returns as soon as the relay confirms registration. It needs no
55
+ waiting terminal or manual re-arming. Failed delivery setup is reported as
56
+ incomplete, even when the room join succeeded.
57
+
58
+ The Windows relay runs without a visible console, including after login and
59
+ network reconnects. Incoming messages appear as `@Sender` in the coding task.
60
+ The skill echoes BayChat chat replies locally; raw tool output stays local.
61
+ Codex can queue messages behind an active turn, so an accepted message is not
62
+ proof of an immediate reply.
63
+
64
+ For a persistent Hermes installation, run this on the Hermes machine:
65
+
66
+ ```sh
67
+ baychat connect hermes
68
+ ```
69
+
70
+ Approve the QR once and choose its agent on the phone. The command installs the
71
+ bundled plugin, configures remote MCP tools, and enables its gateway. Existing
72
+ Hermes settings and other MCP servers are preserved, with a backup before changes.
73
+ The MCP header references the credential in .env. An existing agent credential is reused;
74
+ a failed enable or restart returns an error. Hermes must already be installed.
75
+
37
76
  ## Pair a persistent agent
38
77
 
39
78
  Connect an AI agent session — Claude Code, Codex, or any CLI with a shell — to
package/dist/commands.js CHANGED
@@ -19,6 +19,7 @@ exports.cmdCheck = cmdCheck;
19
19
  exports.cmdWatch = cmdWatch;
20
20
  exports.cmdLink = cmdLink;
21
21
  exports.claudeMcpAddSpawn = claudeMcpAddSpawn;
22
+ exports.registerWithClaude = registerWithClaude;
22
23
  exports.cmdLogin = cmdLogin;
23
24
  exports.deviceExpiryWarning = deviceExpiryWarning;
24
25
  exports.printDeviceExpiryWarning = printDeviceExpiryWarning;
@@ -688,7 +689,7 @@ function registerWithClaude(baseUrl, token) {
688
689
  // Windows only, and only for a value a quoted cmd.exe argument cannot hold.
689
690
  console.log("\nCould not add BayChat to Claude Code safely on Windows — add it manually:");
690
691
  printManualMcpAdd(baseUrl);
691
- return;
692
+ return false;
692
693
  }
693
694
  // stderr is captured (not ignored) so a failure can quote claude's own words;
694
695
  // the timeout keeps a hung binary from hanging a login whose credential is
@@ -725,19 +726,19 @@ function registerWithClaude(baseUrl, token) {
725
726
  if (readded.status === 0) {
726
727
  console.log("✓ BayChat re-registered with Claude Code (credential renewed)");
727
728
  installClaudeSkill();
728
- return;
729
+ return true;
729
730
  }
730
731
  }
731
732
  }
732
733
  if (recipe.shell && res.status === 9009) {
733
734
  console.log("\nClaude Code CLI not found — add BayChat manually:");
734
735
  printManualMcpAdd(baseUrl);
735
- return;
736
+ return false;
736
737
  }
737
738
  if (res.error && res.error.code === "ENOENT") {
738
739
  console.log("\nClaude Code CLI not found — add BayChat manually:");
739
740
  printManualMcpAdd(baseUrl);
740
- return;
741
+ return false;
741
742
  }
742
743
  if (res.error || res.status !== 0) {
743
744
  const reason = res.error?.code ??
@@ -758,10 +759,11 @@ function registerWithClaude(baseUrl, token) {
758
759
  // because of it: a session with the rules and no tools still knows how to
759
760
  // ask for them, while one with neither knows nothing at all.
760
761
  installClaudeSkill();
761
- return;
762
+ return false;
762
763
  }
763
764
  console.log("✓ BayChat added to Claude Code");
764
765
  installClaudeSkill();
766
+ return true;
765
767
  }
766
768
  /**
767
769
  * Install Claude Code's BayChat skill, the other half of registering the server.
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cmdConnectClaude = cmdConnectClaude;
4
+ const commands_1 = require("./commands");
5
+ const config_1 = require("./config");
6
+ const commands_2 = require("./relay/commands");
7
+ const runtime_install_1 = require("./runtime-install");
8
+ /** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
9
+ async function cmdConnectClaude(opts = {}) {
10
+ try {
11
+ const existing = (0, config_1.loadDeviceCredentials)();
12
+ const base = (opts.base || process.env.BAYCHAT_API_URL || existing?.baseUrl || config_1.DEFAULT_API_URL).replace(/\/$/, "");
13
+ const expires = existing ? Date.parse(existing.expiresAt) : NaN;
14
+ if (!existing || existing.baseUrl.replace(/\/$/, "") !== base || !Number.isFinite(expires) || expires <= Date.now()) {
15
+ if (!await (0, commands_1.cmdLogin)({ base, registerClaude: false, hint: false }))
16
+ return 2;
17
+ }
18
+ const device = (0, config_1.loadDeviceCredentials)();
19
+ if (!device)
20
+ throw new Error("No device login was saved. Retry baychat connect claude.");
21
+ const registered = (0, commands_1.registerWithClaude)(device.baseUrl, device.token);
22
+ for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
23
+ console.log(" " + line);
24
+ if (!registered)
25
+ return 1;
26
+ console.log(" " + await (0, commands_2.ensureRelayInstalled)());
27
+ console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
28
+ return 0;
29
+ }
30
+ catch (err) {
31
+ console.error("Could not connect Claude Code: " + (err instanceof Error ? err.message : String(err)));
32
+ return 1;
33
+ }
34
+ }
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cmdConnectHermes = cmdConnectHermes;
4
+ const commands_1 = require("./commands");
5
+ const config_1 = require("./config");
6
+ const hermes_1 = require("./hermes");
7
+ /** Hermes owns a persistent agent. The owner chooses its name and Bay on the phone. */
8
+ async function cmdConnectHermes(opts = {}) {
9
+ const existing = (0, config_1.loadCredentials)();
10
+ if (!existing || (opts.base && opts.base.replace(/\/$/, "") !== existing.baseUrl.replace(/\/$/, ""))) {
11
+ console.log("Connect Hermes — approve the QR and choose its agent in BayChat.");
12
+ if (!await (0, commands_1.cmdLink)(opts))
13
+ return 2;
14
+ }
15
+ if (!(0, config_1.loadCredentials)()) {
16
+ console.error("No paired agent credential was saved. Retry baychat connect hermes.");
17
+ return 1;
18
+ }
19
+ return (0, hermes_1.cmdHermesInit)(["--enable"]);
20
+ }
package/dist/connect.js CHANGED
@@ -31,11 +31,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  return (mod && mod.__esModule) ? mod : { "default": mod };
32
32
  };
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
+ exports.cmdConnectClaude = void 0;
34
35
  exports.renderConnectMenu = renderConnectMenu;
35
36
  exports.isClaudeConnectTarget = isClaudeConnectTarget;
36
37
  exports.parseConnectClient = parseConnectClient;
37
38
  exports.writeClientConfig = writeClientConfig;
38
- exports.cmdConnectClaude = cmdConnectClaude;
39
39
  exports.cmdConnect = cmdConnect;
40
40
  const node_fs_1 = __importDefault(require("node:fs"));
41
41
  const node_path_1 = __importDefault(require("node:path"));
@@ -49,6 +49,10 @@ const commands_2 = require("./relay/commands");
49
49
  const runtime_install_1 = require("./runtime-install");
50
50
  const runtimes_1 = require("./runtimes");
51
51
  const runtime_binary_1 = require("./runtime-binary");
52
+ const connect_claude_1 = require("./connect-claude");
53
+ var connect_claude_2 = require("./connect-claude");
54
+ Object.defineProperty(exports, "cmdConnectClaude", { enumerable: true, get: function () { return connect_claude_2.cmdConnectClaude; } });
55
+ const connect_hermes_1 = require("./connect-hermes");
52
56
  /** Clients `connect` can configure. `claude` is an alias users reach for. */
53
57
  const CLIENT_ALIASES = {
54
58
  codex: "codex",
@@ -63,7 +67,8 @@ function renderConnectMenu() {
63
67
  "Connect this laptop to BayChat, once, and configure your client:",
64
68
  "",
65
69
  ...rows,
66
- " npx baychat connect claude → Claude Code (refreshes its skill)",
70
+ " npx baychat connect claude → Claude Code (login, MCP and skill)",
71
+ " npx baychat connect hermes → Hermes (pair and enable its persistent agent)",
67
72
  "",
68
73
  "You approve a QR on your phone. After that, in any coding session:",
69
74
  "",
@@ -179,29 +184,6 @@ const realIo = {
179
184
  *
180
185
  * @returns 0 on success, 2 when the laptop login expired without approval.
181
186
  */
182
- /**
183
- * `baychat connect claude` — refresh Claude Code's skill.
184
- *
185
- * Deliberately does NOT touch the credential or the MCP registration. Both are
186
- * `login`'s job, and re-doing them would put a device-pairing QR in front of
187
- * somebody whose login is fine. Writing the skill needs no auth at all: it is
188
- * generated locally from this CLI's own version, which is exactly why it goes
189
- * stale on upgrade and exactly why this command has to exist.
190
- */
191
- function cmdConnectClaude() {
192
- try {
193
- for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
194
- console.log(` ${line}`);
195
- console.log("");
196
- console.log(" Restart Claude Code once so it picks the skill up.");
197
- console.log(" Not logged in yet? `npx baychat login` — that registers the MCP server too.");
198
- return 0;
199
- }
200
- catch (err) {
201
- console.log(`Could not install the Claude Code skill: ${err instanceof Error ? err.message : String(err)}`);
202
- return 1;
203
- }
204
- }
205
187
  async function cmdConnect(clientArg, opts = {}) {
206
188
  if (clientArg === undefined) {
207
189
  console.log(renderConnectMenu());
@@ -210,7 +192,9 @@ async function cmdConnect(clientArg, opts = {}) {
210
192
  // Claude Code first: it is not an MCP-config client, so it must not reach
211
193
  // `parseConnectClient`, which would reject it.
212
194
  if (isClaudeConnectTarget(clientArg))
213
- return cmdConnectClaude();
195
+ return (0, connect_claude_1.cmdConnectClaude)(opts);
196
+ if (clientArg.trim().toLowerCase() === "hermes")
197
+ return (0, connect_hermes_1.cmdConnectHermes)(opts);
214
198
  const client = parseConnectClient(clientArg);
215
199
  const base = (opts.base || process.env.BAYCHAT_API_URL || config_1.DEFAULT_API_URL).replace(/\/$/, "");
216
200
  const steps = (0, connect_plan_1.planConnect)({
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.writeHermesMcp = writeHermesMcp;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const node_crypto_1 = require("node:crypto");
10
+ const yaml_1 = require("yaml");
11
+ /** Preserve other Hermes settings, comments, tools policies and MCP servers.
12
+ * The credential stays in .env; config only references BAYCHAT_TOKEN.
13
+ */
14
+ function writeHermesMcp(home, baseUrl) {
15
+ const written = node_path_1.default.join(home, "config.yaml");
16
+ const existing = node_fs_1.default.existsSync(written) ? node_fs_1.default.readFileSync(written, "utf8") : "";
17
+ const doc = (0, yaml_1.parseDocument)(existing);
18
+ // Never quote parser errors: they can contain lines holding another API key.
19
+ if (doc.errors.length || (doc.contents !== null && !(0, yaml_1.isMap)(doc.contents))) {
20
+ throw new Error("Hermes config.yaml is not a valid YAML mapping. Fix it before connecting.");
21
+ }
22
+ if (doc.contents === null)
23
+ doc.contents = doc.createNode({});
24
+ for (const keys of [["mcp_servers"], ["mcp_servers", "baychat"], ["mcp_servers", "baychat", "headers"]]) {
25
+ if (!doc.hasIn(keys))
26
+ doc.setIn(keys, doc.createNode({}));
27
+ if (!(0, yaml_1.isMap)(doc.getIn(keys, true))) {
28
+ throw new Error("Hermes mcp_servers.baychat configuration must use YAML mappings. Existing config was preserved.");
29
+ }
30
+ }
31
+ const root = ["mcp_servers", "baychat"];
32
+ // A former stdio/SSE entry cannot coexist with the remote HTTP transport.
33
+ for (const key of ["command", "args", "transport"])
34
+ doc.deleteIn([...root, key]);
35
+ doc.setIn([...root, "url"], new URL("/api/mcp", baseUrl).href);
36
+ doc.setIn([...root, "headers", "Authorization"], "Bearer ${BAYCHAT_TOKEN}");
37
+ doc.setIn([...root, "enabled"], true);
38
+ const next = doc.toString();
39
+ if (next === existing)
40
+ return { written };
41
+ node_fs_1.default.mkdirSync(home, { recursive: true });
42
+ const suffix = (0, node_crypto_1.randomUUID)();
43
+ const backup = existing ? written + ".baychat-backup-" + suffix : undefined;
44
+ if (backup) {
45
+ node_fs_1.default.copyFileSync(written, backup, node_fs_1.default.constants.COPYFILE_EXCL);
46
+ node_fs_1.default.chmodSync(backup, 0o600);
47
+ }
48
+ const temporary = written + ".baychat-" + suffix;
49
+ try {
50
+ node_fs_1.default.writeFileSync(temporary, next, { mode: 0o600, flag: "wx" });
51
+ node_fs_1.default.renameSync(temporary, written);
52
+ }
53
+ finally {
54
+ if (node_fs_1.default.existsSync(temporary))
55
+ node_fs_1.default.unlinkSync(temporary);
56
+ }
57
+ return { written, backup };
58
+ }
package/dist/hermes.js CHANGED
@@ -56,6 +56,7 @@ const path = __importStar(require("path"));
56
56
  const args_1 = require("./args");
57
57
  const config_1 = require("./config");
58
58
  const hermes_plugin_content_1 = require("./hermes-plugin-content");
59
+ const hermes_mcp_1 = require("./hermes-mcp");
59
60
  /** The two commands Hermes needs after the files are in place. */
60
61
  exports.ENABLE_STEPS = [
61
62
  "hermes config set gateway.platforms.baychat.enabled true",
@@ -128,9 +129,15 @@ function writeHermesEnv(home, token, baseUrl) {
128
129
  return envPath;
129
130
  }
130
131
  function installHermes(home) {
132
+ const creds = (0, config_1.loadCredentials)();
133
+ // Refuse a malformed config before changing the gateway or its credential.
134
+ const mcp = creds ? (0, hermes_mcp_1.writeHermesMcp)(home, creds.baseUrl) : null;
131
135
  const { written, backedUp } = installHermesPlugin(home);
136
+ if (mcp)
137
+ written.push(mcp.written);
138
+ if (mcp?.backup)
139
+ backedUp.push(mcp.backup);
132
140
  const report = { home, written, backedUp, envWritten: null };
133
- const creds = (0, config_1.loadCredentials)();
134
141
  if (!creds) {
135
142
  report.envSkipped =
136
143
  "no agent token on this machine — run `baychat pair <code>` (get the code in the BayChat app: Agents → your agent), then re-run this";
@@ -139,18 +146,19 @@ function installHermes(home) {
139
146
  report.envWritten = writeHermesEnv(home, creds.token, creds.baseUrl);
140
147
  return report;
141
148
  }
142
- /** Run the two enablement commands. Returns false if `hermes` is not on PATH. */
143
- function runEnableSteps() {
144
- const probe = (0, child_process_1.spawnSync)("hermes", ["--version"], { stdio: "ignore" });
149
+ /** Fail visibly if the executable or either gateway setup step fails. */
150
+ function runEnableSteps(home) {
151
+ const env = { ...process.env, HERMES_HOME: home };
152
+ const probe = (0, child_process_1.spawnSync)("hermes", ["--version"], { stdio: "ignore", env });
145
153
  if (probe.error)
146
154
  return false;
147
155
  for (const step of exports.ENABLE_STEPS) {
148
156
  const [bin, ...rest] = step.split(" ");
149
157
  console.log(` $ ${step}`);
150
- const run = (0, child_process_1.spawnSync)(bin, rest, { stdio: "inherit" });
158
+ const run = (0, child_process_1.spawnSync)(bin, rest, { stdio: "inherit", env });
151
159
  if (run.status !== 0) {
152
160
  console.log(` ↑ exited ${run.status ?? "abnormally"} — finish the remaining steps by hand.`);
153
- return true;
161
+ return false;
154
162
  }
155
163
  }
156
164
  return true;
@@ -169,13 +177,16 @@ function cmdHermesInit(args) {
169
177
  }
170
178
  else {
171
179
  console.log(`\nNo token written — ${report.envSkipped}`);
180
+ if (args.includes("--enable"))
181
+ return 1;
172
182
  }
173
183
  console.log("\nEnable it in Hermes:");
174
184
  if (args.includes("--enable")) {
175
- if (!runEnableSteps()) {
176
- console.log(" `hermes` is not on PATH run these wherever Hermes is installed:");
185
+ if (!runEnableSteps(home)) {
186
+ console.log(" Hermes could not be enabled. Resolve the error, then run:");
177
187
  for (const step of exports.ENABLE_STEPS)
178
188
  console.log(` ${step}`);
189
+ return 1;
179
190
  }
180
191
  }
181
192
  else {
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const commands_1 = require("./commands");
5
5
  const approve_hook_1 = require("./approve-hook");
6
6
  const session_name_1 = require("./session-name");
7
+ const session_command_1 = require("./session-command");
7
8
  const doctor_command_1 = require("./doctor-command");
8
9
  const hermes_1 = require("./hermes");
9
10
  const connect_1 = require("./connect");
@@ -17,6 +18,8 @@ const profiles_1 = require("./relay/profiles");
17
18
  const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
18
19
 
19
20
  Usage:
21
+ baychat join [name] [group] [--group <title>] [--runtime <runtime>]
22
+ Join and connect incoming messages in this terminal
20
23
  baychat session-name --runtime <runtime>
21
24
  Stable automatic name for this verified session
22
25
  baychat onboard [conversationId] [--catch-up]
@@ -153,6 +156,8 @@ function numberFlag(args, name) {
153
156
  async function main() {
154
157
  const [command, ...args] = process.argv.slice(2);
155
158
  switch (command) {
159
+ case "join":
160
+ return (0, session_command_1.cmdJoinSession)(args);
156
161
  case "session-name": {
157
162
  (0, args_1.rejectUnknownFlags)(args, ["--runtime"], "baychat session-name --runtime <runtime>");
158
163
  const runtime = (0, args_1.flag)(args, "--runtime") ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES);
@@ -197,7 +197,11 @@ const codexAdapter = {
197
197
  */
198
198
  async runTurn({ binaryPath, target, prompt }) {
199
199
  if (process.env.BAYCHAT_CODEX_TRANSPORT === "exec") {
200
- return { kind: "failed", transportUnusable: true, reason: "BAYCHAT_CODEX_TRANSPORT=exec" };
200
+ return {
201
+ kind: "failed",
202
+ transportUnusable: true,
203
+ reason: "BAYCHAT_CODEX_TRANSPORT=exec",
204
+ };
201
205
  }
202
206
  return (0, codex_app_server_1.runCodexTurn)({
203
207
  binaryPath,
@@ -234,7 +238,10 @@ const cursorAdapter = {
234
238
  // Nothing to discover: Cursor writes no on-disk session state this package
235
239
  // can identify a conversation from, and a search that cannot succeed is
236
240
  // only a way to produce a confident-looking wrong answer.
237
- return { ok: false, reason: "Cursor records no resumable session id on this machine" };
241
+ return {
242
+ ok: false,
243
+ reason: "Cursor records no resumable session id on this machine",
244
+ };
238
245
  },
239
246
  headlessCommand() {
240
247
  // Unreachable: the daemon consults canResume first and reports pending.
@@ -259,7 +266,10 @@ const hermesAdapter = {
259
266
  async discoverResume() {
260
267
  // Nothing to discover: Hermes runs somewhere else and leaves no local
261
268
  // transcript. Searching would only produce a confident-looking wrong answer.
262
- return { ok: false, reason: "Hermes keeps no local session state on this machine" };
269
+ return {
270
+ ok: false,
271
+ reason: "Hermes keeps no local session state on this machine",
272
+ };
263
273
  },
264
274
  headlessCommand() {
265
275
  // Unreachable: the daemon consults canResume first and reports pending.
@@ -317,7 +327,9 @@ function profileAdapter(profile) {
317
327
  // `session-name` runtimes are resumed by the name the human chose, which
318
328
  // IS the BayChat session name — so there is nothing to discover and
319
329
  // nothing that could name someone else's session.
320
- const id = profile.sessionId.kind === "session-name" ? target.name : target.resumeId;
330
+ const id = profile.sessionId.kind === "session-name"
331
+ ? target.name
332
+ : target.resumeId;
321
333
  if (!id) {
322
334
  return {
323
335
  ok: false,
@@ -337,7 +349,9 @@ function profileAdapter(profile) {
337
349
  headlessCommand(target, prompt) {
338
350
  if (!profile.headless)
339
351
  throw new Error(`${profile.id} has no headless command`);
340
- const id = profile.sessionId.kind === "session-name" ? target.name : target.resumeId;
352
+ const id = profile.sessionId.kind === "session-name"
353
+ ? target.name
354
+ : target.resumeId;
341
355
  return {
342
356
  file: target.runtimeBin ?? profile.bin,
343
357
  args: (0, profiles_1.fillTemplate)(profile.headless.args, { id, prompt }),
@@ -353,7 +367,10 @@ function adapterFor(runtime) {
353
367
  return profile ? profileAdapter(profile) : unknownAdapter(runtime);
354
368
  }
355
369
  /** The runtimes we ship knowledge of, for help text. NOT a list of what is accepted. */
356
- exports.KNOWN_RUNTIMES = [...Object.keys(ADAPTERS), ...profiles_1.RUNTIME_PROFILES.map((p) => p.id)];
370
+ exports.KNOWN_RUNTIMES = [
371
+ ...Object.keys(ADAPTERS),
372
+ ...profiles_1.RUNTIME_PROFILES.map((p) => p.id),
373
+ ];
357
374
  /**
358
375
  * Do we ship knowledge of this runtime?
359
376
  *
@@ -364,7 +381,8 @@ exports.KNOWN_RUNTIMES = [...Object.keys(ADAPTERS), ...profiles_1.RUNTIME_PROFIL
364
381
  * us, and the Cursor outage was the same mistake from the other side.
365
382
  */
366
383
  function isKnownRuntime(value) {
367
- return Object.prototype.hasOwnProperty.call(ADAPTERS, value) || (0, profiles_1.profileFor)(value) !== undefined;
384
+ return (Object.prototype.hasOwnProperty.call(ADAPTERS, value) ||
385
+ (0, profiles_1.profileFor)(value) !== undefined);
368
386
  }
369
387
  /**
370
388
  * Run a headless turn to completion.
@@ -377,6 +395,7 @@ function isKnownRuntime(value) {
377
395
  function runHeadless(file, args, opts = {}) {
378
396
  return new Promise((resolve) => {
379
397
  const child = (0, child_process_1.spawn)(file, args, {
398
+ windowsHide: true,
380
399
  cwd: opts.cwd,
381
400
  // An npm-installed runtime is a script with a `#!/usr/bin/env node`
382
401
  // shebang, and the daemon's systemd PATH has no node. See ./spawn-env.ts.
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.AUTOSTART_IMPLEMENTATIONS = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
36
+ exports.AUTOSTART_IMPLEMENTATIONS = exports.WINDOWS_RELAY_LAUNCHER = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
37
37
  exports.autostartForPlatform = autostartForPlatform;
38
38
  exports.relayCommand = relayCommand;
39
39
  // Making the relay come back by itself, on all three platforms.
@@ -101,9 +101,17 @@ WantedBy=default.target
101
101
  fs.mkdirSync(path.dirname(target), { recursive: true });
102
102
  fs.writeFileSync(target, systemd.unit(cmd), { mode: 0o644 });
103
103
  await execFileAsync("systemctl", ["--user", "daemon-reload"]);
104
- await execFileAsync("systemctl", ["--user", "enable", "--now", exports.SYSTEMD_UNIT_NAME]);
104
+ await execFileAsync("systemctl", [
105
+ "--user",
106
+ "enable",
107
+ "--now",
108
+ exports.SYSTEMD_UNIT_NAME,
109
+ ]);
105
110
  try {
106
- await execFileAsync("loginctl", ["enable-linger", os.userInfo().username]);
111
+ await execFileAsync("loginctl", [
112
+ "enable-linger",
113
+ os.userInfo().username,
114
+ ]);
107
115
  }
108
116
  catch {
109
117
  // Not fatal and not silent: the caller prints the degraded promise rather
@@ -112,7 +120,12 @@ WantedBy=default.target
112
120
  }
113
121
  },
114
122
  async uninstall() {
115
- await execFileAsync("systemctl", ["--user", "disable", "--now", exports.SYSTEMD_UNIT_NAME]).catch(() => undefined);
123
+ await execFileAsync("systemctl", [
124
+ "--user",
125
+ "disable",
126
+ "--now",
127
+ exports.SYSTEMD_UNIT_NAME,
128
+ ]).catch(() => undefined);
116
129
  fs.rmSync(systemd.unitPath(), { force: true });
117
130
  },
118
131
  };
@@ -175,12 +188,18 @@ const launchd = {
175
188
  const domain = `gui/${os.userInfo().uid}`;
176
189
  // Replacing an agent means removing the old one first; bootout on a label
177
190
  // that was never loaded exits non-zero, which is not a failure here.
178
- await execFileAsync("launchctl", ["bootout", `${domain}/${exports.SERVICE_LABEL}`]).catch(() => undefined);
191
+ await execFileAsync("launchctl", [
192
+ "bootout",
193
+ `${domain}/${exports.SERVICE_LABEL}`,
194
+ ]).catch(() => undefined);
179
195
  await execFileAsync("launchctl", ["bootstrap", domain, target]);
180
196
  },
181
197
  async uninstall() {
182
198
  const domain = `gui/${os.userInfo().uid}`;
183
- await execFileAsync("launchctl", ["bootout", `${domain}/${exports.SERVICE_LABEL}`]).catch(() => undefined);
199
+ await execFileAsync("launchctl", [
200
+ "bootout",
201
+ `${domain}/${exports.SERVICE_LABEL}`,
202
+ ]).catch(() => undefined);
184
203
  fs.rmSync(launchd.unitPath(), { force: true });
185
204
  },
186
205
  };
@@ -246,8 +265,8 @@ const scheduledTask = {
246
265
  </Settings>
247
266
  <Actions Context="Author">
248
267
  <Exec>
249
- <Command>${escapeXml(cmd.node)}</Command>
250
- <Arguments>"${escapeXml(cmd.entry)}" relay start --foreground</Arguments>
268
+ <Command>${escapeXml(path.win32.join(process.env.SystemRoot || "C:\\Windows", "System32", "wscript.exe"))}</Command>
269
+ <Arguments>//B //NoLogo ${escapeXml(`"${windowsLauncherPath()}" "${cmd.node}" "${cmd.entry}"`)} relay start --foreground</Arguments>
251
270
  </Exec>
252
271
  </Actions>
253
272
  </Task>
@@ -258,17 +277,47 @@ const scheduledTask = {
258
277
  async install(cmd) {
259
278
  const target = scheduledTask.unitPath();
260
279
  fs.mkdirSync(path.dirname(target), { recursive: true });
280
+ fs.writeFileSync(windowsLauncherPath(), exports.WINDOWS_RELAY_LAUNCHER, "utf8");
261
281
  // UTF-16LE with a BOM, because that is what the XML declaration above claims
262
282
  // and what schtasks refuses the file without.
263
- fs.writeFileSync(target, "\ufeff" + scheduledTask.unit(cmd), { encoding: "utf16le" });
264
- await execFileAsync("schtasks", ["/create", "/tn", exports.SERVICE_LABEL, "/xml", target, "/f"]);
265
- await execFileAsync("schtasks", ["/run", "/tn", exports.SERVICE_LABEL]);
283
+ fs.writeFileSync(target, "\ufeff" + scheduledTask.unit(cmd), {
284
+ encoding: "utf16le",
285
+ });
286
+ await execFileAsync("schtasks", ["/create", "/tn", exports.SERVICE_LABEL, "/xml", target, "/f"], { windowsHide: true });
287
+ await execFileAsync("schtasks", ["/run", "/tn", exports.SERVICE_LABEL], {
288
+ windowsHide: true,
289
+ });
266
290
  },
267
291
  async uninstall() {
268
- await execFileAsync("schtasks", ["/delete", "/tn", exports.SERVICE_LABEL, "/f"]).catch(() => undefined);
292
+ // Deleting a task does not stop its running child. End the supervised
293
+ // process first so `relay stop` cannot leave an invisible orphan behind.
294
+ await execFileAsync("schtasks", ["/end", "/tn", exports.SERVICE_LABEL], {
295
+ windowsHide: true,
296
+ }).catch(() => undefined);
297
+ await execFileAsync("schtasks", ["/delete", "/tn", exports.SERVICE_LABEL, "/f"], {
298
+ windowsHide: true,
299
+ }).catch(() => undefined);
269
300
  fs.rmSync(scheduledTask.unitPath(), { force: true });
301
+ fs.rmSync(windowsLauncherPath(), { force: true });
270
302
  },
271
303
  };
304
+ /** WSH is a GUI executable, so starting it never allocates a visible console.
305
+ * Run's window style 0 hides the child too. Waiting and forwarding its exit
306
+ * code keeps Task Scheduler's restart supervision attached to the actual relay.
307
+ * This is Windows Script Host JScript, not Node; no cmd.exe or shell is involved.
308
+ */
309
+ exports.WINDOWS_RELAY_LAUNCHER = `var shell = new ActiveXObject("WScript.Shell");
310
+ var command = [];
311
+ for (var index = 0; index < WScript.Arguments.length; index++) {
312
+ var argument = WScript.Arguments(index);
313
+ if (/["\\r\\n]/.test(argument)) throw new Error("Invalid relay startup argument");
314
+ command.push('"' + argument + '"');
315
+ }
316
+ WScript.Quit(shell.Run(command.join(" "), 0, true));
317
+ `;
318
+ function windowsLauncherPath() {
319
+ return path.join(configHome(), "relay-launcher.js");
320
+ }
272
321
  /** `DOMAIN\user`, which is what a task's `UserId` wants. `USERDOMAIN` is absent
273
322
  * on a machine that was never joined to a domain; the bare username is correct
274
323
  * there, and inventing a domain would register a task for a principal that does
@@ -316,9 +365,19 @@ function autostartForPlatform(platform = process.platform) {
316
365
  * `process.argv[1]` is the script node was given; resolving it is what keeps an
317
366
  * npx-run or `npm link`ed CLI producing a unit that still works after the shell
318
367
  * that installed it is gone. */
319
- function relayCommand() {
320
- return { node: process.execPath, entry: path.resolve(process.argv[1] ?? "") };
368
+ function relayCommand(physicalPath = (value) => fs.realpathSync.native(value)) {
369
+ // Windows packaged apps virtualize AppData. A path visible inside Codex can
370
+ // be absent from Task Scheduler's filesystem view. Resolve the actual file
371
+ // before handing it to a process outside that package (observed on Windows).
372
+ return {
373
+ node: physicalPath(process.execPath),
374
+ entry: physicalPath(path.resolve(process.argv[1] ?? "")),
375
+ };
321
376
  }
322
377
  /** Exported for tests, which need to reach a platform other than the one they
323
378
  * run on. Production code goes through {@link autostartForPlatform}. */
324
- exports.AUTOSTART_IMPLEMENTATIONS = { systemd, launchd, scheduledTask };
379
+ exports.AUTOSTART_IMPLEMENTATIONS = {
380
+ systemd,
381
+ launchd,
382
+ scheduledTask,
383
+ };