@kortix/agent-tunnel 0.13.41 → 0.13.42

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
@@ -2971,19 +2971,33 @@ if (typeof globalThis.WebSocket === "undefined") {
2971
2971
  }
2972
2972
 
2973
2973
  // src/agent/cli.ts
2974
- import { existsSync as existsSync6, readFileSync as readFileSync6, writeFileSync as writeFileSync5 } from "fs";
2974
+ import { existsSync as existsSync7, readFileSync as readFileSync9, rmSync as rmSync5, writeFileSync as writeFileSync7 } from "fs";
2975
2975
  import { hostname as hostname4 } from "os";
2976
- import { join as join10 } from "path";
2976
+ import { join as join12 } from "path";
2977
2977
 
2978
2978
  // src/agent/agent.ts
2979
- import { hostname, platform, arch, release } from "os";
2979
+ import { spawn as spawn3 } from "child_process";
2980
+ import { hostname as hostname2, platform as platform2, arch, release } from "os";
2980
2981
 
2981
2982
  // src/agent/config.ts
2982
- import { chmodSync, existsSync, lstatSync, readFileSync } from "fs";
2983
- import { join } from "path";
2984
- import { homedir } from "os";
2983
+ import { chmodSync, existsSync, lstatSync, readFileSync as readFileSync2 } from "fs";
2984
+ import { basename, dirname, join as join2 } from "path";
2985
+ import { homedir as homedir2 } from "os";
2985
2986
 
2986
2987
  // src/shared/types.ts
