@alan-ai-hq/agent-manager 0.1.93 → 0.1.95

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.cjs +533 -221
  2. package/package.json +3 -3
package/dist/index.cjs CHANGED
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
3404
3404
  var http2 = require("http");
3405
3405
  var net = require("net");
3406
3406
  var tls = require("tls");
3407
- var { randomBytes: randomBytes13, createHash: createHash18 } = require("crypto");
3407
+ var { randomBytes: randomBytes13, createHash: createHash19 } = require("crypto");
3408
3408
  var { Duplex, Readable: Readable2 } = require("stream");
3409
3409
  var { URL: URL2 } = require("url");
3410
3410
  var PerMessageDeflate = require_permessage_deflate();
@@ -4061,7 +4061,7 @@ var require_websocket = __commonJS({
4061
4061
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4062
4062
  return;
4063
4063
  }
4064
- const digest = createHash18("sha1").update(key + GUID).digest("base64");
4064
+ const digest = createHash19("sha1").update(key + GUID).digest("base64");
4065
4065
  if (res.headers["sec-websocket-accept"] !== digest) {
4066
4066
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4067
4067
  return;
@@ -4428,7 +4428,7 @@ var require_websocket_server = __commonJS({
4428
4428
  var EventEmitter = require("events");
4429
4429
  var http2 = require("http");
4430
4430
  var { Duplex } = require("stream");
4431
- var { createHash: createHash18 } = require("crypto");
4431
+ var { createHash: createHash19 } = require("crypto");
4432
4432
  var extension = require_extension();
4433
4433
  var PerMessageDeflate = require_permessage_deflate();
4434
4434
  var subprotocol = require_subprotocol();
@@ -4725,7 +4725,7 @@ var require_websocket_server = __commonJS({
4725
4725
  );
4726
4726
  }
4727
4727
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4728
- const digest = createHash18("sha1").update(key + GUID).digest("base64");
4728
+ const digest = createHash19("sha1").update(key + GUID).digest("base64");
4729
4729
  const headers = [
4730
4730
  "HTTP/1.1 101 Switching Protocols",
4731
4731
  "Upgrade: websocket",
@@ -15451,7 +15451,7 @@ function describeError(error61) {
15451
15451
  }
15452
15452
 
15453
15453
  // src/version.ts
15454
- var AGENT_VERSION = "0.1.93";
15454
+ var AGENT_VERSION = "0.1.95";
15455
15455
 
15456
15456
  // src/daemon-worktree.ts
15457
15457
  var import_node_child_process3 = require("child_process");
@@ -38394,6 +38394,10 @@ var import_crypto7 = require("crypto");
38394
38394
  var import_fs11 = require("fs");
38395
38395
  var import_path11 = require("path");
38396
38396
  var ALAN_MCP_URL_ENV = "ALAN_MCP_URL";
38397
+ var ALAN_RUN_AUTHORIZATION_ENV = "ALAN_RUN_AUTHORIZATION";
38398
+ function managedRunAuthorizationHeader(registration) {
38399
+ return `Bearer ${requireManagedRunReference(registration)}`;
38400
+ }
38397
38401
  function deliversRunReferenceViaEnv(backendKind) {
38398
38402
  return backendKind === "cursor_agent_cli";
38399
38403
  }
@@ -41683,7 +41687,10 @@ function buildClaudeManagedMcpArgs(registration, options = {}) {
41683
41687
  mcpServers.alan = {
41684
41688
  type: "http",
41685
41689
  url: registration.url,
41686
- headers: { "x-alan-run-reference": `\${${ALAN_RUN_REFERENCE_ENV}}` }
41690
+ headers: {
41691
+ "x-alan-run-reference": `\${${ALAN_RUN_REFERENCE_ENV}}`,
41692
+ Authorization: `Bearer \${${ALAN_RUN_REFERENCE_ENV}}`
41693
+ }
41687
41694
  };
41688
41695
  return [
41689
41696
  ...options.strict ? ["--strict-mcp-config"] : [],
@@ -42960,6 +42967,8 @@ function buildCodexManagedMcpArgs(registration, options = {}) {
42960
42967
  "-c",
42961
42968
  `mcp_servers.alan.env_http_headers.x-alan-run-reference=${JSON.stringify(ALAN_RUN_REFERENCE_ENV)}`,
42962
42969
  "-c",
42970
+ `mcp_servers.alan.env_http_headers.Authorization=${JSON.stringify(ALAN_RUN_AUTHORIZATION_ENV)}`,
42971
+ "-c",
42963
42972
  "mcp_servers.alan.required=true",
42964
42973
  "-c",
42965
42974
  `mcp_servers.alan.startup_timeout_sec=${ALAN_MCP_STARTUP_TIMEOUT_SEC}`
@@ -43003,7 +43012,8 @@ function createCodexRuntimeBackend(command = "codex", defaultArgs = []) {
43003
43012
  env: {
43004
43013
  ...context.env,
43005
43014
  ...externalMcp?.env,
43006
- [ALAN_RUN_REFERENCE_ENV]: requireManagedRunReference(managedRegistration)
43015
+ [ALAN_RUN_REFERENCE_ENV]: requireManagedRunReference(managedRegistration),
43016
+ [ALAN_RUN_AUTHORIZATION_ENV]: managedRunAuthorizationHeader(managedRegistration)
43007
43017
  }
43008
43018
  } : context;
43009
43019
  const { paths: imagePaths, cleanup } = writeImagesToTempFiles(
@@ -49600,6 +49610,16 @@ async function probeLocalDaemon(port, token, timeoutMs = 750) {
49600
49610
  const probe = await probeLocalDaemonEndpoint(port, token, timeoutMs);
49601
49611
  return probe.state === "running" ? probe.status : null;
49602
49612
  }
49613
+ function requestLocalDaemonShutdown(port, token) {
49614
+ return fetch(`http://127.0.0.1:${port}/shutdown?force=1`, {
49615
+ method: "POST",
49616
+ headers: {
49617
+ authorization: `Bearer ${token}`,
49618
+ "x-alan-shutdown-reason": "runtime_shutdown"
49619
+ },
49620
+ signal: AbortSignal.timeout(5e3)
49621
+ });
49622
+ }
49603
49623
  async function adoptLocalDaemonControl(input2) {
49604
49624
  const timeoutMs = input2.timeoutMs ?? 750;
49605
49625
  try {
@@ -76677,13 +76697,13 @@ var RuntimeRunJournal = class {
76677
76697
  * Keep the exact interruption outcome until a durable authenticated control
76678
76698
  * channel can prove both identity and ownership.
76679
76699
  */
76680
- assessRecovery(runId, isProcessAlive) {
76700
+ assessRecovery(runId, isProcessAlive2) {
76681
76701
  const entry = this.entries.get(runId);
76682
76702
  if (!entry) return void 0;
76683
76703
  if (entry.providerPid === void 0) {
76684
76704
  return { reattachable: false, reason: "provider_not_recorded" };
76685
76705
  }
76686
- if (!isProcessAlive(entry.providerPid)) {
76706
+ if (!isProcessAlive2(entry.providerPid)) {
76687
76707
  return {
76688
76708
  reattachable: false,
76689
76709
  reason: "provider_not_running",
@@ -80430,6 +80450,17 @@ async function runLocalDaemonRecoveryLadder(input2) {
80430
80450
  error: "The local daemon exhausted its reconnect and safe-restart recovery budget."
80431
80451
  };
80432
80452
  }
80453
+ function writeRegisteredRuntimeConfig(args, previousConfig, registered) {
80454
+ writeConfig({
80455
+ ...registered,
80456
+ localApiPort: getLocalApiPort(args, {}),
80457
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80458
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80459
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80460
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80461
+ daemonEpoch: previousConfig.daemonEpoch
80462
+ });
80463
+ }
80433
80464
  async function setupDaemon(args) {
80434
80465
  bindConfigPath(args);
80435
80466
  const previousConfig = readConfigForRegistration();
@@ -80490,7 +80521,7 @@ async function setupDaemon(args) {
80490
80521
  credentialGeneration: body2.credentialGeneration,
80491
80522
  configPath: getConfigPath()
80492
80523
  });
80493
- return { daemonAlreadyRunning: true };
80524
+ return body2.runtimeId !== liveConfig.runtimeId;
80494
80525
  }
80495
80526
  if (readLocalDaemonOwnerLease(getConfigPath())) {
80496
80527
  throw new Error(
@@ -80504,23 +80535,17 @@ async function setupDaemon(args) {
80504
80535
  if (setupToken && error61 instanceof RuntimeSetupGrantExpiredError) {
80505
80536
  console.info("[alan-agent] Setup link expired; continuing with browser authorization");
80506
80537
  await loginWithDeviceCode(args);
80507
- return { daemonAlreadyRunning: false };
80538
+ return false;
80508
80539
  }
80509
80540
  throw error61;
80510
80541
  }
80511
- writeConfig({
80542
+ writeRegisteredRuntimeConfig(args, previousConfig, {
80512
80543
  apiUrl,
80513
80544
  wsUrl,
80514
80545
  endpointProfile,
80515
80546
  runtimeId: body.runtimeId,
80516
80547
  runtimeToken: body.runtimeToken,
80517
80548
  runtimeRenewalToken: body.runtimeRenewalToken,
80518
- localApiPort: getLocalApiPort(args, {}),
80519
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80520
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80521
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80522
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80523
- daemonEpoch: previousConfig.daemonEpoch,
80524
80549
  displayName: body.displayName,
80525
80550
  installationId
80526
80551
  });
@@ -80529,7 +80554,7 @@ async function setupDaemon(args) {
80529
80554
  runtimeId: body.runtimeId,
80530
80555
  configPath: getConfigPath()
80531
80556
  });
80532
- return { daemonAlreadyRunning: false };
80557
+ return false;
80533
80558
  }
80534
80559
  async function loginWithDeviceCode(args) {
80535
80560
  bindConfigPath(args);
@@ -80618,19 +80643,13 @@ async function loginWithDeviceCode(args) {
80618
80643
  if (ownedPending?.operationId !== operation.id) {
80619
80644
  throw new Error("browser authorization sidecar ownership was lost before finalization");
80620
80645
  }
80621
- writeConfig({
80646
+ writeRegisteredRuntimeConfig(args, previousConfig, {
80622
80647
  apiUrl,
80623
80648
  wsUrl,
80624
80649
  endpointProfile,
80625
80650
  runtimeId: body.runtime.id,
80626
80651
  runtimeToken: body.runtimeToken,
80627
80652
  runtimeRenewalToken: body.runtimeRenewalToken,
80628
- localApiPort: getLocalApiPort(args, {}),
80629
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80630
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80631
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80632
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80633
- daemonEpoch: previousConfig.daemonEpoch,
80634
80653
  displayName: body.runtime.displayName ?? displayName,
80635
80654
  installationId
80636
80655
  });
@@ -80669,14 +80688,7 @@ async function stopDaemon(args) {
80669
80688
  console.info("[alan-agent] No daemon listening", { port: localApiPort });
80670
80689
  return;
80671
80690
  }
80672
- const response = await fetch(`http://127.0.0.1:${localApiPort}/shutdown?force=1`, {
80673
- method: "POST",
80674
- headers: {
80675
- authorization: `Bearer ${localApiToken}`,
80676
- "x-alan-shutdown-reason": "runtime_shutdown"
80677
- },
80678
- signal: AbortSignal.timeout(5e3)
80679
- });
80691
+ const response = await requestLocalDaemonShutdown(localApiPort, localApiToken);
80680
80692
  if (!response.ok) {
80681
80693
  throw new Error(`Failed to stop daemon: HTTP ${response.status}`);
80682
80694
  }
@@ -80821,14 +80833,7 @@ async function drainAndStopLocalDaemonBeforeCredentialCleanup(args, stored) {
80821
80833
  `Logout could not safely drain the local daemon (HTTP ${prepare.status}). Stop or update the local service before removing credentials.`
80822
80834
  );
80823
80835
  }
80824
- const shutdown = await fetch(`http://127.0.0.1:${localApiPort}/shutdown?force=1`, {
80825
- method: "POST",
80826
- headers: {
80827
- authorization: `Bearer ${localApiToken}`,
80828
- "x-alan-shutdown-reason": "runtime_shutdown"
80829
- },
80830
- signal: AbortSignal.timeout(5e3)
80831
- });
80836
+ const shutdown = await requestLocalDaemonShutdown(localApiPort, localApiToken);
80832
80837
  if (!shutdown.ok) {
80833
80838
  throw new Error(`Logout could not stop the local daemon (HTTP ${shutdown.status}).`);
80834
80839
  }
@@ -80989,6 +80994,9 @@ async function printDoctor(args = []) {
80989
80994
  );
80990
80995
  }
80991
80996
 
80997
+ // src/install-command.ts
80998
+ var import_promises6 = require("timers/promises");
80999
+
80992
81000
  // src/mcp-config-adapters.ts
80993
81001
  var import_node_crypto20 = require("crypto");
80994
81002
  var import_node_fs23 = require("fs");
@@ -82373,12 +82381,14 @@ function runMcpIntegrationCommand(args, options = {}) {
82373
82381
 
82374
82382
  // src/service-manager.ts
82375
82383
  var import_node_child_process9 = require("child_process");
82384
+ var import_node_crypto21 = require("crypto");
82376
82385
  var import_node_fs25 = require("fs");
82377
82386
  var import_node_os16 = require("os");
82378
82387
  var import_node_path27 = require("path");
82379
82388
  var SERVICE_LABEL = "ai.tryalan.agent";
82380
82389
  var SYSTEMD_UNIT = "alan-agent.service";
82381
82390
  var WINDOWS_TASK = "Alan Agent";
82391
+ var STOP_POLL_INTERVAL_MS = 200;
82382
82392
  function xml(value2) {
82383
82393
  return value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
82384
82394
  }
@@ -82388,6 +82398,18 @@ function systemdArg(value2) {
82388
82398
  function powershellLiteral(value2) {
82389
82399
  return `'${value2.replaceAll("'", "''")}'`;
82390
82400
  }
82401
+ function powershellCommand(script, tolerateFailure = false) {
82402
+ return {
82403
+ command: "powershell.exe",
82404
+ args: ["-NoProfile", "-NonInteractive", "-Command", script],
82405
+ tolerateFailure
82406
+ };
82407
+ }
82408
+ function windowsCommandLineArgument(value2) {
82409
+ if (value2 && !/[\s"]/u.test(value2)) return value2;
82410
+ const escaped = value2.replace(/(\\*)"/gu, (_match, slashes) => `${slashes}${slashes}\\"`).replace(/\\+$/u, (slashes) => `${slashes}${slashes}`);
82411
+ return `"${escaped}"`;
82412
+ }
82391
82413
  function serviceNames(profile) {
82392
82414
  if (profile === "production") {
82393
82415
  return { launchd: SERVICE_LABEL, systemd: SYSTEMD_UNIT, windows: WINDOWS_TASK };
@@ -82409,7 +82431,7 @@ function buildDaemonServicePlan(input2) {
82409
82431
  const domain2 = `gui/${userId}`;
82410
82432
  const serviceTarget = `${domain2}/${names.launchd}`;
82411
82433
  const manifestPath = (0, import_node_path27.join)(input2.homeDir, "Library", "LaunchAgents", `${names.launchd}.plist`);
82412
- const logDir = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82434
+ const logDir2 = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82413
82435
  const programArguments = daemonArgs.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
82414
82436
  const environmentEntries = Object.entries(input2.environment ?? {}).map(([key, value2]) => ` <key>${xml(key)}</key>
82415
82437
  <string>${xml(value2)}</string>`).join("\n");
@@ -82438,9 +82460,9 @@ ${environmentBlock} <key>RunAtLoad</key>
82438
82460
  <key>ThrottleInterval</key>
82439
82461
  <integer>5</integer>
82440
82462
  <key>StandardOutPath</key>
82441
- <string>${xml((0, import_node_path27.join)(logDir, "daemon.log"))}</string>
82463
+ <string>${xml((0, import_node_path27.join)(logDir2, "daemon.log"))}</string>
82442
82464
  <key>StandardErrorPath</key>
82443
- <string>${xml((0, import_node_path27.join)(logDir, "daemon-error.log"))}</string>
82465
+ <string>${xml((0, import_node_path27.join)(logDir2, "daemon-error.log"))}</string>
82444
82466
  </dict>
82445
82467
  </plist>
82446
82468
  `;
@@ -82448,7 +82470,7 @@ ${environmentBlock} <key>RunAtLoad</key>
82448
82470
  platform: "darwin",
82449
82471
  manifestPath,
82450
82472
  manifest,
82451
- logDirectory: logDir,
82473
+ logDirectory: logDir2,
82452
82474
  loadDefinitionCommands: [
82453
82475
  {
82454
82476
  command: "launchctl",
@@ -82489,23 +82511,6 @@ ${environmentBlock} <key>RunAtLoad</key>
82489
82511
  tolerateFailure: true
82490
82512
  }
82491
82513
  ],
82492
- replaceCommands: [
82493
- {
82494
- command: "launchctl",
82495
- args: ["bootout", domain2, manifestPath],
82496
- tolerateFailure: true
82497
- },
82498
- {
82499
- command: "launchctl",
82500
- args: ["bootstrap", domain2, manifestPath],
82501
- tolerateFailure: false
82502
- },
82503
- {
82504
- command: "launchctl",
82505
- args: ["kickstart", "-k", serviceTarget],
82506
- tolerateFailure: false
82507
- }
82508
- ],
82509
82514
  uninstallCommands: [
82510
82515
  {
82511
82516
  command: "launchctl",
@@ -82522,8 +82527,7 @@ ${environmentBlock} <key>RunAtLoad</key>
82522
82527
  command: "launchctl",
82523
82528
  args: ["print", serviceTarget],
82524
82529
  tolerateFailure: true
82525
- },
82526
- lingerStatusCommand: null
82530
+ }
82527
82531
  };
82528
82532
  }
82529
82533
  if (input2.platform === "linux") {
@@ -82586,18 +82590,6 @@ WantedBy=default.target
82586
82590
  tolerateFailure: true
82587
82591
  }
82588
82592
  ],
82589
- replaceCommands: [
82590
- {
82591
- command: "systemctl",
82592
- args: ["--user", "daemon-reload"],
82593
- tolerateFailure: false
82594
- },
82595
- {
82596
- command: "systemctl",
82597
- args: ["--user", "restart", names.systemd],
82598
- tolerateFailure: false
82599
- }
82600
- ],
82601
82593
  uninstallCommands: [
82602
82594
  {
82603
82595
  command: "systemctl",
@@ -82619,117 +82611,179 @@ WantedBy=default.target
82619
82611
  command: "systemctl",
82620
82612
  args: ["--user", "is-active", names.systemd],
82621
82613
  tolerateFailure: true
82622
- },
82623
- lingerStatusCommand: {
82624
- command: "loginctl",
82625
- args: ["show-user", "$(id -un)", "-p", "Linger"],
82626
- tolerateFailure: true
82627
82614
  }
82628
82615
  };
82629
82616
  }
82630
- const environmentPrefix = Object.entries(input2.environment ?? {}).map(([key, value2]) => `$env:${key} = ${powershellLiteral(value2)}`).join("; ");
82631
- const directArgument = [input2.cliEntry, "daemon", "--profile", input2.profile].map((part) => part.includes(" ") ? `"${part}"` : part).join(" ");
82632
- const executable = powershellLiteral(environmentPrefix ? "powershell.exe" : input2.nodeExecutable);
82633
- const argument = powershellLiteral(
82634
- environmentPrefix ? `-NoProfile -NonInteractive -WindowStyle Hidden -Command "${environmentPrefix}; & ${powershellLiteral(input2.nodeExecutable)} ${powershellLiteral(input2.cliEntry)} daemon --profile ${input2.profile}"` : directArgument
82617
+ const logDir = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82618
+ const legacyEnvironmentPrefix = Object.entries(input2.environment ?? {}).map(([key, value2]) => `$env:${key} = ${powershellLiteral(value2)}`).join("; ");
82619
+ const legacyDirectArguments = [input2.cliEntry, "daemon", "--profile", input2.profile].map((part) => part.includes(" ") ? `"${part}"` : part).join(" ");
82620
+ const legacyExecutable = legacyEnvironmentPrefix ? "powershell.exe" : input2.nodeExecutable;
82621
+ const legacyArguments = legacyEnvironmentPrefix ? `-NoProfile -NonInteractive -WindowStyle Hidden -Command "${legacyEnvironmentPrefix}; & ${powershellLiteral(input2.nodeExecutable)} ${powershellLiteral(input2.cliEntry)} daemon --profile ${input2.profile}"` : legacyDirectArguments;
82622
+ const daemonArguments = [input2.cliEntry, "daemon", "--profile", input2.profile].map(windowsCommandLineArgument).join(" ");
82623
+ const environmentStatements = Object.entries(input2.environment ?? {}).map(
82624
+ ([key, value2]) => `$startInfo.EnvironmentVariables[${powershellLiteral(key)}] = ${powershellLiteral(value2)}`
82635
82625
  );
82626
+ const launcherScript = [
82627
+ "$ErrorActionPreference = 'Stop'",
82628
+ "$startInfo = [System.Diagnostics.ProcessStartInfo]::new()",
82629
+ `$startInfo.FileName = ${powershellLiteral(input2.nodeExecutable)}`,
82630
+ `$startInfo.Arguments = ${powershellLiteral(daemonArguments)}`,
82631
+ `$startInfo.WorkingDirectory = ${powershellLiteral((0, import_node_path27.dirname)(input2.cliEntry))}`,
82632
+ "$startInfo.UseShellExecute = $false",
82633
+ "$startInfo.CreateNoWindow = $true",
82634
+ "$startInfo.RedirectStandardOutput = $true",
82635
+ "$startInfo.RedirectStandardError = $true",
82636
+ ...environmentStatements,
82637
+ `$stdout = [System.IO.File]::Open(${powershellLiteral((0, import_node_path27.join)(logDir, "daemon-launcher.log"))}, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)`,
82638
+ `$stderr = [System.IO.File]::Open(${powershellLiteral((0, import_node_path27.join)(logDir, "daemon-launcher-error.log"))}, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)`,
82639
+ "$process = [System.Diagnostics.Process]::new()",
82640
+ "$process.StartInfo = $startInfo",
82641
+ "try { if (-not $process.Start()) { throw 'Alan daemon process did not start' }; $stdoutCopy = $process.StandardOutput.BaseStream.CopyToAsync($stdout); $stderrCopy = $process.StandardError.BaseStream.CopyToAsync($stderr); $process.WaitForExit(); [System.Threading.Tasks.Task]::WaitAll([System.Threading.Tasks.Task[]]@($stdoutCopy, $stderrCopy)); $exitCode = $process.ExitCode } finally { $stdout.Dispose(); $stderr.Dispose(); $process.Dispose() }",
82642
+ "exit $exitCode"
82643
+ ].join("; ");
82644
+ const executable = powershellLiteral("powershell.exe");
82645
+ const launcherArguments = `-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${Buffer.from(launcherScript, "utf16le").toString("base64")}`;
82646
+ const settingsScript = "New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries";
82647
+ const ownerMarker = `Alan daemon owner v1 ${(0, import_node_crypto21.createHash)("sha256").update(
82648
+ JSON.stringify({
82649
+ executable: input2.nodeExecutable,
82650
+ cliEntry: input2.cliEntry,
82651
+ profile: input2.profile,
82652
+ taskBaseName: names.windows
82653
+ })
82654
+ ).digest("hex")}`;
82655
+ const definitionMarker = `${ownerMarker}; definition v3 ${(0, import_node_crypto21.createHash)("sha256").update(
82656
+ JSON.stringify({
82657
+ executable: "powershell.exe",
82658
+ launcherArguments,
82659
+ workingDirectory: (0, import_node_path27.dirname)(input2.cliEntry),
82660
+ settingsScript,
82661
+ taskBaseName: names.windows
82662
+ })
82663
+ ).digest("hex")}`;
82664
+ const windowsDefinitionContext = [
82665
+ `$expectedLauncherArguments = ${powershellLiteral(launcherArguments)}`,
82666
+ `$expectedWorkingDirectory = ${powershellLiteral((0, import_node_path27.dirname)(input2.cliEntry))}`,
82667
+ `$expectedOwnerMarker = ${powershellLiteral(`${ownerMarker};`)}`,
82668
+ `$expectedDefinitionMarker = ${powershellLiteral(definitionMarker)}`,
82669
+ `$expectedLegacyExecutable = ${powershellLiteral(legacyExecutable)}`,
82670
+ `$expectedLegacyArguments = ${powershellLiteral(legacyArguments)}`
82671
+ ].join("; ");
82672
+ const taskContext = [
82673
+ "$currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()",
82674
+ "$currentUser = $currentIdentity.Name",
82675
+ "$currentSid = $currentIdentity.User.Value",
82676
+ `$taskName = ${powershellLiteral(names.windows)} + ' ' + $currentSid`
82677
+ ].join("; ");
82678
+ const resolveTaskSid = "function Resolve-TaskSid([string]$value) { try { return ([System.Security.Principal.NTAccount]$value).Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { try { return [System.Security.Principal.SecurityIdentifier]::new($value).Value } catch { return '' } } }";
82679
+ const currentLookup = "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]";
82680
+ const checkCurrentOwnership = [
82681
+ currentLookup,
82682
+ "$currentOwned = $false",
82683
+ "if ($null -ne $task) { try {",
82684
+ "$taskAction = @($task.Actions)[0]",
82685
+ "$currentOwned = @($task.Actions).Count -eq 1 -and (Resolve-TaskSid ([string]$task.Principal.UserId)) -eq $currentSid -and (([string]$task.Description).StartsWith($expectedOwnerMarker, [System.StringComparison]::Ordinal) -or (([string]$taskAction.Execute) -ieq 'powershell.exe' -and ([string]$taskAction.Arguments) -ceq $expectedLauncherArguments -and ([string]$taskAction.WorkingDirectory) -ieq $expectedWorkingDirectory))",
82686
+ "} catch { $currentOwned = $false } }"
82687
+ ].join("; ");
82688
+ const legacyLookup = `$legacyTask = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq ${powershellLiteral(names.windows)} })[0]`;
82689
+ const checkLegacyOwnership = [
82690
+ legacyLookup,
82691
+ "$legacyOwned = $false",
82692
+ "$legacyState = 'Missing'",
82693
+ "if ($null -ne $legacyTask) { try {",
82694
+ "$legacyState = [string]$legacyTask.State",
82695
+ "$legacySid = Resolve-TaskSid ([string]$legacyTask.Principal.UserId)",
82696
+ "$legacyAction = @($legacyTask.Actions)[0]",
82697
+ "if ($null -ne $legacyAction) {",
82698
+ "$legacyOwned = $legacySid -eq $currentSid -and @($legacyTask.Actions).Count -eq 1 -and ((([string]$legacyAction.Execute) -ieq $expectedLegacyExecutable -and ([string]$legacyAction.Arguments) -ieq $expectedLegacyArguments -and ([string]$legacyAction.WorkingDirectory) -eq '') -or (([string]$legacyAction.Execute) -ieq 'powershell.exe' -and ([string]$legacyAction.Arguments) -ceq $expectedLauncherArguments -and ([string]$legacyAction.WorkingDirectory) -ieq $expectedWorkingDirectory))",
82699
+ "}",
82700
+ "} catch { $legacyOwned = $false } }"
82701
+ ].join("; ");
82702
+ const assertOwnedTasks = [
82703
+ checkCurrentOwnership,
82704
+ checkLegacyOwnership,
82705
+ `if ($null -ne $task -and -not $currentOwned) { throw ${powershellLiteral(`${names.windows} task is not owned by this Alan installation`)} }`,
82706
+ `if ($null -ne $legacyTask -and -not $legacyOwned) { throw ${powershellLiteral(`Legacy ${names.windows} task is not owned by this Alan installation`)} }`,
82707
+ "if ($null -ne $task -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$task.State)) { throw 'Alan daemon task became active or has unknown state' }",
82708
+ "if ($null -ne $legacyTask -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$legacyTask.State)) { throw 'Legacy Alan daemon task became active or has unknown state' }"
82709
+ ].join("; ");
82710
+ const cleanupLegacyTask = [
82711
+ checkLegacyOwnership,
82712
+ `if ($null -ne $legacyTask -and -not $legacyOwned) { throw ${powershellLiteral(`Legacy ${names.windows} task is not owned by this Alan installation`)} }`,
82713
+ "if ($null -ne $legacyTask -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$legacyTask.State)) { throw 'Legacy Alan daemon task became active or has unknown state' }",
82714
+ `if ($null -ne $legacyTask) { Unregister-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -TaskPath '\\' -Confirm:$false -ErrorAction Stop }`,
82715
+ `$legacyRemaining = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq ${powershellLiteral(names.windows)} })[0]`,
82716
+ "if ($null -ne $legacyRemaining) { throw 'Legacy Alan daemon task still exists after migration' }"
82717
+ ].join("; ");
82636
82718
  const registerScript = [
82637
- "$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
82638
- `$action = New-ScheduledTaskAction -Execute ${executable} -Argument ${argument}`,
82719
+ taskContext,
82720
+ resolveTaskSid,
82721
+ windowsDefinitionContext,
82722
+ `$action = New-ScheduledTaskAction -Execute ${executable} -Argument $expectedLauncherArguments -WorkingDirectory $expectedWorkingDirectory`,
82639
82723
  "$trigger = New-ScheduledTaskTrigger -AtLogOn -User $currentUser",
82640
82724
  "$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Limited",
82641
- "$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)",
82642
- `Register-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null`
82725
+ `$settings = ${settingsScript}`,
82726
+ assertOwnedTasks,
82727
+ "Register-ScheduledTask -TaskName $taskName -Description $expectedDefinitionMarker -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null",
82728
+ cleanupLegacyTask,
82729
+ "$registeredTask = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82730
+ "if ($null -eq $registeredTask -or $registeredTask.Description -cne $expectedDefinitionMarker) { throw 'Alan daemon task definition was not applied' }"
82731
+ ].join("; ");
82732
+ const uninstallScript = [
82733
+ taskContext,
82734
+ resolveTaskSid,
82735
+ windowsDefinitionContext,
82736
+ assertOwnedTasks,
82737
+ "if ($null -ne $legacyTask) { Unregister-ScheduledTask -TaskName $legacyTask.TaskName -TaskPath $legacyTask.TaskPath -Confirm:$false -ErrorAction Stop }",
82738
+ "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82739
+ "if ($null -ne $task) { Unregister-ScheduledTask -TaskName $taskName -TaskPath '\\' -Confirm:$false -ErrorAction Stop }",
82740
+ "$remaining = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and ($_.TaskName -eq $taskName -or $_.TaskName -eq $legacyTask.TaskName) })",
82741
+ "if ($remaining.Count -gt 0) { throw 'Alan daemon task still exists after unregister' }"
82643
82742
  ].join("; ");
82743
+ const statusScript = [
82744
+ taskContext,
82745
+ resolveTaskSid,
82746
+ windowsDefinitionContext,
82747
+ checkCurrentOwnership,
82748
+ checkLegacyOwnership,
82749
+ "$definitionMatches = $false",
82750
+ "if ($null -eq $task) { Write-Output 'State: Missing' } else {",
82751
+ "$taskTrigger = @($task.Triggers)[0]",
82752
+ "$definitionMatches = @($task.Actions).Count -eq 1 -and @($task.Triggers).Count -eq 1 -and $task.Description -ceq $expectedDefinitionMarker -and (Resolve-TaskSid ([string]$task.Principal.UserId)) -eq $currentSid -and [string]$task.Principal.LogonType -eq 'Interactive' -and [string]$task.Principal.RunLevel -eq 'Limited' -and [string]$taskAction.Execute -ieq 'powershell.exe' -and [string]$taskAction.Arguments -ceq $expectedLauncherArguments -and [string]$taskAction.WorkingDirectory -ieq $expectedWorkingDirectory -and $taskTrigger.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' -and (Resolve-TaskSid ([string]$taskTrigger.UserId)) -eq $currentSid -and -not $task.Settings.DisallowStartIfOnBatteries -and -not $task.Settings.StopIfGoingOnBatteries -and $task.Settings.RestartCount -eq 3 -and [string]$task.Settings.RestartInterval -eq 'PT1M' -and [string]$task.Settings.ExecutionTimeLimit -eq 'PT0S'",
82753
+ "Write-Output ('TaskName: ' + $task.TaskName); Write-Output ('State: ' + $task.State); Write-Output ('Definition: ' + $task.Description)",
82754
+ "}",
82755
+ "Write-Output ('DefinitionMatch: ' + $definitionMatches)",
82756
+ "Write-Output ('CurrentOwned: ' + $currentOwned)",
82757
+ "$legacyPresent = $null -ne $legacyTask",
82758
+ "Write-Output ('LegacyTask: ' + $legacyPresent)",
82759
+ "Write-Output ('LegacyOwned: ' + $legacyOwned)",
82760
+ "Write-Output ('LegacyState: ' + $legacyState)"
82761
+ ].join("; ");
82762
+ const activeScript = [
82763
+ taskContext,
82764
+ "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82765
+ "if ($null -eq $task) { Write-Output 'Missing' } else { Write-Output $task.State }"
82766
+ ].join("; ");
82767
+ const registerCommand = powershellCommand(registerScript);
82644
82768
  return {
82645
82769
  platform: "win32",
82646
82770
  manifestPath: null,
82647
82771
  manifest: null,
82648
- logDirectory: null,
82649
- loadDefinitionCommands: [
82650
- {
82651
- command: "powershell.exe",
82652
- args: ["-NoProfile", "-NonInteractive", "-Command", registerScript],
82653
- tolerateFailure: false
82654
- }
82655
- ],
82656
- reloadDefinitionCommands: [
82657
- {
82658
- command: "powershell.exe",
82659
- args: ["-NoProfile", "-NonInteractive", "-Command", registerScript],
82660
- tolerateFailure: false
82661
- }
82662
- ],
82772
+ logDirectory: logDir,
82773
+ loadDefinitionCommands: [registerCommand],
82774
+ reloadDefinitionCommands: [registerCommand],
82663
82775
  enableCommands: [],
82664
- startCommands: [
82665
- {
82666
- command: "powershell.exe",
82667
- args: [
82668
- "-NoProfile",
82669
- "-NonInteractive",
82670
- "-Command",
82671
- `Start-ScheduledTask -TaskName ${powershellLiteral(names.windows)}`
82672
- ],
82673
- tolerateFailure: false
82674
- }
82675
- ],
82776
+ startCommands: [powershellCommand(`${taskContext}; Start-ScheduledTask -TaskName $taskName`)],
82676
82777
  stopCommands: [
82677
- {
82678
- command: "powershell.exe",
82679
- args: [
82680
- "-NoProfile",
82681
- "-NonInteractive",
82682
- "-Command",
82683
- `Stop-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -ErrorAction SilentlyContinue`
82684
- ],
82685
- tolerateFailure: true
82686
- }
82687
- ],
82688
- replaceCommands: [
82689
- {
82690
- command: "powershell.exe",
82691
- args: [
82692
- "-NoProfile",
82693
- "-NonInteractive",
82694
- "-Command",
82695
- `${registerScript}; Start-ScheduledTask -TaskName ${powershellLiteral(names.windows)}`
82696
- ],
82697
- tolerateFailure: false
82698
- }
82699
- ],
82700
- uninstallCommands: [
82701
- {
82702
- command: "powershell.exe",
82703
- args: [
82704
- "-NoProfile",
82705
- "-NonInteractive",
82706
- "-Command",
82707
- `Unregister-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -Confirm:$false -ErrorAction SilentlyContinue`
82708
- ],
82709
- tolerateFailure: true
82710
- }
82778
+ powershellCommand(
82779
+ `${taskContext}; Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
82780
+ true
82781
+ )
82711
82782
  ],
82712
- statusCommand: {
82713
- command: "powershell.exe",
82714
- args: [
82715
- "-NoProfile",
82716
- "-NonInteractive",
82717
- "-Command",
82718
- `Get-ScheduledTask -TaskName ${powershellLiteral(names.windows)} | Format-List TaskName,State`
82719
- ],
82720
- tolerateFailure: true
82721
- },
82722
- isActiveCommand: {
82723
- command: "powershell.exe",
82724
- args: [
82725
- "-NoProfile",
82726
- "-NonInteractive",
82727
- "-Command",
82728
- `(Get-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -ErrorAction SilentlyContinue).State`
82729
- ],
82730
- tolerateFailure: true
82731
- },
82732
- lingerStatusCommand: null
82783
+ uninstallCommands: [powershellCommand(uninstallScript)],
82784
+ statusCommand: powershellCommand(statusScript, true),
82785
+ isActiveCommand: powershellCommand(activeScript, true),
82786
+ definitionMarker
82733
82787
  };
82734
82788
  }
82735
82789
  function resolveCurrentPlatform() {
@@ -82739,14 +82793,14 @@ function resolveCurrentPlatform() {
82739
82793
  }
82740
82794
  return currentPlatform;
82741
82795
  }
82742
- function currentServicePlan(args, runtime) {
82743
- const currentPlatform = resolveCurrentPlatform();
82796
+ function currentServicePlan(args, runtime, options = {}) {
82797
+ const currentPlatform = options.platform ?? resolveCurrentPlatform();
82744
82798
  const profile = resolveEndpointProfileFromArgs(args);
82745
82799
  const cliEntry = runtime?.cliEntry ?? process.argv[1];
82746
82800
  if (!cliEntry) throw new Error("Cannot determine the alan-agent executable path");
82747
82801
  return buildDaemonServicePlan({
82748
82802
  platform: currentPlatform,
82749
- homeDir: (0, import_node_os16.homedir)(),
82803
+ homeDir: options.homeDir ?? (0, import_node_os16.homedir)(),
82750
82804
  nodeExecutable: runtime?.executable ?? process.execPath,
82751
82805
  cliEntry: (0, import_node_path27.resolve)(cliEntry),
82752
82806
  profile,
@@ -82769,9 +82823,13 @@ function defaultReadManifest(path2) {
82769
82823
  return (0, import_node_fs25.readFileSync)(path2, "utf8");
82770
82824
  }
82771
82825
  function runServiceCommand(command) {
82772
- const result = (0, import_node_child_process9.spawnSync)(command.command, command.args, { encoding: "utf8", shell: false });
82826
+ const result = (0, import_node_child_process9.spawnSync)(command.command, command.args, {
82827
+ encoding: "utf8",
82828
+ shell: false,
82829
+ ...(0, import_node_os16.platform)() === "win32" ? { timeout: 15e3, windowsHide: true } : {}
82830
+ });
82773
82831
  const status = result.status ?? 1;
82774
- const output2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
82832
+ const output2 = `${result.stdout ?? ""}${result.stderr ?? ""}${result.error?.message ?? ""}`.trim();
82775
82833
  if (status !== 0 && !command.tolerateFailure) {
82776
82834
  throw new Error(
82777
82835
  `Service command failed (${command.command} ${command.args.join(" ")}): ${output2 || `exit ${status}`}`
@@ -82784,6 +82842,23 @@ function defaultSleep(ms) {
82784
82842
  setTimeout(resolveSleep, ms);
82785
82843
  });
82786
82844
  }
82845
+ function isProcessAlive(pid) {
82846
+ if (pid <= 0) return false;
82847
+ try {
82848
+ process.kill(pid, 0);
82849
+ return true;
82850
+ } catch (error61) {
82851
+ return error61.code === "EPERM";
82852
+ }
82853
+ }
82854
+ async function requestWindowsDaemonShutdown(owner) {
82855
+ try {
82856
+ const response = await requestLocalDaemonShutdown(owner.localApiPort, owner.localApiToken);
82857
+ return response.ok ? "accepted" : "rejected";
82858
+ } catch {
82859
+ return "unreachable";
82860
+ }
82861
+ }
82787
82862
  function parseServiceHostState(servicePlatform, result) {
82788
82863
  const output2 = result.output;
82789
82864
  if (servicePlatform === "linux") {
@@ -82809,9 +82884,62 @@ function parseServiceHostState(servicePlatform, result) {
82809
82884
  const stateMatch = output2.match(/\b(?:State|state)\s*[:=]\s*(\w+)/);
82810
82885
  const state = (stateMatch?.[1] ?? output2.trim()).toLowerCase();
82811
82886
  if (state === "running") return "running";
82812
- if (state === "ready" || state === "disabled" || state === "queued") return "stopped";
82887
+ if (state === "queued") return "starting";
82888
+ if (state === "ready" || state === "disabled") return "stopped";
82813
82889
  return "unknown";
82814
82890
  }
82891
+ function parseWindowsTaskInspection(result) {
82892
+ if (result.status !== 0) {
82893
+ throw new Error("Cannot inspect Windows daemon task definition");
82894
+ }
82895
+ const value2 = (name) => {
82896
+ const match = result.output.match(new RegExp(`^${name}:\\s*(.*)$`, "imu"));
82897
+ if (!match) throw new Error(`Incomplete Windows daemon task inspection: missing ${name}`);
82898
+ return match[1]?.trim() ?? "";
82899
+ };
82900
+ const boolean4 = (name) => {
82901
+ const parsed = value2(name).toLowerCase();
82902
+ if (parsed === "true") return true;
82903
+ if (parsed === "false") return false;
82904
+ throw new Error(`Incomplete Windows daemon task inspection: invalid ${name}`);
82905
+ };
82906
+ const currentPresent = /^TaskName:\s*.+$/imu.test(result.output);
82907
+ const currentStateValue = value2("State");
82908
+ const currentOwned = boolean4("CurrentOwned");
82909
+ const definitionMatches = boolean4("DefinitionMatch");
82910
+ const legacyPresent = boolean4("LegacyTask");
82911
+ const legacyOwned = boolean4("LegacyOwned");
82912
+ const legacyStateValue = value2("LegacyState");
82913
+ if (currentPresent !== (currentStateValue.toLowerCase() !== "missing")) {
82914
+ throw new Error("Incomplete Windows daemon task inspection: inconsistent current task state");
82915
+ }
82916
+ if (!currentPresent && (currentOwned || definitionMatches)) {
82917
+ throw new Error(
82918
+ "Incomplete Windows daemon task inspection: absent current task is marked owned"
82919
+ );
82920
+ }
82921
+ if (legacyPresent !== (legacyStateValue.toLowerCase() !== "missing") || !legacyPresent && legacyOwned) {
82922
+ throw new Error("Incomplete Windows daemon task inspection: inconsistent legacy task state");
82923
+ }
82924
+ return {
82925
+ currentPresent,
82926
+ currentOwned,
82927
+ currentState: currentPresent ? parseServiceHostState("win32", { status: 0, output: currentStateValue }) : "stopped",
82928
+ definitionMatches,
82929
+ definitionMarker: result.output.match(/^Definition:\s*(.+)$/imu)?.[1]?.trim(),
82930
+ legacyPresent,
82931
+ legacyOwned,
82932
+ legacyState: legacyPresent ? parseServiceHostState("win32", { status: 0, output: legacyStateValue }) : "stopped"
82933
+ };
82934
+ }
82935
+ function assertWindowsTaskOwnership(tasks) {
82936
+ if (tasks.currentPresent && !tasks.currentOwned) {
82937
+ throw new Error("Windows daemon task is not owned by this Alan installation");
82938
+ }
82939
+ if (tasks.legacyPresent && !tasks.legacyOwned) {
82940
+ throw new Error("Legacy Windows daemon task is not owned by this Alan installation");
82941
+ }
82942
+ }
82815
82943
  function resolveEnsureDefinitionOutcome(input2) {
82816
82944
  if (!input2.definitionChanged) {
82817
82945
  return { changed: false, updatePending: false };
@@ -82821,9 +82949,6 @@ function resolveEnsureDefinitionOutcome(input2) {
82821
82949
  }
82822
82950
  return { changed: true, updatePending: false };
82823
82951
  }
82824
- function shouldStartOnInstall(hostState) {
82825
- return hostState === "stopped";
82826
- }
82827
82952
  function resolveUsername(options) {
82828
82953
  if (options?.username) return options.username;
82829
82954
  try {
@@ -82832,17 +82957,14 @@ function resolveUsername(options) {
82832
82957
  return process.env.USER || process.env.USERNAME || "unknown";
82833
82958
  }
82834
82959
  }
82835
- function buildLingerStatusCommand(username) {
82836
- return {
82837
- command: "loginctl",
82838
- args: ["show-user", username, "-p", "Linger"],
82839
- tolerateFailure: true
82840
- };
82841
- }
82842
82960
  function assertSystemdUserLingerEnabled(options = {}) {
82843
82961
  const username = resolveUsername(options);
82844
82962
  const run = options.runCommand ?? runServiceCommand;
82845
- const result = run(buildLingerStatusCommand(username));
82963
+ const result = run({
82964
+ command: "loginctl",
82965
+ args: ["show-user", username, "-p", "Linger"],
82966
+ tolerateFailure: true
82967
+ });
82846
82968
  if (result.status !== 0) {
82847
82969
  throw new Error(
82848
82970
  `Cannot verify systemd user linger for ${username} (loginctl failed). For durable headless VM boot run: sudo loginctl enable-linger ${username}`
@@ -82856,9 +82978,9 @@ function assertSystemdUserLingerEnabled(options = {}) {
82856
82978
  }
82857
82979
  function createHostContext(options = {}) {
82858
82980
  const args = options.args ?? [];
82859
- const plan = currentServicePlan(args, options.runtime);
82981
+ const plan = currentServicePlan(args, options.runtime, options);
82860
82982
  const run = options.runCommand ?? runServiceCommand;
82861
- const sleep4 = options.sleep ?? defaultSleep;
82983
+ const sleep5 = options.sleep ?? defaultSleep;
82862
82984
  const nowMs = options.nowMs ?? Date.now;
82863
82985
  const readManifest = options.readManifest ?? defaultReadManifest;
82864
82986
  const writeManifest = options.writeManifest ?? writeManifestAtomic;
@@ -82870,13 +82992,20 @@ function createHostContext(options = {}) {
82870
82992
  args,
82871
82993
  plan,
82872
82994
  run,
82873
- sleep: sleep4,
82995
+ sleep: sleep5,
82874
82996
  nowMs,
82875
82997
  readManifest,
82876
82998
  writeManifest,
82877
82999
  removeManifest,
82878
83000
  ensureLogDir,
82879
- profile: resolveEndpointProfileFromArgs(args)
83001
+ configPath: options.configPath ?? resolveAgentConfigPath({ profile: resolveEndpointProfileFromArgs(args) }),
83002
+ readOwner: options.readOwner ?? readLocalDaemonOwnerLease,
83003
+ isProcessAlive: options.isProcessAlive ?? isProcessAlive,
83004
+ probeEndpoint: options.probeEndpoint ?? probeLocalDaemonEndpoint,
83005
+ requestWindowsShutdown: options.requestWindowsShutdown ?? requestWindowsDaemonShutdown,
83006
+ inspectJournal: options.inspectJournal ?? inspectLocalDaemonRunJournal,
83007
+ acquireMaintenance: options.acquireMaintenance ?? acquireLocalDaemonMaintenanceLease,
83008
+ releaseMaintenance: options.releaseMaintenance ?? releaseLocalDaemonMaintenanceLease
82880
83009
  };
82881
83010
  }
82882
83011
  function inspectDaemonService(options = {}) {
@@ -82892,8 +83021,9 @@ function inspectDaemonService(options = {}) {
82892
83021
  }
82893
83022
  function ensureDaemonServiceDefinition(options = {}) {
82894
83023
  const ctx = createHostContext(options);
82895
- const { plan, run, readManifest, writeManifest, ensureLogDir } = ctx;
83024
+ const { plan, run, nowMs, readManifest, writeManifest, ensureLogDir } = ctx;
82896
83025
  let definitionChanged = false;
83026
+ let inspection;
82897
83027
  if (plan.manifestPath && plan.manifest) {
82898
83028
  ensureLogDir();
82899
83029
  const existing = readManifest(plan.manifestPath);
@@ -82902,20 +83032,38 @@ function ensureDaemonServiceDefinition(options = {}) {
82902
83032
  writeManifest(plan.manifestPath, plan.manifest);
82903
83033
  }
82904
83034
  } else if (plan.platform === "win32") {
82905
- definitionChanged = true;
83035
+ ensureLogDir();
83036
+ const result = run(plan.statusCommand);
83037
+ const tasks = parseWindowsTaskInspection(result);
83038
+ assertWindowsTaskOwnership(tasks);
83039
+ if (tasks.currentPresent && tasks.currentState === "unknown" || tasks.legacyPresent && tasks.legacyState === "unknown") {
83040
+ throw new Error("Cannot safely update a Windows daemon task with unknown state");
83041
+ }
83042
+ definitionChanged = tasks.definitionMarker !== plan.definitionMarker || !tasks.definitionMatches || tasks.legacyPresent;
83043
+ const running = tasks.currentState === "running" || tasks.legacyState === "running";
83044
+ inspection = {
83045
+ state: running ? "running" : tasks.currentState,
83046
+ platform: plan.platform,
83047
+ detail: result.output || void 0,
83048
+ observedAtMs: nowMs()
83049
+ };
82906
83050
  }
82907
- const inspection = inspectDaemonService(options);
83051
+ inspection ??= inspectDaemonService(options);
83052
+ const definitionHostState = plan.platform === "win32" && inspection.state === "starting" ? "stopped" : inspection.state;
82908
83053
  const outcome = resolveEnsureDefinitionOutcome({
82909
83054
  definitionChanged,
82910
- hostState: inspection.state
83055
+ hostState: definitionHostState
82911
83056
  });
82912
83057
  if (outcome.updatePending) {
82913
83058
  return outcome;
82914
83059
  }
82915
- if (inspection.state === "running" || inspection.state === "starting") {
83060
+ if (plan.platform === "win32" && !outcome.changed) {
82916
83061
  return outcome;
82917
83062
  }
82918
- const definitionCommands = inspection.state === "stopped" ? plan.reloadDefinitionCommands : plan.loadDefinitionCommands;
83063
+ if (definitionHostState === "running" || definitionHostState === "starting") {
83064
+ return outcome;
83065
+ }
83066
+ const definitionCommands = definitionHostState === "stopped" ? plan.reloadDefinitionCommands : plan.loadDefinitionCommands;
82919
83067
  for (const command of definitionCommands) run(command);
82920
83068
  return outcome;
82921
83069
  }
@@ -82927,10 +83075,140 @@ function startDaemonService(options = {}) {
82927
83075
  const { plan, run } = createHostContext(options);
82928
83076
  for (const command of plan.startCommands) run(command);
82929
83077
  }
82930
- function uninstallDaemonService(args = [], options = {}) {
83078
+ async function stopDaemonServiceAndWait(identity, timeoutMs, options = {}) {
83079
+ const {
83080
+ plan,
83081
+ run,
83082
+ sleep: sleep5,
83083
+ nowMs,
83084
+ configPath,
83085
+ readOwner,
83086
+ isProcessAlive: processIsAlive,
83087
+ probeEndpoint,
83088
+ requestWindowsShutdown: requestShutdown
83089
+ } = createHostContext(options);
83090
+ let owner;
83091
+ try {
83092
+ owner = readOwner(configPath);
83093
+ } catch {
83094
+ return "identity_changed";
83095
+ }
83096
+ const ownerMatches = (candidate) => candidate.pid === identity.pid && candidate.daemonSessionId === identity.daemonSessionId && candidate.localApiPort === identity.port && (identity.serviceId === void 0 || candidate.localServiceId === identity.serviceId) && (identity.startedAtMs === void 0 || candidate.startedAtMs === identity.startedAtMs);
83097
+ if (!owner || !ownerMatches(owner)) {
83098
+ return "identity_changed";
83099
+ }
83100
+ if (plan.platform === "win32") {
83101
+ const shutdown = await requestShutdown(owner);
83102
+ if (shutdown === "rejected") return "identity_changed";
83103
+ if (shutdown === "unreachable") {
83104
+ for (const command of plan.stopCommands) run(command);
83105
+ }
83106
+ const deadline2 = nowMs() + timeoutMs;
83107
+ while (nowMs() < deadline2) {
83108
+ let currentOwner;
83109
+ try {
83110
+ currentOwner = readOwner(configPath);
83111
+ } catch {
83112
+ return "identity_changed";
83113
+ }
83114
+ if (currentOwner && !ownerMatches(currentOwner)) {
83115
+ return "identity_changed";
83116
+ }
83117
+ if (!processIsAlive(identity.pid)) {
83118
+ const endpoint = await probeEndpoint(identity.port, owner.localApiToken, 250);
83119
+ if (endpoint.state === "absent") return "exited";
83120
+ if (endpoint.state === "listening_unauthorized" || endpoint.status.daemonSessionId !== identity.daemonSessionId) {
83121
+ return "identity_changed";
83122
+ }
83123
+ }
83124
+ await sleep5(STOP_POLL_INTERVAL_MS);
83125
+ }
83126
+ return "timeout";
83127
+ }
83128
+ for (const command of plan.stopCommands) run(command);
83129
+ const deadline = nowMs() + timeoutMs;
83130
+ while (nowMs() < deadline) {
83131
+ const result = run(plan.isActiveCommand);
83132
+ const state = parseServiceHostState(plan.platform, result);
83133
+ if (state === "stopped") return "exited";
83134
+ if (state === "unknown") {
83135
+ } else if (identity.pid > 0) {
83136
+ try {
83137
+ process.kill(identity.pid, 0);
83138
+ } catch {
83139
+ return "exited";
83140
+ }
83141
+ }
83142
+ await sleep5(STOP_POLL_INTERVAL_MS);
83143
+ }
83144
+ return "timeout";
83145
+ }
83146
+ async function uninstallDaemonService(args = [], options = {}) {
82931
83147
  const ctx = createHostContext({ ...options, args: options.args ?? args });
82932
- for (const command of ctx.plan.uninstallCommands) ctx.run(command);
82933
- if (ctx.plan.manifestPath) ctx.removeManifest(ctx.plan.manifestPath);
83148
+ if (ctx.plan.platform === "win32") {
83149
+ const ownership = ctx.run(ctx.plan.statusCommand);
83150
+ let tasks;
83151
+ try {
83152
+ tasks = parseWindowsTaskInspection(ownership);
83153
+ } catch (error61) {
83154
+ throw new Error(
83155
+ `Cannot inspect Windows daemon task ownership before uninstall: ${error61 instanceof Error ? error61.message : String(error61)}`
83156
+ );
83157
+ }
83158
+ assertWindowsTaskOwnership(tasks);
83159
+ if (!tasks.currentPresent && !tasks.legacyPresent) {
83160
+ console.info("[alan-agent] Per-user daemon service removed");
83161
+ return;
83162
+ }
83163
+ const acquired = ctx.acquireMaintenance(ctx.configPath, "watchdog", 3e4);
83164
+ if (!acquired.acquired) {
83165
+ throw new Error("Cannot uninstall the Windows daemon service while maintenance is active");
83166
+ }
83167
+ try {
83168
+ const journal = ctx.inspectJournal(ctx.configPath);
83169
+ if (!journal.safeToRestart) {
83170
+ throw new Error(
83171
+ `Windows daemon service uninstall is deferred while ${journal.activeRunCount} active run${journal.activeRunCount === 1 ? "" : "s"} finish`
83172
+ );
83173
+ }
83174
+ let owner;
83175
+ try {
83176
+ owner = ctx.readOwner(ctx.configPath);
83177
+ } catch (error61) {
83178
+ throw new Error(
83179
+ `Cannot verify Windows daemon ownership before uninstall: ${error61 instanceof Error ? error61.message : String(error61)}`
83180
+ );
83181
+ }
83182
+ if (owner) {
83183
+ const stopped = await stopDaemonServiceAndWait(
83184
+ {
83185
+ pid: owner.pid,
83186
+ serviceId: owner.localServiceId,
83187
+ daemonSessionId: owner.daemonSessionId,
83188
+ port: owner.localApiPort,
83189
+ startedAtMs: owner.startedAtMs
83190
+ },
83191
+ 1e4,
83192
+ { ...options, args: ctx.args }
83193
+ );
83194
+ if (stopped !== "exited") {
83195
+ throw new Error(
83196
+ stopped === "identity_changed" ? "Windows daemon ownership changed before uninstall" : "Timed out waiting for the Windows daemon to stop before uninstall"
83197
+ );
83198
+ }
83199
+ } else {
83200
+ if (tasks.currentState !== "stopped" || tasks.legacyState !== "stopped") {
83201
+ throw new Error("Cannot uninstall a running Windows daemon without verified ownership");
83202
+ }
83203
+ }
83204
+ for (const command of ctx.plan.uninstallCommands) ctx.run(command);
83205
+ } finally {
83206
+ ctx.releaseMaintenance(ctx.configPath, acquired.lease.id);
83207
+ }
83208
+ } else {
83209
+ for (const command of ctx.plan.uninstallCommands) ctx.run(command);
83210
+ if (ctx.plan.manifestPath) ctx.removeManifest(ctx.plan.manifestPath);
83211
+ }
82934
83212
  console.info("[alan-agent] Per-user daemon service removed");
82935
83213
  }
82936
83214
  function installDaemonService(args = [], runtime, options = {}) {
@@ -82958,7 +83236,7 @@ function installDaemonService(args = [], runtime, options = {}) {
82958
83236
  });
82959
83237
  return;
82960
83238
  }
82961
- if (!shouldStartOnInstall(inspection.state)) {
83239
+ if (inspection.state !== "stopped") {
82962
83240
  console.info("[alan-agent] Per-user daemon service already present", {
82963
83241
  profile,
82964
83242
  state: inspection.state
@@ -82982,14 +83260,48 @@ ${result.output}
82982
83260
  }
82983
83261
 
82984
83262
  // src/install-command.ts
82985
- async function runInstallCommand(commandArgs) {
83263
+ var WINDOWS_SETUP_READY_TIMEOUT_MS = 9e3;
83264
+ async function runInstallCommand(commandArgs, currentPlatform = process.platform) {
83265
+ if (currentPlatform === "win32" && commandArgs.includes("--headless")) {
83266
+ throw new Error("--headless is only supported on Linux systemd user services");
83267
+ }
82986
83268
  bindConfigPath(commandArgs);
82987
83269
  const configured = readConfig();
82988
83270
  const configurationMode = resolveInstallConfigurationMode(commandArgs, configured);
82989
- if (configurationMode === "setup") await setupDaemon(commandArgs);
83271
+ const isWindowsSetup = currentPlatform === "win32" && configurationMode === "setup";
83272
+ const deadline = isWindowsSetup ? Date.now() + WINDOWS_SETUP_READY_TIMEOUT_MS : 0;
83273
+ const runtimeIdentityChanged = configurationMode === "setup" ? await setupDaemon(commandArgs) : false;
82990
83274
  if (configurationMode === "login") await loginWithDeviceCode(commandArgs);
83275
+ if (!isWindowsSetup) {
83276
+ installDaemonService(commandArgs);
83277
+ runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83278
+ return;
83279
+ }
83280
+ const { runtimeId, localApiPort, localApiToken } = readConfig();
83281
+ if (!runtimeId || !localApiPort || !localApiToken) {
83282
+ throw new Error("Windows runtime setup completed without a local readiness endpoint");
83283
+ }
83284
+ if (runtimeIdentityChanged) {
83285
+ while (Date.now() < deadline) {
83286
+ const status = await probeLocalDaemon(localApiPort, localApiToken, 250);
83287
+ if (status?.runtimeId === runtimeId && status.connected) break;
83288
+ if (!status) {
83289
+ const service = inspectDaemonService({ args: commandArgs });
83290
+ if (service.state !== "running" && service.state !== "starting") break;
83291
+ }
83292
+ await (0, import_promises6.setTimeout)(100);
83293
+ }
83294
+ }
82991
83295
  installDaemonService(commandArgs);
82992
- runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83296
+ while (Date.now() < deadline) {
83297
+ const status = await probeLocalDaemon(localApiPort, localApiToken, 250);
83298
+ if (status?.runtimeId === runtimeId && status.connected) {
83299
+ runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83300
+ return;
83301
+ }
83302
+ await (0, import_promises6.setTimeout)(100);
83303
+ }
83304
+ throw new Error("Windows runtime did not become ready within 9 seconds");
82993
83305
  }
82994
83306
 
82995
83307
  // src/lifecycle-logger.ts
@@ -86366,7 +86678,7 @@ var REQUEST_STATE_ONLY_LEG_PACING_MS2 = 250;
86366
86678
  function inputRequiredRoundsExceededMessage2(method, maxRounds) {
86367
86679
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
86368
86680
  }
86369
- function sleep3(ms, signal) {
86681
+ function sleep4(ms, signal) {
86370
86682
  return new Promise((resolve15, reject) => {
86371
86683
  if (signal?.aborted) {
86372
86684
  reject(signal.reason instanceof SdkError2 ? signal.reason : new SdkError2(SdkErrorCode2.RequestTimeout, String(signal.reason)));
@@ -94633,7 +94945,7 @@ var LegacyInputRequiredShim = class {
94633
94945
  } finally {
94634
94946
  roundAbort.dispose();
94635
94947
  }
94636
- } else await sleep3(REQUEST_STATE_ONLY_LEG_PACING_MS2, outerSignal);
94948
+ } else await sleep4(REQUEST_STATE_ONLY_LEG_PACING_MS2, outerSignal);
94637
94949
  let ctxNext = {
94638
94950
  ...ctx,
94639
94951
  mcpReq: {
@@ -95430,7 +95742,7 @@ async function runMcpStdioProxy(env = process.env, local = new StdioServerTransp
95430
95742
  }
95431
95743
 
95432
95744
  // src/native-hook-command.ts
95433
- var import_node_crypto21 = require("crypto");
95745
+ var import_node_crypto22 = require("crypto");
95434
95746
  var import_node_fs27 = require("fs");
95435
95747
  var import_node_os17 = require("os");
95436
95748
  var import_node_path29 = require("path");
@@ -95799,7 +96111,7 @@ function parseNativeHookEnvelope(input2) {
95799
96111
  const projectPin = eventType === "session_start" && resolvedCwd ? readAlanProjectPin(resolvedCwd) : void 0;
95800
96112
  return {
95801
96113
  schemaVersion: 1,
95802
- eventId: (0, import_node_crypto21.randomUUID)(),
96114
+ eventId: (0, import_node_crypto22.randomUUID)(),
95803
96115
  provider: capability.provider,
95804
96116
  eventType,
95805
96117
  observedAt: (input2.now ?? /* @__PURE__ */ new Date()).toISOString(),
@@ -96295,7 +96607,7 @@ function delay2(ms) {
96295
96607
  }
96296
96608
 
96297
96609
  // src/web-presenter.ts
96298
- var import_node_crypto22 = require("crypto");
96610
+ var import_node_crypto23 = require("crypto");
96299
96611
  var WebPresenter = class {
96300
96612
  constructor(wsClient, conversationId, cwd) {
96301
96613
  this.conversationId = conversationId;
@@ -96481,7 +96793,7 @@ var WebPresenter = class {
96481
96793
  this.applicationProblem = createAlanProblem({
96482
96794
  domain: "application",
96483
96795
  code: input2.code,
96484
- id: (0, import_node_crypto22.randomUUID)(),
96796
+ id: (0, import_node_crypto23.randomUUID)(),
96485
96797
  title: APPLICATION_PROBLEM_TITLES[input2.code],
96486
96798
  detail: input2.message,
96487
96799
  // The schema's slot for the technical cause, so the problem carries it
@@ -96551,7 +96863,7 @@ var WebPresenter = class {
96551
96863
  };
96552
96864
 
96553
96865
  // src/ws-client.ts
96554
- var import_node_crypto24 = require("crypto");
96866
+ var import_node_crypto25 = require("crypto");
96555
96867
 
96556
96868
  // ../shared/dist/agent-liveness.js
96557
96869
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -96579,13 +96891,13 @@ var agentProbeAckSchema = external_exports.object({
96579
96891
  });
96580
96892
 
96581
96893
  // src/sandbox-outbox.ts
96582
- var import_node_crypto23 = require("crypto");
96894
+ var import_node_crypto24 = require("crypto");
96583
96895
  var import_node_fs28 = require("fs");
96584
96896
  function deriveSandboxOutboxKey(sessionToken) {
96585
96897
  if (!sessionToken) {
96586
96898
  throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
96587
96899
  }
96588
- return (0, import_node_crypto23.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
96900
+ return (0, import_node_crypto24.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
96589
96901
  }
96590
96902
  function sandboxOutboxSpoolPath(conversationId) {
96591
96903
  return `/tmp/alan-agent-outbox-${conversationId}.enc`;
@@ -96762,7 +97074,7 @@ var WSClient = class {
96762
97074
  }
96763
97075
  socket;
96764
97076
  /** Stable id for this agent process; fences stale heartbeats server-side. */
96765
- agentSessionId = (0, import_node_crypto24.randomUUID)();
97077
+ agentSessionId = (0, import_node_crypto25.randomUUID)();
96766
97078
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
96767
97079
  heartbeatSeq = 0;
96768
97080
  heartbeatTimer = null;
@@ -97594,7 +97906,7 @@ async function runSandbox(config2) {
97594
97906
  }
97595
97907
 
97596
97908
  // src/skills/skill-cli.ts
97597
- var import_promises6 = require("fs/promises");
97909
+ var import_promises7 = require("fs/promises");
97598
97910
  var import_node_os19 = require("os");
97599
97911
  var import_node_path30 = require("path");
97600
97912
  async function runSkillsCommand(args) {
@@ -97641,7 +97953,7 @@ async function defaultScanInput(homeDirectory2) {
97641
97953
  }
97642
97954
  async function readProtectedJson(path2) {
97643
97955
  const absolutePath = (0, import_node_path30.resolve)(path2);
97644
- const stat2 = await (0, import_promises6.lstat)(absolutePath);
97956
+ const stat2 = await (0, import_promises7.lstat)(absolutePath);
97645
97957
  if (!stat2.isFile()) throw new Error(`Skill input is not a regular file: ${absolutePath}`);
97646
97958
  if (process.platform !== "win32" && (stat2.mode & 63) !== 0) {
97647
97959
  throw new Error(`Skill input must not be accessible by group or other users: ${absolutePath}`);
@@ -97650,7 +97962,7 @@ async function readProtectedJson(path2) {
97650
97962
  throw new Error(`Skill input must be owned by the current user: ${absolutePath}`);
97651
97963
  }
97652
97964
  if (stat2.size > 2 * 1024 * 1024) throw new Error("Skill input exceeds 2 MiB");
97653
- return JSON.parse(await (0, import_promises6.readFile)(absolutePath, "utf8"));
97965
+ return JSON.parse(await (0, import_promises7.readFile)(absolutePath, "utf8"));
97654
97966
  }
97655
97967
  function option(args, name) {
97656
97968
  const index = args.indexOf(name);
@@ -97808,7 +98120,7 @@ async function main() {
97808
98120
  return;
97809
98121
  }
97810
98122
  if (command === "service" && commandArgs[0] === "uninstall") {
97811
- uninstallDaemonService(commandArgs.slice(1));
98123
+ await uninstallDaemonService(commandArgs.slice(1));
97812
98124
  return;
97813
98125
  }
97814
98126
  if (command === "service" && commandArgs[0] === "status") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alan-ai-hq/agent-manager",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
4
4
  "type": "module",
5
5
  "description": "Alan agent runtime — cloud sandbox and local daemon (alan-agent CLI)",
6
6
  "bin": {
@@ -26,9 +26,9 @@
26
26
  "tsup": "^8.5.1",
27
27
  "tsx": "^4.19.0",
28
28
  "typescript": "~5.9.3",
29
- "@alan-ai/sandbox-runtime-spec": "0.1.0",
29
+ "@alan-ai/agent-core": "0.1.0",
30
30
  "@alan-ai/shared": "0.1.0",
31
- "@alan-ai/agent-core": "0.1.0"
31
+ "@alan-ai/sandbox-runtime-spec": "0.1.0"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsup",