agent-anywhere-gateway 0.1.0 → 0.1.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
@@ -14,7 +14,7 @@ npm install -g agent-anywhere-gateway
14
14
  agent-anywhere-gateway \
15
15
  --server https://agent-anywhere.example.com \
16
16
  --id office-mac \
17
- --token replace-me \
17
+ --token your-real-token \
18
18
  --allowed-roots /Users/me/gitrepo \
19
19
  --provider codex
20
20
  ```
@@ -30,6 +30,31 @@ agent-anywhere-gateway \
30
30
  --providers codex,claude-code
31
31
  ```
32
32
 
33
+ ## Service install
34
+
35
+ Install the gateway as a current-user service:
36
+
37
+ ```bash
38
+ agent-anywhere-gateway service install \
39
+ --server https://agent-anywhere.example.com \
40
+ --id office-mac \
41
+ --token replace-me \
42
+ --allowed-roots /Users/me/gitrepo \
43
+ --provider codex
44
+ ```
45
+
46
+ Manage it with:
47
+
48
+ ```bash
49
+ agent-anywhere-gateway service status
50
+ agent-anywhere-gateway service logs --follow
51
+ agent-anywhere-gateway service restart
52
+ agent-anywhere-gateway service uninstall
53
+ ```
54
+
55
+ The service command uses native user-level managers: macOS LaunchAgent,
56
+ Linux user systemd, and Windows Task Scheduler.
57
+
33
58
  `--server` accepts the Control Server or Worker URL and expands it to
34
59
  `/api/gateways/{gateway_id}/ws`. Use `--server-ws-url` when you need to pass
35
60
  the exact WebSocket URL.
@@ -43,4 +68,4 @@ The same settings can be provided with environment variables:
43
68
  - `AGENT_PROVIDER`
44
69
  - `AGENT_PROVIDERS`
45
70
 
46
- Version: 0.1.0
71
+ Version: 0.1.1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-anywhere-gateway",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Standalone Agent Anywhere Gateway CLI for controlled machines.",
5
5
  "main": "src/gateway/main.js",
6
6
  "bin": {
@@ -5,6 +5,7 @@ const {
5
5
  serverWsUrl,
6
6
  startGateway
7
7
  } = require("./client");
8
+ const { runServiceCli } = require("./service");
8
9
 
9
10
  function parseArgs(argv = process.argv.slice(2)) {
10
11
  const options = {};
@@ -159,6 +160,9 @@ function helpText() {
159
160
  " --dry-run Print resolved config and exit",
160
161
  " --help Show this help",
161
162
  "",
163
+ "Commands:",
164
+ " service <command> Install or manage the gateway as an OS service",
165
+ "",
162
166
  "Environment:",
163
167
  " AGENT_ANYWHERE_SERVER_WS_URL Control Server WebSocket URL",
164
168
  " AGENT_ANYWHERE_GATEWAY_ID Stable gateway machine id",
@@ -179,6 +183,9 @@ function printPlan(plan, log = console.log) {
179
183
  }
180
184
 
181
185
  function runCli(argv = process.argv.slice(2), env = process.env) {
186
+ if (argv[0] === "service") {
187
+ return runServiceCli(argv.slice(1), { env });
188
+ }
182
189
  const options = parseArgs(argv);
183
190
  if (options.help) {
184
191
  console.log(helpText());
@@ -218,6 +225,7 @@ module.exports = {
218
225
  registerUrl,
219
226
  resolveServerWsUrl,
220
227
  runCli,
228
+ runServiceCli,
221
229
  serverWsUrl,
222
230
  startGateway,
223
231
  toGatewayWsUrl
@@ -0,0 +1,568 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const { spawnSync } = require("node:child_process");
5
+ const { parseProviderList } = require("../shared/providers");
6
+
7
+ const DEFAULT_LABEL = "com.agent-anywhere.gateway";
8
+ const DEFAULT_LINUX_SERVICE_NAME = "agent-anywhere-gateway";
9
+ const DEFAULT_WINDOWS_TASK_NAME = "AgentAnywhereGateway";
10
+ const DEFAULT_GATEWAY_ID = "gateway";
11
+
12
+ function readValue(argv, index, name) {
13
+ const value = argv[index + 1];
14
+ if (!value || value.startsWith("--")) {
15
+ throw new Error(`${name} requires a value`);
16
+ }
17
+ return value;
18
+ }
19
+
20
+ function parseServiceArgs(argv = []) {
21
+ const first = argv[0];
22
+ const command = first && !first.startsWith("-") ? first : "help";
23
+ const startIndex = command === "help" && first && first.startsWith("-") ? 0 : 1;
24
+ const options = { command };
25
+ for (let index = startIndex; index < argv.length; index += 1) {
26
+ const arg = argv[index];
27
+ const read = (name) => {
28
+ const value = readValue(argv, index, name);
29
+ index += 1;
30
+ return value;
31
+ };
32
+
33
+ if (arg === "--help" || arg === "-h") {
34
+ options.help = true;
35
+ } else if (arg === "--server" || arg === "--worker-url") {
36
+ options.server = read(arg);
37
+ } else if (arg === "--server-ws-url") {
38
+ options.serverWsUrl = read(arg);
39
+ } else if (arg === "--id") {
40
+ options.id = read(arg);
41
+ } else if (arg === "--name") {
42
+ options.name = read(arg);
43
+ } else if (arg === "--token") {
44
+ options.token = read(arg);
45
+ } else if (arg === "--allowed-roots") {
46
+ options.allowedRoots = read(arg);
47
+ } else if (arg === "--provider") {
48
+ options.provider = read(arg);
49
+ } else if (arg === "--providers") {
50
+ options.providers = read(arg);
51
+ } else if (arg === "--data-dir") {
52
+ options.dataDir = read(arg);
53
+ } else if (arg === "--replay-dir") {
54
+ options.replayDir = read(arg);
55
+ } else if (arg === "--reconnect-min-ms") {
56
+ options.reconnectMinMs = read(arg);
57
+ } else if (arg === "--reconnect-max-ms") {
58
+ options.reconnectMaxMs = read(arg);
59
+ } else if (arg === "--claude-auto-trust") {
60
+ options.claudeAutoTrust = true;
61
+ } else if (arg === "--label" || arg === "--service-name") {
62
+ options.label = read(arg);
63
+ } else if (arg === "--task-name") {
64
+ options.taskName = read(arg);
65
+ } else if (arg === "--cli-path") {
66
+ options.cliPath = read(arg);
67
+ } else if (arg === "--node-path") {
68
+ options.nodePath = read(arg);
69
+ } else if (arg === "--dry-run") {
70
+ options.dryRun = true;
71
+ } else if (arg === "--no-start") {
72
+ options.noStart = true;
73
+ } else if (arg === "--follow") {
74
+ options.follow = true;
75
+ } else {
76
+ throw new Error(`Unknown service option: ${arg}`);
77
+ }
78
+ }
79
+ return options;
80
+ }
81
+
82
+ function normalizeGatewayId(value) {
83
+ return String(value || os.hostname() || DEFAULT_GATEWAY_ID)
84
+ .trim()
85
+ .replace(/[^A-Za-z0-9_.-]+/g, "-")
86
+ .replace(/^-+|-+$/g, "") || DEFAULT_GATEWAY_ID;
87
+ }
88
+
89
+ function normalizeProviderList(value) {
90
+ return parseProviderList(value, ["codex"]).join(",");
91
+ }
92
+
93
+ function primaryProvider(value) {
94
+ return parseProviderList(value, ["codex"])[0] || "codex";
95
+ }
96
+
97
+ function toGatewayWsUrl(serverUrl, id) {
98
+ const url = new URL(serverUrl);
99
+ if (url.protocol === "http:") {
100
+ url.protocol = "ws:";
101
+ } else if (url.protocol === "https:") {
102
+ url.protocol = "wss:";
103
+ }
104
+ if (url.protocol !== "ws:" && url.protocol !== "wss:") {
105
+ throw new Error("--server must be an http(s) or ws(s) URL");
106
+ }
107
+ if (!url.pathname || url.pathname === "/") {
108
+ url.pathname = `/api/gateways/${encodeURIComponent(id)}/ws`;
109
+ }
110
+ return url.toString();
111
+ }
112
+
113
+ function windowsLocalAppData(env = process.env, homeDir = os.homedir()) {
114
+ return env.LOCALAPPDATA || path.join(homeDir, "AppData", "Local");
115
+ }
116
+
117
+ function defaultBaseDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
118
+ if (platform === "darwin") {
119
+ return path.join(homeDir, "Library", "Application Support", "agent-anywhere-gateway");
120
+ }
121
+ if (platform === "win32") {
122
+ return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway");
123
+ }
124
+ return path.join(homeDir, ".local", "share", "agent-anywhere-gateway");
125
+ }
126
+
127
+ function defaultConfigDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
128
+ if (platform === "darwin") {
129
+ return path.join(homeDir, "Library", "Application Support", "agent-anywhere-gateway");
130
+ }
131
+ if (platform === "win32") {
132
+ return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway");
133
+ }
134
+ return path.join(homeDir, ".config", "agent-anywhere-gateway");
135
+ }
136
+
137
+ function defaultLogDir(platform = process.platform, homeDir = os.homedir(), env = process.env) {
138
+ if (platform === "darwin") {
139
+ return path.join(homeDir, "Library", "Logs");
140
+ }
141
+ if (platform === "win32") {
142
+ return path.join(windowsLocalAppData(env, homeDir), "AgentAnywhereGateway", "Logs");
143
+ }
144
+ return path.join(homeDir, ".local", "state", "agent-anywhere-gateway");
145
+ }
146
+
147
+ function fallbackCliPath() {
148
+ return path.resolve(__dirname, "..", "..", "bin", "agent-anywhere-gateway.js");
149
+ }
150
+
151
+ function resolveCliPath(value = process.argv[1]) {
152
+ return path.resolve(value || fallbackCliPath());
153
+ }
154
+
155
+ function resolveServiceSettings(options = {}, {
156
+ env = process.env,
157
+ platform = process.platform,
158
+ homeDir = os.homedir(),
159
+ nodePath = process.execPath,
160
+ cliPath = resolveCliPath()
161
+ } = {}) {
162
+ const id = normalizeGatewayId(options.id || env.AGENT_ANYWHERE_GATEWAY_ID);
163
+ const rawServerWsUrl = options.serverWsUrl || env.AGENT_ANYWHERE_SERVER_WS_URL ||
164
+ (options.server ? toGatewayWsUrl(options.server, id) : `ws://localhost:8787/api/gateways/${encodeURIComponent(id)}/ws`);
165
+ const serverWsUrl = String(rawServerWsUrl).replace("{gateway_id}", encodeURIComponent(id));
166
+ const providers = normalizeProviderList(options.providers || options.provider || env.AGENT_PROVIDERS || env.AGENT_PROVIDER || "codex");
167
+ const token = options.token || env.AGENT_ANYWHERE_GATEWAY_TOKEN || "";
168
+ const baseDir = defaultBaseDir(platform, homeDir, env);
169
+ const logDir = defaultLogDir(platform, homeDir, env);
170
+ const configDir = defaultConfigDir(platform, homeDir, env);
171
+ return {
172
+ platform,
173
+ homeDir,
174
+ id,
175
+ name: options.name || env.AGENT_ANYWHERE_MACHINE_NAME || id,
176
+ token,
177
+ serverWsUrl,
178
+ allowedRoots: options.allowedRoots || env.AGENT_ANYWHERE_ALLOWED_ROOTS || "",
179
+ providers,
180
+ primaryProvider: primaryProvider(providers),
181
+ claudeAutoTrust: options.claudeAutoTrust ? "1" : env.AGENT_ANYWHERE_CLAUDE_AUTO_TRUST || "",
182
+ dataDir: options.dataDir || env.AGENT_ANYWHERE_DATA_DIR || path.join(baseDir, "data"),
183
+ replayDir: options.replayDir || env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR || "",
184
+ reconnectMinMs: options.reconnectMinMs || env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS || "",
185
+ reconnectMaxMs: options.reconnectMaxMs || env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS || "",
186
+ label: options.label || DEFAULT_LABEL,
187
+ linuxServiceName: options.label || DEFAULT_LINUX_SERVICE_NAME,
188
+ windowsTaskName: options.taskName || options.label || DEFAULT_WINDOWS_TASK_NAME,
189
+ nodePath: path.resolve(options.nodePath || nodePath),
190
+ cliPath: resolveCliPath(options.cliPath || cliPath),
191
+ configDir,
192
+ logDir,
193
+ stdoutPath: path.join(logDir, "agent-anywhere-gateway.log"),
194
+ stderrPath: path.join(logDir, "agent-anywhere-gateway.err.log"),
195
+ noStart: Boolean(options.noStart),
196
+ dryRun: Boolean(options.dryRun)
197
+ };
198
+ }
199
+
200
+ function assertInstallSettings(settings) {
201
+ if (!settings.token || /^please-change-me$/i.test(settings.token) || /^replace-/i.test(settings.token)) {
202
+ throw new Error("service install requires a real --token or AGENT_ANYWHERE_GATEWAY_TOKEN");
203
+ }
204
+ if (!settings.allowedRoots) {
205
+ throw new Error("service install requires --allowed-roots or AGENT_ANYWHERE_ALLOWED_ROOTS");
206
+ }
207
+ }
208
+
209
+ function gatewayEnv(settings) {
210
+ const env = {
211
+ AGENT_ANYWHERE_GATEWAY_ID: settings.id,
212
+ AGENT_ANYWHERE_MACHINE_NAME: settings.name,
213
+ AGENT_ANYWHERE_GATEWAY_TOKEN: settings.token,
214
+ AGENT_ANYWHERE_SERVER_WS_URL: settings.serverWsUrl,
215
+ AGENT_ANYWHERE_ALLOWED_ROOTS: settings.allowedRoots,
216
+ AGENT_ANYWHERE_DATA_DIR: settings.dataDir,
217
+ AGENT_PROVIDERS: settings.providers,
218
+ AGENT_PROVIDER: settings.primaryProvider
219
+ };
220
+ if (settings.claudeAutoTrust) env.AGENT_ANYWHERE_CLAUDE_AUTO_TRUST = settings.claudeAutoTrust;
221
+ if (settings.replayDir) env.AGENT_ANYWHERE_GATEWAY_REPLAY_DIR = settings.replayDir;
222
+ if (settings.reconnectMinMs) env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MIN_MS = settings.reconnectMinMs;
223
+ if (settings.reconnectMaxMs) env.AGENT_ANYWHERE_GATEWAY_RECONNECT_MAX_MS = settings.reconnectMaxMs;
224
+ return env;
225
+ }
226
+
227
+ function xmlEscape(value) {
228
+ return String(value)
229
+ .replace(/&/g, "&amp;")
230
+ .replace(/</g, "&lt;")
231
+ .replace(/>/g, "&gt;")
232
+ .replace(/"/g, "&quot;")
233
+ .replace(/'/g, "&apos;");
234
+ }
235
+
236
+ function plistForSettings(settings) {
237
+ const env = gatewayEnv(settings);
238
+ const envXml = Object.keys(env).sort().map((key) => (
239
+ ` <key>${xmlEscape(key)}</key>\n <string>${xmlEscape(env[key])}</string>`
240
+ )).join("\n");
241
+ return `<?xml version="1.0" encoding="UTF-8"?>
242
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
243
+ <plist version="1.0">
244
+ <dict>
245
+ <key>Label</key>
246
+ <string>${xmlEscape(settings.label)}</string>
247
+ <key>ProgramArguments</key>
248
+ <array>
249
+ <string>${xmlEscape(settings.nodePath)}</string>
250
+ <string>${xmlEscape(settings.cliPath)}</string>
251
+ </array>
252
+ <key>WorkingDirectory</key>
253
+ <string>${xmlEscape(settings.homeDir || os.homedir())}</string>
254
+ <key>EnvironmentVariables</key>
255
+ <dict>
256
+ ${envXml}
257
+ </dict>
258
+ <key>RunAtLoad</key>
259
+ <true/>
260
+ <key>KeepAlive</key>
261
+ <true/>
262
+ <key>ThrottleInterval</key>
263
+ <integer>10</integer>
264
+ <key>StandardOutPath</key>
265
+ <string>${xmlEscape(settings.stdoutPath)}</string>
266
+ <key>StandardErrorPath</key>
267
+ <string>${xmlEscape(settings.stderrPath)}</string>
268
+ </dict>
269
+ </plist>
270
+ `;
271
+ }
272
+
273
+ function shellSingleQuote(value) {
274
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
275
+ }
276
+
277
+ function systemdQuote(value) {
278
+ return `"${String(value).replace(/\\/g, "\\\\").replace(/"/g, "\\\"")}"`;
279
+ }
280
+
281
+ function envFileContent(settings) {
282
+ return Object.keys(gatewayEnv(settings)).sort()
283
+ .map((key) => `${key}=${shellSingleQuote(gatewayEnv(settings)[key])}`)
284
+ .join("\n") + "\n";
285
+ }
286
+
287
+ function systemdUnitForSettings(settings) {
288
+ const envFile = path.join(settings.configDir, "gateway.env");
289
+ return `[Unit]
290
+ Description=Agent Anywhere Gateway
291
+ After=network-online.target
292
+ Wants=network-online.target
293
+
294
+ [Service]
295
+ Type=simple
296
+ EnvironmentFile=${systemdQuote(envFile)}
297
+ ExecStart=${systemdQuote(settings.nodePath)} ${systemdQuote(settings.cliPath)}
298
+ Restart=always
299
+ RestartSec=5
300
+ WorkingDirectory=${systemdQuote(settings.homeDir || os.homedir())}
301
+
302
+ [Install]
303
+ WantedBy=default.target
304
+ `;
305
+ }
306
+
307
+ function windowsCmdEscape(value) {
308
+ return String(value).replace(/%/g, "%%");
309
+ }
310
+
311
+ function windowsRunnerForSettings(settings) {
312
+ const lines = ["@echo off"];
313
+ for (const key of Object.keys(gatewayEnv(settings)).sort()) {
314
+ lines.push(`set "${key}=${windowsCmdEscape(gatewayEnv(settings)[key])}"`);
315
+ }
316
+ lines.push(`if not exist "${windowsCmdEscape(settings.logDir)}" mkdir "${windowsCmdEscape(settings.logDir)}"`);
317
+ lines.push(`"${windowsCmdEscape(settings.nodePath)}" "${windowsCmdEscape(settings.cliPath)}" >> "${windowsCmdEscape(settings.stdoutPath)}" 2>> "${windowsCmdEscape(settings.stderrPath)}"`);
318
+ return `${lines.join("\r\n")}\r\n`;
319
+ }
320
+
321
+ function chmodPrivate(filePath) {
322
+ if (process.platform !== "win32") {
323
+ fs.chmodSync(filePath, 0o600);
324
+ }
325
+ }
326
+
327
+ function runCommand(command, args, { dryRun = false, executor = spawnSync, allowFailure = false } = {}) {
328
+ if (dryRun) {
329
+ console.log([command, ...args].join(" "));
330
+ return { status: 0 };
331
+ }
332
+ const result = executor(command, args, { stdio: "inherit" });
333
+ if (result.error && !allowFailure) {
334
+ throw result.error;
335
+ }
336
+ if (result.status !== 0 && !allowFailure) {
337
+ throw new Error(`${command} ${args.join(" ")} failed with status ${result.status}`);
338
+ }
339
+ return result;
340
+ }
341
+
342
+ function writeFile(filePath, content, { dryRun = false } = {}) {
343
+ if (dryRun) {
344
+ console.log(`Would write ${filePath}`);
345
+ return;
346
+ }
347
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
348
+ fs.writeFileSync(filePath, content, "utf8");
349
+ }
350
+
351
+ function macPaths(settings, homeDir = os.homedir()) {
352
+ return {
353
+ plistPath: path.join(homeDir, "Library", "LaunchAgents", `${settings.label}.plist`)
354
+ };
355
+ }
356
+
357
+ function linuxPaths(settings, homeDir = os.homedir()) {
358
+ return {
359
+ envPath: path.join(settings.configDir, "gateway.env"),
360
+ servicePath: path.join(homeDir, ".config", "systemd", "user", `${settings.linuxServiceName}.service`)
361
+ };
362
+ }
363
+
364
+ function windowsPaths(settings) {
365
+ return {
366
+ runnerPath: path.join(settings.configDir, "run-gateway.cmd")
367
+ };
368
+ }
369
+
370
+ function installMacService(settings, options = {}) {
371
+ const { plistPath } = macPaths(settings, options.homeDir || settings.homeDir || os.homedir());
372
+ writeFile(plistPath, plistForSettings(settings), settings);
373
+ if (!settings.dryRun) {
374
+ fs.mkdirSync(settings.logDir, { recursive: true });
375
+ fs.mkdirSync(settings.dataDir, { recursive: true });
376
+ chmodPrivate(plistPath);
377
+ }
378
+ const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : ""}`;
379
+ runCommand("launchctl", ["bootout", domain, plistPath], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync, allowFailure: true });
380
+ runCommand("launchctl", ["bootstrap", domain, plistPath], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
381
+ runCommand("launchctl", ["enable", `${domain}/${settings.label}`], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
382
+ if (!settings.noStart) {
383
+ runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { ...options, dryRun: settings.dryRun, executor: options.executor || spawnSync });
384
+ }
385
+ return { path: plistPath };
386
+ }
387
+
388
+ function installLinuxService(settings, options = {}) {
389
+ const { envPath, servicePath } = linuxPaths(settings, options.homeDir || settings.homeDir || os.homedir());
390
+ writeFile(envPath, envFileContent(settings), settings);
391
+ writeFile(servicePath, systemdUnitForSettings(settings), settings);
392
+ if (!settings.dryRun) {
393
+ fs.mkdirSync(settings.dataDir, { recursive: true });
394
+ fs.mkdirSync(settings.logDir, { recursive: true });
395
+ chmodPrivate(envPath);
396
+ }
397
+ const executor = options.executor || spawnSync;
398
+ runCommand("systemctl", ["--user", "daemon-reload"], { dryRun: settings.dryRun, executor });
399
+ runCommand("systemctl", ["--user", "enable", `${settings.linuxServiceName}.service`], { dryRun: settings.dryRun, executor });
400
+ if (!settings.noStart) {
401
+ runCommand("systemctl", ["--user", "restart", `${settings.linuxServiceName}.service`], { dryRun: settings.dryRun, executor });
402
+ }
403
+ return { path: servicePath, envPath };
404
+ }
405
+
406
+ function installWindowsService(settings, options = {}) {
407
+ const { runnerPath } = windowsPaths(settings);
408
+ writeFile(runnerPath, windowsRunnerForSettings(settings), settings);
409
+ const executor = options.executor || spawnSync;
410
+ runCommand("schtasks", ["/Create", "/TN", settings.windowsTaskName, "/TR", `"${runnerPath}"`, "/SC", "ONLOGON", "/F"], {
411
+ dryRun: settings.dryRun,
412
+ executor
413
+ });
414
+ if (!settings.noStart) {
415
+ runCommand("schtasks", ["/Run", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
416
+ }
417
+ return { path: runnerPath };
418
+ }
419
+
420
+ function installService(settings, options = {}) {
421
+ assertInstallSettings(settings);
422
+ if (settings.platform === "darwin") return installMacService(settings, options);
423
+ if (settings.platform === "win32") return installWindowsService(settings, options);
424
+ return installLinuxService(settings, options);
425
+ }
426
+
427
+ function controlService(command, settings, options = {}) {
428
+ const executor = options.executor || spawnSync;
429
+ if (settings.platform === "darwin") {
430
+ const { plistPath } = macPaths(settings, options.homeDir || settings.homeDir || os.homedir());
431
+ const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : ""}`;
432
+ if (command === "status") return runCommand("launchctl", ["print", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
433
+ if (command === "start") {
434
+ runCommand("launchctl", ["bootstrap", domain, plistPath], { dryRun: settings.dryRun, executor, allowFailure: true });
435
+ return runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
436
+ }
437
+ if (command === "restart") return runCommand("launchctl", ["kickstart", "-k", `${domain}/${settings.label}`], { dryRun: settings.dryRun, executor });
438
+ if (command === "stop") return runCommand("launchctl", ["bootout", domain, plistPath], { dryRun: settings.dryRun, executor });
439
+ if (command === "uninstall") {
440
+ runCommand("launchctl", ["bootout", domain, plistPath], { dryRun: settings.dryRun, executor, allowFailure: true });
441
+ if (!settings.dryRun && fs.existsSync(plistPath)) fs.unlinkSync(plistPath);
442
+ return { status: 0 };
443
+ }
444
+ if (command === "logs") {
445
+ const args = settings.follow ?
446
+ ["-f", settings.stdoutPath, settings.stderrPath] :
447
+ ["-n", "100", settings.stdoutPath, settings.stderrPath];
448
+ return runCommand("tail", args, { dryRun: settings.dryRun, executor });
449
+ }
450
+ }
451
+ if (settings.platform === "win32") {
452
+ if (command === "status") return runCommand("schtasks", ["/Query", "/TN", settings.windowsTaskName, "/V", "/FO", "LIST"], { dryRun: settings.dryRun, executor });
453
+ if (command === "start" || command === "restart") return runCommand("schtasks", ["/Run", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
454
+ if (command === "stop") return runCommand("schtasks", ["/End", "/TN", settings.windowsTaskName], { dryRun: settings.dryRun, executor });
455
+ if (command === "uninstall") return runCommand("schtasks", ["/Delete", "/TN", settings.windowsTaskName, "/F"], { dryRun: settings.dryRun, executor, allowFailure: true });
456
+ if (command === "logs") {
457
+ if (settings.follow) {
458
+ return runCommand("powershell.exe", [
459
+ "-NoProfile",
460
+ "-Command",
461
+ `Get-Content -LiteralPath '${settings.stdoutPath.replace(/'/g, "''")}', '${settings.stderrPath.replace(/'/g, "''")}' -Tail 100 -Wait`
462
+ ], { dryRun: settings.dryRun, executor });
463
+ }
464
+ return runCommand("cmd.exe", ["/d", "/c", "type", settings.stdoutPath, settings.stderrPath], { dryRun: settings.dryRun, executor });
465
+ }
466
+ }
467
+ const serviceName = `${settings.linuxServiceName}.service`;
468
+ if (command === "status") return runCommand("systemctl", ["--user", "status", serviceName, "--no-pager"], { dryRun: settings.dryRun, executor });
469
+ if (command === "start") return runCommand("systemctl", ["--user", "start", serviceName], { dryRun: settings.dryRun, executor });
470
+ if (command === "restart") return runCommand("systemctl", ["--user", "restart", serviceName], { dryRun: settings.dryRun, executor });
471
+ if (command === "stop") return runCommand("systemctl", ["--user", "stop", serviceName], { dryRun: settings.dryRun, executor });
472
+ if (command === "uninstall") {
473
+ runCommand("systemctl", ["--user", "disable", "--now", serviceName], { dryRun: settings.dryRun, executor, allowFailure: true });
474
+ const { envPath, servicePath } = linuxPaths(settings, options.homeDir || settings.homeDir || os.homedir());
475
+ if (!settings.dryRun) {
476
+ if (fs.existsSync(servicePath)) fs.unlinkSync(servicePath);
477
+ if (fs.existsSync(envPath)) fs.unlinkSync(envPath);
478
+ }
479
+ runCommand("systemctl", ["--user", "daemon-reload"], { dryRun: settings.dryRun, executor });
480
+ return { status: 0 };
481
+ }
482
+ if (command === "logs") {
483
+ const args = ["--user", "-u", serviceName, "-n", "100", "--no-pager"];
484
+ if (settings.follow) args.push("-f");
485
+ return runCommand("journalctl", args, { dryRun: settings.dryRun, executor });
486
+ }
487
+ throw new Error(`Unknown service command: ${command}`);
488
+ }
489
+
490
+ function redacted(value) {
491
+ if (!value) return "";
492
+ const text = String(value);
493
+ return `${text.slice(0, 4)}...${text.slice(-4)}`;
494
+ }
495
+
496
+ function printServicePlan(settings, log = console.log) {
497
+ log(`Service platform: ${settings.platform}`);
498
+ log(`Gateway id: ${settings.id}`);
499
+ log(`Machine name: ${settings.name}`);
500
+ log(`Server WS URL: ${settings.serverWsUrl}`);
501
+ log(`Allowed roots: ${settings.allowedRoots}`);
502
+ log(`Providers: ${settings.providers}`);
503
+ log(`Gateway token: ${settings.token ? redacted(settings.token) : "(missing)"}`);
504
+ log(`Data dir: ${settings.dataDir}`);
505
+ log(`Log dir: ${settings.logDir}`);
506
+ }
507
+
508
+ function serviceHelpText() {
509
+ return `Usage:
510
+ agent-anywhere-gateway service install --server <url> --id <id> --token <token> --allowed-roots <paths> [--provider <provider>]
511
+ agent-anywhere-gateway service status
512
+ agent-anywhere-gateway service start
513
+ agent-anywhere-gateway service stop
514
+ agent-anywhere-gateway service restart
515
+ agent-anywhere-gateway service logs [--follow]
516
+ agent-anywhere-gateway service uninstall
517
+
518
+ Install options:
519
+ --server <url> Control Server or Worker URL, http(s) or ws(s).
520
+ --server-ws-url <url> Explicit gateway WebSocket URL.
521
+ --id <id> Stable gateway machine id.
522
+ --name <name> Human-readable machine name.
523
+ --token <token> Shared gateway token.
524
+ --allowed-roots <paths> Comma-separated allowed project roots.
525
+ --provider <provider> Local provider. Default: codex.
526
+ --providers <providers> Comma-separated local providers, e.g. codex,claude-code.
527
+ --data-dir <path> Gateway data directory.
528
+ --claude-auto-trust Trust allowed project paths for Claude Remote Control.
529
+ --label <name> Service label/name. macOS default: ${DEFAULT_LABEL}; Linux default: ${DEFAULT_LINUX_SERVICE_NAME}.
530
+ --task-name <name> Windows scheduled task name. Default: ${DEFAULT_WINDOWS_TASK_NAME}.
531
+ --no-start Install and enable without starting immediately.
532
+ --dry-run Print service actions without writing or running commands.
533
+ `;
534
+ }
535
+
536
+ function runServiceCli(argv = process.argv.slice(2), options = {}) {
537
+ const parsed = parseServiceArgs(argv);
538
+ if (parsed.help || parsed.command === "help") {
539
+ console.log(serviceHelpText());
540
+ return Promise.resolve();
541
+ }
542
+ const settings = resolveServiceSettings(parsed, options);
543
+ if (parsed.command === "install") {
544
+ printServicePlan(settings);
545
+ installService(settings, options);
546
+ return Promise.resolve();
547
+ }
548
+ controlService(parsed.command, settings, options);
549
+ return Promise.resolve();
550
+ }
551
+
552
+ module.exports = {
553
+ DEFAULT_LABEL,
554
+ DEFAULT_LINUX_SERVICE_NAME,
555
+ DEFAULT_WINDOWS_TASK_NAME,
556
+ controlService,
557
+ envFileContent,
558
+ gatewayEnv,
559
+ installService,
560
+ parseServiceArgs,
561
+ plistForSettings,
562
+ resolveServiceSettings,
563
+ runServiceCli,
564
+ serviceHelpText,
565
+ systemdUnitForSettings,
566
+ toGatewayWsUrl,
567
+ windowsRunnerForSettings
568
+ };