2988
+ var TunnelErrorCode = {
2989
+ PERMISSION_DENIED: -32000,
2990
+ CAPABILITY_NOT_REGISTERED: -32001,
2991
+ TIMEOUT: -32002,
2992
+ LOCAL_ERROR: -32003,
2993
+ NOT_CONNECTED: -32004,
2994
+ EXPIRED: -32005,
2995
+ RATE_LIMITED: -32006,
2996
+ AUTH_FAILED: -32007,
2997
+ ACCESS_PENDING: -32010,
2998
+ ACCESS_DENIED: -32011,
2999
+ ACCESS_OFF: -32012
3000
+ };
2987
3001
  var TunnelMethods = {
2988
3002
  "fs.read": "filesystem",
2989
3003
  "fs.write": "filesystem",
@@ -3273,14 +3287,77 @@ function desktopFeatureForMethod(method, args = {}) {
3273
3287
  return DESKTOP_METHOD_FEATURES[method];
3274
3288
  }
3275
3289
 
3290
+ // src/agent/service-paths.ts
3291
+ import { createHash } from "crypto";
3292
+ import { readFileSync, writeFileSync } from "fs";
3293
+ import { homedir } from "os";
3294
+ import { join, resolve } from "path";
3295
+ var SERVICE_LABEL = "ai.kortix.agent-tunnel";
3296
+ function defaultAgentTunnelHome() {
3297
+ return join(homedir(), ".agent-tunnel");
3298
+ }
3299
+ function agentTunnelHome(env = process.env) {
3300
+ const override = env.AGENT_TUNNEL_HOME?.trim();
3301
+ return override ? resolve(override) : defaultAgentTunnelHome();
3302
+ }
3303
+ function serviceLabelFor(home) {
3304
+ const dir = resolve(home);
3305
+ if (dir === defaultAgentTunnelHome())
3306
+ return SERVICE_LABEL;
3307
+ return `${SERVICE_LABEL}.${createHash("sha256").update(dir).digest("hex").slice(0, 8)}`;
3308
+ }
3309
+ function serviceHomeEnv(home = agentTunnelHome()) {
3310
+ return resolve(home) === defaultAgentTunnelHome() ? {} : { AGENT_TUNNEL_HOME: resolve(home) };
3311
+ }
3312
+ var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
3313
+ var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
3314
+ var RETAINED_LOG_LINES = 500;
3315
+ function getServicePaths(configDir = agentTunnelHome()) {
3316
+ const home = homedir();
3317
+ const label = serviceLabelFor(configDir);
3318
+ const binDir = join(configDir, "bin");
3319
+ return {
3320
+ label,
3321
+ configDir,
3322
+ logDir: join(configDir, "logs"),
3323
+ binDir,
3324
+ vendoredRunner: join(binDir, "agent-cli.js"),
3325
+ launchdPlist: join(home, "Library", "LaunchAgents", `${label}.plist`),
3326
+ systemdUnit: join(home, ".config", "systemd", "user", `${label}.service`),
3327
+ windowsScript: join(configDir, "agent-tunnel-service.ps1")
3328
+ };
3329
+ }
3330
+ function serviceLogFiles(paths = getServicePaths()) {
3331
+ return [
3332
+ join(paths.logDir, "agent-tunnel.out.log"),
3333
+ join(paths.logDir, "agent-tunnel.err.log")
3334
+ ];
3335
+ }
3336
+ function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
3337
+ const rotated = [];
3338
+ for (const file of serviceLogFiles(paths)) {
3339
+ try {
3340
+ const contents = readFileSync(file, "utf8");
3341
+ if (Buffer.byteLength(contents, "utf8") <= maxBytes)
3342
+ continue;
3343
+ const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
3344
+ `);
3345
+ writeFileSync(file, `[agent-tunnel] earlier entries trimmed
3346
+ ${kept}`, { mode: 384 });
3347
+ rotated.push(file);
3348
+ } catch {}
3349
+ }
3350
+ return rotated;
3351
+ }
3352
+
3276
3353
  // src/agent/config.ts
3277
- var CONFIG_DIR = join(homedir(), ".agent-tunnel");
3278
- var CONFIG_FILE = join(CONFIG_DIR, "config.json");
3354
+ var CONFIG_DIR = agentTunnelHome();
3355
+ var CONFIG_FILE = join2(CONFIG_DIR, "config.json");
3279
3356
  var DEFAULTS = {
3280
3357
  apiUrl: "http://localhost:8080",
3281
3358
  wsPath: "/ws",
3282
3359
  maxFileSize: 10 * 1024 * 1024,
3283
- allowedPaths: [homedir()],
3360
+ allowedPaths: [homedir2()],
3284
3361
  allowedCommands: [],
3285
3362
  blockedCommands: [],
3286
3363
  blockedPaths: [
@@ -3293,7 +3370,7 @@ var DEFAULTS = {
3293
3370
  "/sys",
3294
3371
  "/dev"
3295
3372
  ],
3296
- workingDir: homedir(),
3373
+ workingDir: homedir2(),
3297
3374
  shellTimeout: 30000,
3298
3375
  shellMaxTimeout: 120000,
3299
3376
  shellMaxOutputSize: 1024 * 1024,
@@ -3420,6 +3497,12 @@ function buildTunnelWsUrl(config) {
3420
3497
  });
3421
3498
  return `${base}${wsPath}?${params.toString()}`;
3422
3499
  }
3500
+ function protectedAgentPaths(home = CONFIG_DIR) {
3501
+ const paths = new Set([home, defaultAgentTunnelHome()]);
3502
+ if (basename(dirname(home)) === "agent-tunnel")
3503
+ paths.add(dirname(home));
3504
+ return [...paths];
3505
+ }
3423
3506
  function loadConfig(overrides = {}) {
3424
3507
  let fileConfig = {};
3425
3508
  if (existsSync(CONFIG_FILE)) {
@@ -3428,7 +3511,7 @@ function loadConfig(overrides = {}) {
3428
3511
  assertPrivateOwnedPath(CONFIG_DIR, "directory", 448);
3429
3512
  assertPrivateOwnedPath(CONFIG_FILE, "file", 384);
3430
3513
  try {
3431
- const parsed = JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
3514
+ const parsed = JSON.parse(readFileSync2(CONFIG_FILE, "utf-8"));
3432
3515
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
3433
3516
  throw new Error("root value must be an object");
3434
3517
  }
@@ -3457,19 +3540,327 @@ function loadConfig(overrides = {}) {
3457
3540
  merged.apiUrl = normalizeApiUrl(merged.apiUrl);
3458
3541
  merged.wsPath = absoluteWsPath(merged.wsPath);
3459
3542
  validateConfigValues(merged);
3543
+ merged.blockedPaths = [...merged.blockedPaths, ...protectedAgentPaths()];
3460
3544
  return merged;
3461
3545
  }
3462
3546
 
3547
+ // src/agent/access.ts
3548
+ import { spawn } from "child_process";
3549
+ import { randomUUID } from "crypto";
3550
+ import { chmodSync as chmodSync2, existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, statSync, writeFileSync as writeFileSync2 } from "fs";
3551
+ import { isAbsolute, join as join3 } from "path";
3552
+ var ACCESS_HOLD_MS = 20000;
3553
+ var DESKTOP_APP_HEARTBEAT_MS = 30000;
3554
+ var MAX_GRANT_MS = 24 * 3600000;
3555
+ var MODES = ["ask", "always", "off"];
3556
+ var COMPAT = { mode: "always", grantedUntil: null, deniedUntil: null, keepAwake: false };
3557
+ var accessFilePath = (home = agentTunnelHome()) => join3(home, "access.json");
3558
+ var accessRequestPath = (home = agentTunnelHome()) => join3(home, "access-request.json");
3559
+ var desktopAppPath = (home = agentTunnelHome()) => join3(home, "desktop-app.json");
3560
+ var isoOrNull = (value) => typeof value === "string" && !Number.isNaN(Date.parse(value)) ? value : null;
3561
+ function readAccess(home = agentTunnelHome()) {
3562
+ let raw;
3563
+ try {
3564
+ raw = readFileSync3(accessFilePath(home), "utf8");
3565
+ } catch {
3566
+ return { ...COMPAT };
3567
+ }
3568
+ try {
3569
+ const parsed = JSON.parse(raw);
3570
+ return {
3571
+ mode: MODES.includes(parsed.mode) ? parsed.mode : "ask",
3572
+ grantedUntil: isoOrNull(parsed.grantedUntil),
3573
+ deniedUntil: isoOrNull(parsed.deniedUntil),
3574
+ keepAwake: parsed.keepAwake === true
3575
+ };
3576
+ } catch {
3577
+ return { mode: "ask", grantedUntil: null, deniedUntil: null, keepAwake: false };
3578
+ }
3579
+ }
3580
+ function writePrivateJson(file, home, value) {
3581
+ mkdirSync(home, { recursive: true, mode: 448 });
3582
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
3583
+ writeFileSync2(tmp, `${JSON.stringify(value, null, 2)}
3584
+ `, { mode: 384 });
3585
+ try {
3586
+ chmodSync2(tmp, 384);
3587
+ } catch {}
3588
+ renameSync(tmp, file);
3589
+ }
3590
+ function writeAccess(state, home = agentTunnelHome()) {
3591
+ writePrivateJson(accessFilePath(home), home, state);
3592
+ }
3593
+ function decideAccess(state, now = Date.now()) {
3594
+ if (state.mode === "always")
3595
+ return "run";
3596
+ if (state.mode === "off")
3597
+ return "off";
3598
+ const denied = state.deniedUntil ? Date.parse(state.deniedUntil) : 0;
3599
+ if (denied > now)
3600
+ return "denied";
3601
+ const granted = state.grantedUntil ? Date.parse(state.grantedUntil) : 0;
3602
+ return granted > now && granted - now <= MAX_GRANT_MS ? "run" : "ask";
3603
+ }
3604
+ function writeAccessRequest(input, home = agentTunnelHome()) {
3605
+ const request = { id: randomUUID(), requestedAt: new Date().toISOString(), ...input };
3606
+ writePrivateJson(accessRequestPath(home), home, request);
3607
+ return request;
3608
+ }
3609
+ function readAccessRequest(home = agentTunnelHome()) {
3610
+ try {
3611
+ const parsed = JSON.parse(readFileSync3(accessRequestPath(home), "utf8"));
3612
+ return typeof parsed?.id === "string" ? parsed : null;
3613
+ } catch {
3614
+ return null;
3615
+ }
3616
+ }
3617
+ function clearAccessRequest(id, home = agentTunnelHome()) {
3618
+ if (readAccessRequest(home)?.id === id)
3619
+ rmSync(accessRequestPath(home), { force: true });
3620
+ }
3621
+ function isAlive(pid) {
3622
+ if (typeof pid !== "number" || !Number.isSafeInteger(pid) || pid <= 0)
3623
+ return false;
3624
+ try {
3625
+ process.kill(pid, 0);
3626
+ return true;
3627
+ } catch (error) {
3628
+ return error.code === "EPERM";
3629
+ }
3630
+ }
3631
+ function wakeDesktopApp(home = agentTunnelHome(), now = Date.now()) {
3632
+ let app;
3633
+ let touchedAt = 0;
3634
+ try {
3635
+ app = JSON.parse(readFileSync3(desktopAppPath(home), "utf8"));
3636
+ touchedAt = statSync(desktopAppPath(home)).mtimeMs;
3637
+ } catch {
3638
+ return "no-app";
3639
+ }
3640
+ if (isAlive(app.pid) && now - touchedAt < DESKTOP_APP_HEARTBEAT_MS)
3641
+ return "running";
3642
+ if (typeof app.command !== "string" || !isAbsolute(app.command) || !existsSync2(app.command))
3643
+ return "no-app";
3644
+ const args = Array.isArray(app.args) ? app.args.filter((arg) => typeof arg === "string") : [];
3645
+ const recorded = app.env && typeof app.env === "object" ? app.env : {};
3646
+ const env = { ...process.env };
3647
+ delete env.ELECTRON_RUN_AS_NODE;
3648
+ delete env.AGENT_TUNNEL_HOME;
3649
+ for (const [key, value] of Object.entries(recorded))
3650
+ if (typeof value === "string")
3651
+ env[key] = value;
3652
+ try {
3653
+ const child = spawn(app.command, args, { detached: true, stdio: "ignore", env });
3654
+ child.on("error", () => {});
3655
+ child.unref();
3656
+ return "launched";
3657
+ } catch {
3658
+ return "no-app";
3659
+ }
3660
+ }
3661
+ function keepAwakeCommand(platform, pid) {
3662
+ if (platform === "darwin")
3663
+ return { command: "caffeinate", args: ["-s", "-w", String(pid)] };
3664
+ if (platform === "linux") {
3665
+ return {
3666
+ command: "systemd-inhibit",
3667
+ args: ["--what=sleep", "--who=Kortix", "--why=Keep this computer reachable", "--mode=block", "tail", `--pid=${pid}`, "-f", "/dev/null"]
3668
+ };
3669
+ }
3670
+ return null;
3671
+ }
3672
+
3673
+ // src/agent/device-auth.ts
3674
+ import { execFileSync, spawn as spawn2 } from "child_process";
3675
+ import { hostname, platform } from "os";
3676
+ var UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3677
+ 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;
3678
+ var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
3679
+ var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
3680
+ var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
3681
+ var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
3682
+
3683
+ class InvalidDeviceAuthResponseError extends Error {
3684
+ constructor(message) {
3685
+ super(message);
3686
+ this.name = "InvalidDeviceAuthResponseError";
3687
+ }
3688
+ }
3689
+ function invalid(what) {
3690
+ throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
3691
+ }
3692
+ function isJsonRecord(value) {
3693
+ return value !== null && typeof value === "object" && !Array.isArray(value);
3694
+ }
3695
+ function normalizeBrowserUrl(value) {
3696
+ try {
3697
+ const url = new URL(value);
3698
+ return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
3699
+ } catch {
3700
+ return null;
3701
+ }
3702
+ }
3703
+ function isLoopback(url) {
3704
+ return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
3705
+ }
3706
+ function assertSafeVerificationUrl(value) {
3707
+ if (value.length > 2048)
3708
+ invalid("verification URL");
3709
+ const browserUrl = normalizeBrowserUrl(value);
3710
+ if (!browserUrl)
3711
+ invalid("verification URL");
3712
+ const url = new URL(browserUrl);
3713
+ if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
3714
+ throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
3715
+ }
3716
+ return browserUrl;
3717
+ }
3718
+ function parseDeviceAuthChallenge(value) {
3719
+ if (!isJsonRecord(value))
3720
+ invalid("challenge");
3721
+ const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
3722
+ if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
3723
+ invalid("device code");
3724
+ if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
3725
+ invalid("device secret");
3726
+ }
3727
+ if (typeof verificationUrl !== "string")
3728
+ invalid("verification URL");
3729
+ if (typeof expiresAt !== "string")
3730
+ invalid("expiration");
3731
+ const expiresAtMs = Date.parse(expiresAt);
3732
+ const now = Date.now();
3733
+ if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
3734
+ invalid("expiration");
3735
+ }
3736
+ if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
3737
+ invalid("poll interval");
3738
+ }
3739
+ return {
3740
+ deviceCode,
3741
+ deviceSecret,
3742
+ verificationUrl: assertSafeVerificationUrl(verificationUrl),
3743
+ expiresAt,
3744
+ pollIntervalMs
3745
+ };
3746
+ }
3747
+ function parseApprovedCredentials(value) {
3748
+ const { tunnelId, token } = value;
3749
+ if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
3750
+ invalid("tunnel ID");
3751
+ if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
3752
+ invalid("setup token");
3753
+ return { tunnelId, token };
3754
+ }
3755
+ function parseApprovedCapabilities(value) {
3756
+ if (!Array.isArray(value))
3757
+ return [];
3758
+ return [...new Set(value)].filter((item) => isTunnelCapability(item));
3759
+ }
3760
+ function parseDeviceAuthStatus(value) {
3761
+ if (!isJsonRecord(value) || typeof value.status !== "string")
3762
+ invalid("status response");
3763
+ switch (value.status) {
3764
+ case "approved": {
3765
+ if (!value.tunnelId || !value.token)
3766
+ return { status: "approved-without-token" };
3767
+ return {
3768
+ status: "approved",
3769
+ ...parseApprovedCredentials(value),
3770
+ capabilities: parseApprovedCapabilities(value.capabilities)
3771
+ };
3772
+ }
3773
+ case "denied":
3774
+ return { status: "denied" };
3775
+ case "expired":
3776
+ return { status: "expired" };
3777
+ default:
3778
+ return null;
3779
+ }
3780
+ }
3781
+ function machineDisplayName({
3782
+ os = platform(),
3783
+ host = hostname(),
3784
+ env = process.env,
3785
+ run = (command, args) => execFileSync(command, args, { encoding: "utf8", timeout: 1000, stdio: ["ignore", "pipe", "ignore"] })
3786
+ } = {}) {
3787
+ const fallback = host.replace(/\.local$/i, "") || host;
3788
+ try {
3789
+ const name = os === "darwin" ? run("scutil", ["--get", "ComputerName"]) : os === "win32" ? env.COMPUTERNAME : "";
3790
+ return name?.trim() || fallback;
3791
+ } catch {
3792
+ return fallback;
3793
+ }
3794
+ }
3795
+ async function requestDeviceAuthorization(apiUrl, options = {}) {
3796
+ const response = await fetch(`${apiUrl}/device-auth`, {
3797
+ method: "POST",
3798
+ headers: { "Content-Type": "application/json" },
3799
+ body: JSON.stringify({
3800
+ machineHostname: machineDisplayName(),
3801
+ ...options.projectId ? { project_id: options.projectId } : {}
3802
+ })
3803
+ });
3804
+ if (!response.ok) {
3805
+ const body = await response.text().catch(() => "");
3806
+ throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
3807
+ }
3808
+ return parseDeviceAuthChallenge(await response.json());
3809
+ }
3810
+ async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
3811
+ const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
3812
+ const expiresAtMs = Date.parse(challenge.expiresAt);
3813
+ for (;; ) {
3814
+ const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
3815
+ if (secondsRemaining <= 0)
3816
+ return { status: "expired" };
3817
+ options.onWaiting?.(secondsRemaining);
3818
+ let payload;
3819
+ try {
3820
+ const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
3821
+ headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
3822
+ });
3823
+ if (!response.ok) {
3824
+ await wait(challenge.pollIntervalMs);
3825
+ continue;
3826
+ }
3827
+ payload = await response.json();
3828
+ } catch {
3829
+ await wait(challenge.pollIntervalMs);
3830
+ continue;
3831
+ }
3832
+ const outcome = parseDeviceAuthStatus(payload);
3833
+ if (outcome)
3834
+ return outcome;
3835
+ await wait(challenge.pollIntervalMs);
3836
+ }
3837
+ }
3838
+ function openBrowser(url) {
3839
+ if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
3840
+ return;
3841
+ const safeUrl = normalizeBrowserUrl(url);
3842
+ if (!safeUrl)
3843
+ return;
3844
+ const opener = {
3845
+ darwin: ["open", [safeUrl]],
3846
+ win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
3847
+ };
3848
+ const [command, args] = opener[platform()] ?? ["xdg-open", [safeUrl]];
3849
+ try {
3850
+ spawn2(command, args, { detached: true, stdio: "ignore" }).unref();
3851
+ } catch {}
3852
+ }
3853
+
3463
3854
  // src/agent/version.ts
3464
- import { readFileSync as readFileSync2 } from "fs";
3465
- import { dirname, join as join2 } from "path";
3855
+ import { readFileSync as readFileSync4 } from "fs";
3856
+ import { dirname as dirname2, join as join4 } from "path";
3466
3857
  import { fileURLToPath } from "url";
3467
3858
  var PACKAGE_NAME = "@kortix/agent-tunnel";
3468
3859
  var MAX_LOOKUP_DEPTH = 8;
3469
3860
  var cached = null;
3470
3861
  function moduleDirectory() {
3471
3862
  try {
3472
- return dirname(fileURLToPath(import.meta.url));
3863
+ return dirname2(fileURLToPath(import.meta.url));
3473
3864
  } catch {
3474
3865
  return process.cwd();
3475
3866
  }
@@ -3480,14 +3871,14 @@ function agentTunnelVersion() {
3480
3871
  let directory = moduleDirectory();
3481
3872
  for (let depth = 0;depth < MAX_LOOKUP_DEPTH; depth++) {
3482
3873
  try {
3483
- const manifest = JSON.parse(readFileSync2(join2(directory, "package.json"), "utf8"));
3874
+ const manifest = JSON.parse(readFileSync4(join4(directory, "package.json"), "utf8"));
3484
3875
  if (manifest?.name === PACKAGE_NAME && typeof manifest.version === "string") {
3485
3876
  const version = manifest.version;
3486
3877
  cached = version;
3487
3878
  return version;
3488
3879
  }
3489
3880
  } catch {}
3490
- const parent = dirname(directory);
3881
+ const parent = dirname2(directory);
3491
3882
  if (parent === directory)
3492
3883
  break;
3493
3884
  directory = parent;
@@ -3588,7 +3979,7 @@ class PermissionGuard {
3588
3979
  }
3589
3980
 
3590
3981
  // src/shared/crypto.ts
3591
- import { createHash, createHmac, timingSafeEqual, randomInt } from "crypto";
3982
+ import { createHash as createHash2, createHmac, timingSafeEqual, randomInt } from "crypto";
3592
3983
  function signMessage(signingKey, payload, nonce) {
3593
3984
  return createHmac("sha256", signingKey).update(`${nonce}:${payload}`).digest("hex");
3594
3985
  }
@@ -3608,6 +3999,12 @@ function verifyMessageSignature(signingKey, payload, nonce, signature) {
3608
3999
  // src/agent/agent.ts
3609
4000
  var AGENT_VERSION = agentTunnelVersion();
3610
4001
  var AUTH_REJECTED_CLOSE_CODES = [4001, 4003];
4002
+ var RELAY_FAULT_4001_REASONS = new Set(["auth timeout", "authentication error", "authentication response failed"]);
4003
+ function isCredentialRejection(code, reason = "") {
4004
+ if (!AUTH_REJECTED_CLOSE_CODES.includes(code))
4005
+ return false;
4006
+ return !(code === 4001 && RELAY_FAULT_4001_REASONS.has(reason));
4007
+ }
3611
4008
  var AGENT_REPLACED_CLOSE_CODE = 4004;
3612
4009
  var MAX_RPC_MESSAGE_SIZE = 5 * 1024 * 1024;
3613
4010
  function log(icon, msg) {
@@ -3616,36 +4013,84 @@ function log(icon, msg) {
3616
4013
  process.stdout.write(` ${safeIcon} ${c.dim}${safeMsg}${c.reset}
3617
4014
  `);
3618
4015
  }
4016
+ var LIVENESS_TIMEOUT_MS = 75000;
4017
+ var WATCHDOG_INTERVAL_MS = 5000;
4018
+ var CLOCK_JUMP_MS = 30000;
4019
+ var REJECTED_RETRY_MS = 5 * 60000;
4020
+ var STANDBY_RETRY_MS = 60000;
4021
+ var MAX_STANDBY_RETRY_MS = 30 * 60000;
4022
+ var CONNECT_DEADLINE_MS = 20000;
4023
+ var KEEP_AWAKE_MIN_LIFETIME_MS = 5000;
4024
+ var REJECTED_LOG_EVERY_MS = 3600000;
4025
+ var BASE_RECONNECT_DELAY_MS = 1000;
4026
+ var MAX_RECONNECT_DELAY_MS = 30000;
4027
+ function reconnectDelay(attempt, random = Math.random()) {
4028
+ const raw = Math.min(BASE_RECONNECT_DELAY_MS * 2 ** Math.min(attempt - 1, 30), MAX_RECONNECT_DELAY_MS);
4029
+ return Math.round(Math.min(raw * (0.8 + 0.4 * random), MAX_RECONNECT_DELAY_MS));
4030
+ }
4031
+ var ACCESS_ERRORS = {
4032
+ pending: {
4033
+ code: TunnelErrorCode.ACCESS_PENDING,
4034
+ message: "computer_access_pending: The owner has not allowed access yet. Ask them to approve the prompt on their computer, then try again."
4035
+ },
4036
+ denied: {
4037
+ code: TunnelErrorCode.ACCESS_DENIED,
4038
+ message: "computer_access_denied: The owner denied access on their computer. Try again later."
4039
+ },
4040
+ off: {
4041
+ code: TunnelErrorCode.ACCESS_OFF,
4042
+ message: "computer_access_off: The owner turned off access to this computer."
4043
+ }
4044
+ };
3619
4045
 
3620
4046
  class TunnelAgent {
3621
4047
  hooks;
4048
+ options;
4049
+ home;
4050
+ now;
3622
4051
  ws = null;
3623
4052
  registry;
3624
4053
  permissionGuard;
3625
4054
  config;
3626
4055
  reconnectAttempts = 0;
3627
- maxReconnectDelay = 30000;
3628
- baseReconnectDelay = 1000;
4056
+ closeCurrentSocket = null;
4057
+ watchdog = null;
4058
+ lastTick = 0;
4059
+ lastPingAt = 0;
4060
+ connectStartedAt = 0;
4061
+ standbyAttempts = 0;
4062
+ lastRejectedLogAt = 0;
4063
+ sentAccessKey = null;
4064
+ keepAwake = null;
4065
+ keepAwakeFailed = false;
3629
4066
  reconnectTimer = null;
3630
4067
  stableConnectionTimer = null;
3631
4068
  isShuttingDown = false;
4069
+ status = null;
3632
4070
  uptime = 0;
4071
+ displayName;
3633
4072
  uptimeInterval = null;
3634
4073
  signingKey = null;
3635
4074
  lastNonce = 0;
3636
4075
  responseNonce = 0;
3637
- constructor(config, registry, hooks = {}) {
4076
+ constructor(config, registry, hooks = {}, options = {}) {
3638
4077
  this.config = config;
3639
4078
  this.registry = registry;
3640
4079
  this.permissionGuard = new PermissionGuard;
3641
4080
  this.hooks = hooks;
4081
+ this.options = options;
4082
+ this.home = options.home ?? agentTunnelHome();
4083
+ this.now = options.now ?? Date.now;
3642
4084
  }
3643
4085
  connect() {
3644
4086
  if (this.ws) {
3645
4087
  this.ws.close();
3646
4088
  }
4089
+ this.startWatchdog();
3647
4090
  const wsUrl = this.buildWsUrl();
3648
4091
  log(`${c.cyan}◆${c.reset}`, `Connecting…`);
4092
+ this.connectStartedAt = this.now();
4093
+ this.setStatus("connecting");
3649
4094
  try {
3650
4095
  this.ws = new WebSocket(new URL(wsUrl));
3651
4096
  this.setupWsHandlers();
@@ -3664,6 +4109,11 @@ class TunnelAgent {
3664
4109
  clearInterval(this.uptimeInterval);
3665
4110
  this.uptimeInterval = null;
3666
4111
  }
4112
+ if (this.watchdog) {
4113
+ clearInterval(this.watchdog);
4114
+ this.watchdog = null;
4115
+ }
4116
+ this.syncKeepAwake(false);
3667
4117
  if (this.ws) {
3668
4118
  try {
3669
4119
  this.ws.close(1000, "client shutdown");
@@ -3672,6 +4122,15 @@ class TunnelAgent {
3672
4122
  }
3673
4123
  this.permissionGuard.clear();
3674
4124
  log(`${c.gray}○${c.reset}`, `Disconnected`);
4125
+ this.setStatus("offline");
4126
+ }
4127
+ setStatus(status) {
4128
+ if (this.status === status)
4129
+ return;
4130
+ this.status = status;
4131
+ try {
4132
+ this.hooks.onStatus?.(status, this.config);
4133
+ } catch {}
3675
4134
  }
3676
4135
  isConnected() {
3677
4136
  return this.ws?.readyState === WebSocket.OPEN;
@@ -3691,13 +4150,19 @@ class TunnelAgent {
3691
4150
  type: "auth",
3692
4151
  token: trustedCredential(this.config.token, "token"),
3693
4152
  capabilities: this.registry.getCapabilityNames(),
3694
- agentVersion: AGENT_VERSION
4153
+ agentVersion: AGENT_VERSION,
4154
+ reportsAccess: true
3695
4155
  });
3696
4156
  });
3697
4157
  this.ws.addEventListener("message", (event) => {
3698
4158
  this.handleMessage(event.data);
3699
4159
  });
3700
- this.ws.addEventListener("close", (event) => {
4160
+ let closeHandled = false;
4161
+ const onClose = (event) => {
4162
+ if (closeHandled)
4163
+ return;
4164
+ closeHandled = true;
4165
+ this.setStatus("offline");
3701
4166
  if (this.uptimeInterval) {
3702
4167
  clearInterval(this.uptimeInterval);
3703
4168
  this.uptimeInterval = null;
@@ -3707,7 +4172,23 @@ class TunnelAgent {
3707
4172
  this.stableConnectionTimer = null;
3708
4173
  }
3709
4174
  if (!this.isShuttingDown) {
3710
- if (event.code === 4001) {
4175
+ if (this.options.persistent && isCredentialRejection(event.code, event.reason)) {
4176
+ this.setStatus("rejected");
4177
+ if (this.now() - this.lastRejectedLogAt >= REJECTED_LOG_EVERY_MS) {
4178
+ this.lastRejectedLogAt = this.now();
4179
+ log(`${c.red}✗${c.reset}`, `Credential rejected — waiting for this computer to be paired again (retrying every 5 min)`);
4180
+ }
4181
+ this.retryAfter(this.options.rejectedRetryMs ?? REJECTED_RETRY_MS);
4182
+ return;
4183
+ }
4184
+ if (this.options.persistent && event.code === AGENT_REPLACED_CLOSE_CODE) {
4185
+ this.setStatus("standby");
4186
+ const wait = Math.min((this.options.standbyRetryMs ?? STANDBY_RETRY_MS) * 2 ** this.standbyAttempts++, MAX_STANDBY_RETRY_MS);
4187
+ log(`${c.yellow}○${c.reset}`, `Another Agent Tunnel process holds this computer — standing by, next try in ${Math.round(wait / 1000)}s`);
4188
+ this.retryAfter(wait);
4189
+ return;
4190
+ }
4191
+ if (event.code === 4001 && isCredentialRejection(event.code, event.reason)) {
3711
4192
  this.isShuttingDown = true;
3712
4193
  log(`${c.red}✗${c.reset}`, `Credential rejected — run \`agent-tunnel connect --reauth\` to pair again`);
3713
4194
  this.hooks.onTerminalClose?.({ code: event.code, reason: "credential-rejected" });
@@ -3728,9 +4209,12 @@ class TunnelAgent {
3728
4209
  log(`${c.yellow}○${c.reset}`, `Disconnected ${c.gray}(code: ${event.code})${c.reset}`);
3729
4210
  this.scheduleReconnect();
3730
4211
  }
3731
- });
4212
+ };
4213
+ this.ws.addEventListener("close", onClose);
4214
+ this.closeCurrentSocket = onClose;
3732
4215
  this.ws.addEventListener("error", () => {
3733
4216
  log(`${c.red}✗${c.reset}`, `WebSocket error`);
4217
+ setTimeout(() => onClose({ code: 1006 }), 1000);
3734
4218
  });
