@kortix/agent-tunnel 0.12.7 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agent-cli.js CHANGED
@@ -2968,6 +2968,14 @@ if (typeof globalThis.WebSocket === "undefined") {
2968
2968
  }
2969
2969
  }
2970
2970
 
2971
+ // src/agent/cli.ts
2972
+ import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2973
+ import { hostname as hostname4 } from "os";
2974
+ import { join as join10 } from "path";
2975
+
2976
+ // src/agent/agent.ts
2977
+ import { hostname, platform, arch, release } from "os";
2978
+
2971
2979
  // src/agent/config.ts
2972
2980
  import { chmodSync, existsSync, lstatSync, readFileSync } from "fs";
2973
2981
  import { join } from "path";
@@ -3402,6 +3410,14 @@ function absoluteWsPath(value) {
3402
3410
  }
3403
3411
  return value;
3404
3412
  }
3413
+ function buildTunnelWsUrl(config) {
3414
+ const base = trustedHttpUrl(config.apiUrl).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
3415
+ const wsPath = absoluteWsPath(config.wsPath || "/ws");
3416
+ const params = new URLSearchParams({
3417
+ tunnelId: trustedCredential(config.tunnelId, "tunnelId")
3418
+ });
3419
+ return `${base}${wsPath}?${params.toString()}`;
3420
+ }
3405
3421
  function loadConfig(overrides = {}) {
3406
3422
  let fileConfig = {};
3407
3423
  if (existsSync(CONFIG_FILE)) {
@@ -3442,8 +3458,78 @@ function loadConfig(overrides = {}) {
3442
3458
  return merged;
3443
3459
  }
3444
3460
 
3445
- // src/agent/agent.ts
3446
- import { hostname, platform, arch, release } from "os";
3461
+ // src/agent/version.ts
3462
+ import { readFileSync as readFileSync2 } from "fs";
3463
+ import { dirname, join as join2 } from "path";
3464
+ import { fileURLToPath } from "url";
3465
+ var PACKAGE_NAME = "@kortix/agent-tunnel";
3466
+ var MAX_LOOKUP_DEPTH = 8;
3467
+ var cached = null;
3468
+ function moduleDirectory() {
3469
+ try {
3470
+ return dirname(fileURLToPath(import.meta.url));
3471
+ } catch {
3472
+ return process.cwd();
3473
+ }
3474
+ }
3475
+ function agentTunnelVersion() {
3476
+ if (cached !== null)
3477
+ return cached;
3478
+ let directory = moduleDirectory();
3479
+ for (let depth = 0;depth < MAX_LOOKUP_DEPTH; depth++) {
3480
+ try {
3481
+ const manifest = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
3482
+ if (manifest?.name === PACKAGE_NAME && typeof manifest.version === "string") {
3483
+ const version = manifest.version;
3484
+ cached = version;
3485
+ return version;
3486
+ }
3487
+ } catch {}
3488
+ const parent = dirname(directory);
3489
+ if (parent === directory)
3490
+ break;
3491
+ directory = parent;
3492
+ }
3493
+ cached = "unknown";
3494
+ return cached;
3495
+ }
3496
+
3497
+ // src/agent/terminal.ts
3498
+ var c = {
3499
+ reset: "\x1B[0m",
3500
+ bold: "\x1B[1m",
3501
+ dim: "\x1B[2m",
3502
+ cyan: "\x1B[36m",
3503
+ green: "\x1B[32m",
3504
+ yellow: "\x1B[33m",
3505
+ red: "\x1B[31m",
3506
+ white: "\x1B[97m",
3507
+ gray: "\x1B[90m"
3508
+ };
3509
+ var ANSI = /\x1b\[[0-9;]*m/g;
3510
+ function stripAnsi(value) {
3511
+ return value.replace(ANSI, "");
3512
+ }
3513
+ function visibleLength(value) {
3514
+ return stripAnsi(value).length;
3515
+ }
3516
+ function clearScreen() {
3517
+ process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
3518
+ }
3519
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
3520
+ var glyph = {
3521
+ on: `${c.green}●${c.reset}`,
3522
+ off: `${c.gray}○${c.reset}`,
3523
+ warn: `${c.yellow}!${c.reset}`,
3524
+ bad: `${c.red}✗${c.reset}`,
3525
+ mark: `${c.cyan}◆${c.reset}`
3526
+ };
3527
+ function field(label, value, width = 14) {
3528
+ console.log(` ${c.dim}${label.padEnd(width)}${c.reset}${value}`);
3529
+ }
3530
+ function blankLine() {
3531
+ console.log("");
3532
+ }
3447
3533
 
3448
3534
  // src/agent/security/permission-guard.ts
3449
3535
  class PermissionGuard {
@@ -3518,19 +3604,10 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
3518
3604
  }
3519
3605
 
3520
3606
  // src/agent/agent.ts
3521
- var AGENT_VERSION = "0.1.2";
3607
+ var AGENT_VERSION = agentTunnelVersion();
3608
+ var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
3609
+ var AGENT_REPLACED_CLOSE_CODE = 4004;
3522
3610
  var MAX_RPC_MESSAGE_SIZE = 5 * 1024 * 1024;
3523
- var c = {
3524
- reset: "\x1B[0m",
3525
- bold: "\x1B[1m",
3526
- dim: "\x1B[2m",
3527
- cyan: "\x1B[36m",
3528
- green: "\x1B[32m",
3529
- yellow: "\x1B[33m",
3530
- red: "\x1B[31m",
3531
- white: "\x1B[97m",
3532
- gray: "\x1B[90m"
3533
- };
3534
3611
  function log(icon, msg) {
3535
3612
  const safeIcon = icon.replace(/[\r\n]/g, " ");
3536
3613
  const safeMsg = msg.replace(/[\r\n]/g, " ");
@@ -3539,6 +3616,7 @@ function log(icon, msg) {
3539
3616
  }
3540
3617
 
3541
3618
  class TunnelAgent {
3619
+ hooks;
3542
3620
  ws = null;
3543
3621
  registry;
3544
3622
  permissionGuard;
@@ -3554,10 +3632,11 @@ class TunnelAgent {
3554
3632
  signingKey = null;
3555
3633
  lastNonce = 0;
3556
3634
  responseNonce = 0;
3557
- constructor(config, registry) {
3635
+ constructor(config, registry, hooks = {}) {
3558
3636
  this.config = config;
3559
3637
  this.registry = registry;
3560
3638
  this.permissionGuard = new PermissionGuard;
3639
+ this.hooks = hooks;
3561
3640
  }
3562
3641
  connect() {
3563
3642
  if (this.ws) {
@@ -3627,16 +3706,21 @@ class TunnelAgent {
3627
3706
  }
3628
3707
  if (!this.isShuttingDown) {
3629
3708
  if (event.code === 4001) {
3630
- log(`${c.red}✗${c.reset}`, `Authentication failed — check your token`);
3709
+ this.isShuttingDown = true;
3710
+ log(`${c.red}✗${c.reset}`, `Credential rejected — run \`agent-tunnel connect --reauth\` to pair again`);
3711
+ this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
3631
3712
  return;
3632
3713
  }
3633
3714
  if (event.code === 4003) {
3634
- log(`${c.red}✗${c.reset}`, `Device credential was revoked — run connect again`);
3715
+ this.isShuttingDown = true;
3716
+ log(`${c.red}✗${c.reset}`, `Device credential was revoked — run \`agent-tunnel connect --reauth\` to pair again`);
3717
+ this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
3635
3718
  return;
3636
3719
  }
3637
- if (event.code === 4004) {
3720
+ if (event.code === AGENT_REPLACED_CLOSE_CODE) {
3638
3721
  this.isShuttingDown = true;
3639
3722
  log(`${c.yellow}○${c.reset}`, `Another Agent Tunnel process connected with these credentials — stopping this process`);
3723
+ this.hooks.onTerminalClose?.({ code: event.code, reason: "replaced" });
3640
3724
  return;
3641
3725
  }
3642
3726
  log(`${c.yellow}○${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
@@ -3664,7 +3748,12 @@ class TunnelAgent {
3664
3748
  }
3665
3749
  if (msg.type === "auth_ok" && msg.signingKey) {
3666
3750
  this.signingKey = msg.signingKey;
3667
- log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${this.registry.getCapabilityNames().join(", ")})${c.reset}`);
3751
+ const capabilityNames = this.registry.getCapabilityNames();
3752
+ if (capabilityNames.length === 0) {
3753
+ log(`${c.yellow}!${c.reset}`, `Connected, but no capabilities are enabled — this tunnel cannot do anything. Run \`agent-tunnel connect --reauth\` to pair again.`);
3754
+ } else {
3755
+ log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${capabilityNames.join(", ")})${c.reset}`);
3756
+ }
3668
3757
  if (this.stableConnectionTimer)
3669
3758
  clearTimeout(this.stableConnectionTimer);
3670
3759
  this.stableConnectionTimer = setTimeout(() => {
@@ -3829,13 +3918,66 @@ class TunnelAgent {
3829
3918
  }, delay);
3830
3919
  }
3831
3920
  buildWsUrl() {
3832
- const base = trustedHttpUrl(this.config.apiUrl).replace(/^http:/, "ws:").replace(/^https:/, "wss:");
3833
- const wsPath = this.config.wsPath || "/ws";
3834
- const params = new URLSearchParams({
3835
- tunnelId: trustedCredential(this.config.tunnelId, "tunnelId")
3836
- });
3837
- return `${base}${wsPath}?${params.toString()}`;
3921
+ return buildTunnelWsUrl(this.config);
3922
+ }
3923
+ }
3924
+
3925
+ // src/agent/banner.ts
3926
+ import { arch as arch2, hostname as hostname2, platform as platform2 } from "os";
3927
+ var BOX_WIDTH = 60;
3928
+ var BAR_WIDTH = 50;
3929
+ var BAR_FRAMES = 14;
3930
+ var WORDMARK = [
3931
+ `${c.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c.reset} ${c.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c.reset}`,
3932
+ `${c.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c.reset} ${c.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c.reset}`
3933
+ ];
3934
+ function truncate(value, max) {
3935
+ return value.length > max ? `${value.slice(0, max)}…` : value;
3936
+ }
3937
+ async function animateConnectingBar() {
3938
+ for (let frame = 0;frame <= BAR_FRAMES; frame++) {
3939
+ const filled = Math.round(frame / BAR_FRAMES * BAR_WIDTH);
3940
+ process.stdout.write(`\r ${c.cyan}◇${c.reset} ${c.cyan}${"═".repeat(filled)}${c.reset}${c.gray}${"─".repeat(BAR_WIDTH - filled)}${c.reset} `);
3941
+ await sleep(20);
3838
3942
  }
3943
+ process.stdout.write(`\r ${c.cyan}◇ ${"═".repeat(BAR_WIDTH)} ◆${c.reset}
3944
+ `);
3945
+ await sleep(120);
3946
+ }
3947
+ async function printStartupBanner({
3948
+ tunnelId,
3949
+ apiUrl,
3950
+ capabilities,
3951
+ version
3952
+ }) {
3953
+ console.log("");
3954
+ for (const line of WORDMARK)
3955
+ console.log(` ${line}`);
3956
+ console.log("");
3957
+ await animateConnectingBar();
3958
+ const row = (content) => {
3959
+ const pad = Math.max(0, BOX_WIDTH - visibleLength(content));
3960
+ console.log(` ${c.gray}│${c.reset}${content}${" ".repeat(pad)}${c.gray}│${c.reset}`);
3961
+ };
3962
+ const blank = () => row("");
3963
+ const titleLeft = ` ${c.cyan}◆${c.reset} ${c.bold}${c.white}Agent Tunnel${c.reset}`;
3964
+ const titleRight = `${c.dim}v${version}${c.reset} `;
3965
+ const titlePad = Math.max(1, BOX_WIDTH - visibleLength(titleLeft) - visibleLength(titleRight));
3966
+ const capabilityRow = capabilities.length > 0 ? capabilities.map((name) => `${c.green}●${c.reset} ${c.white}${name}${c.reset}`).join(" ") : `${c.yellow}none — this tunnel cannot act${c.reset}`;
3967
+ const brand = "created by kortix";
3968
+ console.log("");
3969
+ console.log(` ${c.gray}╭${"─".repeat(BOX_WIDTH)}╮${c.reset}`);
3970
+ blank();
3971
+ row(`${titleLeft}${" ".repeat(titlePad)}${titleRight}`);
3972
+ row(` ${c.dim}Bridge between AI agents & local machines${c.reset}`);
3973
+ blank();
3974
+ row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
3975
+ row(` ${c.dim}relay${c.reset} ${c.white}${truncate(apiUrl, 40)}${c.reset}`);
3976
+ row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname2(), 28)}${c.reset} ${c.dim}(${platform2()} ${arch2()})${c.reset}`);
3977
+ row(` ${c.dim}access${c.reset} ${capabilityRow}`);
3978
+ blank();
3979
+ console.log(` ${c.gray}╰${"─".repeat(BOX_WIDTH - brand.length - 3)} ${c.dim}created by ${c.cyan}kortix${c.reset} ${c.gray}─╯${c.reset}`);
3980
+ console.log("");
3839
3981
  }
3840
3982
 
3841
3983
  // src/agent/capabilities/index.ts
@@ -3866,8 +4008,8 @@ class CapabilityRegistry {
3866
4008
  // src/agent/capabilities/desktop/cua-driver.ts
3867
4009
  import { spawn } from "child_process";
3868
4010
  import { existsSync as existsSync2, realpathSync, statSync } from "fs";
3869
- import { homedir as homedir2, platform as platform2 } from "os";
3870
- import { join as join2 } from "path";
4011
+ import { homedir as homedir2, platform as platform3 } from "os";
4012
+ import { join as join3 } from "path";
3871
4013
  var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
3872
4014
  var DRIVER_ENV_KEYS = [
3873
4015
  "PATH",
@@ -3886,7 +4028,7 @@ var DRIVER_ENV_KEYS = [
3886
4028
  function candidateBins() {
3887
4029
  const candidates = [
3888
4030
  process.env.CUA_DRIVER_BIN,
3889
- join2(homedir2(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
4031
+ join3(homedir2(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
3890
4032
  "/usr/local/bin/cua-driver",
3891
4033
  "/opt/homebrew/bin/cua-driver"
3892
4034
  ];
@@ -3991,7 +4133,7 @@ function parseJsonOutput(stdout) {
3991
4133
  function isDaemonProxyFallback(message) {
3992
4134
  return message.includes("daemon proxy") && message.includes("Resource temporarily unavailable");
3993
4135
  }
3994
- async function sleep(ms) {
4136
+ async function sleep2(ms) {
3995
4137
  await new Promise((resolve) => setTimeout(resolve, ms));
3996
4138
  }
3997
4139
  function sanitizeArgs(args) {
@@ -4050,7 +4192,7 @@ class CuaDriver {
4050
4192
  const { stdout, stderr } = await execFile(bin, ["call", tool, payload], 60000);
4051
4193
  if (isDaemonProxyFallback(stderr)) {
4052
4194
  lastError = new Error(stderr.trim());
4053
- await sleep(150 * (attempt + 1));
4195
+ await sleep2(150 * (attempt + 1));
4054
4196
  continue;
4055
4197
  }
4056
4198
  return parseJsonOutput(stdout);
@@ -4060,14 +4202,14 @@ class CuaDriver {
4060
4202
  throw err;
4061
4203
  }
4062
4204
  lastError = err;
4063
- await sleep(150 * (attempt + 1));
4205
+ await sleep2(150 * (attempt + 1));
4064
4206
  }
4065
4207
  }
4066
4208
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
4067
4209
  }
4068
4210
  async startDaemon() {
4069
4211
  const bin = await this.ensureInstalled();
4070
- if (platform2() === "darwin") {
4212
+ if (platform3() === "darwin") {
4071
4213
  const child = spawn("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
4072
4214
  detached: true,
4073
4215
  stdio: "ignore",
@@ -4222,10 +4364,10 @@ function createDesktopCapability() {
4222
4364
 
4223
4365
  // src/agent/capabilities/filesystem.ts
4224
4366
  import { open, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
4225
- import { join as join4, dirname as dirname2 } from "path";
4367
+ import { join as join5, dirname as dirname3 } from "path";
4226
4368
 
4227
4369
  // src/agent/security/path-validator.ts
4228
- import { dirname, basename, join as join3, resolve, normalize, relative, isAbsolute } from "path";
4370
+ import { dirname as dirname2, basename, join as join4, resolve, normalize, relative, isAbsolute } from "path";
4229
4371
  import { realpathSync as realpathSync2 } from "fs";
4230
4372
  function resolveExistingRoot(path) {
4231
4373
  const normalized = normalize(resolve(path));
@@ -4244,10 +4386,10 @@ function resolvePathForValidation(path) {
4244
4386
  if (code !== "ENOENT") {
4245
4387
  throw new Error(`Access denied: cannot resolve path "${path}" (${code})`);
4246
4388
  }
4247
- const parent = dirname(normalized);
4389
+ const parent = dirname2(normalized);
4248
4390
  if (parent === normalized)
4249
4391
  return normalized;
4250
- return join3(resolvePathForValidation(parent), basename(normalized));
4392
+ return join4(resolvePathForValidation(parent), basename(normalized));
4251
4393
  }
4252
4394
  }
4253
4395
  function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blockedPaths = []) {
@@ -4281,8 +4423,8 @@ function validatePath(path, allowedPaths, blockedPaths = []) {
4281
4423
  }
4282
4424
  function validateWritePath(path, allowedPaths, blockedPaths = []) {
4283
4425
  const resolved = validatePath(path, allowedPaths, blockedPaths);
4284
- let parent = dirname(normalize(resolve(path)));
4285
- while (parent && parent !== dirname(parent)) {
4426
+ let parent = dirname2(normalize(resolve(path)));
4427
+ while (parent && parent !== dirname2(parent)) {
4286
4428
  try {
4287
4429
  const resolvedParent = realpathSync2(parent);
4288
4430
  assertAllowedResolvedPath(path, resolvedParent, allowedPaths, blockedPaths);
@@ -4292,7 +4434,7 @@ function validateWritePath(path, allowedPaths, blockedPaths = []) {
4292
4434
  if (code !== "ENOENT") {
4293
4435
  throw new Error(`Access denied: cannot resolve parent for "${path}" (${code})`);
4294
4436
  }
4295
- parent = dirname(parent);
4437
+ parent = dirname2(parent);
4296
4438
  }
4297
4439
  }
4298
4440
  throw new Error(`Access denied: cannot resolve parent for "${path}"`);
@@ -4387,7 +4529,7 @@ function createFilesystemCapability(config) {
4387
4529
  if (contentBytes > maxFileSize) {
4388
4530
  throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
4389
4531
  }
4390
- await mkdir(dirname2(path), { recursive: true });
4532
+ await mkdir(dirname3(path), { recursive: true });
4391
4533
  validateFilesystemPath(path, config, params, true);
4392
4534
  await writeFile(path, content, { encoding });
4393
4535
  validateFilesystemPath(path, config, params);
@@ -4405,7 +4547,7 @@ function createFilesystemCapability(config) {
4405
4547
  const entries = await readdir(path, { withFileTypes: true });
4406
4548
  const result = entries.map((entry) => ({
4407
4549
  name: entry.name,
4408
- path: join4(path, entry.name),
4550
+ path: join5(path, entry.name),
4409
4551
  isDirectory: entry.isDirectory(),
4410
4552
  isFile: entry.isFile(),
4411
4553
  isSymlink: entry.isSymbolicLink()
@@ -4418,7 +4560,7 @@ function createFilesystemCapability(config) {
4418
4560
  for (const sub of subEntries) {
4419
4561
  result.push({
4420
4562
  name: sub.name,
4421
- path: join4(dir.path, sub.name),
4563
+ path: join5(dir.path, sub.name),
4422
4564
  isDirectory: sub.isDirectory(),
4423
4565
  isFile: sub.isFile(),
4424
4566
  isSymlink: sub.isSymbolicLink()
@@ -4597,771 +4739,868 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
4597
4739
  return registry;
4598
4740
  }
4599
4741
 
4600
- // src/agent/service.ts
4601
- import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
4602
- import { homedir as homedir3, platform as platform3, userInfo } from "os";
4603
- import { dirname as dirname3, join as join5 } from "path";
4604
- import { spawnSync } from "child_process";
4605
- var SERVICE_LABEL = "ai.kortix.agent-tunnel";
4606
- var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
4607
- function getServicePaths() {
4608
- const home = homedir3();
4609
- const configDir = join5(home, ".agent-tunnel");
4610
- return {
4611
- configDir,
4612
- logDir: join5(configDir, "logs"),
4613
- launchdPlist: join5(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
4614
- systemdUnit: join5(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
4615
- windowsScript: join5(configDir, "agent-tunnel-service.ps1")
4616
- };
4617
- }
4618
- function shellQuote(value) {
4619
- return `'${value.replace(/'/g, `'\\''`)}'`;
4742
+ // src/agent/credential-store.ts
4743
+ import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
4744
+ import { homedir as homedir3 } from "os";
4745
+ import { join as join6 } from "path";
4746
+ var CONFIG_DIR2 = join6(homedir3(), ".agent-tunnel");
4747
+ var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
4748
+ var PAIRING_FIELDS = ["token", "tunnelId", "enabledCapabilities"];
4749
+ function readConfigFile() {
4750
+ try {
4751
+ const parsed = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
4752
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4753
+ } catch {
4754
+ return {};
4755
+ }
4620
4756
  }
4621
- function powershellQuote(value) {
4622
- return `'${value.replace(/'/g, "''")}'`;
4757
+ function writeConfigFileAtomic(next) {
4758
+ mkdirSync(CONFIG_DIR2, { recursive: true, mode: 448 });
4759
+ try {
4760
+ chmodSync2(CONFIG_DIR2, 448);
4761
+ } catch {}
4762
+ const tmpFile = join6(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
4763
+ writeFileSync(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
4764
+ try {
4765
+ chmodSync2(tmpFile, 384);
4766
+ } catch {}
4767
+ renameSync(tmpFile, CONFIG_FILE2);
4768
+ try {
4769
+ chmodSync2(CONFIG_FILE2, 384);
4770
+ } catch {}
4623
4771
  }
4624
- function xmlEscape(value) {
4625
- return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
4772
+ function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
4773
+ writeConfigFileAtomic({
4774
+ ...readConfigFile(),
4775
+ tunnelId,
4776
+ token,
4777
+ apiUrl,
4778
+ ...enabledCapabilities !== undefined ? { enabledCapabilities } : {}
4779
+ });
4626
4780
  }
4627
- function currentRunnerParts() {
4628
- const exec = process.execPath;
4629
- const script = process.argv[1];
4630
- if (script && existsSync3(script)) {
4631
- return { command: exec, args: [script, "run", "--service"] };
4781
+ function clearSavedCredentials() {
4782
+ if (!existsSync3(CONFIG_FILE2))
4783
+ return false;
4784
+ let existing;
4785
+ try {
4786
+ existing = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
4787
+ } catch {
4788
+ rmSync(CONFIG_FILE2, { force: true });
4789
+ return true;
4632
4790
  }
4633
- throw new Error("Cannot install the background service because the current Agent Tunnel executable was not found");
4791
+ const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
4792
+ for (const key of PAIRING_FIELDS)
4793
+ delete existing[key];
4794
+ writeConfigFileAtomic(existing);
4795
+ return hadCredentials;
4634
4796
  }
4635
- function currentRunnerCommand() {
4636
- const runner = currentRunnerParts();
4637
- return [runner.command, ...runner.args].map(shellQuote).join(" ");
4797
+
4798
+ // src/agent/credential-probe.ts
4799
+ var DEFAULT_PROBE_TIMEOUT_MS = 1e4;
4800
+ function probeCredentials(config, options = {}) {
4801
+ return new Promise((resolve2) => {
4802
+ let socket;
4803
+ try {
4804
+ socket = new WebSocket(new URL(buildTunnelWsUrl(config)));
4805
+ } catch {
4806
+ resolve2("unreachable");
4807
+ return;
4808
+ }
4809
+ const settle = (result) => {
4810
+ clearTimeout(timer);
4811
+ try {
4812
+ socket.close(1000, "probe complete");
4813
+ } catch {}
4814
+ resolve2(result);
4815
+ };
4816
+ const timer = setTimeout(() => settle("unreachable"), options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
4817
+ socket.addEventListener("open", () => {
4818
+ try {
4819
+ socket.send(JSON.stringify({
4820
+ type: "auth",
4821
+ token: trustedCredential(config.token, "token"),
4822
+ capabilities: options.capabilities ?? [],
4823
+ agentVersion: AGENT_VERSION
4824
+ }));
4825
+ } catch {
4826
+ settle("unreachable");
4827
+ }
4828
+ });
4829
+ socket.addEventListener("message", (event) => {
4830
+ try {
4831
+ const message = JSON.parse(String(event.data));
4832
+ if (isJsonRecord(message) && message.type === "auth_ok")
4833
+ settle("valid");
4834
+ } catch {}
4835
+ });
4836
+ socket.addEventListener("close", (event) => {
4837
+ const { code } = event;
4838
+ if (AUTH_REJECTED_CLOSE_CODES.includes(code))
4839
+ return settle("rejected");
4840
+ if (code === AGENT_REPLACED_CLOSE_CODE)
4841
+ return settle("valid");
4842
+ settle("unreachable");
4843
+ });
4844
+ socket.addEventListener("error", () => settle("unreachable"));
4845
+ });
4638
4846
  }
4639
- function buildServiceShellCommand() {
4640
- return `exec ${currentRunnerCommand()}`;
4847
+ function isJsonRecord(value) {
4848
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4641
4849
  }
4642
- function renderWindowsPowerShellScript(runner = currentRunnerParts()) {
4643
- const command = powershellQuote(runner.command);
4644
- const args = runner.args.map(powershellQuote).join(" ");
4645
- return `$ErrorActionPreference = 'Continue'
4646
- while ($true) {
4647
- & ${command}${args ? ` ${args}` : ""}
4648
- Start-Sleep -Seconds 5
4850
+
4851
+ // src/agent/device-auth.ts
4852
+ import { spawn as spawn3 } from "child_process";
4853
+ import { hostname as hostname3, platform as platform4 } from "os";
4854
+ var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4855
+ var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
4856
+ var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
4857
+ var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
4858
+ var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
4859
+
4860
+ class InvalidDeviceAuthResponseError extends Error {
4861
+ constructor(message) {
4862
+ super(message);
4863
+ this.name = "InvalidDeviceAuthResponseError";
4864
+ }
4649
4865
  }
4650
- `;
4866
+ function invalid(what) {
4867
+ throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
4651
4868
  }
4652
- function windowsTaskCommand(paths) {
4653
- return `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${paths.windowsScript}"`;
4869
+ function isJsonRecord2(value) {
4870
+ return value !== null && typeof value === "object" && !Array.isArray(value);
4654
4871
  }
4655
- function renderLaunchdPlist(command, paths = getServicePaths()) {
4656
- const stdout = join5(paths.logDir, "agent-tunnel.out.log");
4657
- const stderr = join5(paths.logDir, "agent-tunnel.err.log");
4658
- return `<?xml version="1.0" encoding="UTF-8"?>
4659
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
4660
- <plist version="1.0">
4661
- <dict>
4662
- <key>Label</key>
4663
- <string>${xmlEscape(SERVICE_LABEL)}</string>
4664
- <key>ProgramArguments</key>
4665
- <array>
4666
- <string>/bin/sh</string>
4667
- <string>-lc</string>
4668
- <string>${xmlEscape(command)}</string>
4669
- </array>
4670
- <key>RunAtLoad</key>
4671
- <true/>
4672
- <key>KeepAlive</key>
4673
- <true/>
4674
- <key>Umask</key>
4675
- <integer>63</integer>
4676
- <key>StandardOutPath</key>
4677
- <string>${xmlEscape(stdout)}</string>
4678
- <key>StandardErrorPath</key>
4679
- <string>${xmlEscape(stderr)}</string>
4680
- <key>WorkingDirectory</key>
4681
- <string>${xmlEscape(homedir3())}</string>
4682
- <key>EnvironmentVariables</key>
4683
- <dict>
4684
- <key>PATH</key>
4685
- <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
4686
- </dict>
4687
- </dict>
4688
- </plist>
4689
- `;
4872
+ function normalizeBrowserUrl(value) {
4873
+ try {
4874
+ const url = new URL(value);
4875
+ return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
4876
+ } catch {
4877
+ return null;
4878
+ }
4690
4879
  }
4691
- function renderSystemdUnit(command, paths = getServicePaths()) {
4692
- const stdout = join5(paths.logDir, "agent-tunnel.out.log");
4693
- const stderr = join5(paths.logDir, "agent-tunnel.err.log");
4694
- return `[Unit]
4695
- Description=Kortix Agent Tunnel
4696
- After=network-online.target
4697
- Wants=network-online.target
4698
-
4699
- [Service]
4700
- Type=simple
4701
- UMask=0077
4702
- ExecStart=/bin/sh -lc ${shellQuote(command)}
4703
- Restart=always
4704
- RestartSec=5
4705
- WorkingDirectory=${homedir3()}
4706
- Environment=PATH=/usr/local/bin:/usr/bin:/bin
4707
- StandardOutput=append:${stdout}
4708
- StandardError=append:${stderr}
4709
-
4710
- [Install]
4711
- WantedBy=default.target
4712
- `;
4880
+ function isLoopback(url) {
4881
+ return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
4713
4882
  }
4714
- function run(command, args) {
4715
- const result = spawnSync(command, args, { encoding: "utf8" });
4716
- const detail = [result.stdout, result.stderr].filter(Boolean).join(`
4717
- `).trim();
4718
- return { ok: result.status === 0, detail };
4883
+ function assertSafeVerificationUrl(value) {
4884
+ if (value.length > 2048)
4885
+ invalid("verification URL");
4886
+ const browserUrl = normalizeBrowserUrl(value);
4887
+ if (!browserUrl)
4888
+ invalid("verification URL");
4889
+ const url = new URL(browserUrl);
4890
+ if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
4891
+ throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
4892
+ }
4893
+ return browserUrl;
4719
4894
  }
4720
- function launchdTarget() {
4721
- const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
4722
- return `gui/${uid}`;
4895
+ function parseDeviceAuthChallenge(value) {
4896
+ if (!isJsonRecord2(value))
4897
+ invalid("challenge");
4898
+ const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
4899
+ if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
4900
+ invalid("device code");
4901
+ if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
4902
+ invalid("device secret");
4903
+ }
4904
+ if (typeof verificationUrl !== "string")
4905
+ invalid("verification URL");
4906
+ if (typeof expiresAt !== "string")
4907
+ invalid("expiration");
4908
+ const expiresAtMs = Date.parse(expiresAt);
4909
+ const now = Date.now();
4910
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
4911
+ invalid("expiration");
4912
+ }
4913
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
4914
+ invalid("poll interval");
4915
+ }
4916
+ return {
4917
+ deviceCode,
4918
+ deviceSecret,
4919
+ verificationUrl: assertSafeVerificationUrl(verificationUrl),
4920
+ expiresAt,
4921
+ pollIntervalMs
4922
+ };
4723
4923
  }
4724
- function installService() {
4725
- const paths = getServicePaths();
4726
- mkdirSync(paths.configDir, { recursive: true, mode: 448 });
4727
- mkdirSync(paths.logDir, { recursive: true, mode: 448 });
4728
- const command = buildServiceShellCommand();
4729
- if (platform3() === "darwin") {
4730
- mkdirSync(dirname3(paths.launchdPlist), { recursive: true });
4731
- writeFileSync(paths.launchdPlist, renderLaunchdPlist(command, paths), { mode: 384 });
4732
- run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
4733
- const boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
4734
- const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
4735
- return {
4736
- platform: platform3(),
4737
- installed: true,
4738
- active: boot.ok || kick.ok ? true : null,
4739
- path: paths.launchdPlist,
4740
- detail: [boot.detail, kick.detail].filter(Boolean).join(`
4741
- `)
4742
- };
4924
+ function parseApprovedCredentials(value) {
4925
+ const { tunnelId, token } = value;
4926
+ if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
4927
+ invalid("tunnel ID");
4928
+ if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
4929
+ invalid("setup token");
4930
+ return { tunnelId, token };
4931
+ }
4932
+ function parseApprovedCapabilities(value) {
4933
+ if (!Array.isArray(value))
4934
+ return [];
4935
+ return [...new Set(value)].filter((item) => isTunnelCapability(item));
4936
+ }
4937
+ function parseDeviceAuthStatus(value) {
4938
+ if (!isJsonRecord2(value) || typeof value.status !== "string")
4939
+ invalid("status response");
4940
+ switch (value.status) {
4941
+ case "approved": {
4942
+ if (!value.tunnelId || !value.token)
4943
+ return { status: "approved-without-token" };
4944
+ return {
4945
+ status: "approved",
4946
+ ...parseApprovedCredentials(value),
4947
+ capabilities: parseApprovedCapabilities(value.capabilities)
4948
+ };
4949
+ }
4950
+ case "denied":
4951
+ return { status: "denied" };
4952
+ case "expired":
4953
+ return { status: "expired" };
4954
+ default:
4955
+ return null;
4743
4956
  }
4744
- if (platform3() === "linux") {
4745
- mkdirSync(dirname3(paths.systemdUnit), { recursive: true });
4746
- writeFileSync(paths.systemdUnit, renderSystemdUnit(command, paths), { mode: 384 });
4747
- const reload = run("systemctl", ["--user", "daemon-reload"]);
4748
- const enable = run("systemctl", ["--user", "enable", "--now", `${SERVICE_LABEL}.service`]);
4749
- return {
4750
- platform: platform3(),
4751
- installed: true,
4752
- active: enable.ok ? true : null,
4753
- path: paths.systemdUnit,
4754
- detail: [reload.detail, enable.detail].filter(Boolean).join(`
4755
- `)
4756
- };
4957
+ }
4958
+ async function requestDeviceAuthorization(apiUrl) {
4959
+ const response = await fetch(`${apiUrl}/device-auth`, {
4960
+ method: "POST",
4961
+ headers: { "Content-Type": "application/json" },
4962
+ body: JSON.stringify({ machineHostname: hostname3() })
4963
+ });
4964
+ if (!response.ok) {
4965
+ const body = await response.text().catch(() => "");
4966
+ throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
4757
4967
  }
4758
- if (platform3() === "win32") {
4759
- writeFileSync(paths.windowsScript, renderWindowsPowerShellScript(), { mode: 384 });
4760
- const create = run("schtasks.exe", [
4761
- "/Create",
4762
- "/TN",
4763
- SERVICE_LABEL,
4764
- "/TR",
4765
- windowsTaskCommand(paths),
4766
- "/SC",
4767
- "ONLOGON",
4768
- "/F",
4769
- "/RL",
4770
- "LIMITED"
4771
- ]);
4772
- const start = run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]);
4773
- return {
4774
- platform: platform3(),
4775
- installed: create.ok,
4776
- active: start.ok ? true : null,
4777
- path: paths.windowsScript,
4778
- detail: [create.detail, start.detail].filter(Boolean).join(`
4779
- `)
4780
- };
4968
+ return parseDeviceAuthChallenge(await response.json());
4969
+ }
4970
+ async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
4971
+ const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
4972
+ const expiresAtMs = Date.parse(challenge.expiresAt);
4973
+ for (;; ) {
4974
+ const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
4975
+ if (secondsRemaining <= 0)
4976
+ return { status: "expired" };
4977
+ options.onWaiting?.(secondsRemaining);
4978
+ let payload;
4979
+ try {
4980
+ const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
4981
+ headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
4982
+ });
4983
+ if (!response.ok) {
4984
+ await wait(challenge.pollIntervalMs);
4985
+ continue;
4986
+ }
4987
+ payload = await response.json();
4988
+ } catch {
4989
+ await wait(challenge.pollIntervalMs);
4990
+ continue;
4991
+ }
4992
+ const outcome = parseDeviceAuthStatus(payload);
4993
+ if (outcome)
4994
+ return outcome;
4995
+ await wait(challenge.pollIntervalMs);
4781
4996
  }
4782
- throw new Error("Background service install is currently supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.");
4783
4997
  }
4784
- function uninstallService() {
4785
- const paths = getServicePaths();
4786
- if (platform3() === "darwin") {
4787
- const existed = existsSync3(paths.launchdPlist);
4998
+ function openBrowser(url) {
4999
+ if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
5000
+ return;
5001
+ const safeUrl = normalizeBrowserUrl(url);
5002
+ if (!safeUrl)
5003
+ return;
5004
+ const opener = {
5005
+ darwin: ["open", [safeUrl]],
5006
+ win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
5007
+ };
5008
+ const [command, args] = opener[platform4()] ?? ["xdg-open", [safeUrl]];
5009
+ try {
5010
+ spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
5011
+ } catch {}
5012
+ }
5013
+
5014
+ // src/agent/log-format.ts
5015
+ function collapseRepeatedLines(lines) {
5016
+ const collapsed = [];
5017
+ let index = 0;
5018
+ while (index < lines.length) {
5019
+ let repeats = 1;
5020
+ while (index + repeats < lines.length && lines[index + repeats] === lines[index])
5021
+ repeats++;
5022
+ collapsed.push(repeats > 1 ? `${lines[index]} (x${repeats})` : lines[index]);
5023
+ index += repeats;
5024
+ }
5025
+ return collapsed;
5026
+ }
5027
+ function isShellStartupNoise(line) {
5028
+ return /\/\.(profile|bash_profile|zprofile|zshrc|bashrc)\b.*:.*(No such file or directory|command not found)/.test(line);
5029
+ }
5030
+
5031
+ // src/agent/prompts.ts
5032
+ import { createInterface } from "readline/promises";
5033
+ function isInteractiveTerminal() {
5034
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
5035
+ }
5036
+ function isTruthyFlag(value) {
5037
+ return value === "true" || value === "1" || value === "yes";
5038
+ }
5039
+ function anyFlag(flags, names) {
5040
+ return names.some((name) => isTruthyFlag(flags[name]));
5041
+ }
5042
+ async function promptYesNo(question, defaultValue) {
5043
+ const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
5044
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
5045
+ try {
5046
+ for (;; ) {
5047
+ const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
5048
+ if (!answer)
5049
+ return defaultValue;
5050
+ if (["y", "yes"].includes(answer))
5051
+ return true;
5052
+ if (["n", "no"].includes(answer))
5053
+ return false;
5054
+ console.log(` ${c.yellow}!${c.reset} Please answer yes or no.`);
5055
+ }
5056
+ } catch (error) {
5057
+ if (error instanceof Error && error.name === "AbortError") {
5058
+ process.stdout.write(`
5059
+ `);
5060
+ process.exit(130);
5061
+ }
5062
+ throw error;
5063
+ } finally {
5064
+ rl.close();
5065
+ }
5066
+ }
5067
+
5068
+ // src/agent/service.ts
5069
+ import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5070
+ import { platform as platform6 } from "os";
5071
+ import { join as join9 } from "path";
5072
+
5073
+ // src/agent/service-drivers.ts
5074
+ import { spawnSync } from "child_process";
5075
+ import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5076
+ import { homedir as homedir5, platform as platform5, userInfo } from "os";
5077
+ import { dirname as dirname4, join as join8 } from "path";
5078
+
5079
+ // src/agent/service-quoting.ts
5080
+ function shellQuote(value) {
5081
+ return `'${value.replace(/'/g, `'\\''`)}'`;
5082
+ }
5083
+ function powershellQuote(value) {
5084
+ return `'${value.replace(/'/g, "''")}'`;
5085
+ }
5086
+ function xmlEscape(value) {
5087
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5088
+ }
5089
+
5090
+ // src/agent/service-paths.ts
5091
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
5092
+ import { homedir as homedir4 } from "os";
5093
+ import { join as join7 } from "path";
5094
+ var SERVICE_LABEL = "ai.kortix.agent-tunnel";
5095
+ var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
5096
+ var TERMINAL_SERVICE_EXIT_CODE = 0;
5097
+ var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
5098
+ var RETAINED_LOG_LINES = 500;
5099
+ function getServicePaths() {
5100
+ const home = homedir4();
5101
+ const configDir = join7(home, ".agent-tunnel");
5102
+ const binDir = join7(configDir, "bin");
5103
+ return {
5104
+ configDir,
5105
+ logDir: join7(configDir, "logs"),
5106
+ binDir,
5107
+ vendoredRunner: join7(binDir, "agent-cli.js"),
5108
+ launchdPlist: join7(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
5109
+ systemdUnit: join7(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
5110
+ windowsScript: join7(configDir, "agent-tunnel-service.ps1")
5111
+ };
5112
+ }
5113
+ function serviceLogFiles(paths = getServicePaths()) {
5114
+ return [
5115
+ join7(paths.logDir, "agent-tunnel.out.log"),
5116
+ join7(paths.logDir, "agent-tunnel.err.log")
5117
+ ];
5118
+ }
5119
+ function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
5120
+ const rotated = [];
5121
+ for (const file of serviceLogFiles(paths)) {
5122
+ try {
5123
+ const contents = readFileSync4(file, "utf8");
5124
+ if (Buffer.byteLength(contents, "utf8") <= maxBytes)
5125
+ continue;
5126
+ const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
5127
+ `);
5128
+ writeFileSync2(file, `[agent-tunnel] earlier entries trimmed
5129
+ ${kept}`, { mode: 384 });
5130
+ rotated.push(file);
5131
+ } catch {}
5132
+ }
5133
+ return rotated;
5134
+ }
5135
+
5136
+ // src/agent/service-drivers.ts
5137
+ function posixShellCommand(runner) {
5138
+ const interpreter = `"$(command -v ${shellQuote(runner.command)} 2>/dev/null || command -v node)"`;
5139
+ return `exec ${interpreter} ${runner.args.map(shellQuote).join(" ")}`;
5140
+ }
5141
+ function run(command, args) {
5142
+ const result = spawnSync(command, args, { encoding: "utf8" });
5143
+ return {
5144
+ ok: result.status === 0,
5145
+ detail: [result.stdout, result.stderr].filter(Boolean).join(`
5146
+ `).trim()
5147
+ };
5148
+ }
5149
+ var notInstalled = (what) => ({ ok: false, detail: `${what} is not installed.` });
5150
+ function joinDetails(...results) {
5151
+ return results.map((result) => result.detail).filter(Boolean).join(`
5152
+ `);
5153
+ }
5154
+ function launchdTarget() {
5155
+ const uid = typeof process.getuid === "function" ? process.getuid() : userInfo().uid;
5156
+ return `gui/${uid}`;
5157
+ }
5158
+ function logPaths(paths) {
5159
+ return {
5160
+ stdout: join8(paths.logDir, "agent-tunnel.out.log"),
5161
+ stderr: join8(paths.logDir, "agent-tunnel.err.log")
5162
+ };
5163
+ }
5164
+ function renderLaunchdPlist(command, paths = getServicePaths()) {
5165
+ const { stdout, stderr } = logPaths(paths);
5166
+ return `<?xml version="1.0" encoding="UTF-8"?>
5167
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
5168
+ <plist version="1.0">
5169
+ <dict>
5170
+ <key>Label</key>
5171
+ <string>${xmlEscape(SERVICE_LABEL)}</string>
5172
+ <key>ProgramArguments</key>
5173
+ <array>
5174
+ <string>/bin/sh</string>
5175
+ <string>-lc</string>
5176
+ <string>${xmlEscape(command)}</string>
5177
+ </array>
5178
+ <key>RunAtLoad</key>
5179
+ <true/>
5180
+ <key>KeepAlive</key>
5181
+ <dict>
5182
+ <key>SuccessfulExit</key>
5183
+ <false/>
5184
+ </dict>
5185
+ <key>Umask</key>
5186
+ <integer>63</integer>
5187
+ <key>StandardOutPath</key>
5188
+ <string>${xmlEscape(stdout)}</string>
5189
+ <key>StandardErrorPath</key>
5190
+ <string>${xmlEscape(stderr)}</string>
5191
+ <key>WorkingDirectory</key>
5192
+ <string>${xmlEscape(homedir5())}</string>
5193
+ <key>EnvironmentVariables</key>
5194
+ <dict>
5195
+ <key>PATH</key>
5196
+ <string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
5197
+ </dict>
5198
+ </dict>
5199
+ </plist>
5200
+ `;
5201
+ }
5202
+ function renderSystemdUnit(command, paths = getServicePaths()) {
5203
+ const { stdout, stderr } = logPaths(paths);
5204
+ return `[Unit]
5205
+ Description=Kortix Agent Tunnel
5206
+ After=network-online.target
5207
+ Wants=network-online.target
5208
+
5209
+ [Service]
5210
+ Type=simple
5211
+ UMask=0077
5212
+ ExecStart=/bin/sh -lc ${shellQuote(command)}
5213
+ Restart=on-failure
5214
+ RestartSec=5
5215
+ WorkingDirectory=${homedir5()}
5216
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
5217
+ StandardOutput=append:${stdout}
5218
+ StandardError=append:${stderr}
5219
+
5220
+ [Install]
5221
+ WantedBy=default.target
5222
+ `;
5223
+ }
5224
+ function renderWindowsPowerShellScript(runner) {
5225
+ const command = powershellQuote(runner.command);
5226
+ const args = runner.args.map(powershellQuote).join(" ");
5227
+ return `$ErrorActionPreference = 'Continue'
5228
+ while ($true) {
5229
+ & ${command}${args ? ` ${args}` : ""}
5230
+ # A clean exit means the agent stopped for a reason restarting cannot fix,
5231
+ # such as a missing or revoked credential. Anything else is a crash worth retrying.
5232
+ if ($LASTEXITCODE -eq ${TERMINAL_SERVICE_EXIT_CODE}) { break }
5233
+ Start-Sleep -Seconds 5
5234
+ }
5235
+ `;
5236
+ }
5237
+ var launchd = {
5238
+ unitPath: (paths) => paths.launchdPlist,
5239
+ install(paths, runner) {
5240
+ mkdirSync2(dirname4(paths.launchdPlist), { recursive: true });
5241
+ writeFileSync3(paths.launchdPlist, renderLaunchdPlist(posixShellCommand(runner), paths), { mode: 384 });
5242
+ run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5243
+ const boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
5244
+ const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
5245
+ return { installed: true, active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
5246
+ },
5247
+ uninstall(paths) {
5248
+ const existed = existsSync4(paths.launchdPlist);
4788
5249
  const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
4789
5250
  if (existed)
4790
- rmSync(paths.launchdPlist, { force: true });
5251
+ rmSync2(paths.launchdPlist, { force: true });
5252
+ return { detail: stop.detail };
5253
+ },
5254
+ start(paths, installed) {
5255
+ const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
5256
+ const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
5257
+ return { active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
5258
+ },
5259
+ stop(paths, installed) {
5260
+ const stop = installed ? run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
5261
+ return { detail: stop.detail };
5262
+ },
5263
+ status(paths, installed) {
5264
+ const status = run("launchctl", ["print", `${launchdTarget()}/${SERVICE_LABEL}`]);
4791
5265
  return {
4792
- platform: platform3(),
4793
- installed: false,
4794
- active: false,
4795
- path: paths.launchdPlist,
4796
- detail: stop.detail
5266
+ active: status.ok,
5267
+ detail: status.detail || (installed ? readFileSync5(paths.launchdPlist, "utf8") : undefined)
4797
5268
  };
4798
5269
  }
4799
- if (platform3() === "linux") {
4800
- const existed = existsSync3(paths.systemdUnit);
5270
+ };
5271
+ var systemd = {
5272
+ unitPath: (paths) => paths.systemdUnit,
5273
+ install(paths, runner) {
5274
+ mkdirSync2(dirname4(paths.systemdUnit), { recursive: true });
5275
+ writeFileSync3(paths.systemdUnit, renderSystemdUnit(posixShellCommand(runner), paths), { mode: 384 });
5276
+ const reload = run("systemctl", ["--user", "daemon-reload"]);
5277
+ const enable = run("systemctl", ["--user", "enable", "--now", `${SERVICE_LABEL}.service`]);
5278
+ return { installed: true, active: enable.ok ? true : null, detail: joinDetails(reload, enable) };
5279
+ },
5280
+ uninstall(paths) {
5281
+ const existed = existsSync4(paths.systemdUnit);
4801
5282
  const disable = run("systemctl", ["--user", "disable", "--now", `${SERVICE_LABEL}.service`]);
4802
5283
  if (existed)
4803
- rmSync(paths.systemdUnit, { force: true });
5284
+ rmSync2(paths.systemdUnit, { force: true });
4804
5285
  run("systemctl", ["--user", "daemon-reload"]);
4805
- return {
4806
- platform: platform3(),
4807
- installed: false,
4808
- active: false,
4809
- path: paths.systemdUnit,
4810
- detail: disable.detail
4811
- };
5286
+ return { detail: disable.detail };
5287
+ },
5288
+ start(_paths, installed) {
5289
+ const start = installed ? run("systemctl", ["--user", "start", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
5290
+ return { active: start.ok ? true : null, detail: start.detail };
5291
+ },
5292
+ stop(_paths, installed) {
5293
+ const stop = installed ? run("systemctl", ["--user", "stop", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
5294
+ return { detail: stop.detail };
5295
+ },
5296
+ status() {
5297
+ const status = run("systemctl", ["--user", "is-active", `${SERVICE_LABEL}.service`]);
5298
+ return { active: status.ok, detail: status.detail };
4812
5299
  }
4813
- if (platform3() === "win32") {
4814
- const existed = existsSync3(paths.windowsScript);
5300
+ };
5301
+ var scheduledTask = {
5302
+ unitPath: (paths) => paths.windowsScript,
5303
+ install(paths, runner) {
5304
+ writeFileSync3(paths.windowsScript, renderWindowsPowerShellScript(runner), { mode: 384 });
5305
+ const create = run("schtasks.exe", [
5306
+ "/Create",
5307
+ "/TN",
5308
+ SERVICE_LABEL,
5309
+ "/TR",
5310
+ `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${paths.windowsScript}"`,
5311
+ "/SC",
5312
+ "ONLOGON",
5313
+ "/F",
5314
+ "/RL",
5315
+ "LIMITED"
5316
+ ]);
5317
+ const start = run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]);
5318
+ return { installed: create.ok, active: start.ok ? true : null, detail: joinDetails(create, start) };
5319
+ },
5320
+ uninstall(paths) {
5321
+ const existed = existsSync4(paths.windowsScript);
4815
5322
  const stop = run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]);
4816
5323
  const del = run("schtasks.exe", ["/Delete", "/TN", SERVICE_LABEL, "/F"]);
4817
5324
  if (existed)
4818
- rmSync(paths.windowsScript, { force: true });
4819
- return {
4820
- platform: platform3(),
4821
- installed: false,
4822
- active: false,
4823
- path: paths.windowsScript,
4824
- detail: [stop.detail, del.detail].filter(Boolean).join(`
4825
- `)
4826
- };
5325
+ rmSync2(paths.windowsScript, { force: true });
5326
+ return { detail: joinDetails(stop, del) };
5327
+ },
5328
+ start(_paths, installed) {
5329
+ const start = installed ? run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
5330
+ return { active: start.ok ? true : null, detail: start.detail };
5331
+ },
5332
+ stop(_paths, installed) {
5333
+ const stop = installed ? run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
5334
+ return { detail: stop.detail };
5335
+ },
5336
+ status(paths, installed) {
5337
+ const status = run("schtasks.exe", ["/Query", "/TN", SERVICE_LABEL, "/FO", "LIST", "/V"]);
5338
+ const detail = status.detail || (installed ? readFileSync5(paths.windowsScript, "utf8") : undefined);
5339
+ return { active: status.ok ? /Status:\s*Running/i.test(detail ?? "") : false, detail };
4827
5340
  }
4828
- throw new Error("Background service uninstall is currently supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.");
5341
+ };
5342
+ var DRIVERS = {
5343
+ darwin: launchd,
5344
+ linux: systemd,
5345
+ win32: scheduledTask
5346
+ };
5347
+ var SUPPORTED_PLATFORMS_MESSAGE = "Background services are supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.";
5348
+ function serviceDriver() {
5349
+ return DRIVERS[platform5()];
4829
5350
  }
4830
- function startService() {
4831
- const paths = getServicePaths();
4832
- if (platform3() === "darwin") {
4833
- const installed = existsSync3(paths.launchdPlist);
4834
- const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : { ok: false, detail: "LaunchAgent is not installed." };
4835
- const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
4836
- return {
4837
- platform: platform3(),
4838
- installed,
4839
- active: boot.ok || kick.ok ? true : null,
4840
- path: paths.launchdPlist,
4841
- detail: [boot.detail, kick.detail].filter(Boolean).join(`
4842
- `)
4843
- };
4844
- }
4845
- if (platform3() === "linux") {
4846
- const installed = existsSync3(paths.systemdUnit);
4847
- const start = installed ? run("systemctl", ["--user", "start", `${SERVICE_LABEL}.service`]) : { ok: false, detail: "systemd unit is not installed." };
4848
- return {
4849
- platform: platform3(),
4850
- installed,
4851
- active: start.ok ? true : null,
4852
- path: paths.systemdUnit,
4853
- detail: start.detail
4854
- };
4855
- }
4856
- if (platform3() === "win32") {
4857
- const installed = existsSync3(paths.windowsScript);
4858
- const start = installed ? run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]) : { ok: false, detail: "Scheduled Task is not installed." };
4859
- return {
4860
- platform: platform3(),
4861
- installed,
4862
- active: start.ok ? true : null,
4863
- path: paths.windowsScript,
4864
- detail: start.detail
4865
- };
5351
+
5352
+ // src/agent/service.ts
5353
+ function isEphemeralRunnerPath(path) {
5354
+ const normalized = path.replace(/\\/g, "/");
5355
+ return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/");
5356
+ }
5357
+ function vendorRunner(scriptPath, paths = getServicePaths()) {
5358
+ if (!isEphemeralRunnerPath(scriptPath))
5359
+ return scriptPath;
5360
+ mkdirSync3(paths.binDir, { recursive: true, mode: 448 });
5361
+ const source = realpathSync3(scriptPath);
5362
+ copyFileSync(source, paths.vendoredRunner);
5363
+ try {
5364
+ chmodSync3(paths.vendoredRunner, 448);
5365
+ } catch {}
5366
+ writeFileSync4(join9(paths.binDir, "agent-cli.source.json"), JSON.stringify({ source, vendoredFrom: scriptPath }, null, 2), { mode: 384 });
5367
+ return paths.vendoredRunner;
5368
+ }
5369
+ function currentRunnerParts() {
5370
+ const script = process.argv[1];
5371
+ if (script && existsSync5(script)) {
5372
+ return { command: process.execPath, args: [vendorRunner(script), "run", "--service"] };
4866
5373
  }
4867
- throw new Error("Background service start is currently supported on macOS, Linux, and Windows.");
5374
+ throw new Error("Cannot install the background service because the current Agent Tunnel executable was not found");
4868
5375
  }
4869
- function stopService() {
5376
+ function withDriver(operate, fallback) {
5377
+ const driver = serviceDriver();
4870
5378
  const paths = getServicePaths();
4871
- if (platform3() === "darwin") {
4872
- const installed = existsSync3(paths.launchdPlist);
4873
- const stop = installed ? run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]) : { ok: false, detail: "LaunchAgent is not installed." };
4874
- return {
4875
- platform: platform3(),
4876
- installed,
4877
- active: false,
4878
- path: paths.launchdPlist,
4879
- detail: stop.detail
4880
- };
4881
- }
4882
- if (platform3() === "linux") {
4883
- const installed = existsSync3(paths.systemdUnit);
4884
- const stop = installed ? run("systemctl", ["--user", "stop", `${SERVICE_LABEL}.service`]) : { ok: false, detail: "systemd unit is not installed." };
4885
- return {
4886
- platform: platform3(),
4887
- installed,
4888
- active: false,
4889
- path: paths.systemdUnit,
4890
- detail: stop.detail
4891
- };
4892
- }
4893
- if (platform3() === "win32") {
4894
- const installed = existsSync3(paths.windowsScript);
4895
- const stop = installed ? run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]) : { ok: false, detail: "Scheduled Task is not installed." };
4896
- return {
4897
- platform: platform3(),
4898
- installed,
4899
- active: false,
4900
- path: paths.windowsScript,
4901
- detail: stop.detail
4902
- };
4903
- }
4904
- throw new Error("Background service stop is currently supported on macOS, Linux, and Windows.");
5379
+ if (!driver)
5380
+ throw new Error(SUPPORTED_PLATFORMS_MESSAGE);
5381
+ const path = driver.unitPath(paths);
5382
+ const installed = existsSync5(path);
5383
+ const outcome = operate(driver, paths, installed);
5384
+ return {
5385
+ platform: platform6(),
5386
+ installed: outcome.installed ?? installed,
5387
+ active: outcome.active ?? fallback.active,
5388
+ path,
5389
+ detail: outcome.detail
5390
+ };
5391
+ }
5392
+ function installService() {
5393
+ const paths = getServicePaths();
5394
+ mkdirSync3(paths.configDir, { recursive: true, mode: 448 });
5395
+ mkdirSync3(paths.logDir, { recursive: true, mode: 448 });
5396
+ rotateServiceLogs(paths);
5397
+ const runner = currentRunnerParts();
5398
+ return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
5399
+ }
5400
+ function uninstallService() {
5401
+ rmSync3(getServicePaths().binDir, { recursive: true, force: true });
5402
+ return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
5403
+ }
5404
+ function startService() {
5405
+ return withDriver((driver, paths, installed) => driver.start(paths, installed), { installed: false, active: null });
5406
+ }
5407
+ function stopService() {
5408
+ return withDriver((driver, paths, installed) => ({ ...driver.stop(paths, installed), active: false }), { installed: false, active: false });
4905
5409
  }
4906
5410
  function restartService() {
4907
5411
  stopService();
4908
5412
  return startService();
4909
5413
  }
4910
5414
  function getServiceStatus() {
4911
- const paths = getServicePaths();
4912
- if (platform3() === "darwin") {
4913
- const installed = existsSync3(paths.launchdPlist);
4914
- const status = run("launchctl", ["print", `${launchdTarget()}/${SERVICE_LABEL}`]);
4915
- return {
4916
- platform: platform3(),
4917
- installed,
4918
- active: status.ok,
4919
- path: paths.launchdPlist,
4920
- detail: status.detail || (installed ? readFileSync2(paths.launchdPlist, "utf8") : undefined)
4921
- };
4922
- }
4923
- if (platform3() === "linux") {
4924
- const installed = existsSync3(paths.systemdUnit);
4925
- const status = run("systemctl", ["--user", "is-active", `${SERVICE_LABEL}.service`]);
4926
- return {
4927
- platform: platform3(),
4928
- installed,
4929
- active: status.ok,
4930
- path: paths.systemdUnit,
4931
- detail: status.detail
4932
- };
4933
- }
4934
- if (platform3() === "win32") {
4935
- const installed = existsSync3(paths.windowsScript);
4936
- const status = run("schtasks.exe", ["/Query", "/TN", SERVICE_LABEL, "/FO", "LIST", "/V"]);
4937
- const detail = status.detail || (installed ? readFileSync2(paths.windowsScript, "utf8") : undefined);
5415
+ if (!serviceDriver()) {
4938
5416
  return {
4939
- platform: platform3(),
4940
- installed,
4941
- active: status.ok ? /Status:\s*Running/i.test(detail ?? "") : false,
4942
- path: paths.windowsScript,
4943
- detail
4944
- };
4945
- }
4946
- return {
4947
- platform: platform3(),
4948
- installed: false,
4949
- active: null,
4950
- detail: "Background service status is currently supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks."
4951
- };
4952
- }
4953
-
4954
- // src/agent/cli.ts
4955
- import { hostname as hostname2, platform as platform4, arch as arch2 } from "os";
4956
- import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, readFileSync as readFileSync3, renameSync } from "fs";
4957
- import { join as join6 } from "path";
4958
- import { homedir as homedir4 } from "os";
4959
- import { spawn as spawn3 } from "child_process";
4960
- import { createInterface } from "readline/promises";
4961
- var c2 = {
4962
- reset: "\x1B[0m",
4963
- bold: "\x1B[1m",
4964
- dim: "\x1B[2m",
4965
- italic: "\x1B[3m",
4966
- cyan: "\x1B[36m",
4967
- blue: "\x1B[34m",
4968
- green: "\x1B[32m",
4969
- yellow: "\x1B[33m",
4970
- red: "\x1B[31m",
4971
- magenta: "\x1B[35m",
4972
- white: "\x1B[97m",
4973
- gray: "\x1B[90m",
4974
- bgCyan: "\x1B[46m",
4975
- bgBlue: "\x1B[44m"
4976
- };
4977
- function parseArgs(argv) {
4978
- const command = argv[2] || "help";
4979
- const flags = {};
4980
- for (let i = 3;i < argv.length; i++) {
4981
- const arg = argv[i];
4982
- if (arg.startsWith("--")) {
4983
- const key = arg.slice(2);
4984
- const value = argv[i + 1] && !argv[i + 1].startsWith("--") ? argv[++i] : "true";
4985
- flags[key] = value;
4986
- }
5417
+ platform: platform6(),
5418
+ installed: false,
5419
+ active: null,
5420
+ detail: SUPPORTED_PLATFORMS_MESSAGE
5421
+ };
4987
5422
  }
4988
- return { command, flags };
4989
- }
4990
- function clearScreen() {
4991
- process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
5423
+ return withDriver((driver, paths, installed) => driver.status(paths, installed), { installed: false, active: null });
4992
5424
  }
4993
- var sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
4994
- var TUNNEL_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
4995
- var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
4996
5425
 
4997
- class InvalidDeviceAuthResponseError extends Error {
4998
- constructor(message) {
4999
- super(message);
5000
- this.name = "InvalidDeviceAuthResponseError";
5001
- }
5426
+ // src/agent/service-control.ts
5427
+ function acquireTunnelLease() {
5428
+ const serviceWasActive = getServiceStatus().active === true;
5429
+ if (serviceWasActive)
5430
+ stopService();
5431
+ let resumed = false;
5432
+ return {
5433
+ serviceWasActive,
5434
+ resumeService() {
5435
+ if (!serviceWasActive || resumed)
5436
+ return;
5437
+ resumed = true;
5438
+ startService();
5439
+ }
5440
+ };
5002
5441
  }
5003
- function isJsonRecord(value) {
5004
- return value !== null && typeof value === "object" && !Array.isArray(value);
5442
+ var SERVICE_ACTIONS = {
5443
+ start: { run: startService, label: "started" },
5444
+ stop: { run: stopService, label: "stopped" },
5445
+ restart: { run: restartService, label: "restarted" },
5446
+ uninstall: { run: uninstallService, label: "removed" },
5447
+ install: { run: installService, label: "installed" }
5448
+ };
5449
+ function describeService(status) {
5450
+ if (!status.installed)
5451
+ return `${glyph.off} not installed`;
5452
+ if (status.active)
5453
+ return `${glyph.on} running ${c.dim}· starts at login${c.reset}`;
5454
+ return `${c.yellow}○${c.reset} installed ${c.dim}· stopped${c.reset}`;
5005
5455
  }
5006
- function parseApprovedDeviceCredentials(value) {
5007
- const { tunnelId, token } = value;
5008
- if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId)) {
5009
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid tunnel ID");
5010
- }
5011
- if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token)) {
5012
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid setup token");
5013
- }
5014
- return { tunnelId, token };
5456
+ function renderServiceAction(action, status) {
5457
+ blankLine();
5458
+ console.log(` ${glyph.on} ${c.bold}Background service ${SERVICE_ACTIONS[action].label}${c.reset}`);
5459
+ if (status.path)
5460
+ field("", `${c.dim}${status.path}${c.reset}`);
5461
+ if (status.detail)
5462
+ field("", `${c.gray}${status.detail}${c.reset}`);
5463
+ blankLine();
5015
5464
  }
5016
- function parseDeviceAuthChallenge(value) {
5017
- if (!isJsonRecord(value)) {
5018
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid challenge");
5019
- }
5020
- const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
5021
- if (typeof deviceCode !== "string" || !/^[A-Z]{4}-[0-9]{4}$/.test(deviceCode)) {
5022
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid device code");
5023
- }
5024
- if (typeof deviceSecret !== "string" || !/^[A-Za-z0-9]{32}$/.test(deviceSecret)) {
5025
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid device secret");
5026
- }
5027
- if (typeof verificationUrl !== "string" || verificationUrl.length > 2048) {
5028
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid verification URL");
5029
- }
5030
- const browserUrl = normalizeBrowserUrl(verificationUrl);
5031
- if (!browserUrl) {
5032
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid verification URL");
5033
- }
5034
- const parsedVerificationUrl = new URL(browserUrl);
5035
- const loopback = parsedVerificationUrl.hostname === "localhost" || parsedVerificationUrl.hostname === "127.0.0.1" || parsedVerificationUrl.hostname === "[::1]" || parsedVerificationUrl.hostname === "::1";
5036
- if (parsedVerificationUrl.username || parsedVerificationUrl.password || parsedVerificationUrl.protocol !== "https:" && !loopback) {
5037
- throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
5038
- }
5039
- if (typeof expiresAt !== "string") {
5040
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid expiration");
5041
- }
5042
- const expiresAtMs = Date.parse(expiresAt);
5043
- const now = Date.now();
5044
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + 10 * 60000) {
5045
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid expiration");
5046
- }
5047
- if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
5048
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid poll interval");
5465
+
5466
+ // src/agent/cli.ts
5467
+ var ALL_CAPABILITIES = ["filesystem", "shell", "desktop"];
5468
+ var BACKGROUND_FLAGS = ["daemon", "service", "background", "always-online"];
5469
+ var FOREGROUND_FLAGS = ["foreground", "no-daemon", "no-service", "no-background"];
5470
+ var DEFAULT_LOG_LINES = 60;
5471
+ function parseArgs(argv) {
5472
+ const flags = {};
5473
+ for (let i = 3;i < argv.length; i++) {
5474
+ const arg = argv[i];
5475
+ if (!arg.startsWith("--"))
5476
+ continue;
5477
+ const next = argv[i + 1];
5478
+ flags[arg.slice(2)] = next && !next.startsWith("--") ? argv[++i] : "true";
5049
5479
  }
5050
- return {
5051
- deviceCode,
5052
- deviceSecret,
5053
- verificationUrl: browserUrl,
5054
- expiresAt,
5055
- pollIntervalMs
5056
- };
5480
+ return { command: argv[2] || "help", flags };
5057
5481
  }
5058
- async function printStartup(config, capabilities, version) {
5059
- const machine = hostname2();
5060
- const plat = `${platform4()} ${arch2()}`;
5061
- const truncate = (s, max) => s.length > max ? s.slice(0, max) + "…" : s;
5062
- const tunnelDisplay = truncate(config.tunnelId, 40);
5063
- const apiDisplay = truncate(config.apiUrl, 40);
5064
- const machineDisplay = truncate(machine, 28);
5065
- console.log("");
5066
- console.log(` ${c2.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c2.reset} ${c2.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c2.reset}`);
5067
- console.log(` ${c2.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c2.reset} ${c2.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c2.reset}`);
5068
- console.log("");
5069
- const barW = 50;
5070
- const frames = 14;
5071
- for (let i = 0;i <= frames; i++) {
5072
- const filled = Math.round(i / frames * barW);
5073
- const empty = barW - filled;
5074
- process.stdout.write(`\r ${c2.cyan}◇${c2.reset} ${c2.cyan}${"═".repeat(filled)}${c2.reset}${c2.gray}${"─".repeat(empty)}${c2.reset} `);
5075
- await sleep2(20);
5076
- }
5077
- process.stdout.write(`\r ${c2.cyan}◇ ${"═".repeat(barW)} ◆${c2.reset}
5078
- `);
5079
- await sleep2(120);
5080
- const W = 60;
5081
- const vLen = (s) => s.replace(/\x1b\[[0-9;]*m/g, "").length;
5082
- const row = (content) => {
5083
- const pad = Math.max(0, W - vLen(content));
5084
- console.log(` ${c2.gray}│${c2.reset}${content}${" ".repeat(pad)}${c2.gray}│${c2.reset}`);
5085
- };
5086
- const blank = () => console.log(` ${c2.gray}│${c2.reset}${" ".repeat(W)}${c2.gray}│${c2.reset}`);
5087
- const titleL = ` ${c2.cyan}◆${c2.reset} ${c2.bold}${c2.white}Agent Tunnel${c2.reset}`;
5088
- const titleR = `${c2.dim}v${version}${c2.reset} `;
5089
- const titleLLen = 18;
5090
- const titleRLen = 1 + version.length + 3;
5091
- const titlePad = Math.max(1, W - titleLLen - titleRLen);
5092
- const capStr = capabilities.map((name) => `${c2.green}●${c2.reset} ${c2.white}${name}${c2.reset}`).join(" ");
5093
- const brand = "created by kortix";
5094
- const brandFill = W - brand.length - 3;
5095
- console.log("");
5096
- console.log(` ${c2.gray}╭${"─".repeat(W)}╮${c2.reset}`);
5097
- blank();
5098
- row(`${titleL}${" ".repeat(titlePad)}${titleR}`);
5099
- row(` ${c2.dim}Bridge between AI agents & local machines${c2.reset}`);
5100
- blank();
5101
- row(` ${c2.dim}tunnel${c2.reset} ${c2.white}${tunnelDisplay}${c2.reset}`);
5102
- row(` ${c2.dim}relay${c2.reset} ${c2.white}${apiDisplay}${c2.reset}`);
5103
- row(` ${c2.dim}machine${c2.reset} ${c2.white}${machineDisplay}${c2.reset} ${c2.dim}(${plat})${c2.reset}`);
5104
- blank();
5105
- console.log(` ${c2.gray}╰${"─".repeat(brandFill)} ${c2.dim}created by ${c2.cyan}kortix${c2.reset} ${c2.gray}─╯${c2.reset}`);
5106
- console.log("");
5482
+ function fail(message) {
5483
+ console.error(` ${glyph.bad} ${message}`);
5484
+ process.exit(1);
5485
+ }
5486
+ function shortenHomePath(path) {
5487
+ const home = process.env.HOME ?? "";
5488
+ return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
5107
5489
  }
5108
5490
  function startAgent(config, options = {}) {
5109
5491
  const registry = createEnabledCapabilityRegistry(config);
5110
5492
  if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
5111
5493
  console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
5112
5494
  }
5113
- if (!options.service) {
5114
- clearScreen();
5115
- printStartup(config, registry.getCapabilityNames(), "0.1.2");
5116
- } else {
5495
+ if (options.service) {
5117
5496
  console.log(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}`);
5497
+ } else {
5498
+ clearScreen();
5499
+ printStartupBanner({
5500
+ tunnelId: config.tunnelId,
5501
+ apiUrl: config.apiUrl,
5502
+ capabilities: registry.getCapabilityNames(),
5503
+ version: agentTunnelVersion()
5504
+ });
5118
5505
  }
5119
- const agent = new TunnelAgent(config, registry);
5506
+ const agent = new TunnelAgent(config, registry, {
5507
+ onTerminalClose: ({ reason }) => {
5508
+ if (!options.service)
5509
+ return;
5510
+ console.log(`[agent-tunnel] stopping service: ${reason}`);
5511
+ process.exit(TERMINAL_SERVICE_EXIT_CODE);
5512
+ }
5513
+ });
5120
5514
  agent.connect();
5121
5515
  const shutdown = () => {
5122
5516
  if (!options.service)
5123
5517
  console.log(`
5124
- ${c2.dim} Shutting down…${c2.reset}`);
5518
+ ${c.dim} Shutting down…${c.reset}`);
5125
5519
  agent.disconnect();
5126
5520
  process.exit(0);
5127
5521
  };
5128
5522
  process.on("SIGTERM", shutdown);
5129
5523
  process.on("SIGINT", shutdown);
5130
5524
  }
5131
- function normalizeBrowserUrl(value) {
5132
- try {
5133
- const url = new URL(value);
5134
- return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
5135
- } catch {
5136
- return null;
5137
- }
5525
+ async function chooseBackgroundMode(flags) {
5526
+ if (anyFlag(flags, BACKGROUND_FLAGS))
5527
+ return true;
5528
+ if (anyFlag(flags, FOREGROUND_FLAGS))
5529
+ return false;
5530
+ if (!isInteractiveTerminal())
5531
+ return false;
5532
+ blankLine();
5533
+ console.log(` ${glyph.warn} ${c.bold}Security note${c.reset}`);
5534
+ console.log(` ${c.dim}Background mode starts at login, continues after this terminal closes, and restarts after failures.${c.reset}`);
5535
+ console.log(` ${c.dim}The computer must remain powered on, awake, and connected to the internet.${c.reset}`);
5536
+ blankLine();
5537
+ return promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
5138
5538
  }
5139
- function openBrowser(url) {
5140
- if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
5141
- return;
5142
- const safeUrl = normalizeBrowserUrl(url);
5143
- if (!safeUrl)
5539
+ async function launch(config, flags, lease) {
5540
+ if (await chooseBackgroundMode(flags)) {
5541
+ saveCredentials(config.tunnelId, config.token, config.apiUrl);
5542
+ renderServiceAction("install", SERVICE_ACTIONS.install.run());
5144
5543
  return;
5145
- try {
5146
- const plat = platform4();
5147
- let command;
5148
- let args;
5149
- if (plat === "darwin") {
5150
- command = "open";
5151
- args = [safeUrl];
5152
- } else if (plat === "win32") {
5153
- command = "rundll32.exe";
5154
- args = ["url.dll,FileProtocolHandler", safeUrl];
5155
- } else {
5156
- command = "xdg-open";
5157
- args = [safeUrl];
5158
- }
5159
- const child = spawn3(command, args, { detached: true, stdio: "ignore" });
5160
- child.unref();
5161
- } catch {}
5162
- }
5163
- var CONFIG_DIR2 = join6(homedir4(), ".agent-tunnel");
5164
- var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
5165
- function isSetupTunnelToken(token) {
5166
- return token.startsWith("kortix_tnl_") || token.startsWith("tnl_");
5167
- }
5168
- function isTruthyFlag(value) {
5169
- return value === "true" || value === "1" || value === "yes";
5170
- }
5171
- function isInteractiveTerminal() {
5172
- return process.stdin.isTTY === true && process.stdout.isTTY === true;
5173
- }
5174
- async function promptYesNo(question, defaultValue) {
5175
- const suffix = defaultValue ? " [Y/n] " : " [y/N] ";
5176
- const rl = createInterface({ input: process.stdin, output: process.stdout });
5177
- try {
5178
- for (;; ) {
5179
- const answer = (await rl.question(`${question}${suffix}`)).trim().toLowerCase();
5180
- if (!answer)
5181
- return defaultValue;
5182
- if (["y", "yes"].includes(answer))
5183
- return true;
5184
- if (["n", "no"].includes(answer))
5185
- return false;
5186
- console.log(` ${c2.yellow}!${c2.reset} Please answer yes or no.`);
5187
- }
5188
- } catch (error) {
5189
- if (error instanceof Error && error.name === "AbortError") {
5190
- process.stdout.write(`
5191
- `);
5192
- process.exit(130);
5193
- }
5194
- throw error;
5195
- } finally {
5196
- rl.close();
5197
- }
5198
- }
5199
- async function chooseConnectMode(flags) {
5200
- const explicitBackground = isTruthyFlag(flags.daemon) || isTruthyFlag(flags.service) || isTruthyFlag(flags.background) || isTruthyFlag(flags["always-online"]);
5201
- const explicitForeground = isTruthyFlag(flags.foreground) || isTruthyFlag(flags["no-daemon"]) || isTruthyFlag(flags["no-service"]) || isTruthyFlag(flags["no-background"]);
5202
- if (explicitBackground) {
5203
- return { background: true };
5204
5544
  }
5205
- if (explicitForeground) {
5206
- return { background: false };
5545
+ if (lease?.serviceWasActive) {
5546
+ console.log(` ${c.dim}Background service stays paused while this terminal holds the tunnel.${c.reset}`);
5547
+ console.log(` ${c.dim}Resume it with${c.reset} ${c.white}agent-tunnel start${c.reset}${c.dim}, or leave it — it starts again at login.${c.reset}`);
5207
5548
  }
5208
- if (!isInteractiveTerminal()) {
5209
- return { background: false };
5210
- }
5211
- console.log("");
5212
- console.log(` ${c2.yellow}!${c2.reset} ${c2.bold}Security note${c2.reset}`);
5213
- console.log(` ${c2.dim}Background mode starts at login, continues after this terminal closes, and restarts after failures.${c2.reset}`);
5214
- console.log(` ${c2.dim}The computer must remain powered on, awake, and connected to the internet.${c2.reset}`);
5215
- console.log("");
5216
- const background = await promptYesNo(" Install the background service now?", DEFAULT_INSTALL_BACKGROUND_SERVICE);
5217
- return { background };
5218
- }
5219
- function installBackgroundService() {
5220
- const status = installService();
5221
- console.log("");
5222
- console.log(` ${c2.green}●${c2.reset} ${c2.bold}Background service installed${c2.reset}`);
5223
- if (status.path)
5224
- console.log(` ${c2.dim}${status.path}${c2.reset}`);
5225
- console.log(` ${c2.dim}Starts at login and restarts after failures.${c2.reset}`);
5226
- if (status.detail)
5227
- console.log(` ${c2.gray}${status.detail}${c2.reset}`);
5228
- console.log("");
5549
+ startAgent(config);
5229
5550
  }
5230
- function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
5231
- mkdirSync2(CONFIG_DIR2, { recursive: true, mode: 448 });
5232
- try {
5233
- chmodSync2(CONFIG_DIR2, 448);
5234
- } catch {}
5235
- let existing = {};
5236
- if (existsSync4(CONFIG_FILE2)) {
5237
- try {
5238
- existing = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
5239
- } catch {}
5240
- }
5241
- const tmpFile = join6(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
5242
- const next = {
5243
- ...existing,
5244
- tunnelId,
5245
- token,
5246
- apiUrl,
5247
- ...enabledCapabilities !== undefined ? { enabledCapabilities } : {}
5248
- };
5249
- writeFileSync2(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
5250
- try {
5251
- chmodSync2(tmpFile, 384);
5252
- } catch {}
5253
- renameSync(tmpFile, CONFIG_FILE2);
5551
+ async function pairThisMachine(apiUrl, flags) {
5552
+ blankLine();
5553
+ console.log(` ${glyph.mark} ${c.bold}Device Authorization${c.reset}`);
5554
+ blankLine();
5555
+ let challenge;
5254
5556
  try {
5255
- chmodSync2(CONFIG_FILE2, 384);
5256
- } catch {}
5257
- }
5258
- async function commandConnectDeviceAuth(config, flags) {
5259
- console.log("");
5260
- console.log(` ${c2.cyan}◆${c2.reset} ${c2.bold}Device Authorization${c2.reset}`);
5261
- console.log("");
5262
- let deviceCode;
5263
- let deviceSecret;
5264
- let verificationUrl;
5265
- let expiresAt;
5266
- let pollIntervalMs;
5557
+ challenge = await requestDeviceAuthorization(apiUrl);
5558
+ } catch (error) {
5559
+ fail(error instanceof InvalidDeviceAuthResponseError ? error.message : "Failed to start device authorization");
5560
+ }
5561
+ console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${challenge.deviceCode}${c.reset}`);
5562
+ blankLine();
5563
+ console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
5564
+ console.log(` ${c.cyan}${challenge.verificationUrl}${c.reset}`);
5565
+ blankLine();
5566
+ openBrowser(challenge.verificationUrl);
5567
+ let outcome;
5267
5568
  try {
5268
- const res = await fetch(`${config.apiUrl}/device-auth`, {
5269
- method: "POST",
5270
- headers: { "Content-Type": "application/json" },
5271
- body: JSON.stringify({ machineHostname: hostname2() })
5569
+ outcome = await awaitDeviceAuthorization(apiUrl, challenge, {
5570
+ onWaiting: (secondsRemaining) => {
5571
+ const minutes = Math.floor(secondsRemaining / 60);
5572
+ const seconds = String(secondsRemaining % 60).padStart(2, "0");
5573
+ process.stdout.write(`\r ${c.dim}Waiting for approval... ${c.white}${minutes}:${seconds}${c.reset} `);
5574
+ }
5272
5575
  });
5273
- if (!res.ok) {
5274
- const text = await res.text().catch(() => "");
5275
- console.error(` ${c2.red}✗${c2.reset} Failed to create device auth request: ${res.status} ${text.slice(0, 200)}`);
5276
- process.exit(1);
5277
- }
5278
- const challenge = parseDeviceAuthChallenge(await res.json());
5279
- deviceCode = challenge.deviceCode;
5280
- deviceSecret = challenge.deviceSecret;
5281
- verificationUrl = challenge.verificationUrl;
5282
- expiresAt = challenge.expiresAt;
5283
- pollIntervalMs = challenge.pollIntervalMs;
5284
- } catch (err) {
5285
- const detail = err instanceof InvalidDeviceAuthResponseError ? `: ${err.message}` : "";
5286
- console.error(` ${c2.red}✗${c2.reset} Failed to start device authorization${detail}`);
5576
+ } catch (error) {
5577
+ process.stdout.write(`\r${" ".repeat(60)}\r`);
5578
+ fail(error instanceof Error ? error.message : "Device authorization failed");
5579
+ }
5580
+ process.stdout.write(`\r${" ".repeat(60)}\r`);
5581
+ if (outcome.status === "denied")
5582
+ fail("Authorization denied.");
5583
+ if (outcome.status === "expired")
5584
+ fail("Authorization expired. Please try again.");
5585
+ if (outcome.status === "approved-without-token") {
5586
+ fail("Authorization was approved, but the setup token was not available. Run connect again.");
5587
+ }
5588
+ if (outcome.capabilities.length === 0) {
5589
+ console.log(` ${glyph.bad} ${c.bold}No capabilities were approved${c.reset}`);
5590
+ blankLine();
5591
+ console.log(` ${c.dim}A tunnel with no capabilities connects but cannot act, and the${c.reset}`);
5592
+ console.log(` ${c.dim}approved set can only be changed by pairing again. Nothing was saved.${c.reset}`);
5593
+ blankLine();
5594
+ console.log(` ${c.dim}Run connect again and approve at least one of${c.reset} ${c.white}${ALL_CAPABILITIES.join(", ")}${c.reset}${c.dim}.${c.reset}`);
5595
+ blankLine();
5287
5596
  process.exit(1);
5288
- return;
5289
- }
5290
- console.log(` ${c2.dim}Code:${c2.reset} ${c2.bold}${c2.white}${deviceCode}${c2.reset}`);
5291
- console.log("");
5292
- console.log(` ${c2.dim}Open this URL on any device to approve:${c2.reset}`);
5293
- console.log(` ${c2.cyan}${verificationUrl}${c2.reset}`);
5294
- console.log("");
5295
- openBrowser(verificationUrl);
5296
- const expiresAtMs = new Date(expiresAt).getTime();
5297
- while (true) {
5298
- const remaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
5299
- if (remaining <= 0) {
5300
- console.log(`
5301
- ${c2.red}✗${c2.reset} Authorization expired. Please try again.`);
5302
- process.exit(1);
5303
- }
5304
- const min = Math.floor(remaining / 60);
5305
- const sec = remaining % 60;
5306
- process.stdout.write(`\r ${c2.dim}Waiting for approval... ${c2.white}${min}:${sec.toString().padStart(2, "0")}${c2.reset} `);
5307
- try {
5308
- const res = await fetch(`${config.apiUrl}/device-auth/${deviceCode}/status`, {
5309
- headers: { Authorization: `Bearer ${deviceSecret}` }
5310
- });
5311
- if (res.ok) {
5312
- const data = await res.json();
5313
- if (!isJsonRecord(data) || typeof data.status !== "string") {
5314
- throw new InvalidDeviceAuthResponseError("Authorization server returned an invalid status response");
5315
- }
5316
- if (data.status === "approved" && data.tunnelId && data.token) {
5317
- const credentials = parseApprovedDeviceCredentials(data);
5318
- process.stdout.write("\r" + " ".repeat(60) + "\r");
5319
- console.log(` ${c2.green}●${c2.reset} ${c2.bold}Authorized!${c2.reset}`);
5320
- console.log("");
5321
- const enabledCapabilities = Array.isArray(data.capabilities) ? [...new Set(data.capabilities)].filter((capability) => typeof capability === "string" && ["filesystem", "shell", "desktop"].includes(capability)) : [];
5322
- saveCredentials(credentials.tunnelId, credentials.token, config.apiUrl, enabledCapabilities);
5323
- console.log(` ${c2.dim}Credentials saved to ${CONFIG_FILE2}${c2.reset}`);
5324
- console.log(` ${c2.dim}Local capabilities: ${enabledCapabilities.join(", ") || "none"}${c2.reset}`);
5325
- console.log("");
5326
- const mode = await chooseConnectMode(flags);
5327
- if (mode.background) {
5328
- installBackgroundService();
5329
- return;
5330
- }
5331
- const fullConfig = loadConfig({
5332
- token: credentials.token,
5333
- tunnelId: credentials.tunnelId,
5334
- apiUrl: config.apiUrl
5335
- });
5336
- startAgent(fullConfig);
5337
- return;
5338
- }
5339
- if (data.status === "approved") {
5340
- process.stdout.write("\r" + " ".repeat(60) + "\r");
5341
- console.log(` ${c2.red}✗${c2.reset} Authorization was approved, but the setup token was not available.`);
5342
- console.log(` ${c2.dim}Run the connect command again to create a fresh device authorization code.${c2.reset}`);
5343
- process.exit(1);
5344
- }
5345
- if (data.status === "denied") {
5346
- process.stdout.write("\r" + " ".repeat(60) + "\r");
5347
- console.log(` ${c2.red}✗${c2.reset} Authorization denied.`);
5348
- process.exit(1);
5349
- }
5350
- if (data.status === "expired") {
5351
- process.stdout.write("\r" + " ".repeat(60) + "\r");
5352
- console.log(` ${c2.red}✗${c2.reset} Authorization expired. Please try again.`);
5353
- process.exit(1);
5354
- }
5355
- }
5356
- } catch (error) {
5357
- if (error instanceof InvalidDeviceAuthResponseError) {
5358
- process.stdout.write("\r" + " ".repeat(60) + "\r");
5359
- console.error(` ${c2.red}✗${c2.reset} ${error.message}`);
5360
- process.exit(1);
5361
- }
5362
- }
5363
- await sleep2(pollIntervalMs);
5364
5597
  }
5598
+ console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
5599
+ saveCredentials(outcome.tunnelId, outcome.token, apiUrl, outcome.capabilities);
5600
+ console.log(` ${c.dim}Saved to ${CONFIG_FILE2}${c.reset}`);
5601
+ console.log(` ${c.dim}Access: ${outcome.capabilities.join(", ")}${c.reset}`);
5602
+ blankLine();
5603
+ await launch(loadConfig({ apiUrl }), flags);
5365
5604
  }
5366
5605
  async function commandConnect(flags) {
5367
5606
  const config = loadConfig({
@@ -5369,195 +5608,260 @@ async function commandConnect(flags) {
5369
5608
  tunnelId: flags["tunnel-id"],
5370
5609
  apiUrl: flags["api-url"]
5371
5610
  });
5372
- if (config.token && config.tunnelId) {
5373
- const mode = await chooseConnectMode(flags);
5374
- if (mode.background) {
5375
- saveCredentials(config.tunnelId, config.token, config.apiUrl);
5376
- installBackgroundService();
5377
- return;
5378
- }
5379
- startAgent(config);
5611
+ const explicitCredentials = Boolean(flags.token && flags["tunnel-id"]);
5612
+ if (Boolean(config.token) !== Boolean(config.tunnelId)) {
5613
+ fail("Provide both --token and --tunnel-id, or neither (for device auth)");
5614
+ }
5615
+ if (!config.token) {
5616
+ await pairThisMachine(config.apiUrl, flags);
5380
5617
  return;
5381
5618
  }
5382
- if (!config.token && !config.tunnelId) {
5383
- await commandConnectDeviceAuth(config, flags);
5619
+ if (isTruthyFlag(flags.reauth) && !explicitCredentials) {
5620
+ clearSavedCredentials();
5621
+ await pairThisMachine(config.apiUrl, flags);
5384
5622
  return;
5385
5623
  }
5386
- console.error(`${c2.red}${c2.bold} error${c2.reset} Provide both --token and --tunnel-id, or neither (for device auth)`);
5387
- process.exit(1);
5624
+ const lease = acquireTunnelLease();
5625
+ blankLine();
5626
+ console.log(` ${glyph.mark} ${c.dim}Checking saved credentials…${c.reset}`);
5627
+ let probe;
5628
+ try {
5629
+ probe = await probeCredentials(config, {
5630
+ capabilities: createEnabledCapabilityRegistry(config).getCapabilityNames()
5631
+ });
5632
+ } catch (error) {
5633
+ lease.resumeService();
5634
+ throw error;
5635
+ }
5636
+ if (probe === "unreachable") {
5637
+ lease.resumeService();
5638
+ fail(`Cannot reach the relay at ${config.apiUrl}. Check your network, then run connect again.`);
5639
+ }
5640
+ if (probe === "rejected") {
5641
+ if (explicitCredentials)
5642
+ fail("The supplied --token was rejected for this tunnel.");
5643
+ console.log(` ${glyph.warn} ${c.dim}Saved token rejected — re-authorizing${c.reset}`);
5644
+ clearSavedCredentials();
5645
+ await pairThisMachine(config.apiUrl, flags);
5646
+ return;
5647
+ }
5648
+ await launch(config, flags, lease);
5388
5649
  }
5389
- async function commandRun(flags) {
5650
+ function commandRun(flags) {
5390
5651
  const config = loadConfig({
5391
5652
  token: flags.token,
5392
5653
  tunnelId: flags["tunnel-id"],
5393
5654
  apiUrl: flags["api-url"]
5394
5655
  });
5656
+ const asService = flags.service === "true";
5395
5657
  if (!config.token || !config.tunnelId) {
5396
- console.error(`${c2.red}${c2.bold} error${c2.reset} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
5397
- process.exit(1);
5658
+ console.error(` ${glyph.bad} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
5659
+ process.exit(asService ? TERMINAL_SERVICE_EXIT_CODE : 1);
5398
5660
  }
5399
- startAgent(config, { service: flags.service === "true" });
5661
+ if (asService)
5662
+ rotateServiceLogs();
5663
+ startAgent(config, { service: asService });
5400
5664
  }
5401
- async function commandStatus(flags) {
5402
- const config = loadConfig({
5403
- token: flags.token,
5404
- tunnelId: flags["tunnel-id"],
5405
- apiUrl: flags["api-url"]
5406
- });
5407
- if (!config.token || !config.tunnelId) {
5408
- console.error("Error: --token and --tunnel-id are required");
5409
- process.exit(1);
5665
+ function lastServiceActivity() {
5666
+ try {
5667
+ const lines = readFileSync6(join10(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
5668
+ return lines.at(-1) ?? null;
5669
+ } catch {
5670
+ return null;
5410
5671
  }
5411
- if (isSetupTunnelToken(config.token)) {
5672
+ }
5673
+ function commandStatus(flags) {
5674
+ const config = loadConfig({ apiUrl: flags["api-url"] });
5675
+ const service = getServiceStatus();
5676
+ const paired = Boolean(config.token && config.tunnelId);
5677
+ const approved = new Set(config.enabledCapabilities ?? []);
5678
+ if (isTruthyFlag(flags.json)) {
5412
5679
  console.log(JSON.stringify({
5413
- tunnelId: config.tunnelId,
5680
+ paired,
5681
+ tunnelId: paired ? config.tunnelId : null,
5414
5682
  apiUrl: config.apiUrl,
5415
- credential: "device-setup-token",
5416
- note: "Saved device credentials authenticate the local WebSocket agent. HTTP live status requires a user or sandbox API key.",
5417
- service: getServiceStatus()
5683
+ capabilities: [...approved],
5684
+ version: agentTunnelVersion(),
5685
+ service,
5686
+ lastActivity: lastServiceActivity()
5418
5687
  }, null, 2));
5419
5688
  return;
5420
5689
  }
5421
- try {
5422
- const res = await fetch(`${config.apiUrl}/connections/${config.tunnelId}`, {
5423
- headers: { Authorization: `Bearer ${config.token}` }
5424
- });
5425
- if (!res.ok) {
5426
- console.error(`Error: ${res.status} ${await res.text()}`);
5427
- process.exit(1);
5428
- }
5429
- const data = await res.json();
5430
- console.log(JSON.stringify(data, null, 2));
5431
- } catch (err) {
5432
- console.error("Error:", err);
5433
- process.exit(1);
5434
- }
5435
- }
5436
- function commandInstallService(flags) {
5437
- const config = loadConfig({
5438
- token: flags.token,
5439
- tunnelId: flags["tunnel-id"],
5440
- apiUrl: flags["api-url"]
5441
- });
5442
- if (!config.token || !config.tunnelId) {
5443
- console.error(`${c2.red}${c2.bold} error${c2.reset} No saved tunnel credentials found. Run \`agent-tunnel connect\` first, or pass --token and --tunnel-id.`);
5444
- process.exit(1);
5445
- }
5446
- if (flags.token && flags["tunnel-id"]) {
5447
- saveCredentials(config.tunnelId, config.token, config.apiUrl);
5690
+ blankLine();
5691
+ console.log(` ${glyph.mark} ${c.bold}${c.white}Agent Tunnel${c.reset} ${c.dim}v${agentTunnelVersion()}${c.reset} ${c.dim}${hostname4()}${c.reset}`);
5692
+ blankLine();
5693
+ if (!paired) {
5694
+ console.log(` ${glyph.off} ${c.bold}Not paired${c.reset}`);
5695
+ blankLine();
5696
+ console.log(` ${c.dim}Pair this machine:${c.reset} ${c.white}agent-tunnel connect --api-url <url>${c.reset}`);
5697
+ blankLine();
5698
+ return;
5448
5699
  }
5449
- const status = installService();
5450
- console.log(JSON.stringify(status, null, 2));
5451
- }
5452
- function commandUninstallService() {
5453
- console.log(JSON.stringify(uninstallService(), null, 2));
5454
- }
5455
- function commandStartService() {
5456
- console.log(JSON.stringify(startService(), null, 2));
5700
+ field("tunnel", `${c.white}${config.tunnelId}${c.reset}`);
5701
+ field("relay", `${c.white}${config.apiUrl}${c.reset}`);
5702
+ field("capabilities", ALL_CAPABILITIES.map((name) => approved.has(name) ? `${glyph.on} ${c.white}${name}${c.reset}` : `${c.gray}○ ${name}${c.reset}`).join(" "));
5703
+ blankLine();
5704
+ field("service", describeService(service));
5705
+ if (service.installed && service.path)
5706
+ field("", `${c.dim}${shortenHomePath(service.path)}${c.reset}`);
5707
+ const activity = lastServiceActivity();
5708
+ if (activity)
5709
+ field("last log", `${c.dim}${activity}${c.reset}`);
5710
+ blankLine();
5711
+ if (approved.size === 0) {
5712
+ console.log(` ${glyph.warn} ${c.dim}No capabilities approved — this tunnel cannot act.${c.reset}`);
5713
+ console.log(` ${c.dim}Pair again with${c.reset} ${c.white}agent-tunnel connect --reauth${c.reset}`);
5714
+ blankLine();
5715
+ }
5716
+ console.log(` ${c.dim}Recent logs:${c.reset} ${c.white}agent-tunnel logs${c.reset}`);
5717
+ blankLine();
5457
5718
  }
5458
- function commandStopService() {
5459
- console.log(JSON.stringify(stopService(), null, 2));
5719
+ function commandLogout(flags) {
5720
+ const removed = clearSavedCredentials();
5721
+ const keepService = isTruthyFlag(flags["keep-service"]);
5722
+ if (!keepService)
5723
+ SERVICE_ACTIONS.uninstall.run();
5724
+ blankLine();
5725
+ console.log(removed ? ` ${glyph.on} ${c.bold}Signed out${c.reset} ${c.dim}(credentials cleared from ${CONFIG_FILE2})${c.reset}` : ` ${glyph.off} ${c.dim}No saved credentials to clear${c.reset}`);
5726
+ console.log(keepService ? ` ${glyph.warn} ${c.dim}Background service kept — it cannot authenticate until you connect again${c.reset}` : ` ${c.dim}Background service removed${c.reset}`);
5727
+ blankLine();
5728
+ console.log(` ${c.dim}Pair again with:${c.reset} ${c.white}agent-tunnel connect --api-url <url>${c.reset}`);
5729
+ blankLine();
5460
5730
  }
5461
- function commandRestartService() {
5462
- console.log(JSON.stringify(restartService(), null, 2));
5463
- }
5464
- function commandServiceStatus() {
5465
- console.log(JSON.stringify(getServiceStatus(), null, 2));
5466
- }
5467
- function commandLogs() {
5731
+ function commandLogs(flags) {
5468
5732
  const paths = getServicePaths();
5469
- const files = [
5470
- join6(paths.logDir, "agent-tunnel.out.log"),
5471
- join6(paths.logDir, "agent-tunnel.err.log")
5472
- ];
5473
- for (const file of files) {
5474
- console.log(`
5475
- ${c2.bold}${file}${c2.reset}`);
5476
- if (!existsSync4(file)) {
5477
- console.log(`${c2.dim}not created yet${c2.reset}`);
5733
+ if (isTruthyFlag(flags.clear)) {
5734
+ for (const file of serviceLogFiles(paths)) {
5735
+ try {
5736
+ writeFileSync5(file, "", { mode: 384 });
5737
+ } catch {}
5738
+ }
5739
+ blankLine();
5740
+ console.log(` ${glyph.on} ${c.dim}Service logs cleared${c.reset}`);
5741
+ blankLine();
5742
+ return;
5743
+ }
5744
+ const requested = Number.parseInt(flags.lines ?? "", 10);
5745
+ const limit = Number.isSafeInteger(requested) && requested > 0 ? requested : DEFAULT_LOG_LINES;
5746
+ const showAll = isTruthyFlag(flags.all);
5747
+ for (const [label, file] of [
5748
+ ["output", join10(paths.logDir, "agent-tunnel.out.log")],
5749
+ ["errors", join10(paths.logDir, "agent-tunnel.err.log")]
5750
+ ]) {
5751
+ blankLine();
5752
+ console.log(` ${c.bold}${c.white}${label}${c.reset} ${c.dim}${shortenHomePath(file)}${c.reset}`);
5753
+ if (!existsSync6(file)) {
5754
+ console.log(` ${c.dim}not created yet${c.reset}`);
5755
+ continue;
5756
+ }
5757
+ const kept = readFileSync6(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
5758
+ const lines = collapseRepeatedLines(kept).slice(-limit);
5759
+ if (lines.length === 0) {
5760
+ console.log(` ${c.dim}empty${c.reset}`);
5478
5761
  continue;
5479
5762
  }
5480
- const body = readFileSync3(file, "utf8");
5481
- const lines = body.split(/\r?\n/).slice(-120).join(`
5482
- `).trim();
5483
- console.log(lines || `${c2.dim}empty${c2.reset}`);
5763
+ for (const line of lines)
5764
+ console.log(` ${line}`);
5765
+ }
5766
+ blankLine();
5767
+ console.log(` ${c.dim}--lines <n> to show more, --all to keep shell noise, --clear to empty them.${c.reset}`);
5768
+ blankLine();
5769
+ }
5770
+ function commandServiceAction(action, flags) {
5771
+ if (action === "install") {
5772
+ const config = loadConfig({
5773
+ token: flags.token,
5774
+ tunnelId: flags["tunnel-id"],
5775
+ apiUrl: flags["api-url"]
5776
+ });
5777
+ if (!config.token || !config.tunnelId) {
5778
+ fail("No saved tunnel credentials found. Run `agent-tunnel connect` first, or pass --token and --tunnel-id.");
5779
+ }
5780
+ if (flags.token && flags["tunnel-id"]) {
5781
+ saveCredentials(config.tunnelId, config.token, config.apiUrl);
5782
+ }
5484
5783
  }
5784
+ renderServiceAction(action, SERVICE_ACTIONS[action].run());
5485
5785
  }
5786
+ var COMMANDS = {
5787
+ connect: {
5788
+ summary: "Pair this machine, then run it in the background or this terminal",
5789
+ run: commandConnect
5790
+ },
5791
+ status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
5792
+ logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
5793
+ start: { summary: "Start the background service", run: (f) => commandServiceAction("start", f) },
5794
+ stop: {
5795
+ summary: "Stop the background service (keeps it installed)",
5796
+ run: (f) => commandServiceAction("stop", f),
5797
+ aliases: ["disable"]
5798
+ },
5799
+ restart: { summary: "Restart the background service", run: (f) => commandServiceAction("restart", f) },
5800
+ "install-service": {
5801
+ summary: "Install and start the background service",
5802
+ run: (f) => commandServiceAction("install", f)
5803
+ },
5804
+ "uninstall-service": {
5805
+ summary: "Stop and remove the background service",
5806
+ run: (f) => commandServiceAction("uninstall", f)
5807
+ },
5808
+ "service-status": {
5809
+ summary: "Show the background service state (same view as status)",
5810
+ run: commandStatus
5811
+ },
5812
+ logout: { summary: "Clear saved credentials and remove the service", run: commandLogout },
5813
+ run: { summary: "Run using saved credentials (used by the service)", run: commandRun },
5814
+ "start-service": { summary: "", run: (f) => commandServiceAction("start", f), hidden: true },
5815
+ "stop-service": { summary: "", run: (f) => commandServiceAction("stop", f), hidden: true },
5816
+ "restart-service": { summary: "", run: (f) => commandServiceAction("restart", f), hidden: true },
5817
+ "sign-out": { summary: "", run: commandLogout, hidden: true },
5818
+ unpair: { summary: "", run: commandLogout, hidden: true }
5819
+ };
5820
+ var OPTIONS = [
5821
+ ["--api-url <url>", "Relay API URL"],
5822
+ ["--token <token> --tunnel-id <id>", "Skip device auth and use an explicit credential"],
5823
+ ["--reauth", "With connect: discard the saved credential and pair again"],
5824
+ ["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
5825
+ ["--json", "With status: machine-readable output"],
5826
+ ["--keep-service", "With logout: keep the background service installed"]
5827
+ ];
5486
5828
  function showHelp() {
5487
- console.log("");
5488
- console.log(` ${c2.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c2.reset} ${c2.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c2.reset}`);
5489
- console.log(` ${c2.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c2.reset} ${c2.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c2.reset}`);
5490
- console.log("");
5491
- console.log(` ${c2.dim}Secure bridge between AI agents & local machines${c2.reset}`);
5492
- console.log("");
5493
- console.log(` ${c2.bold}Usage${c2.reset} ${c2.dim}npx --yes @kortix/agent-tunnel@latest <command> [options]${c2.reset}`);
5494
- console.log("");
5495
- console.log(`${c2.gray} ── Commands ────────────────────────────────────────${c2.reset}`);
5496
- console.log(` ${c2.cyan}connect${c2.reset} Connect via device auth; interactively choose foreground/background`);
5497
- console.log(` ${c2.cyan}run${c2.reset} Run using saved credentials ${c2.dim}(used by service)${c2.reset}`);
5498
- console.log(` ${c2.cyan}install-service${c2.reset} Install/start a persistent background service`);
5499
- console.log(` ${c2.cyan}start${c2.reset} Start the installed background service`);
5500
- console.log(` ${c2.cyan}stop${c2.reset} Stop the installed background service ${c2.dim}(keeps it installed)${c2.reset}`);
5501
- console.log(` ${c2.cyan}restart${c2.reset} Restart the installed background service`);
5502
- console.log(` ${c2.cyan}service-status${c2.reset} Check persistent service status`);
5503
- console.log(` ${c2.cyan}logs${c2.reset} Show recent service logs`);
5504
- console.log(` ${c2.cyan}uninstall-service${c2.reset} Stop/remove the persistent service`);
5505
- console.log(` ${c2.cyan}status${c2.reset} Check tunnel connection status`);
5506
- console.log(` ${c2.cyan}help${c2.reset} Show this help message`);
5507
- console.log("");
5508
- console.log(`${c2.gray} ── Options ─────────────────────────────────────────${c2.reset}`);
5509
- console.log(` ${c2.white}--token${c2.reset} ${c2.dim}<token>${c2.reset} Skip device auth, connect directly`);
5510
- console.log(` ${c2.white}--tunnel-id${c2.reset} ${c2.dim}<id>${c2.reset} Tunnel ID ${c2.dim}(required with --token)${c2.reset}`);
5511
- console.log(` ${c2.white}--api-url${c2.reset} ${c2.dim}<url>${c2.reset} API URL ${c2.dim}(default: http://localhost:8080)${c2.reset}`);
5512
- console.log(` ${c2.white}--daemon${c2.reset} With connect: skip the prompt and install the background service`);
5513
- console.log(` ${c2.white}--foreground${c2.reset} With connect: skip prompts and run only in this terminal`);
5514
- console.log("");
5515
- console.log(` ${c2.dim}Config: ~/.agent-tunnel/config.json${c2.reset}`);
5516
- console.log(` ${c2.dim}powered by ${c2.cyan}kortix${c2.reset}`);
5517
- console.log("");
5829
+ blankLine();
5830
+ console.log(` ${c.cyan}▄▀█ █▀▀ █▀▀ █▄ █ ▀█▀${c.reset} ${c.cyan}▀█▀ █ █ █▄ █ █▄ █ █▀▀ █ ${c.reset}`);
5831
+ console.log(` ${c.cyan}█▀█ █▄█ ██▄ █ ▀█ █${c.reset} ${c.cyan} █ █▄█ █ ▀█ █ ▀█ ██▄ █▄▄${c.reset}`);
5832
+ blankLine();
5833
+ console.log(` ${c.dim}Secure bridge between AI agents & local machines${c.reset}`);
5834
+ blankLine();
5835
+ console.log(` ${c.bold}Usage${c.reset} ${c.dim}npx --yes @kortix/agent-tunnel@latest <command> [options]${c.reset}`);
5836
+ blankLine();
5837
+ console.log(`${c.gray} ── Commands ────────────────────────────────────────${c.reset}`);
5838
+ const visible = Object.entries(COMMANDS).filter(([, command]) => !command.hidden);
5839
+ const width = Math.max(...visible.map(([name]) => name.length)) + 2;
5840
+ for (const [name, command] of visible) {
5841
+ console.log(` ${c.cyan}${name.padEnd(width)}${c.reset}${command.summary}`);
5842
+ }
5843
+ blankLine();
5844
+ console.log(`${c.gray} ── Options ─────────────────────────────────────────${c.reset}`);
5845
+ const optionWidth = Math.max(...OPTIONS.map(([flag]) => flag.length)) + 2;
5846
+ for (const [flag, description] of OPTIONS) {
5847
+ console.log(` ${c.white}${flag.padEnd(optionWidth)}${c.reset}${c.dim}${description}${c.reset}`);
5848
+ }
5849
+ blankLine();
5850
+ console.log(` ${c.dim}Config: ${CONFIG_FILE2}${c.reset}`);
5851
+ console.log(` ${c.dim}powered by ${c.cyan}kortix${c.reset}`);
5852
+ blankLine();
5518
5853
  }
5519
5854
  var { command, flags } = parseArgs(process.argv);
5520
5855
  if (Object.prototype.hasOwnProperty.call(flags, "keep-awake")) {
5521
- console.error(`${c2.red}${c2.bold} error${c2.reset} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
5856
+ console.error(` ${glyph.bad} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
5522
5857
  process.exit(2);
5523
5858
  }
5524
- switch (command) {
5525
- case "connect":
5526
- commandConnect(flags);
5527
- break;
5528
- case "run":
5529
- commandRun(flags);
5530
- break;
5531
- case "install-service":
5532
- commandInstallService(flags);
5533
- break;
5534
- case "start":
5535
- case "start-service":
5536
- commandStartService();
5537
- break;
5538
- case "stop":
5539
- case "stop-service":
5540
- case "disable":
5541
- commandStopService();
5542
- break;
5543
- case "restart":
5544
- case "restart-service":
5545
- commandRestartService();
5546
- break;
5547
- case "service-status":
5548
- commandServiceStatus();
5549
- break;
5550
- case "logs":
5551
- commandLogs();
5552
- break;
5553
- case "uninstall-service":
5554
- commandUninstallService();
5555
- break;
5556
- case "status":
5557
- commandStatus(flags);
5558
- break;
5559
- case "help":
5560
- default:
5561
- showHelp();
5562
- break;
5859
+ var resolved = COMMANDS[command] ?? Object.values(COMMANDS).find((entry) => entry.aliases?.includes(command));
5860
+ if (!resolved) {
5861
+ showHelp();
5862
+ } else {
5863
+ Promise.resolve(resolved.run(flags)).catch((error) => {
5864
+ console.error(` ${glyph.bad} ${error instanceof Error ? error.message : String(error)}`);
5865
+ process.exit(1);
5866
+ });
5563
5867
  }