3735
4219
  }
3736
4220
  async handleMessage(raw) {
@@ -3756,6 +4240,11 @@ class TunnelAgent {
3756
4240
  } else {
3757
4241
  log(`${c.green}●${c.reset}`, `Connected ${c.reset}${c.gray}(${capabilityNames.join(", ")})${c.reset}`);
3758
4242
  }
4243
+ this.lastPingAt = this.now();
4244
+ this.sentAccessKey = null;
4245
+ this.standbyAttempts = 0;
4246
+ this.setStatus("online");
4247
+ this.reportAccess();
3759
4248
  if (this.stableConnectionTimer)
3760
4249
  clearTimeout(this.stableConnectionTimer);
3761
4250
  this.stableConnectionTimer = setTimeout(() => {
@@ -3775,6 +4264,7 @@ class TunnelAgent {
3775
4264
  return;
3776
4265
  }
3777
4266
  if ("method" in msg && msg.method === "tunnel.ping") {
4267
+ this.lastPingAt = this.now();
3778
4268
  this.sendPong();
3779
4269
  return;
3780
4270
  }
@@ -3842,6 +4332,11 @@ class TunnelAgent {
3842
4332
  this.sendSignedError(id, -32001, `Capability not registered for method: ${method}`);
3843
4333
  return;
3844
4334
  }
4335
+ const refusal = await this.awaitAccess(method);
4336
+ if (refusal) {
4337
+ this.sendSignedError(id, refusal.code, refusal.message);
4338
+ return;
4339
+ }
3845
4340
  try {
3846
4341
  const result = await handler({
3847
4342
  ...params,
@@ -3900,22 +4395,166 @@ class TunnelAgent {
3900
4395
  uptime: this.uptime,
3901
4396
  capabilities: this.registry.getCapabilityNames(),
3902
4397
  machineInfo: {
3903
- hostname: hostname(),
3904
- platform: platform(),
4398
+ hostname: hostname2(),
4399
+ displayName: this.displayName ??= machineDisplayName(),
4400
+ platform: platform2(),
3905
4401
  arch: arch(),
3906
4402
  osVersion: release(),
3907
- agentVersion: AGENT_VERSION
4403
+ agentVersion: AGENT_VERSION,
4404
+ homeDir: this.config.workingDir,
4405
+ allowedPaths: this.config.allowedPaths
3908
4406
  }
3909
4407
  }
3910
4408
  });
3911
4409
  }
4410
+ async awaitAccess(method) {
4411
+ const first = decideAccess(readAccess(this.home));
4412
+ if (first === "run")
4413
+ return null;
4414
+ if (first !== "ask")
4415
+ return ACCESS_ERRORS[first];
4416
+ const hold = this.options.accessHoldMs ?? ACCESS_HOLD_MS;
4417
+ let requestId = null;
4418
+ try {
4419
+ const pending = readAccessRequest(this.home);
4420
+ if (pending && Date.now() - Date.parse(pending.requestedAt) < hold) {
4421
+ requestId = pending.id;
4422
+ } else {
4423
+ requestId = writeAccessRequest({ capability: capabilityForMethod(method) ?? method, method }, this.home).id;
4424
+ wakeDesktopApp(this.home);
4425
+ }
4426
+ } catch {}
4427
+ const deadline = Date.now() + hold;
4428
+ while (Date.now() < deadline) {
4429
+ await new Promise((resolve) => setTimeout(resolve, Math.min(100, hold / 4)));
4430
+ const decision = decideAccess(readAccess(this.home));
4431
+ if (decision === "ask")
4432
+ continue;
4433
+ if (requestId)
4434
+ clearAccessRequest(requestId, this.home);
4435
+ return decision === "run" ? null : ACCESS_ERRORS[decision];
4436
+ }
4437
+ return ACCESS_ERRORS.pending;
4438
+ }
4439
+ reportAccess() {
4440
+ const access = readAccess(this.home);
4441
+ const { mode } = access;
4442
+ const grantedUntil = access.grantedUntil && Date.parse(access.grantedUntil) > this.now() ? access.grantedUntil : null;
4443
+ const key = `${mode}|${grantedUntil}`;
4444
+ if (key === this.sentAccessKey || this.status !== "online")
4445
+ return;
4446
+ this.sentAccessKey = key;
4447
+ this.sendSigned({ jsonrpc: "2.0", method: "tunnel.access.state", params: { mode, grantedUntil } });
4448
+ }
4449
+ syncKeepAwake(want) {
4450
+ if (!want) {
4451
+ this.keepAwake?.kill();
4452
+ this.keepAwake = null;
4453
+ this.keepAwakeFailed = false;
4454
+ return;
4455
+ }
4456
+ if (this.keepAwake || this.keepAwakeFailed)
4457
+ return;
4458
+ const blocker = keepAwakeCommand(platform2(), process.pid);
4459
+ if (!blocker)
4460
+ return;
4461
+ const child = spawn3(blocker.command, blocker.args, { stdio: "ignore" });
4462
+ child.on("error", () => {
4463
+ this.keepAwakeFailed = true;
4464
+ log(`${c.yellow}!${c.reset}`, `Keep awake is unavailable: ${blocker.command} could not start`);
4465
+ });
4466
+ const startedAt = this.now();
4467
+ child.on("exit", (code) => {
4468
+ if (this.keepAwake !== child)
4469
+ return;
4470
+ this.keepAwake = null;
4471
+ if (code !== 0 || this.now() - startedAt < KEEP_AWAKE_MIN_LIFETIME_MS) {
4472
+ this.keepAwakeFailed = true;
4473
+ log(`${c.yellow}!${c.reset}`, `Keep awake is unavailable: ${blocker.command} exited (code ${code})`);
4474
+ }
4475
+ });
4476
+ this.keepAwake = child;
4477
+ }
4478
+ startWatchdog() {
4479
+ if (this.watchdog)
4480
+ return;
4481
+ const interval = this.options.watchdogIntervalMs ?? WATCHDOG_INTERVAL_MS;
4482
+ this.lastTick = this.now();
4483
+ this.watchdog = setInterval(() => {
4484
+ const now = this.now();
4485
+ const slept = now - this.lastTick > interval + CLOCK_JUMP_MS;
4486
+ this.lastTick = now;
4487
+ if (slept) {
4488
+ this.reconnectAttempts = 0;
4489
+ this.forceReconnect("clock jump");
4490
+ } else if (this.status === "online" && now - this.lastPingAt > (this.options.livenessTimeoutMs ?? LIVENESS_TIMEOUT_MS)) {
4491
+ this.forceReconnect("liveness timeout");
4492
+ } else if (this.status === "connecting" && this.ws && now - this.connectStartedAt > (this.options.connectDeadlineMs ?? CONNECT_DEADLINE_MS)) {
4493
+ this.forceReconnect("handshake timeout");
4494
+ } else if (this.status === "rejected" && this.credentialChanged()) {
4495
+ this.forceReconnect("new credential");
4496
+ }
4497
+ try {
4498
+ const access = readAccess(this.home);
4499
+ this.syncKeepAwake(access.keepAwake);
4500
+ this.reportAccess();
4501
+ } catch {}
4502
+ }, interval);
4503
+ this.watchdog.unref?.();
4504
+ }
4505
+ credentialChanged() {
4506
+ try {
4507
+ const next = this.options.reloadConfig?.();
4508
+ if (!next || next.token === this.config.token && next.tunnelId === this.config.tunnelId)
4509
+ return false;
4510
+ this.config = next;
4511
+ return true;
4512
+ } catch {
4513
+ return false;
4514
+ }
4515
+ }
4516
+ forceReconnect(reason) {
4517
+ if (this.isShuttingDown)
4518
+ return;
4519
+ if (!this.ws) {
4520
+ if (this.reconnectTimer) {
4521
+ clearTimeout(this.reconnectTimer);
4522
+ this.reconnectTimer = null;
4523
+ this.connect();
4524
+ }
4525
+ return;
4526
+ }
4527
+ log(`${c.yellow}○${c.reset}`, `Reconnecting (${reason})`);
4528
+ try {
4529
+ this.ws.close(4000, reason);
4530
+ } catch {}
4531
+ this.ws = null;
4532
+ this.closeCurrentSocket?.({ code: 4000, reason });
4533
+ }
4534
+ retryAfter(ms) {
4535
+ if (this.reconnectTimer)
4536
+ clearTimeout(this.reconnectTimer);
4537
+ this.ws = null;
4538
+ this.reconnectTimer = setTimeout(() => {
4539
+ this.reconnectTimer = null;
4540
+ try {
4541
+ if (this.options.reloadConfig)
4542
+ this.config = this.options.reloadConfig();
4543
+ } catch {}
4544
+ this.connect();
4545
+ }, ms);
4546
+ }
3912
4547
  scheduleReconnect() {
3913
4548
  if (this.isShuttingDown)
3914
4549
  return;
3915
4550
  this.reconnectAttempts++;
3916
- const delay = Math.min(this.baseReconnectDelay * Math.pow(2, this.reconnectAttempts - 1), this.maxReconnectDelay);
4551
+ const delay = reconnectDelay(this.reconnectAttempts);
4552
+ this.ws = null;
3917
4553
  log(`${c.cyan}◆${c.reset}`, `Reconnecting in ${c.reset}${c.white}${(delay / 1000).toFixed(1)}s${c.dim} (attempt ${this.reconnectAttempts})`);
4554
+ if (this.reconnectTimer)
4555
+ clearTimeout(this.reconnectTimer);
3918
4556
  this.reconnectTimer = setTimeout(() => {
4557
+ this.reconnectTimer = null;
3919
4558
  this.connect();
3920
4559
  }, delay);
3921
4560
  }
@@ -3925,7 +4564,7 @@ class TunnelAgent {
3925
4564
  }
3926
4565
 
3927
4566
  // src/agent/banner.ts
3928
- import { arch as arch2, hostname as hostname2, platform as platform2 } from "os";
4567
+ import { arch as arch2, hostname as hostname3, platform as platform3 } from "os";
3929
4568
  var BOX_WIDTH = 60;
3930
4569
  var BAR_WIDTH = 50;
3931
4570
  var BAR_FRAMES = 14;
@@ -3975,7 +4614,7 @@ async function printStartupBanner({
3975
4614
  blank();
3976
4615
  row(` ${c.dim}tunnel${c.reset} ${c.white}${truncate(tunnelId, 40)}${c.reset}`);
3977
4616
  row(` ${c.dim}relay${c.reset} ${c.white}${truncate(apiUrl, 40)}${c.reset}`);
3978
- row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname2(), 28)}${c.reset} ${c.dim}(${platform2()} ${arch2()})${c.reset}`);
4617
+ row(` ${c.dim}machine${c.reset} ${c.white}${truncate(hostname3(), 28)}${c.reset} ${c.dim}(${platform3()} ${arch2()})${c.reset}`);
3979
4618
  row(` ${c.dim}access${c.reset} ${capabilityRow}`);
3980
4619
  blank();
3981
4620
  console.log(` ${c.gray}╰${"─".repeat(BOX_WIDTH - brand.length - 3)} ${c.dim}created by ${c.cyan}kortix${c.reset} ${c.gray}─╯${c.reset}`);
@@ -4008,10 +4647,10 @@ class CapabilityRegistry {
4008
4647
  }
4009
4648
 
4010
4649
  // src/agent/capabilities/desktop/cua-driver.ts
4011
- import { spawn } from "child_process";
4012
- import { existsSync as existsSync2, realpathSync, statSync } from "fs";
4013
- import { homedir as homedir2, platform as platform3 } from "os";
4014
- import { join as join3 } from "path";
4650
+ import { spawn as spawn4 } from "child_process";
4651
+ import { existsSync as existsSync3, realpathSync, statSync as statSync2 } from "fs";
4652
+ import { homedir as homedir3, platform as platform4 } from "os";
4653
+ import { join as join5 } from "path";
4015
4654
  var MAX_DRIVER_OUTPUT_BYTES = 5 * 1024 * 1024;
4016
4655
  var DRIVER_ENV_KEYS = [
4017
4656
  "PATH",
@@ -4030,7 +4669,7 @@ var DRIVER_ENV_KEYS = [
4030
4669
  function candidateBins() {
4031
4670
  const candidates = [
4032
4671
  process.env.CUA_DRIVER_BIN,
4033
- join3(homedir2(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
4672
+ join5(homedir3(), ".local", "bin", process.platform === "win32" ? "cua-driver.exe" : "cua-driver"),
4034
4673
  "/usr/local/bin/cua-driver",
4035
4674
  "/opt/homebrew/bin/cua-driver"
4036
4675
  ];
@@ -4038,10 +4677,10 @@ function candidateBins() {
4038
4677
  }
4039
4678
  function findCuaDriverBinary() {
4040
4679
  for (const candidate of candidateBins()) {
4041
- if (!existsSync2(candidate))
4680
+ if (!existsSync3(candidate))
4042
4681
  continue;
4043
4682
  const resolved = realpathSync(candidate);
4044
- const stats = statSync(resolved);
4683
+ const stats = statSync2(resolved);
4045
4684
  if (!stats.isFile())
4046
4685
  throw new Error(`cua-driver is not a regular file: ${candidate}`);
4047
4686
  if (process.platform !== "win32") {
@@ -4070,7 +4709,7 @@ function driverEnvironment() {
4070
4709
  }
4071
4710
  function execFile(cmd, args, timeoutMs = 30000) {
4072
4711
  return new Promise((resolve, reject) => {
4073
- const proc = spawn(cmd, args, {
4712
+ const proc = spawn4(cmd, args, {
4074
4713
  stdio: ["ignore", "pipe", "pipe"],
4075
4714
  env: driverEnvironment()
4076
4715
  });
@@ -4153,7 +4792,7 @@ class CuaDriver {
4153
4792
  binary = null;
4154
4793
  daemonReady = false;
4155
4794
  async ensureInstalled() {
4156
- if (this.binary && existsSync2(this.binary))
4795
+ if (this.binary && existsSync3(this.binary))
4157
4796
  return this.binary;
4158
4797
  const found = findCuaDriverBinary();
4159
4798
  if (found) {
@@ -4211,15 +4850,15 @@ class CuaDriver {
4211
4850
  }
4212
4851
  async startDaemon() {
4213
4852
  const bin = await this.ensureInstalled();
4214
- if (platform3() === "darwin") {
4215
- const child = spawn("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
4853
+ if (platform4() === "darwin") {
4854
+ const child = spawn4("open", ["-n", "-g", "-a", "CuaDriver", "--args", "serve"], {
4216
4855
  detached: true,
4217
4856
  stdio: "ignore",
4218
4857
  env: driverEnvironment()
4219
4858
  });
4220
4859
  child.unref();
4221
4860
  } else {
4222
- const child = spawn(bin, ["serve"], {
4861
+ const child = spawn4(bin, ["serve"], {
4223
4862
  detached: true,
4224
4863
  stdio: "ignore",
4225
4864
  env: driverEnvironment()
@@ -4366,7 +5005,7 @@ function createDesktopCapability() {
4366
5005
 
4367
5006
  // src/agent/capabilities/filesystem.ts
4368
5007
  import { open, writeFile, readdir, stat, unlink, mkdir } from "fs/promises";
4369
- import { createHash as createHash2 } from "node:crypto";
5008
+ import { createHash as createHash3 } from "node:crypto";
4370
5009
 
4371
5010
  // src/shared/filesystem-validation.ts
4372
5011
  function validateFilesystemParams(method, params) {
@@ -4399,13 +5038,13 @@ function validateFilesystemParams(method, params) {
4399
5038
  }
4400
5039
 
4401
5040
  // src/agent/capabilities/filesystem.ts
4402
- import { join as join5, dirname as dirname3 } from "path";
5041
+ import { join as join7, dirname as dirname4 } from "path";
4403
5042
 
4404
5043
  // src/agent/security/path-validator.ts
4405
- import { dirname as dirname2, basename, join as join4, resolve, normalize, relative, isAbsolute } from "path";
5044
+ import { dirname as dirname3, basename as basename2, join as join6, resolve as resolve2, normalize, relative, isAbsolute as isAbsolute2 } from "path";
4406
5045
  import { realpathSync as realpathSync2 } from "fs";
4407
5046
  function resolveExistingRoot(path) {
4408
- const normalized = normalize(resolve(path));
5047
+ const normalized = normalize(resolve2(path));
4409
5048
  try {
4410
5049
  return resolvePathForValidation(normalized);
4411
5050
  } catch {
@@ -4413,7 +5052,7 @@ function resolveExistingRoot(path) {
4413
5052
  }
4414
5053
  }
4415
5054
  function resolvePathForValidation(path) {
4416
- const normalized = normalize(resolve(path));
5055
+ const normalized = normalize(resolve2(path));
4417
5056
  try {
4418
5057
  return realpathSync2(normalized);
4419
5058
  } catch (err) {
@@ -4421,10 +5060,10 @@ function resolvePathForValidation(path) {
4421
5060
  if (code !== "ENOENT") {
4422
5061
  throw new Error(`Access denied: cannot resolve path "${path}" (${code})`);
4423
5062
  }
4424
- const parent = dirname2(normalized);
5063
+ const parent = dirname3(normalized);
4425
5064
  if (parent === normalized)
4426
5065
  return normalized;
4427
- return join4(resolvePathForValidation(parent), basename(normalized));
5066
+ return join6(resolvePathForValidation(parent), basename2(normalized));
4428
5067
  }
4429
5068
  }
4430
5069
  function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blockedPaths = []) {
@@ -4440,13 +5079,13 @@ function assertAllowedResolvedPath(originalPath, resolved, allowedPaths, blocked
4440
5079
  return isPathInside(resolved, normalizedAllowed);
4441
5080
  });
4442
5081
  if (!withinAllowed) {
4443
- throw new Error(`Access denied: path "${originalPath}" is outside allowed directories`);
5082
+ throw new Error(`Access denied: path "${originalPath}" is outside allowed directories (${allowedPaths.join(", ")})`);
4444
5083
  }
4445
5084
  }
4446
5085
  }
4447
5086
  function isPathInside(target, root) {
4448
5087
  const child = relative(root, target);
4449
- return child === "" || !child.startsWith("..") && !isAbsolute(child);
5088
+ return child === "" || !child.startsWith("..") && !isAbsolute2(child);
4450
5089
  }
4451
5090
  function validatePath(path, allowedPaths, blockedPaths = []) {
4452
5091
  if (!path) {
@@ -4553,18 +5192,18 @@ function createFilesystemCapability(config) {
4553
5192
  throw new Error(`Content exceeds max size (${contentBytes} > ${maxFileSize})`);
4554
5193
  }
4555
5194
  const bytes = Buffer.from(content, encoding);
4556
- const expectedHash = createHash2("sha256").update(bytes).digest("hex");
5195
+ const expectedHash = createHash3("sha256").update(bytes).digest("hex");
4557
5196
  if (params.sha256 !== undefined && params.sha256.toLowerCase() !== expectedHash) {
4558
5197
  throw new Error("SHA-256 mismatch: content differs from the source; destination was not modified");
4559
5198
  }
4560
- await mkdir(dirname3(path), { recursive: true });
5199
+ await mkdir(dirname4(path), { recursive: true });
4561
5200
  validateFilesystemPath(path, config, params, true);
4562
5201
  await writeFile(path, bytes);
4563
5202
  validateFilesystemPath(path, config, params);
4564
5203
  const handle = await open(path, "r");
4565
5204
  try {
4566
5205
  const stats = await handle.stat();
4567
- const persistedHash = createHash2("sha256").update(await handle.readFile()).digest("hex");
5206
+ const persistedHash = createHash3("sha256").update(await handle.readFile()).digest("hex");
4568
5207
  if (persistedHash !== expectedHash)
4569
5208
  throw new Error("SHA-256 mismatch after write; destination verification failed");
4570
5209
  return {
@@ -4584,7 +5223,7 @@ function createFilesystemCapability(config) {
4584
5223
  const entries = await readdir(path, { withFileTypes: true });
4585
5224
  const result = entries.map((entry) => ({
4586
5225
  name: entry.name,
4587
- path: join5(path, entry.name),
5226
+ path: join7(path, entry.name),
4588
5227
  isDirectory: entry.isDirectory(),
4589
5228
  isFile: entry.isFile(),
4590
5229
  isSymlink: entry.isSymbolicLink()
@@ -4597,7 +5236,7 @@ function createFilesystemCapability(config) {
4597
5236
  for (const sub of subEntries) {
4598
5237
  result.push({
4599
5238
  name: sub.name,
4600
- path: join5(dir.path, sub.name),
5239
+ path: join7(dir.path, sub.name),
4601
5240
  isDirectory: sub.isDirectory(),
4602
5241
  isFile: sub.isFile(),
4603
5242
  isSymlink: sub.isSymbolicLink()
@@ -4638,7 +5277,7 @@ function createFilesystemCapability(config) {
4638
5277
  }
4639
5278
 
4640
5279
  // src/agent/capabilities/shell.ts
4641
- import { spawn as spawn2 } from "child_process";
5280
+ import { spawn as spawn5 } from "child_process";
4642
5281
 
4643
5282
  // src/agent/security/command-validator.ts
4644
5283
  var SHELL_METACHAR_REGEX = /[;&|`$(){}[\]<>!#~]/;
@@ -4703,7 +5342,7 @@ function createShellCapability(config) {
4703
5342
  }
4704
5343
  }
4705
5344
  return new Promise((resolve, reject) => {
4706
- const proc = spawn2(executable, args, {
5345
+ const proc = spawn5(executable, args, {
4707
5346
  cwd,
4708
5347
  shell: false,
4709
5348
  timeout,
@@ -4777,33 +5416,32 @@ function createEnabledCapabilityRegistry(config, findDesktopDriver = findCuaDriv
4777
5416
  }
4778
5417
 
4779
5418
  // src/agent/credential-store.ts
4780
- import { chmodSync as chmodSync2, existsSync as existsSync3, mkdirSync, readFileSync as readFileSync3, renameSync, rmSync, writeFileSync } from "fs";
4781
- import { homedir as homedir3 } from "os";
4782
- import { join as join6 } from "path";
4783
- var CONFIG_DIR2 = join6(homedir3(), ".agent-tunnel");
4784
- var CONFIG_FILE2 = join6(CONFIG_DIR2, "config.json");
5419
+ import { chmodSync as chmodSync3, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5420
+ import { join as join8 } from "path";
5421
+ var CONFIG_DIR2 = agentTunnelHome();
5422
+ var CONFIG_FILE2 = join8(CONFIG_DIR2, "config.json");
4785
5423
  var PAIRING_FIELDS = ["token", "tunnelId", "enabledCapabilities"];
4786
5424
  function readConfigFile() {
4787
5425
  try {
4788
- const parsed = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
5426
+ const parsed = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
4789
5427
  return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
4790
5428
  } catch {
4791
5429
  return {};
4792
5430
  }
4793
5431
  }
4794
5432
  function writeConfigFileAtomic(next) {
4795
- mkdirSync(CONFIG_DIR2, { recursive: true, mode: 448 });
5433
+ mkdirSync2(CONFIG_DIR2, { recursive: true, mode: 448 });
4796
5434
  try {
4797
- chmodSync2(CONFIG_DIR2, 448);
5435
+ chmodSync3(CONFIG_DIR2, 448);
4798
5436
  } catch {}
4799
- const tmpFile = join6(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
4800
- writeFileSync(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
5437
+ const tmpFile = join8(CONFIG_DIR2, `config.${process.pid}.${Date.now()}.tmp`);
5438
+ writeFileSync3(tmpFile, JSON.stringify(next, null, 2), { mode: 384, flag: "wx" });
4801
5439
  try {
4802
- chmodSync2(tmpFile, 384);
5440
+ chmodSync3(tmpFile, 384);
4803
5441
  } catch {}
4804
- renameSync(tmpFile, CONFIG_FILE2);
5442
+ renameSync2(tmpFile, CONFIG_FILE2);
4805
5443
  try {
4806
- chmodSync2(CONFIG_FILE2, 384);
5444
+ chmodSync3(CONFIG_FILE2, 384);
4807
5445
  } catch {}
4808
5446
  }
4809
5447
  function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
@@ -4816,13 +5454,13 @@ function saveCredentials(tunnelId, token, apiUrl, enabledCapabilities) {
4816
5454
  });
4817
5455
  }
4818
5456
  function clearSavedCredentials() {
4819
- if (!existsSync3(CONFIG_FILE2))
5457
+ if (!existsSync4(CONFIG_FILE2))
4820
5458
  return false;
4821
5459
  let existing;
4822
5460
  try {
4823
- existing = JSON.parse(readFileSync3(CONFIG_FILE2, "utf-8"));
5461
+ existing = JSON.parse(readFileSync5(CONFIG_FILE2, "utf-8"));
4824
5462
  } catch {
4825
- rmSync(CONFIG_FILE2, { force: true });
5463
+ rmSync2(CONFIG_FILE2, { force: true });
4826
5464
  return true;
4827
5465
  }
4828
5466
  const hadCredentials = PAIRING_FIELDS.some((key) => (key in existing) && existing[key] != null);
@@ -4866,13 +5504,13 @@ function probeCredentials(config, options = {}) {
4866
5504
  socket.addEventListener("message", (event) => {
4867
5505
  try {
4868
5506
  const message = JSON.parse(String(event.data));
4869
- if (isJsonRecord(message) && message.type === "auth_ok")
5507
+ if (isJsonRecord2(message) && message.type === "auth_ok")
4870
5508
  settle("valid");
4871
5509
  } catch {}
4872
5510
  });
4873
5511
  socket.addEventListener("close", (event) => {
4874
- const { code } = event;
4875
- if (AUTH_REJECTED_CLOSE_CODES.includes(code))
5512
+ const { code, reason } = event;
5513
+ if (isCredentialRejection(code, reason))
4876
5514
  return settle("rejected");
4877
5515
  if (code === AGENT_REPLACED_CLOSE_CODE)
4878
5516
  return settle("valid");
@@ -4881,172 +5519,9 @@ function probeCredentials(config, options = {}) {
4881
5519
  socket.addEventListener("error", () => settle("unreachable"));
4882
5520
  });
4883
5521
  }
4884
- function isJsonRecord(value) {
4885
- return value !== null && typeof value === "object" && !Array.isArray(value);
4886
- }
4887
-
4888
- // src/agent/device-auth.ts
4889
- import { spawn as spawn3 } from "child_process";
4890
- import { hostname as hostname3, platform as platform4 } from "os";
4891
- 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;
4892
- var SETUP_TOKEN_PATTERN = /^kortix_tnl_[A-Za-z0-9_-]{32,64}$/;
4893
- var DEVICE_CODE_PATTERN = /^[A-Z]{4}-[0-9]{4}$/;
4894
- var DEVICE_SECRET_PATTERN = /^[A-Za-z0-9]{32}$/;
4895
- var MAX_CHALLENGE_LIFETIME_MS = 10 * 60000;
4896
-
4897
- class InvalidDeviceAuthResponseError extends Error {
4898
- constructor(message) {
4899
- super(message);
4900
- this.name = "InvalidDeviceAuthResponseError";
4901
- }
4902
- }
4903
- function invalid(what) {
4904
- throw new InvalidDeviceAuthResponseError(`Authorization server returned an invalid ${what}`);
4905
- }
4906
5522
  function isJsonRecord2(value) {
4907
5523
  return value !== null && typeof value === "object" && !Array.isArray(value);
4908
5524
  }
4909
- function normalizeBrowserUrl(value) {
4910
- try {
4911
- const url = new URL(value);
4912
- return url.protocol === "https:" || url.protocol === "http:" ? url.toString() : null;
4913
- } catch {
4914
- return null;
4915
- }
4916
- }
4917
- function isLoopback(url) {
4918
- return ["localhost", "127.0.0.1", "[::1]", "::1"].includes(url.hostname);
4919
- }
4920
- function assertSafeVerificationUrl(value) {
4921
- if (value.length > 2048)
4922
- invalid("verification URL");
4923
- const browserUrl = normalizeBrowserUrl(value);
4924
- if (!browserUrl)
4925
- invalid("verification URL");
4926
- const url = new URL(browserUrl);
4927
- if (url.username || url.password || url.protocol !== "https:" && !isLoopback(url)) {
4928
- throw new InvalidDeviceAuthResponseError("Authorization server returned an unsafe verification URL");
4929
- }
4930
- return browserUrl;
4931
- }
4932
- function parseDeviceAuthChallenge(value) {
4933
- if (!isJsonRecord2(value))
4934
- invalid("challenge");
4935
- const { deviceCode, deviceSecret, verificationUrl, expiresAt, pollIntervalMs } = value;
4936
- if (typeof deviceCode !== "string" || !DEVICE_CODE_PATTERN.test(deviceCode))
4937
- invalid("device code");
4938
- if (typeof deviceSecret !== "string" || !DEVICE_SECRET_PATTERN.test(deviceSecret)) {
4939
- invalid("device secret");
4940
- }
4941
- if (typeof verificationUrl !== "string")
4942
- invalid("verification URL");
4943
- if (typeof expiresAt !== "string")
4944
- invalid("expiration");
4945
- const expiresAtMs = Date.parse(expiresAt);
4946
- const now = Date.now();
4947
- if (!Number.isFinite(expiresAtMs) || expiresAtMs <= now || expiresAtMs > now + MAX_CHALLENGE_LIFETIME_MS) {
4948
- invalid("expiration");
4949
- }
4950
- if (!Number.isSafeInteger(pollIntervalMs) || pollIntervalMs < 250 || pollIntervalMs > 1e4) {
4951
- invalid("poll interval");
4952
- }
4953
- return {
4954
- deviceCode,
4955
- deviceSecret,
4956
- verificationUrl: assertSafeVerificationUrl(verificationUrl),
4957
- expiresAt,
4958
- pollIntervalMs
4959
- };
4960
- }
4961
- function parseApprovedCredentials(value) {
4962
- const { tunnelId, token } = value;
4963
- if (typeof tunnelId !== "string" || !TUNNEL_ID_PATTERN.test(tunnelId))
4964
- invalid("tunnel ID");
4965
- if (typeof token !== "string" || !SETUP_TOKEN_PATTERN.test(token))
4966
- invalid("setup token");
4967
- return { tunnelId, token };
4968
- }
4969
- function parseApprovedCapabilities(value) {
4970
- if (!Array.isArray(value))
4971
- return [];
4972
- return [...new Set(value)].filter((item) => isTunnelCapability(item));
4973
- }
4974
- function parseDeviceAuthStatus(value) {
4975
- if (!isJsonRecord2(value) || typeof value.status !== "string")
4976
- invalid("status response");
4977
- switch (value.status) {
4978
- case "approved": {
4979
- if (!value.tunnelId || !value.token)
4980
- return { status: "approved-without-token" };
4981
- return {
4982
- status: "approved",
4983
- ...parseApprovedCredentials(value),
4984
- capabilities: parseApprovedCapabilities(value.capabilities)
4985
- };
4986
- }
4987
- case "denied":
4988
- return { status: "denied" };
4989
- case "expired":
4990
- return { status: "expired" };
4991
- default:
4992
- return null;
4993
- }
4994
- }
4995
- async function requestDeviceAuthorization(apiUrl) {
4996
- const response = await fetch(`${apiUrl}/device-auth`, {
4997
- method: "POST",
4998
- headers: { "Content-Type": "application/json" },
4999
- body: JSON.stringify({ machineHostname: hostname3() })
5000
- });
5001
- if (!response.ok) {
5002
- const body = await response.text().catch(() => "");
5003
- throw new InvalidDeviceAuthResponseError(`Failed to create device auth request: ${response.status} ${body.slice(0, 200)}`);
5004
- }
5005
- return parseDeviceAuthChallenge(await response.json());
5006
- }
5007
- async function awaitDeviceAuthorization(apiUrl, challenge, options = {}) {
5008
- const wait = options.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
5009
- const expiresAtMs = Date.parse(challenge.expiresAt);
5010
- for (;; ) {
5011
- const secondsRemaining = Math.max(0, Math.floor((expiresAtMs - Date.now()) / 1000));
5012
- if (secondsRemaining <= 0)
5013
- return { status: "expired" };
5014
- options.onWaiting?.(secondsRemaining);
5015
- let payload;
5016
- try {
5017
- const response = await fetch(`${apiUrl}/device-auth/${challenge.deviceCode}/status`, {
5018
- headers: { Authorization: `Bearer ${challenge.deviceSecret}` }
5019
- });
5020
- if (!response.ok) {
5021
- await wait(challenge.pollIntervalMs);
5022
- continue;
5023
- }
5024
- payload = await response.json();
5025
- } catch {
5026
- await wait(challenge.pollIntervalMs);
5027
- continue;
5028
- }
5029
- const outcome = parseDeviceAuthStatus(payload);
5030
- if (outcome)
5031
- return outcome;
5032
- await wait(challenge.pollIntervalMs);
5033
- }
5034
- }
5035
- function openBrowser(url) {
5036
- if (process.env.KORTIX_AGENT_TUNNEL_NO_BROWSER === "1")
5037
- return;
5038
- const safeUrl = normalizeBrowserUrl(url);
5039
- if (!safeUrl)
5040
- return;
5041
- const opener = {
5042
- darwin: ["open", [safeUrl]],
5043
- win32: ["rundll32.exe", ["url.dll,FileProtocolHandler", safeUrl]]
5044
- };
5045
- const [command, args] = opener[platform4()] ?? ["xdg-open", [safeUrl]];
5046
- try {
5047
- spawn3(command, args, { detached: true, stdio: "ignore" }).unref();
5048
- } catch {}
5049
- }
5050
5525
 
5051
5526
  // src/agent/log-format.ts
5052
5527
  function collapseRepeatedLines(lines) {
@@ -5103,15 +5578,15 @@ async function promptYesNo(question, defaultValue) {
5103
5578
  }
5104
5579
 
5105
5580
  // src/agent/service.ts
5106
- import { chmodSync as chmodSync3, copyFileSync, existsSync as existsSync5, mkdirSync as mkdirSync3, realpathSync as realpathSync3, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5581
+ import { chmodSync as chmodSync4, copyFileSync, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync7, realpathSync as realpathSync3, rmSync as rmSync4, writeFileSync as writeFileSync5 } from "fs";
5107
5582
  import { platform as platform6 } from "os";
5108
- import { join as join9 } from "path";
5583
+ import { join as join10 } from "path";
5109
5584
 
5110
5585
  // src/agent/service-drivers.ts
5111
5586
  import { spawnSync } from "child_process";
5112
- import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5, rmSync as rmSync2, writeFileSync as writeFileSync3 } from "fs";
5113
- import { homedir as homedir5, platform as platform5, userInfo } from "os";
5114
- import { dirname as dirname4, join as join8 } from "path";
5587
+ import { existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync6, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "fs";
5588
+ import { homedir as homedir4, platform as platform5, userInfo } from "os";
5589
+ import { dirname as dirname5, join as join9 } from "path";
5115
5590
 
5116
5591
  // src/agent/service-quoting.ts
5117
5592
  function shellQuote(value) {
@@ -5124,66 +5599,28 @@ function xmlEscape(value) {
5124
5599
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
5125
5600
  }
5126
5601
 
5127
- // src/agent/service-paths.ts
5128
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
5129
- import { homedir as homedir4 } from "os";
5130
- import { join as join7 } from "path";
5131
- var SERVICE_LABEL = "ai.kortix.agent-tunnel";
5132
- var DEFAULT_INSTALL_BACKGROUND_SERVICE = true;
5133
- var TERMINAL_SERVICE_EXIT_CODE = 0;
5134
- var MAX_SERVICE_LOG_BYTES = 5 * 1024 * 1024;
5135
- var RETAINED_LOG_LINES = 500;
5136
- function getServicePaths() {
5137
- const home = homedir4();
5138
- const configDir = join7(home, ".agent-tunnel");
5139
- const binDir = join7(configDir, "bin");
5140
- return {
5141
- configDir,
5142
- logDir: join7(configDir, "logs"),
5143
- binDir,
5144
- vendoredRunner: join7(binDir, "agent-cli.js"),
5145
- launchdPlist: join7(home, "Library", "LaunchAgents", `${SERVICE_LABEL}.plist`),
5146
- systemdUnit: join7(home, ".config", "systemd", "user", `${SERVICE_LABEL}.service`),
5147
- windowsScript: join7(configDir, "agent-tunnel-service.ps1")
5148
- };
5149
- }
5150
- function serviceLogFiles(paths = getServicePaths()) {
5151
- return [
5152
- join7(paths.logDir, "agent-tunnel.out.log"),
5153
- join7(paths.logDir, "agent-tunnel.err.log")
5154
- ];
5155
- }
5156
- function rotateServiceLogs(paths = getServicePaths(), maxBytes = MAX_SERVICE_LOG_BYTES) {
5157
- const rotated = [];
5158
- for (const file of serviceLogFiles(paths)) {
5159
- try {
5160
- const contents = readFileSync4(file, "utf8");
5161
- if (Buffer.byteLength(contents, "utf8") <= maxBytes)
5162
- continue;
5163
- const kept = contents.split(/\r?\n/).slice(-RETAINED_LOG_LINES).join(`
5164
- `);
5165
- writeFileSync2(file, `[agent-tunnel] earlier entries trimmed
5166
- ${kept}`, { mode: 384 });
5167
- rotated.push(file);
5168
- } catch {}
5169
- }
5170
- return rotated;
5171
- }
5172
-
5173
5602
  // src/agent/service-drivers.ts
5174
5603
  function posixShellCommand(runner) {
5175
5604
  const interpreter = `"$(command -v ${shellQuote(runner.command)} 2>/dev/null || command -v node)"`;
5176
- return `exec ${interpreter} ${runner.args.map(shellQuote).join(" ")}`;
5605
+ const env = Object.entries(runner.env ?? {});
5606
+ const exports = env.length ? `export ${env.map(([key, value]) => `${key}=${shellQuote(value)}`).join(" ")}; ` : "";
5607
+ return `${exports}exec ${interpreter} ${runner.args.map(shellQuote).join(" ")}`;
5177
5608
  }
5178
- function run(command, args) {
5179
- const result = spawnSync(command, args, { encoding: "utf8" });
5180
- return {
5181
- ok: result.status === 0,
5182
- detail: [result.stdout, result.stderr].filter(Boolean).join(`
5609
+ var supervisor = {
5610
+ run(command, args) {
5611
+ const result = spawnSync(command, args, { encoding: "utf8" });
5612
+ return {
5613
+ ok: result.status === 0,
5614
+ detail: [result.stdout, result.stderr].filter(Boolean).join(`
5183
5615
  `).trim()
5184
- };
5185
- }
5616
+ };
5617
+ }
5618
+ };
5619
+ var run = (command, args) => supervisor.run(command, args);
5186
5620
  var notInstalled = (what) => ({ ok: false, detail: `${what} is not installed.` });
5621
+ var pause = { ms(ms) {
5622
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
5623
+ } };
5187
5624
  function joinDetails(...results) {
5188
5625
  return results.map((result) => result.detail).filter(Boolean).join(`
5189
5626
  `);
@@ -5194,8 +5631,8 @@ function launchdTarget() {
5194
5631
  }
5195
5632
  function logPaths(paths) {
5196
5633
  return {
5197
- stdout: join8(paths.logDir, "agent-tunnel.out.log"),
5198
- stderr: join8(paths.logDir, "agent-tunnel.err.log")
5634
+ stdout: join9(paths.logDir, "agent-tunnel.out.log"),
5635
+ stderr: join9(paths.logDir, "agent-tunnel.err.log")
5199
5636
  };
5200
5637
  }
5201
5638
  function renderLaunchdPlist(command, paths = getServicePaths()) {
@@ -5205,7 +5642,7 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
5205
5642
  <plist version="1.0">
5206
5643
  <dict>
5207
5644
  <key>Label</key>
5208
- <string>${xmlEscape(SERVICE_LABEL)}</string>
5645
+ <string>${xmlEscape(paths.label)}</string>
5209
5646
  <key>ProgramArguments</key>
5210
5647
  <array>
5211
5648
  <string>/bin/sh</string>
@@ -5215,10 +5652,9 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
5215
5652
  <key>RunAtLoad</key>
5216
5653
  <true/>
5217
5654
  <key>KeepAlive</key>
5218
- <dict>
5219
- <key>SuccessfulExit</key>
5220
- <false/>
5221
- </dict>
5655
+ <true/>
5656
+ <key>ThrottleInterval</key>
5657
+ <integer>10</integer>
5222
5658
  <key>Umask</key>
5223
5659
  <integer>63</integer>
5224
5660
  <key>StandardOutPath</key>
@@ -5226,7 +5662,7 @@ function renderLaunchdPlist(command, paths = getServicePaths()) {
5226
5662
  <key>StandardErrorPath</key>
5227
5663
  <string>${xmlEscape(stderr)}</string>
5228
5664
  <key>WorkingDirectory</key>
5229
- <string>${xmlEscape(homedir5())}</string>
5665
+ <string>${xmlEscape(homedir4())}</string>
5230
5666
  <key>EnvironmentVariables</key>
5231
5667
  <dict>
5232
5668
  <key>PATH</key>
@@ -5247,9 +5683,9 @@ Wants=network-online.target
5247
5683
  Type=simple
5248
5684
  UMask=0077
5249
5685
  ExecStart=/bin/sh -lc ${shellQuote(command)}
5250
- Restart=on-failure
5686
+ Restart=always
5251
5687
  RestartSec=5
5252
- WorkingDirectory=${homedir5()}
5688
+ WorkingDirectory=${homedir4()}
5253
5689
  Environment=PATH=/usr/local/bin:/usr/bin:/bin
5254
5690
  StandardOutput=append:${stdout}
5255
5691
  StandardError=append:${stderr}
@@ -5261,119 +5697,206 @@ WantedBy=default.target
5261
5697
  function renderWindowsPowerShellScript(runner) {
5262
5698
  const command = powershellQuote(runner.command);
5263
5699
  const args = runner.args.map(powershellQuote).join(" ");
5700
+ const env = Object.entries(runner.env ?? {}).map(([key, value]) => `$env:${key} = ${powershellQuote(value)}
5701
+ `).join("");
5264
5702
  return `$ErrorActionPreference = 'Continue'
5265
- while ($true) {
5266
- & ${command}${args ? ` ${args}` : ""}
5267
- # A clean exit means the agent stopped for a reason restarting cannot fix,
5268
- # such as a missing or revoked credential. Anything else is a crash worth retrying.
5269
- if ($LASTEXITCODE -eq ${TERMINAL_SERVICE_EXIT_CODE}) { break }
5703
+ ${env}while ($true) {
5704
+ # The pipe makes PowerShell wait for a GUI-subsystem binary (the desktop
5705
+ # app's Kortix.exe) and set $LASTEXITCODE; a bare call returns at once.
5706
+ & ${command}${args ? ` ${args}` : ""} | Out-Null
5707
+ # The agent never exits on its own (R2), so every exit is restarted.
5270
5708
  Start-Sleep -Seconds 5
5271
5709
  }
5272
5710
  `;
5273
5711
  }
5274
5712
  var launchd = {
5275
5713
  unitPath: (paths) => paths.launchdPlist,
5714
+ render: (paths, runner) => renderLaunchdPlist(posixShellCommand(runner), paths),
5276
5715
  install(paths, runner) {
5277
- mkdirSync2(dirname4(paths.launchdPlist), { recursive: true });
5278
- writeFileSync3(paths.launchdPlist, renderLaunchdPlist(posixShellCommand(runner), paths), { mode: 384 });
5716
+ mkdirSync3(dirname5(paths.launchdPlist), { recursive: true });
5717
+ writeFileSync4(paths.launchdPlist, launchd.render(paths, runner), { mode: 384 });
5279
5718
  run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5280
- const boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
5281
- const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
5282
- return { installed: true, active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
5719
+ run("launchctl", ["enable", `${launchdTarget()}/${paths.label}`]);
5720
+ let boot = { ok: false, detail: "" };
5721
+ let kick = { ok: false, detail: "" };
5722
+ for (let attempt = 0;attempt < 3; attempt++) {
5723
+ if (attempt > 0)
5724
+ pause.ms(1000);
5725
+ boot = run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]);
5726
+ kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${paths.label}`]);
5727
+ if (run("launchctl", ["print", `${launchdTarget()}/${paths.label}`]).ok) {
5728
+ return { installed: true, active: true, detail: joinDetails(boot, kick) };
5729
+ }
5730
+ }
5731
+ return { installed: true, active: null, detail: joinDetails(boot, kick) };
5283
5732
  },
5284
5733
  uninstall(paths) {
5285
- const existed = existsSync4(paths.launchdPlist);
5734
+ const existed = existsSync5(paths.launchdPlist);
5286
5735
  const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5287
5736
  if (existed)
5288
- rmSync2(paths.launchdPlist, { force: true });
5737
+ rmSync3(paths.launchdPlist, { force: true });
5289
5738
  return { detail: stop.detail };
5290
5739
  },
5291
5740
  start(paths, installed) {
5292
5741
  const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
5293
- const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${SERVICE_LABEL}`]);
5742
+ const kick = run("launchctl", ["kickstart", "-k", `${launchdTarget()}/${paths.label}`]);
5294
5743
  return { active: boot.ok || kick.ok ? true : null, detail: joinDetails(boot, kick) };
5295
5744
  },
5296
5745
  stop(paths, installed) {
5297
5746
  const stop = installed ? run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
5298
5747
  return { detail: stop.detail };
5299
5748
  },
5749
+ pause(paths) {
5750
+ const disable = run("launchctl", ["disable", `${launchdTarget()}/${paths.label}`]);
5751
+ const stop = run("launchctl", ["bootout", launchdTarget(), paths.launchdPlist]);
5752
+ return { enabled: disable.ok ? false : undefined, detail: joinDetails(disable, stop) };
5753
+ },
5754
+ resume(paths, installed) {
5755
+ const enable = run("launchctl", ["enable", `${launchdTarget()}/${paths.label}`]);
5756
+ const boot = installed ? run("launchctl", ["bootstrap", launchdTarget(), paths.launchdPlist]) : notInstalled("LaunchAgent");
5757
+ const kick = run("launchctl", ["kickstart", `${launchdTarget()}/${paths.label}`]);
5758
+ return { enabled: enable.ok, active: boot.ok || kick.ok ? true : null, detail: joinDetails(enable, boot, kick) };
5759
+ },
5300
5760
  status(paths, installed) {
5301
- const status = run("launchctl", ["print", `${launchdTarget()}/${SERVICE_LABEL}`]);
5761
+ const disabled = run("launchctl", ["print-disabled", launchdTarget()]);
5762
+ const enabled = !new RegExp(`"${paths.label.replace(/\./g, "\\.")}" => (disabled|true)`).test(disabled.detail);
5763
+ const status = run("launchctl", ["print", `${launchdTarget()}/${paths.label}`]);
5302
5764
  return {
5303
5765
  active: status.ok,
5304
- detail: status.detail || (installed ? readFileSync5(paths.launchdPlist, "utf8") : undefined)
5766
+ enabled,
5767
+ detail: status.detail || (installed ? readFileSync6(paths.launchdPlist, "utf8") : undefined)
5305
5768
  };
5306
5769
  }
5307
5770
  };
5308
5771
  var systemd = {
5309
5772
  unitPath: (paths) => paths.systemdUnit,
5773
+ render: (paths, runner) => renderSystemdUnit(posixShellCommand(runner), paths),
5310
5774
  install(paths, runner) {
5311
- mkdirSync2(dirname4(paths.systemdUnit), { recursive: true });
5312
- writeFileSync3(paths.systemdUnit, renderSystemdUnit(posixShellCommand(runner), paths), { mode: 384 });
5775
+ mkdirSync3(dirname5(paths.systemdUnit), { recursive: true });
5776
+ writeFileSync4(paths.systemdUnit, systemd.render(paths, runner), { mode: 384 });
5313
5777
  const reload = run("systemctl", ["--user", "daemon-reload"]);
5314
- const enable = run("systemctl", ["--user", "enable", "--now", `${SERVICE_LABEL}.service`]);
5315
- return { installed: true, active: enable.ok ? true : null, detail: joinDetails(reload, enable) };
5778
+ const enable = run("systemctl", ["--user", "enable", `${paths.label}.service`]);
5779
+ const restart = run("systemctl", ["--user", "restart", `${paths.label}.service`]);
5780
+ return {
5781
+ installed: true,
5782
+ active: restart.ok ? true : null,
5783
+ detail: joinDetails(reload, enable, restart, ensureLinger())
5784
+ };
5316
5785
  },
5317
5786
  uninstall(paths) {
5318
- const existed = existsSync4(paths.systemdUnit);
5319
- const disable = run("systemctl", ["--user", "disable", "--now", `${SERVICE_LABEL}.service`]);
5787
+ const existed = existsSync5(paths.systemdUnit);
5788
+ const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
5320
5789
  if (existed)
5321
- rmSync2(paths.systemdUnit, { force: true });
5790
+ rmSync3(paths.systemdUnit, { force: true });
5322
5791
  run("systemctl", ["--user", "daemon-reload"]);
5323
5792
  return { detail: disable.detail };
5324
5793
  },
5325
- start(_paths, installed) {
5326
- const start = installed ? run("systemctl", ["--user", "start", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
5794
+ start(paths, installed) {
5795
+ const start = installed ? run("systemctl", ["--user", "start", `${paths.label}.service`]) : notInstalled("systemd unit");
5327
5796
  return { active: start.ok ? true : null, detail: start.detail };
5328
5797
  },
5329
- stop(_paths, installed) {
5330
- const stop = installed ? run("systemctl", ["--user", "stop", `${SERVICE_LABEL}.service`]) : notInstalled("systemd unit");
5798
+ stop(paths, installed) {
5799
+ const stop = installed ? run("systemctl", ["--user", "stop", `${paths.label}.service`]) : notInstalled("systemd unit");
5331
5800
  return { detail: stop.detail };
5332
5801
  },
5333
- status() {
5334
- const status = run("systemctl", ["--user", "is-active", `${SERVICE_LABEL}.service`]);
5335
- return { active: status.ok, detail: status.detail };
5802
+ pause(paths) {
5803
+ const disable = run("systemctl", ["--user", "disable", "--now", `${paths.label}.service`]);
5804
+ return { enabled: disable.ok ? false : undefined, active: false, detail: disable.detail };
5805
+ },
5806
+ resume(paths) {
5807
+ const enable = run("systemctl", ["--user", "enable", "--now", `${paths.label}.service`]);
5808
+ return { enabled: enable.ok, active: enable.ok ? true : null, detail: enable.detail };
5809
+ },
5810
+ status(paths) {
5811
+ const status = run("systemctl", ["--user", "is-active", `${paths.label}.service`]);
5812
+ const enabled = run("systemctl", ["--user", "is-enabled", `${paths.label}.service`]);
5813
+ return { active: status.ok, enabled: enabled.ok, detail: status.detail };
5336
5814
  }
5337
5815
  };
5816
+ function ensureLinger() {
5817
+ const user = userInfo().username;
5818
+ const linger = run("loginctl", ["show-user", user, "-p", "Linger"]);
5819
+ if (/Linger=yes/.test(linger.detail))
5820
+ return { ok: true, detail: "" };
5821
+ if (run("loginctl", ["enable-linger", user]).ok)
5822
+ return { ok: true, detail: "Enabled lingering: the service keeps running after logout and starts at boot." };
5823
+ return {
5824
+ ok: false,
5825
+ detail: `Warning: the service stops when you log out and starts only at your next login. Run \`loginctl enable-linger ${user}\` (may need sudo) to keep it running.`
5826
+ };
5827
+ }
5828
+ function renderWindowsTaskXml(paths, user) {
5829
+ return `<?xml version="1.0" encoding="UTF-16"?>
5830
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
5831
+ <RegistrationInfo><Description>Kortix Agent Tunnel</Description></RegistrationInfo>
5832
+ <Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xmlEscape(user)}</UserId></LogonTrigger></Triggers>
5833
+ <Principals><Principal id="Author"><UserId>${xmlEscape(user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
5834
+ <Settings>
5835
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
5836
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
5837
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
5838
+ <ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
5839
+ <StartWhenAvailable>true</StartWhenAvailable>
5840
+ <Enabled>true</Enabled>
5841
+ </Settings>
5842
+ <Actions Context="Author"><Exec><Command>powershell.exe</Command><Arguments>${xmlEscape(`-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${paths.windowsScript}"`)}</Arguments></Exec></Actions>
5843
+ </Task>
5844
+ `;
5845
+ }
5846
+ var windowsUser = () => process.env.USERDOMAIN ? `${process.env.USERDOMAIN}\\${userInfo().username}` : userInfo().username;
5847
+ var windowsTaskXmlPath = (paths) => paths.windowsScript.replace(/\.ps1$/, ".xml");
5338
5848
  var scheduledTask = {
5339
5849
  unitPath: (paths) => paths.windowsScript,
5850
+ render: (_paths, runner) => renderWindowsPowerShellScript(runner),
5340
5851
  install(paths, runner) {
5341
- writeFileSync3(paths.windowsScript, renderWindowsPowerShellScript(runner), { mode: 384 });
5342
- const create = run("schtasks.exe", [
5343
- "/Create",
5344
- "/TN",
5345
- SERVICE_LABEL,
5346
- "/TR",
5347
- `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "${paths.windowsScript}"`,
5348
- "/SC",
5349
- "ONLOGON",
5350
- "/F",
5351
- "/RL",
5352
- "LIMITED"
5353
- ]);
5354
- const start = run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]);
5852
+ writeFileSync4(paths.windowsScript, renderWindowsPowerShellScript(runner), { mode: 384 });
5853
+ const xml = windowsTaskXmlPath(paths);
5854
+ writeFileSync4(xml, Buffer.from(`\uFEFF${renderWindowsTaskXml(paths, windowsUser())}`, "utf16le"), { mode: 384 });
5855
+ const create = run("schtasks.exe", ["/Create", "/TN", paths.label, "/XML", xml, "/F"]);
5856
+ run("schtasks.exe", ["/End", "/TN", paths.label]);
5857
+ const start = run("schtasks.exe", ["/Run", "/TN", paths.label]);
5355
5858
  return { installed: create.ok, active: start.ok ? true : null, detail: joinDetails(create, start) };
5356
5859
  },
5357
5860
  uninstall(paths) {
5358
- const existed = existsSync4(paths.windowsScript);
5359
- const stop = run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]);
5360
- const del = run("schtasks.exe", ["/Delete", "/TN", SERVICE_LABEL, "/F"]);
5861
+ const existed = existsSync5(paths.windowsScript);
5862
+ const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
5863
+ const del = run("schtasks.exe", ["/Delete", "/TN", paths.label, "/F"]);
5361
5864
  if (existed)
5362
- rmSync2(paths.windowsScript, { force: true });
5865
+ rmSync3(paths.windowsScript, { force: true });
5866
+ rmSync3(windowsTaskXmlPath(paths), { force: true });
5363
5867
  return { detail: joinDetails(stop, del) };
5364
5868
  },
5365
- start(_paths, installed) {
5366
- const start = installed ? run("schtasks.exe", ["/Run", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
5869
+ start(paths, installed) {
5870
+ const start = installed ? run("schtasks.exe", ["/Run", "/TN", paths.label]) : notInstalled("Scheduled Task");
5367
5871
  return { active: start.ok ? true : null, detail: start.detail };
5368
5872
  },
5369
- stop(_paths, installed) {
5370
- const stop = installed ? run("schtasks.exe", ["/End", "/TN", SERVICE_LABEL]) : notInstalled("Scheduled Task");
5873
+ stop(paths, installed) {
5874
+ const stop = installed ? run("schtasks.exe", ["/End", "/TN", paths.label]) : notInstalled("Scheduled Task");
5371
5875
  return { detail: stop.detail };
5372
5876
  },
5877
+ pause(paths) {
5878
+ const disable = run("schtasks.exe", ["/Change", "/TN", paths.label, "/DISABLE"]);
5879
+ const stop = run("schtasks.exe", ["/End", "/TN", paths.label]);
5880
+ return { enabled: disable.ok ? false : undefined, active: false, detail: joinDetails(disable, stop) };
5881
+ },
5882
+ resume(paths) {
5883
+ const enable = run("schtasks.exe", ["/Change", "/TN", paths.label, "/ENABLE"]);
5884
+ const start = run("schtasks.exe", ["/Run", "/TN", paths.label]);
5885
+ return { enabled: enable.ok, active: start.ok ? true : null, detail: joinDetails(enable, start) };
5886
+ },
5373
5887
  status(paths, installed) {
5374
- const status = run("schtasks.exe", ["/Query", "/TN", SERVICE_LABEL, "/FO", "LIST", "/V"]);
5375
- const detail = status.detail || (installed ? readFileSync5(paths.windowsScript, "utf8") : undefined);
5376
- return { active: status.ok ? /Status:\s*Running/i.test(detail ?? "") : false, detail };
5888
+ const status = run("powershell.exe", [
5889
+ "-NoProfile",
5890
+ "-NonInteractive",
5891
+ "-Command",
5892
+ `(Get-ScheduledTask -TaskName ${powershellQuote(paths.label)} -ErrorAction Stop).State`
5893
+ ]);
5894
+ const state = status.ok ? status.detail.trim() : "";
5895
+ return {
5896
+ active: state === "Running",
5897
+ enabled: state !== "Disabled",
5898
+ detail: state || (installed ? readFileSync6(paths.windowsScript, "utf8") : undefined)
5899
+ };
5377
5900
  }
5378
5901
  };
5379
5902
  var DRIVERS = {
@@ -5382,32 +5905,51 @@ var DRIVERS = {
5382
5905
  win32: scheduledTask
5383
5906
  };
5384
5907
  var SUPPORTED_PLATFORMS_MESSAGE = "Background services are supported on macOS launchd, Linux systemd user services, and Windows Scheduled Tasks.";
5908
+ function serviceDriverFor(os) {
5909
+ return DRIVERS[os];
5910
+ }
5385
5911
  function serviceDriver() {
5386
- return DRIVERS[platform5()];
5912
+ return serviceDriverFor(platform5());
5387
5913
  }
5388
5914
 
5389
5915
  // src/agent/service.ts
5390
5916
  function isEphemeralRunnerPath(path) {
5391
5917
  const normalized = path.replace(/\\/g, "/");
5392
- return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/");
5918
+ return normalized.includes("/_npx/") || normalized.includes("/_cacache/") || normalized.includes("/.pnpm-store/") || normalized.includes("/.yarn/$$virtual/") || normalized.includes("/.mount_");
5393
5919
  }
5394
5920
  function vendorRunner(scriptPath, paths = getServicePaths()) {
5395
5921
  if (!isEphemeralRunnerPath(scriptPath))
5396
5922
  return scriptPath;
5397
- mkdirSync3(paths.binDir, { recursive: true, mode: 448 });
5923
+ mkdirSync4(paths.binDir, { recursive: true, mode: 448 });
5398
5924
  const source = realpathSync3(scriptPath);
5399
5925
  copyFileSync(source, paths.vendoredRunner);
5400
5926
  try {
5401
- chmodSync3(paths.vendoredRunner, 448);
5927
+ chmodSync4(paths.vendoredRunner, 448);
5402
5928
  } catch {}
5403
- writeFileSync4(join9(paths.binDir, "agent-cli.source.json"), JSON.stringify({ source, vendoredFrom: scriptPath }, null, 2), { mode: 384 });
5929
+ writeFileSync5(join10(paths.binDir, "package.json"), JSON.stringify({ name: "@kortix/agent-tunnel", version: agentTunnelVersion(), private: true }), { mode: 384 });
5930
+ writeFileSync5(join10(paths.binDir, "agent-cli.source.json"), JSON.stringify({ source, vendoredFrom: scriptPath }, null, 2), { mode: 384 });
5404
5931
  return paths.vendoredRunner;
5405
5932
  }
5406
- function currentRunnerParts() {
5407
- const script = process.argv[1];
5408
- if (script && existsSync5(script)) {
5409
- return { command: process.execPath, args: [vendorRunner(script), "run", "--service"] };
5933
+ function runnerPartsFor(script, runtime = {
5934
+ execPath: process.execPath,
5935
+ electron: process.versions.electron,
5936
+ appImage: process.env.APPIMAGE
5937
+ }, paths = getServicePaths(), vendor = vendorRunner) {
5938
+ const env = serviceHomeEnv(paths.configDir);
5939
+ if (runtime.electron) {
5940
+ return {
5941
+ command: runtime.appImage || runtime.execPath,
5942
+ args: [runtime.appImage ? vendor(script, paths) : script, "run", "--service"],
5943
+ env: { ...env, ELECTRON_RUN_AS_NODE: "1" }
5944
+ };
5410
5945
  }
5946
+ return { command: runtime.execPath, args: [vendor(script, paths), "run", "--service"], env };
5947
+ }
5948
+ var plannedRunner = (script, paths) => isEphemeralRunnerPath(script) ? paths.vendoredRunner : script;
5949
+ function currentRunnerParts(vendor = vendorRunner) {
5950
+ const script = process.argv[1];
5951
+ if (script && existsSync6(script))
5952
+ return runnerPartsFor(script, undefined, getServicePaths(), vendor);
5411
5953
  throw new Error("Cannot install the background service because the current Agent Tunnel executable was not found");
5412
5954
  }
5413
5955
  function withDriver(operate, fallback) {
@@ -5416,26 +5958,28 @@ function withDriver(operate, fallback) {
5416
5958
  if (!driver)
5417
5959
  throw new Error(SUPPORTED_PLATFORMS_MESSAGE);
5418
5960
  const path = driver.unitPath(paths);
5419
- const installed = existsSync5(path);
5961
+ const installed = existsSync6(path);
5420
5962
  const outcome = operate(driver, paths, installed);
5421
5963
  return {
5422
5964
  platform: platform6(),
5423
5965
  installed: outcome.installed ?? installed,
5424
5966
  active: outcome.active ?? fallback.active,
5967
+ ...outcome.enabled !== undefined ? { enabled: outcome.enabled } : {},
5968
+ ...outcome.upToDate !== undefined ? { upToDate: outcome.upToDate } : {},
5425
5969
  path,
5426
5970
  detail: outcome.detail
5427
5971
  };
5428
5972
  }
5429
5973
  function installService() {
5430
5974
  const paths = getServicePaths();
5431
- mkdirSync3(paths.configDir, { recursive: true, mode: 448 });
5432
- mkdirSync3(paths.logDir, { recursive: true, mode: 448 });
5975
+ mkdirSync4(paths.configDir, { recursive: true, mode: 448 });
5976
+ mkdirSync4(paths.logDir, { recursive: true, mode: 448 });
5433
5977
  rotateServiceLogs(paths);
5434
5978
  const runner = currentRunnerParts();
5435
5979
  return withDriver((driver, servicePaths) => driver.install(servicePaths, runner), { installed: false, active: null });
5436
5980
  }
5437
5981
  function uninstallService() {
5438
- rmSync3(getServicePaths().binDir, { recursive: true, force: true });
5982
+ rmSync4(getServicePaths().binDir, { recursive: true, force: true });
5439
5983
  return withDriver((driver, paths) => ({ ...driver.uninstall(paths), installed: false, active: false }), { installed: false, active: false });
5440
5984
  }
5441
5985
  function startService() {
@@ -5444,6 +5988,18 @@ function startService() {
5444
5988
  function stopService() {
5445
5989
  return withDriver((driver, paths, installed) => ({ ...driver.stop(paths, installed), active: false }), { installed: false, active: false });
5446
5990
  }
5991
+ function pauseService() {
5992
+ return withDriver((driver, paths, installed) => ({ ...driver.pause(paths, installed), active: false }), { installed: false, active: false });
5993
+ }
5994
+ function resumeService() {
5995
+ const current = getServiceStatus();
5996
+ if (current.installed && current.upToDate === false)
5997
+ return installService();
5998
+ return resumeUnit();
5999
+ }
6000
+ function resumeUnit() {
6001
+ return withDriver((driver, paths, installed) => driver.resume(paths, installed), { installed: false, active: null });
6002
+ }
5447
6003
  function restartService() {
5448
6004
  stopService();
5449
6005
  return startService();
@@ -5457,7 +6013,13 @@ function getServiceStatus() {
5457
6013
  detail: SUPPORTED_PLATFORMS_MESSAGE
5458
6014
  };
5459
6015
  }
5460
- return withDriver((driver, paths, installed) => driver.status(paths, installed), { installed: false, active: null });
6016
+ return withDriver((driver, paths, installed) => {
6017
+ let upToDate = false;
6018
+ try {
6019
+ upToDate = installed && readFileSync7(driver.unitPath(paths), "utf8") === driver.render(paths, currentRunnerParts(plannedRunner));
6020
+ } catch {}
6021
+ return { ...driver.status(paths, installed), upToDate };
6022
+ }, { installed: false, active: null });
5461
6023
  }
5462
6024
 
5463
6025
  // src/agent/service-control.ts
@@ -5477,8 +6039,8 @@ function acquireTunnelLease() {
5477
6039
  };
5478
6040
  }
5479
6041
  var SERVICE_ACTIONS = {
5480
- start: { run: startService, label: "started" },
5481
- stop: { run: stopService, label: "stopped" },
6042
+ start: { run: resumeService, label: "started" },
6043
+ stop: { run: pauseService, label: "paused until `agent-tunnel start`" },
5482
6044
  restart: { run: restartService, label: "restarted" },
5483
6045
  uninstall: { run: uninstallService, label: "removed" },
5484
6046
  install: { run: installService, label: "installed" }
@@ -5486,6 +6048,8 @@ var SERVICE_ACTIONS = {
5486
6048
  function describeService(status) {
5487
6049
  if (!status.installed)
5488
6050
  return `${glyph.off} not installed`;
6051
+ if (status.enabled === false)
6052
+ return `${c.yellow}○${c.reset} paused ${c.dim}· stays stopped until \`agent-tunnel start\`${c.reset}`;
5489
6053
  if (status.active)
5490
6054
  return `${glyph.on} running ${c.dim}· starts at login${c.reset}`;
5491
6055
  return `${c.yellow}○${c.reset} installed ${c.dim}· stopped${c.reset}`;
@@ -5500,6 +6064,41 @@ function renderServiceAction(action, status) {
5500
6064
  blankLine();
5501
6065
  }
5502
6066
 
6067
+ // src/agent/state-file.ts
6068
+ import { chmodSync as chmodSync5, mkdirSync as mkdirSync5, readFileSync as readFileSync8, renameSync as renameSync3, writeFileSync as writeFileSync6 } from "fs";
6069
+ import { join as join11 } from "path";
6070
+ function stateFilePath(home = agentTunnelHome()) {
6071
+ return join11(home, "state.json");
6072
+ }
6073
+ function writeAgentState(status, tunnel, home = agentTunnelHome()) {
6074
+ const state = {
6075
+ tunnelId: tunnel.tunnelId,
6076
+ apiUrl: tunnel.apiUrl,
6077
+ status,
6078
+ since: new Date().toISOString(),
6079
+ agentVersion: agentTunnelVersion(),
6080
+ pid: process.pid
6081
+ };
6082
+ try {
6083
+ mkdirSync5(home, { recursive: true, mode: 448 });
6084
+ const tmp = join11(home, `state.${process.pid}.${Date.now()}.tmp`);
6085
+ writeFileSync6(tmp, `${JSON.stringify(state, null, 2)}
6086
+ `, { mode: 384 });
6087
+ try {
6088
+ chmodSync5(tmp, 384);
6089
+ } catch {}
6090
+ renameSync3(tmp, stateFilePath(home));
6091
+ } catch {}
6092
+ }
6093
+ function readAgentState(home = agentTunnelHome()) {
6094
+ try {
6095
+ const parsed = JSON.parse(readFileSync8(stateFilePath(home), "utf8"));
6096
+ return parsed && typeof parsed === "object" && typeof parsed.status === "string" ? parsed : null;
6097
+ } catch {
6098
+ return null;
6099
+ }
6100
+ }
6101
+
5503
6102
  // src/agent/cli.ts
5504
6103
  var ALL_CAPABILITIES = ["filesystem", "shell", "desktop"];
5505
6104
  var BACKGROUND_FLAGS = ["daemon", "service", "background", "always-online"];
@@ -5516,8 +6115,17 @@ function parseArgs(argv) {
5516
6115
  }
5517
6116
  return { command: argv[2] || "help", flags };
5518
6117
  }
6118
+ var jsonMode = false;
6119
+ var writeStdout = process.stdout.write.bind(process.stdout);
6120
+ function emit(event, fields = {}) {
6121
+ writeStdout(`${JSON.stringify({ event, ...fields })}
6122
+ `);
6123
+ }
5519
6124
  function fail(message) {
5520
- console.error(` ${glyph.bad} ${message}`);
6125
+ if (jsonMode)
6126
+ emit("error", { message });
6127
+ else
6128
+ console.error(` ${glyph.bad} ${message}`);
5521
6129
  process.exit(1);
5522
6130
  }
5523
6131
  function shortenHomePath(path) {
@@ -5525,12 +6133,17 @@ function shortenHomePath(path) {
5525
6133
  return home && path.startsWith(home) ? `~${path.slice(home.length)}` : path;
5526
6134
  }
5527
6135
  function startAgent(config, options = {}) {
6136
+ if (jsonMode) {
6137
+ process.stdout.write = process.stderr.write.bind(process.stderr);
6138
+ options = { ...options, service: true };
6139
+ }
5528
6140
  const registry = createEnabledCapabilityRegistry(config);
5529
6141
  if (config.enabledCapabilities?.includes("desktop") && !registry.has("desktop")) {
5530
6142
  console.error("[agent-tunnel] Computer Use is approved but unavailable: install the trusted cua-driver locally, then restart Agent Tunnel.");
5531
6143
  }
5532
6144
  if (options.service) {
5533
- console.log(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}`);
6145
+ process.stdout.write(`[agent-tunnel] service starting: ${config.tunnelId} -> ${config.apiUrl}
6146
+ `);
5534
6147
  } else {
5535
6148
  clearScreen();
5536
6149
  printStartupBanner({
@@ -5540,13 +6153,9 @@ function startAgent(config, options = {}) {
5540
6153
  version: agentTunnelVersion()
5541
6154
  });
5542
6155
  }
5543
- const agent = new TunnelAgent(config, registry, {
5544
- onTerminalClose: ({ reason }) => {
5545
- if (!options.service)
5546
- return;
5547
- console.log(`[agent-tunnel] stopping service: ${reason}`);
5548
- process.exit(TERMINAL_SERVICE_EXIT_CODE);
5549
- }
6156
+ const agent = new TunnelAgent(config, registry, { onStatus: (status, current) => writeAgentState(status, current) }, {
6157
+ persistent: options.service === true,
6158
+ reloadConfig: () => loadConfig({ apiUrl: config.apiUrl })
5550
6159
  });
5551
6160
  agent.connect();
5552
6161
  const shutdown = () => {
@@ -5564,7 +6173,7 @@ async function chooseBackgroundMode(flags) {
5564
6173
  return true;
5565
6174
  if (anyFlag(flags, FOREGROUND_FLAGS))
5566
6175
  return false;
5567
- if (!isInteractiveTerminal())
6176
+ if (jsonMode || !isInteractiveTerminal())
5568
6177
  return false;
5569
6178
  blankLine();
5570
6179
  console.log(` ${glyph.warn} ${c.bold}Security note${c.reset}`);
@@ -5576,45 +6185,72 @@ async function chooseBackgroundMode(flags) {
5576
6185
  async function launch(config, flags, lease) {
5577
6186
  if (await chooseBackgroundMode(flags)) {
5578
6187
  saveCredentials(config.tunnelId, config.token, config.apiUrl);
5579
- renderServiceAction("install", SERVICE_ACTIONS.install.run());
6188
+ if (!jsonMode) {
6189
+ renderServiceAction("install", SERVICE_ACTIONS.install.run());
6190
+ return;
6191
+ }
6192
+ try {
6193
+ const status = SERVICE_ACTIONS.install.run();
6194
+ const ok = status.installed && status.active === true;
6195
+ emit("service", { action: "install", ok, active: status.active, detail: status.detail ?? "" });
6196
+ if (!ok)
6197
+ process.exitCode = 1;
6198
+ } catch (error) {
6199
+ emit("service", { action: "install", ok: false, detail: error instanceof Error ? error.message : String(error) });
6200
+ process.exitCode = 1;
6201
+ }
5580
6202
  return;
5581
6203
  }
5582
- if (lease?.serviceWasActive) {
6204
+ if (lease?.serviceWasActive && !jsonMode) {
5583
6205
  console.log(` ${c.dim}Background service stays paused while this terminal holds the tunnel.${c.reset}`);
5584
6206
  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}`);
5585
6207
  }
5586
6208
  startAgent(config);
5587
6209
  }
5588
- async function pairThisMachine(apiUrl, flags) {
5589
- blankLine();
5590
- console.log(` ${glyph.mark} ${c.bold}Device Authorization${c.reset}`);
5591
- blankLine();
6210
+ async function pairThisMachine(apiUrl, flags, lease) {
6211
+ if (!jsonMode) {
6212
+ blankLine();
6213
+ console.log(` ${glyph.mark} ${c.bold}Device Authorization${c.reset}`);
6214
+ blankLine();
6215
+ }
5592
6216
  let challenge;
5593
6217
  try {
5594
- challenge = await requestDeviceAuthorization(apiUrl);
6218
+ challenge = await requestDeviceAuthorization(apiUrl, { projectId: flags["project-id"] });
5595
6219
  } catch (error) {
5596
6220
  fail(error instanceof InvalidDeviceAuthResponseError ? error.message : "Failed to start device authorization");
5597
6221
  }
5598
- console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${challenge.deviceCode}${c.reset}`);
5599
- blankLine();
5600
- console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
5601
- console.log(` ${c.cyan}${challenge.verificationUrl}${c.reset}`);
5602
- blankLine();
5603
- openBrowser(challenge.verificationUrl);
6222
+ if (jsonMode) {
6223
+ emit("challenge", {
6224
+ deviceCode: challenge.deviceCode,
6225
+ verificationUrl: challenge.verificationUrl,
6226
+ expiresAt: challenge.expiresAt
6227
+ });
6228
+ } else {
6229
+ console.log(` ${c.dim}Code:${c.reset} ${c.bold}${c.white}${challenge.deviceCode}${c.reset}`);
6230
+ blankLine();
6231
+ console.log(` ${c.dim}Open this URL on any device to approve:${c.reset}`);
6232
+ console.log(` ${c.cyan}${challenge.verificationUrl}${c.reset}`);
6233
+ blankLine();
6234
+ openBrowser(challenge.verificationUrl);
6235
+ }
6236
+ const clearWaiting = () => {
6237
+ if (!jsonMode)
6238
+ process.stdout.write(`\r${" ".repeat(60)}\r`);
6239
+ };
5604
6240
  let outcome;
5605
6241
  try {
5606
6242
  outcome = await awaitDeviceAuthorization(apiUrl, challenge, {
5607
- onWaiting: (secondsRemaining) => {
6243
+ onWaiting: jsonMode ? undefined : (secondsRemaining) => {
5608
6244
  const minutes = Math.floor(secondsRemaining / 60);
5609
6245
  const seconds = String(secondsRemaining % 60).padStart(2, "0");
5610
6246
  process.stdout.write(`\r ${c.dim}Waiting for approval... ${c.white}${minutes}:${seconds}${c.reset} `);
5611
6247
  }
5612
6248
  });
5613
6249
  } catch (error) {
5614
- process.stdout.write(`\r${" ".repeat(60)}\r`);
6250
+ clearWaiting();
5615
6251
  fail(error instanceof Error ? error.message : "Device authorization failed");
5616
6252
  }
5617
- process.stdout.write(`\r${" ".repeat(60)}\r`);
6253
+ clearWaiting();
5618
6254
  if (outcome.status === "denied")
5619
6255
  fail("Authorization denied.");
5620
6256
  if (outcome.status === "expired")
@@ -5623,6 +6259,8 @@ async function pairThisMachine(apiUrl, flags) {
5623
6259
  fail("Authorization was approved, but the setup token was not available. Run connect again.");
5624
6260
  }
5625
6261
  if (outcome.capabilities.length === 0) {
6262
+ if (jsonMode)
6263
+ fail("No capabilities were approved. Nothing was saved. Pair again and approve at least one capability.");
5626
6264
  console.log(` ${glyph.bad} ${c.bold}No capabilities were approved${c.reset}`);
5627
6265
  blankLine();
5628
6266
  console.log(` ${c.dim}A tunnel with no capabilities connects but cannot act, and the${c.reset}`);
@@ -5632,12 +6270,22 @@ async function pairThisMachine(apiUrl, flags) {
5632
6270
  blankLine();
5633
6271
  process.exit(1);
5634
6272
  }
5635
- console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
5636
6273
  saveCredentials(outcome.tunnelId, outcome.token, apiUrl, outcome.capabilities);
5637
- console.log(` ${c.dim}Saved to ${CONFIG_FILE2}${c.reset}`);
5638
- console.log(` ${c.dim}Access: ${outcome.capabilities.join(", ")}${c.reset}`);
5639
- blankLine();
5640
- await launch(loadConfig({ apiUrl }), flags);
6274
+ writeAccess({
6275
+ mode: existsSync7(desktopAppPath()) ? "ask" : "always",
6276
+ grantedUntil: null,
6277
+ deniedUntil: null,
6278
+ keepAwake: readAccess().keepAwake
6279
+ });
6280
+ if (jsonMode) {
6281
+ emit("approved", { tunnelId: outcome.tunnelId, capabilities: outcome.capabilities });
6282
+ } else {
6283
+ console.log(` ${glyph.on} ${c.bold}Authorized${c.reset}`);
6284
+ console.log(` ${c.dim}Saved to ${CONFIG_FILE2}${c.reset}`);
6285
+ console.log(` ${c.dim}Access: ${outcome.capabilities.join(", ")}${c.reset}`);
6286
+ blankLine();
6287
+ }
6288
+ await launch(loadConfig({ apiUrl }), flags, lease);
5641
6289
  }
5642
6290
  async function commandConnect(flags) {
5643
6291
  const config = loadConfig({
@@ -5649,18 +6297,24 @@ async function commandConnect(flags) {
5649
6297
  if (Boolean(config.token) !== Boolean(config.tunnelId)) {
5650
6298
  fail("Provide both --token and --tunnel-id, or neither (for device auth)");
5651
6299
  }
5652
- if (!config.token) {
5653
- await pairThisMachine(config.apiUrl, flags);
6300
+ if (flags["project-id"] !== undefined && !UUID_PATTERN.test(flags["project-id"])) {
6301
+ fail("--project-id must be a project UUID");
6302
+ }
6303
+ const savedForOtherBackend = Boolean(config.token) && !explicitCredentials && Boolean(flags["api-url"]) && new URL(loadConfig().apiUrl).origin !== new URL(config.apiUrl).origin;
6304
+ const lease = acquireTunnelLease();
6305
+ if (!config.token || savedForOtherBackend) {
6306
+ await pairThisMachine(config.apiUrl, flags, lease);
5654
6307
  return;
5655
6308
  }
5656
6309
  if (isTruthyFlag(flags.reauth) && !explicitCredentials) {
5657
6310
  clearSavedCredentials();
5658
- await pairThisMachine(config.apiUrl, flags);
6311
+ await pairThisMachine(config.apiUrl, flags, lease);
5659
6312
  return;
5660
6313
  }
5661
- const lease = acquireTunnelLease();
5662
- blankLine();
5663
- console.log(` ${glyph.mark} ${c.dim}Checking saved credentials…${c.reset}`);
6314
+ if (!jsonMode) {
6315
+ blankLine();
6316
+ console.log(` ${glyph.mark} ${c.dim}Checking saved credentials…${c.reset}`);
6317
+ }
5664
6318
  let probe;
5665
6319
  try {
5666
6320
  probe = await probeCredentials(config, {
@@ -5677,11 +6331,15 @@ async function commandConnect(flags) {
5677
6331
  if (probe === "rejected") {
5678
6332
  if (explicitCredentials)
5679
6333
  fail("The supplied --token was rejected for this tunnel.");
5680
- console.log(` ${glyph.warn} ${c.dim}Saved token rejected — re-authorizing${c.reset}`);
6334
+ if (!jsonMode)
6335
+ console.log(` ${glyph.warn} ${c.dim}Saved token rejected — re-authorizing${c.reset}`);
5681
6336
  clearSavedCredentials();
5682
- await pairThisMachine(config.apiUrl, flags);
6337
+ await pairThisMachine(config.apiUrl, flags, lease);
5683
6338
  return;
5684
6339
  }
6340
+ if (jsonMode) {
6341
+ emit("approved", { tunnelId: config.tunnelId, capabilities: config.enabledCapabilities ?? [], existing: true });
6342
+ }
5685
6343
  await launch(config, flags, lease);
5686
6344
  }
5687
6345
  function commandRun(flags) {
@@ -5692,8 +6350,21 @@ function commandRun(flags) {
5692
6350
  });
5693
6351
  const asService = flags.service === "true";
5694
6352
  if (!config.token || !config.tunnelId) {
5695
- console.error(` ${glyph.bad} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
5696
- process.exit(asService ? TERMINAL_SERVICE_EXIT_CODE : 1);
6353
+ if (!asService) {
6354
+ console.error(` ${glyph.bad} No saved tunnel credentials found. Run \`agent-tunnel connect\` first.`);
6355
+ process.exit(1);
6356
+ }
6357
+ process.stdout.write(`[agent-tunnel] no saved credential — waiting for a credential (pair this computer to start)
6358
+ `);
6359
+ const wait = setInterval(() => {
6360
+ const next = loadConfig({ apiUrl: flags["api-url"] });
6361
+ if (!next.token || !next.tunnelId)
6362
+ return;
6363
+ clearInterval(wait);
6364
+ rotateServiceLogs();
6365
+ startAgent(next, { service: true });
6366
+ }, 60000);
6367
+ return;
5697
6368
  }
5698
6369
  if (asService)
5699
6370
  rotateServiceLogs();
@@ -5701,7 +6372,7 @@ function commandRun(flags) {
5701
6372
  }
5702
6373
  function lastServiceActivity() {
5703
6374
  try {
5704
- const lines = readFileSync6(join10(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
6375
+ const lines = readFileSync9(join12(getServicePaths().logDir, "agent-tunnel.out.log"), "utf8").split(/\r?\n/).map((line) => stripAnsi(line).trim()).filter((line) => line.length > 0);
5705
6376
  return lines.at(-1) ?? null;
5706
6377
  } catch {
5707
6378
  return null;
@@ -5719,7 +6390,11 @@ function commandStatus(flags) {
5719
6390
  apiUrl: config.apiUrl,
5720
6391
  capabilities: [...approved],
5721
6392
  version: agentTunnelVersion(),
6393
+ home: agentTunnelHome(),
6394
+ serviceLabel: getServicePaths().label,
5722
6395
  service,
6396
+ state: readAgentState(),
6397
+ access: readAccess(),
5723
6398
  lastActivity: lastServiceActivity()
5724
6399
  }, null, 2));
5725
6400
  return;
@@ -5753,12 +6428,40 @@ function commandStatus(flags) {
5753
6428
  console.log(` ${c.dim}Recent logs:${c.reset} ${c.white}agent-tunnel logs${c.reset}`);
5754
6429
  blankLine();
5755
6430
  }
5756
- function commandLogout(flags) {
6431
+ async function unpairOnServer() {
6432
+ let config;
6433
+ try {
6434
+ config = loadConfig();
6435
+ } catch {
6436
+ return false;
6437
+ }
6438
+ if (!config.token || !config.tunnelId)
6439
+ return false;
6440
+ try {
6441
+ const response = await fetch(`${config.apiUrl}/self`, {
6442
+ method: "DELETE",
6443
+ headers: { Authorization: `Bearer ${config.token}`, "X-Tunnel-Id": config.tunnelId },
6444
+ signal: AbortSignal.timeout(1e4)
6445
+ });
6446
+ return response.ok;
6447
+ } catch {
6448
+ return false;
6449
+ }
6450
+ }
6451
+ async function commandLogout(flags) {
6452
+ const serverUnpaired = await unpairOnServer();
5757
6453
  const removed = clearSavedCredentials();
6454
+ rmSync5(accessFilePath(), { force: true });
6455
+ rmSync5(accessRequestPath(), { force: true });
5758
6456
  const keepService = isTruthyFlag(flags["keep-service"]);
5759
6457
  if (!keepService)
5760
6458
  SERVICE_ACTIONS.uninstall.run();
6459
+ if (isTruthyFlag(flags.json)) {
6460
+ console.log(JSON.stringify({ ok: true, serverUnpaired, credentialsCleared: removed, serviceRemoved: !keepService }));
6461
+ return;
6462
+ }
5761
6463
  blankLine();
6464
+ console.log(serverUnpaired ? ` ${glyph.on} ${c.dim}Removed this computer from Kortix${c.reset}` : ` ${glyph.warn} ${c.dim}Could not reach Kortix — remove this computer from its settings${c.reset}`);
5762
6465
  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}`);
5763
6466
  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}`);
5764
6467
  blankLine();
@@ -5770,7 +6473,7 @@ function commandLogs(flags) {
5770
6473
  if (isTruthyFlag(flags.clear)) {
5771
6474
  for (const file of serviceLogFiles(paths)) {
5772
6475
  try {
5773
- writeFileSync5(file, "", { mode: 384 });
6476
+ writeFileSync7(file, "", { mode: 384 });
5774
6477
  } catch {}
5775
6478
  }
5776
6479
  blankLine();
@@ -5782,16 +6485,16 @@ function commandLogs(flags) {
5782
6485
  const limit = Number.isSafeInteger(requested) && requested > 0 ? requested : DEFAULT_LOG_LINES;
5783
6486
  const showAll = isTruthyFlag(flags.all);
5784
6487
  for (const [label, file] of [
5785
- ["output", join10(paths.logDir, "agent-tunnel.out.log")],
5786
- ["errors", join10(paths.logDir, "agent-tunnel.err.log")]
6488
+ ["output", join12(paths.logDir, "agent-tunnel.out.log")],
6489
+ ["errors", join12(paths.logDir, "agent-tunnel.err.log")]
5787
6490
  ]) {
5788
6491
  blankLine();
5789
6492
  console.log(` ${c.bold}${c.white}${label}${c.reset} ${c.dim}${shortenHomePath(file)}${c.reset}`);
5790
- if (!existsSync6(file)) {
6493
+ if (!existsSync7(file)) {
5791
6494
  console.log(` ${c.dim}not created yet${c.reset}`);
5792
6495
  continue;
5793
6496
  }
5794
- const kept = readFileSync6(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
6497
+ const kept = readFileSync9(file, "utf8").split(/\r?\n/).map((line) => line.trimEnd()).filter((line) => line.trim().length > 0).filter((line) => showAll || !isShellStartupNoise(line));
5795
6498
  const lines = collapseRepeatedLines(kept).slice(-limit);
5796
6499
  if (lines.length === 0) {
5797
6500
  console.log(` ${c.dim}empty${c.reset}`);
@@ -5827,11 +6530,15 @@ var COMMANDS = {
5827
6530
  },
5828
6531
  status: { summary: "Show pairing, capabilities, and service state (--json)", run: commandStatus },
5829
6532
  logs: { summary: "Show recent service logs (--lines <n>, --all, --clear)", run: commandLogs },
5830
- start: { summary: "Start the background service", run: (f) => commandServiceAction("start", f) },
6533
+ start: {
6534
+ summary: "Start the background service (resumes a paused one)",
6535
+ run: (f) => commandServiceAction("start", f),
6536
+ aliases: ["resume"]
6537
+ },
5831
6538
  stop: {
5832
- summary: "Stop the background service (keeps it installed)",
6539
+ summary: "Pause the background service; it stays stopped after restart until `start`",
5833
6540
  run: (f) => commandServiceAction("stop", f),
5834
- aliases: ["disable"]
6541
+ aliases: ["disable", "pause"]
5835
6542
  },
5836
6543
  restart: { summary: "Restart the background service", run: (f) => commandServiceAction("restart", f) },
5837
6544
  "install-service": {
@@ -5843,10 +6550,10 @@ var COMMANDS = {
5843
6550
  run: (f) => commandServiceAction("uninstall", f)
5844
6551
  },
5845
6552
  "service-status": {
5846
- summary: "Show the background service state (same view as status)",
6553
+ summary: "Show the background service state (same view as status, --json)",
5847
6554
  run: commandStatus
5848
6555
  },
5849
- logout: { summary: "Clear saved credentials and remove the service", run: commandLogout },
6556
+ logout: { summary: "Remove this computer from Kortix, clear credentials, remove the service", run: commandLogout },
5850
6557
  run: { summary: "Run using saved credentials (used by the service)", run: commandRun },
5851
6558
  "start-service": { summary: "", run: (f) => commandServiceAction("start", f), hidden: true },
5852
6559
  "stop-service": { summary: "", run: (f) => commandServiceAction("stop", f), hidden: true },
@@ -5859,7 +6566,8 @@ var OPTIONS = [
5859
6566
  ["--token <token> --tunnel-id <id>", "Skip device auth and use an explicit credential"],
5860
6567
  ["--reauth", "With connect: discard the saved credential and pair again"],
5861
6568
  ["--daemon / --foreground", "With connect: skip the prompt and choose the mode"],
5862
- ["--json", "With status: machine-readable output"],
6569
+ ["--project-id <uuid>", 'With connect: offer "Also share with <project>" on the approval page; the computer is yours in every project either way'],
6570
+ ["--json", "With connect: NDJSON events, no prompts. With status and logout: machine-readable output"],
5863
6571
  ["--keep-service", "With logout: keep the background service installed"]
5864
6572
  ];
5865
6573
  function showHelp() {
@@ -5885,10 +6593,12 @@ function showHelp() {
5885
6593
  }
5886
6594
  blankLine();
5887
6595
  console.log(` ${c.dim}Config: ${CONFIG_FILE2}${c.reset}`);
6596
+ console.log(` ${c.dim}Set AGENT_TUNNEL_HOME to use another config directory and service.${c.reset}`);
5888
6597
  console.log(` ${c.dim}powered by ${c.cyan}kortix${c.reset}`);
5889
6598
  blankLine();
5890
6599
  }
5891
6600
  var { command, flags } = parseArgs(process.argv);
6601
+ jsonMode = command === "connect" && isTruthyFlag(flags.json);
5892
6602
  if (Object.prototype.hasOwnProperty.call(flags, "keep-awake")) {
5893
6603
  console.error(` ${glyph.bad} --keep-awake is not supported. Configure sleep behavior in the operating system.`);
5894
6604
  process.exit(2);
@@ -5898,7 +6608,6 @@ if (!resolved) {
5898
6608
  showHelp();
5899
6609
  } else {
5900
6610
  Promise.resolve(resolved.run(flags)).catch((error) => {
5901
- console.error(` ${glyph.bad} ${error instanceof Error ? error.message : String(error)}`);
5902
- process.exit(1);
6611
+ fail(error instanceof Error ? error.message : String(error));
5903
6612
  });
5904
6613
  }