aicomputer 0.1.12 → 0.1.13

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.js +329 -116
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ import { readFileSync as readFileSync3 } from "fs";
7
7
  import { basename as basename2 } from "path";
8
8
 
9
9
  // src/commands/access.ts
10
- import { spawn } from "child_process";
11
10
  import { Command } from "commander";
12
11
  import chalk3 from "chalk";
13
12
  import ora from "ora";
@@ -447,6 +446,59 @@ async function generateSSHKey() {
447
446
  return { publicKeyPath, privateKeyPath };
448
447
  }
449
448
 
449
+ // src/lib/ssh-access.ts
450
+ import { spawn } from "child_process";
451
+ async function prepareSSHConnection(computer) {
452
+ const registered = await ensureDefaultSSHKeyRegistered();
453
+ const info = await getConnectionInfo(computer.id);
454
+ if (!info.connection.ssh_available) {
455
+ throw new Error("SSH is not available for this computer");
456
+ }
457
+ return {
458
+ computer,
459
+ command: formatSSHCommand(
460
+ info.connection.ssh_user,
461
+ info.connection.ssh_host,
462
+ info.connection.ssh_port
463
+ ),
464
+ args: [
465
+ "-i",
466
+ registered.privateKeyPath,
467
+ "-p",
468
+ String(info.connection.ssh_port),
469
+ `${info.connection.ssh_user}@${info.connection.ssh_host}`
470
+ ]
471
+ };
472
+ }
473
+ async function prepareSSHConnectionByIdentifier(identifier) {
474
+ const computer = await resolveComputer(identifier);
475
+ return prepareSSHConnection(computer);
476
+ }
477
+ async function openSSHConnection(connection) {
478
+ await new Promise((resolve, reject) => {
479
+ const child = spawn("ssh", connection.args, {
480
+ stdio: "inherit"
481
+ });
482
+ child.on("error", reject);
483
+ child.on("exit", (code) => {
484
+ if (code === 0) {
485
+ resolve();
486
+ return;
487
+ }
488
+ reject(new Error(`ssh exited with code ${code ?? 1}`));
489
+ });
490
+ });
491
+ }
492
+ function formatSSHCommand(user, host, port) {
493
+ if (!user.trim() || !host.trim()) {
494
+ return "ssh unavailable";
495
+ }
496
+ if (port <= 0 || port === 22) {
497
+ return `ssh ${user}@${host}`;
498
+ }
499
+ return `ssh -p ${port} ${user}@${host}`;
500
+ }
501
+
450
502
  // src/lib/open-browser.ts
451
503
  import { constants } from "fs";
452
504
  import { access } from "fs/promises";
@@ -528,41 +580,39 @@ var openCommand = new Command("open").description("Open a computer in your brows
528
580
  await openBrowserURL(access2.access_url);
529
581
  console.log(chalk3.dim(` ${access2.access_url}`));
530
582
  } catch (error) {
531
- spinner.fail(error instanceof Error ? error.message : "Failed to open computer");
583
+ spinner.fail(
584
+ error instanceof Error ? error.message : "Failed to open computer"
585
+ );
532
586
  process.exit(1);
533
587
  }
534
588
  });
535
- var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "22").action(async (identifier, options) => {
536
- if (options.setup) {
537
- await setupSSHAlias(options);
538
- return;
539
- }
540
- const spinner = ora(
541
- identifier ? "Preparing SSH access..." : "Fetching computers..."
542
- ).start();
543
- try {
544
- const computer = await resolveSSHComputer(identifier, spinner);
545
- const info = await getConnectionInfo(computer.id);
546
- if (!info.connection.ssh_available) {
547
- throw new Error("SSH is not available for this computer");
589
+ var sshCommand = new Command("ssh").description("Open an SSH session to a computer").argument("[id-or-handle]", "Computer id or handle").option("--setup", "Register key and configure a global SSH alias").option("--alias <alias>", "SSH host alias", "agentcomputer.ai").option("--host <host>", "SSH gateway host", "ssh.agentcomputer.ai").option("--port <port>", "SSH gateway port", "22").action(
590
+ async (identifier, options) => {
591
+ if (options.setup) {
592
+ await setupSSHAlias(options);
593
+ return;
594
+ }
595
+ const spinner = ora(
596
+ identifier ? "Preparing SSH access..." : "Fetching computers..."
597
+ ).start();
598
+ try {
599
+ const computer = await resolveSSHComputer(identifier, spinner);
600
+ const connection = await prepareSSHConnection(computer);
601
+ spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
602
+ console.log(chalk3.dim(` ${connection.command}`));
603
+ console.log();
604
+ await openSSHConnection(connection);
605
+ } catch (error) {
606
+ spinner.fail(
607
+ error instanceof Error ? error.message : "Failed to prepare SSH access"
608
+ );
609
+ process.exit(1);
548
610
  }
549
- const registered = await ensureDefaultSSHKeyRegistered();
550
- spinner.succeed(`Connecting to ${chalk3.bold(computer.handle)}`);
551
- console.log(chalk3.dim(` ${formatSSHCommand(info.connection.ssh_user, info.connection.ssh_host, info.connection.ssh_port)}`));
552
- console.log();
553
- await runSSH([
554
- "-i",
555
- registered.privateKeyPath,
556
- "-p",
557
- String(info.connection.ssh_port),
558
- `${info.connection.ssh_user}@${info.connection.ssh_host}`
559
- ]);
560
- } catch (error) {
561
- spinner.fail(error instanceof Error ? error.message : "Failed to prepare SSH access");
562
- process.exit(1);
563
611
  }
564
- });
565
- var portsCommand = new Command("ports").description("Manage published app ports");
612
+ );
613
+ var portsCommand = new Command("ports").description(
614
+ "Manage published app ports"
615
+ );
566
616
  portsCommand.command("ls").description("List published ports for a computer").argument("<id-or-handle>", "Computer id or handle").action(async (identifier) => {
567
617
  const spinner = ora("Fetching ports...").start();
568
618
  try {
@@ -588,17 +638,20 @@ portsCommand.command("ls").description("List published ports for a computer").ar
588
638
  console.log(
589
639
  ` ${padEnd(port.subdomain, subWidth + 2)}${padEnd(String(port.target_port), 8)}${port.protocol}`
590
640
  );
591
- console.log(
592
- ` ${chalk3.dim(url)}`
593
- );
641
+ console.log(` ${chalk3.dim(url)}`);
594
642
  }
595
643
  console.log();
596
644
  } catch (error) {
597
- spinner.fail(error instanceof Error ? error.message : "Failed to fetch ports");
645
+ spinner.fail(
646
+ error instanceof Error ? error.message : "Failed to fetch ports"
647
+ );
598
648
  process.exit(1);
599
649
  }
600
650
  });
601
- portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option("--subdomain <value>", "Custom left-hand host label before --<handle>").option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
651
+ portsCommand.command("publish").description("Publish an HTTP app port").argument("<id-or-handle>", "Computer id or handle").argument("<port>", "Target port").option(
652
+ "--subdomain <value>",
653
+ "Custom left-hand host label before --<handle>"
654
+ ).option("--protocol <value>", "http or https", "http").action(async (identifier, port, options) => {
602
655
  const spinner = ora("Publishing port...").start();
603
656
  try {
604
657
  const targetPort = Number.parseInt(port, 10);
@@ -612,10 +665,14 @@ portsCommand.command("publish").description("Publish an HTTP app port").argument
612
665
  protocol: options.protocol
613
666
  });
614
667
  const url = `https://${published.subdomain}--${computer.handle}.computer.agentcomputer.ai`;
615
- spinner.succeed(`Published port ${chalk3.bold(String(published.target_port))}`);
668
+ spinner.succeed(
669
+ `Published port ${chalk3.bold(String(published.target_port))}`
670
+ );
616
671
  console.log(chalk3.dim(` ${url}`));
617
672
  } catch (error) {
618
- spinner.fail(error instanceof Error ? error.message : "Failed to publish port");
673
+ spinner.fail(
674
+ error instanceof Error ? error.message : "Failed to publish port"
675
+ );
619
676
  process.exit(1);
620
677
  }
621
678
  });
@@ -630,25 +687,12 @@ portsCommand.command("rm").description("Unpublish an app port").argument("<id-or
630
687
  await deletePublishedPort(computer.id, targetPort);
631
688
  spinner.succeed(`Removed port ${chalk3.bold(String(targetPort))}`);
632
689
  } catch (error) {
633
- spinner.fail(error instanceof Error ? error.message : "Failed to remove port");
690
+ spinner.fail(
691
+ error instanceof Error ? error.message : "Failed to remove port"
692
+ );
634
693
  process.exit(1);
635
694
  }
636
695
  });
637
- async function runSSH(args) {
638
- await new Promise((resolve, reject) => {
639
- const child = spawn("ssh", args, {
640
- stdio: "inherit"
641
- });
642
- child.on("error", reject);
643
- child.on("exit", (code) => {
644
- if (code === 0) {
645
- resolve();
646
- return;
647
- }
648
- reject(new Error(`ssh exited with code ${code ?? 1}`));
649
- });
650
- });
651
- }
652
696
  async function setupSSHAlias(options) {
653
697
  const spinner = ora("Configuring global SSH access...").start();
654
698
  try {
@@ -672,7 +716,9 @@ async function setupSSHAlias(options) {
672
716
  console.log(` ${chalk3.bold("Direct:")} ssh <handle>@${alias}`);
673
717
  console.log();
674
718
  } catch (error) {
675
- spinner.fail(error instanceof Error ? error.message : "Failed to configure SSH alias");
719
+ spinner.fail(
720
+ error instanceof Error ? error.message : "Failed to configure SSH alias"
721
+ );
676
722
  process.exit(1);
677
723
  }
678
724
  }
@@ -682,7 +728,9 @@ function normalizeSSHAlias(value) {
682
728
  throw new Error("ssh alias cannot be empty");
683
729
  }
684
730
  if (!/^[A-Za-z0-9._-]+$/.test(alias)) {
685
- throw new Error("ssh alias may contain only letters, numbers, dot, dash, or underscore");
731
+ throw new Error(
732
+ "ssh alias may contain only letters, numbers, dot, dash, or underscore"
733
+ );
686
734
  }
687
735
  return alias;
688
736
  }
@@ -703,15 +751,6 @@ function parseSSHPort(value) {
703
751
  }
704
752
  return parsed;
705
753
  }
706
- function formatSSHCommand(user, host, port) {
707
- if (!user.trim() || !host.trim()) {
708
- return "ssh unavailable";
709
- }
710
- if (port <= 0 || port === 22) {
711
- return `ssh ${user}@${host}`;
712
- }
713
- return `ssh -p ${port} ${user}@${host}`;
714
- }
715
754
  async function resolveSSHComputer(identifier, spinner) {
716
755
  const trimmed = identifier?.trim();
717
756
  if (trimmed) {
@@ -720,7 +759,10 @@ async function resolveSSHComputer(identifier, spinner) {
720
759
  const computers = await listComputers();
721
760
  spinner.stop();
722
761
  try {
723
- return await promptForSSHComputer(computers, "Select a computer to SSH into");
762
+ return await promptForSSHComputer(
763
+ computers,
764
+ "Select a computer to SSH into"
765
+ );
724
766
  } finally {
725
767
  spinner.start("Preparing SSH access...");
726
768
  }
@@ -1736,6 +1778,16 @@ var CLAUDE_OAUTH_TOKEN_URL = process.env.CLAUDE_OAUTH_TOKEN_URL ?? "https://plat
1736
1778
  var CLAUDE_OAUTH_REDIRECT_URL = process.env.CLAUDE_OAUTH_REDIRECT_URL ?? "https://platform.claude.com/oauth/code/callback";
1737
1779
  var CLAUDE_OAUTH_SCOPES = (process.env.CLAUDE_OAUTH_SCOPES ?? "user:profile user:inference user:sessions:claude_code user:mcp_servers user:file_upload").split(/\s+/).filter(Boolean);
1738
1780
  var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").description("Authenticate Claude Code on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
1781
+ try {
1782
+ await runClaudeLogin(options);
1783
+ } catch (error) {
1784
+ const message = error instanceof Error ? error.message : "Failed to authenticate Claude";
1785
+ console.error(chalk5.red(`
1786
+ ${message}`));
1787
+ process.exit(1);
1788
+ }
1789
+ });
1790
+ async function runClaudeLogin(options) {
1739
1791
  const todos = createTodoList();
1740
1792
  let target = null;
1741
1793
  let helperCreated = false;
@@ -1749,12 +1801,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
1749
1801
  target = prepared.computer;
1750
1802
  helperCreated = prepared.helperCreated;
1751
1803
  sharedInstall = prepared.sharedInstall;
1752
- markTodo(
1753
- todos,
1754
- "target",
1755
- "done",
1756
- prepared.detail
1757
- );
1804
+ markTodo(todos, "target", "done", prepared.detail);
1758
1805
  activeTodoID = "ready";
1759
1806
  target = await waitForRunning(target);
1760
1807
  markTodo(todos, "ready", "done", `${target.handle} is running`);
@@ -1771,7 +1818,10 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
1771
1818
  sharedInstall ? `installed Claude login on shared home via ${target.handle}` : `installed Claude login on ${target.handle}`
1772
1819
  );
1773
1820
  activeTodoID = "verify-primary";
1774
- const primaryVerification = await verifyTargetMachine(target.handle, sshTarget);
1821
+ const primaryVerification = await verifyTargetMachine(
1822
+ target.handle,
1823
+ sshTarget
1824
+ );
1775
1825
  markVerificationTodo(
1776
1826
  todos,
1777
1827
  "verify-primary",
@@ -1817,12 +1867,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
1817
1867
  markTodo(todos, "cleanup", "failed", message);
1818
1868
  }
1819
1869
  } else if (helperCreated && target && options.keepHelper) {
1820
- markTodo(
1821
- todos,
1822
- "cleanup",
1823
- "skipped",
1824
- `kept helper ${target.handle}`
1825
- );
1870
+ markTodo(todos, "cleanup", "skipped", `kept helper ${target.handle}`);
1826
1871
  } else {
1827
1872
  markTodo(todos, "cleanup", "skipped", "no helper created");
1828
1873
  }
@@ -1831,9 +1876,7 @@ var claudeLoginCommand = new Command4("claude-login").alias("claude-auth").descr
1831
1876
  }
1832
1877
  }
1833
1878
  if (failureMessage) {
1834
- console.error(chalk5.red(`
1835
- ${failureMessage}`));
1836
- process.exit(1);
1879
+ throw new Error(failureMessage);
1837
1880
  }
1838
1881
  if (target) {
1839
1882
  console.log(
@@ -1841,15 +1884,23 @@ ${failureMessage}`));
1841
1884
  );
1842
1885
  console.log();
1843
1886
  }
1844
- });
1887
+ }
1845
1888
  function createTodoList() {
1846
1889
  return [
1847
1890
  { id: "target", label: "Pick target computer", state: "pending" },
1848
1891
  { id: "ready", label: "Wait for machine readiness", state: "pending" },
1849
1892
  { id: "oauth", label: "Complete Claude browser auth", state: "pending" },
1850
1893
  { id: "install", label: "Install stored Claude login", state: "pending" },
1851
- { id: "verify-primary", label: "Verify on target machine", state: "pending" },
1852
- { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
1894
+ {
1895
+ id: "verify-primary",
1896
+ label: "Verify on target machine",
1897
+ state: "pending"
1898
+ },
1899
+ {
1900
+ id: "verify-shared",
1901
+ label: "Verify shared-home propagation",
1902
+ state: "pending"
1903
+ },
1853
1904
  { id: "cleanup", label: "Clean up temporary helper", state: "pending" }
1854
1905
  ];
1855
1906
  }
@@ -1889,10 +1940,14 @@ async function prepareTargetMachine(options) {
1889
1940
  async function runManualOAuthFlow() {
1890
1941
  const codeVerifier = base64url(randomBytes2(32));
1891
1942
  const state = randomBytes2(16).toString("hex");
1892
- const codeChallenge = base64url(createHash("sha256").update(codeVerifier).digest());
1943
+ const codeChallenge = base64url(
1944
+ createHash("sha256").update(codeVerifier).digest()
1945
+ );
1893
1946
  const url = buildAuthorizationURL(codeChallenge, state);
1894
1947
  console.log("We will open your browser so you can authenticate with Claude.");
1895
- console.log("If the browser does not open automatically, use the URL below:\n");
1948
+ console.log(
1949
+ "If the browser does not open automatically, use the URL below:\n"
1950
+ );
1896
1951
  console.log(url);
1897
1952
  console.log();
1898
1953
  try {
@@ -1903,7 +1958,9 @@ async function runManualOAuthFlow() {
1903
1958
  console.log(
1904
1959
  "After completing authentication, copy the code shown on the success page."
1905
1960
  );
1906
- console.log("You can paste either the full URL, or a value formatted as CODE#STATE.\n");
1961
+ console.log(
1962
+ "You can paste either the full URL, or a value formatted as CODE#STATE.\n"
1963
+ );
1907
1964
  const pasted = (await textInput({
1908
1965
  message: "Paste the authorization code (or URL) here:"
1909
1966
  })).trim();
@@ -1970,7 +2027,9 @@ function parseAuthorizationInput(value, expectedState) {
1970
2027
  throw new Error("pasted URL is missing code or state");
1971
2028
  }
1972
2029
  if (state2 !== expectedState) {
1973
- throw new Error("state mismatch detected; restart the authentication flow");
2030
+ throw new Error(
2031
+ "state mismatch detected; restart the authentication flow"
2032
+ );
1974
2033
  }
1975
2034
  return { code: code2, state: state2 };
1976
2035
  }
@@ -1984,10 +2043,16 @@ function parseAuthorizationInput(value, expectedState) {
1984
2043
  return { code, state };
1985
2044
  }
1986
2045
  async function installClaudeAuth(target, oauth) {
1987
- const spinner = ora4(`Installing Claude auth on ${chalk5.bold(target.handle)}...`).start();
2046
+ const spinner = ora4(
2047
+ `Installing Claude auth on ${chalk5.bold(target.handle)}...`
2048
+ ).start();
1988
2049
  try {
1989
2050
  const installScript = buildInstallScript(oauth.refreshToken, oauth.scope);
1990
- const result = await runRemoteCommand(target, ["bash", "-s"], installScript);
2051
+ const result = await runRemoteCommand(
2052
+ target,
2053
+ ["bash", "-s"],
2054
+ installScript
2055
+ );
1991
2056
  if (result.stdout.trim()) {
1992
2057
  spinner.succeed(`Installed Claude auth on ${chalk5.bold(target.handle)}`);
1993
2058
  return;
@@ -2001,7 +2066,9 @@ async function installClaudeAuth(target, oauth) {
2001
2066
  }
2002
2067
  }
2003
2068
  async function verifyTargetMachine(handle, target) {
2004
- const spinner = ora4(`Verifying Claude login on ${chalk5.bold(handle)}...`).start();
2069
+ const spinner = ora4(
2070
+ `Verifying Claude login on ${chalk5.bold(handle)}...`
2071
+ ).start();
2005
2072
  const result = await verifyStoredAuth(target);
2006
2073
  if (result.status === "verified") {
2007
2074
  spinner.succeed(`Verified Claude login on ${chalk5.bold(handle)}`);
@@ -2021,7 +2088,9 @@ async function verifySharedInstall(primaryHandle, primaryComputerID, sharedInsta
2021
2088
  verify
2022
2089
  );
2023
2090
  if (result.status === "verified") {
2024
- spinner.succeed(`Verified shared-home Claude login on ${chalk5.bold(result.handle)}`);
2091
+ spinner.succeed(
2092
+ `Verified shared-home Claude login on ${chalk5.bold(result.handle)}`
2093
+ );
2025
2094
  return result;
2026
2095
  }
2027
2096
  spinner.info(result.reason);
@@ -2934,6 +3003,16 @@ import { Command as Command7 } from "commander";
2934
3003
  import chalk7 from "chalk";
2935
3004
  import ora6 from "ora";
2936
3005
  var codexLoginCommand = new Command7("codex-login").alias("codex-auth").description("Authenticate Codex on a computer").option("--machine <id-or-handle>", "Use a specific computer").option("--keep-helper", "Keep a temporary helper machine if one is created").option("--skip-cross-check", "Skip verification on a second shared machine").option("--verbose", "Show step-by-step auth diagnostics").action(async (options) => {
3006
+ try {
3007
+ await runCodexLogin(options);
3008
+ } catch (error) {
3009
+ const message = error instanceof Error ? error.message : "Failed to authenticate Codex";
3010
+ console.error(chalk7.red(`
3011
+ ${message}`));
3012
+ process.exit(1);
3013
+ }
3014
+ });
3015
+ async function runCodexLogin(options) {
2937
3016
  const todos = createTodoList2();
2938
3017
  let target = null;
2939
3018
  let helperCreated = false;
@@ -2964,7 +3043,10 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
2964
3043
  sharedInstall ? `installed Codex login on shared home via ${target.handle}` : `installed Codex login on ${target.handle}`
2965
3044
  );
2966
3045
  activeTodoID = "verify-primary";
2967
- const primaryVerification = await verifyTargetMachine2(target.handle, sshTarget);
3046
+ const primaryVerification = await verifyTargetMachine2(
3047
+ target.handle,
3048
+ sshTarget
3049
+ );
2968
3050
  markVerificationTodo2(
2969
3051
  todos,
2970
3052
  "verify-primary",
@@ -3019,9 +3101,7 @@ var codexLoginCommand = new Command7("codex-login").alias("codex-auth").descript
3019
3101
  }
3020
3102
  }
3021
3103
  if (failureMessage) {
3022
- console.error(chalk7.red(`
3023
- ${failureMessage}`));
3024
- process.exit(1);
3104
+ throw new Error(failureMessage);
3025
3105
  }
3026
3106
  if (target) {
3027
3107
  console.log(
@@ -3029,15 +3109,23 @@ ${failureMessage}`));
3029
3109
  );
3030
3110
  console.log();
3031
3111
  }
3032
- });
3112
+ }
3033
3113
  function createTodoList2() {
3034
3114
  return [
3035
3115
  { id: "target", label: "Pick target computer", state: "pending" },
3036
3116
  { id: "ready", label: "Wait for machine readiness", state: "pending" },
3037
3117
  { id: "local-auth", label: "Complete local Codex auth", state: "pending" },
3038
3118
  { id: "install", label: "Install stored Codex login", state: "pending" },
3039
- { id: "verify-primary", label: "Verify on target machine", state: "pending" },
3040
- { id: "verify-shared", label: "Verify shared-home propagation", state: "pending" },
3119
+ {
3120
+ id: "verify-primary",
3121
+ label: "Verify on target machine",
3122
+ state: "pending"
3123
+ },
3124
+ {
3125
+ id: "verify-shared",
3126
+ label: "Verify shared-home propagation",
3127
+ state: "pending"
3128
+ },
3041
3129
  { id: "cleanup", label: "Clean up temporary helper", state: "pending" }
3042
3130
  ];
3043
3131
  }
@@ -3083,10 +3171,16 @@ async function ensureLocalCodexAuth() {
3083
3171
  };
3084
3172
  }
3085
3173
  if (!process.stdin.isTTY || !process.stdout.isTTY) {
3086
- throw new Error("local Codex login is required when not running interactively");
3174
+ throw new Error(
3175
+ "local Codex login is required when not running interactively"
3176
+ );
3087
3177
  }
3088
- console.log("We will open your browser so you can authenticate Codex locally.");
3089
- console.log("If Codex falls back to device auth, complete that flow and return here.\n");
3178
+ console.log(
3179
+ "We will open your browser so you can authenticate Codex locally."
3180
+ );
3181
+ console.log(
3182
+ "If Codex falls back to device auth, complete that flow and return here.\n"
3183
+ );
3090
3184
  await runInteractiveCodexLogin();
3091
3185
  const refreshedStatus = await getLocalCodexStatus();
3092
3186
  if (!refreshedStatus.loggedIn) {
@@ -3166,7 +3260,9 @@ async function captureLocalCommand(command, args) {
3166
3260
  });
3167
3261
  }
3168
3262
  async function installCodexAuth(target, authJSON) {
3169
- const spinner = ora6(`Installing Codex login on ${chalk7.bold(target.handle)}...`).start();
3263
+ const spinner = ora6(
3264
+ `Installing Codex login on ${chalk7.bold(target.handle)}...`
3265
+ ).start();
3170
3266
  try {
3171
3267
  const installScript = buildInstallScript2(authJSON);
3172
3268
  await runRemoteCommand(target, ["bash", "-s"], installScript);
@@ -3179,7 +3275,9 @@ async function installCodexAuth(target, authJSON) {
3179
3275
  }
3180
3276
  }
3181
3277
  async function verifyTargetMachine2(handle, target) {
3182
- const spinner = ora6(`Verifying Codex login on ${chalk7.bold(handle)}...`).start();
3278
+ const spinner = ora6(
3279
+ `Verifying Codex login on ${chalk7.bold(handle)}...`
3280
+ ).start();
3183
3281
  const result = await verifyStoredCodexAuth(target);
3184
3282
  if (result.status === "verified") {
3185
3283
  spinner.succeed(`Verified Codex login on ${chalk7.bold(handle)}`);
@@ -3199,7 +3297,9 @@ async function verifySharedInstall2(primaryHandle, primaryComputerID, sharedInst
3199
3297
  verify
3200
3298
  );
3201
3299
  if (result.status === "verified") {
3202
- spinner.succeed(`Verified shared-home Codex login on ${chalk7.bold(result.handle)}`);
3300
+ spinner.succeed(
3301
+ `Verified shared-home Codex login on ${chalk7.bold(result.handle)}`
3302
+ );
3203
3303
  return result;
3204
3304
  }
3205
3305
  spinner.info(result.reason);
@@ -3653,10 +3753,12 @@ import ora8 from "ora";
3653
3753
 
3654
3754
  // src/lib/browser-login.ts
3655
3755
  import { randomBytes as randomBytes3 } from "crypto";
3656
- import { createServer } from "http";
3756
+ import {
3757
+ createServer
3758
+ } from "http";
3657
3759
  var CALLBACK_HOST = "127.0.0.1";
3658
3760
  var CALLBACK_PATH = "/callback";
3659
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1e3;
3761
+ var LOGIN_TIMEOUT_MS = 15 * 60 * 1e3;
3660
3762
  async function createBrowserLoginAttempt() {
3661
3763
  const state = randomBytes3(16).toString("hex");
3662
3764
  const deferred = createDeferred();
@@ -3736,7 +3838,11 @@ async function handleRequest(input) {
3736
3838
  return;
3737
3839
  }
3738
3840
  if (settledRef.current) {
3739
- writeHTML(response, 409, renderErrorPage("This login link has already been used."));
3841
+ writeHTML(
3842
+ response,
3843
+ 409,
3844
+ renderErrorPage("This login link has already been used.")
3845
+ );
3740
3846
  return;
3741
3847
  }
3742
3848
  const returnedState = url.searchParams.get("state")?.trim();
@@ -3763,6 +3869,9 @@ async function handleRequest(input) {
3763
3869
  writeHTML(response, 400, renderErrorPage(error.message));
3764
3870
  return;
3765
3871
  }
3872
+ const machineHandle = url.searchParams.get("machine_handle")?.trim() || void 0;
3873
+ const provider = parseProvider(url.searchParams.get("provider"));
3874
+ const autoSSH = parseAutoSSH(url.searchParams.get("auto_ssh"));
3766
3875
  try {
3767
3876
  const me = await apiWithKey(apiKey, "/v1/me");
3768
3877
  setAPIKey(apiKey);
@@ -3771,9 +3880,21 @@ async function handleRequest(input) {
3771
3880
  apiKey,
3772
3881
  callbackURL,
3773
3882
  loginURL: buildBrowserLoginURL(callbackURL, state),
3774
- me
3883
+ me,
3884
+ machineHandle,
3885
+ provider,
3886
+ autoSSH
3775
3887
  });
3776
- writeHTML(response, 200, renderSuccessPage(me.user.email));
3888
+ writeHTML(
3889
+ response,
3890
+ 200,
3891
+ renderSuccessPage({
3892
+ autoSSH,
3893
+ email: me.user.email,
3894
+ machineHandle,
3895
+ provider
3896
+ })
3897
+ );
3777
3898
  } catch (error) {
3778
3899
  settledRef.current = true;
3779
3900
  const message = error instanceof Error ? error.message : "Failed to validate browser login";
@@ -3781,6 +3902,22 @@ async function handleRequest(input) {
3781
3902
  writeHTML(response, 401, renderErrorPage(message));
3782
3903
  }
3783
3904
  }
3905
+ function parseProvider(rawValue) {
3906
+ switch (rawValue?.trim()) {
3907
+ case "claude":
3908
+ case "codex":
3909
+ case "skip":
3910
+ return rawValue.trim();
3911
+ default:
3912
+ return void 0;
3913
+ }
3914
+ }
3915
+ function parseAutoSSH(rawValue) {
3916
+ if (rawValue === null) {
3917
+ return void 0;
3918
+ }
3919
+ return !(rawValue === "0" || rawValue === "false");
3920
+ }
3784
3921
  function createDeferred() {
3785
3922
  let resolve;
3786
3923
  let reject;
@@ -3824,7 +3961,9 @@ function writeHTML(response, statusCode, body) {
3824
3961
  response.setHeader("content-type", "text/html; charset=utf-8");
3825
3962
  response.end(body);
3826
3963
  }
3827
- function renderSuccessPage(email) {
3964
+ function renderSuccessPage(input) {
3965
+ const providerMessage = input.provider && input.provider !== "skip" ? ` The CLI will continue with ${escapeHTML(input.provider)} setup.` : "";
3966
+ const machineMessage = input.machineHandle ? ` Your new sandbox <code>${escapeHTML(input.machineHandle)}</code> is ready.${providerMessage} Return to the terminal to finish setup.` : " You can close this tab.";
3828
3967
  return `<!doctype html>
3829
3968
  <html lang="en">
3830
3969
  <head>
@@ -3842,7 +3981,7 @@ function renderSuccessPage(email) {
3842
3981
  <body>
3843
3982
  <main>
3844
3983
  <h1>Computer CLI login complete</h1>
3845
- <p>Signed in as <code>${escapeHTML(email)}</code>. You can close this tab.</p>
3984
+ <p>Signed in as <code>${escapeHTML(input.email)}</code>.${machineMessage}</p>
3846
3985
  </main>
3847
3986
  <script>
3848
3987
  window.setTimeout(() => window.close(), 150);
@@ -3881,7 +4020,9 @@ var loginCommand = new Command9("login").description("Authenticate the CLI").opt
3881
4020
  const existingKey = getStoredAPIKey();
3882
4021
  if (existingKey && !options.force) {
3883
4022
  console.log();
3884
- console.log(chalk9.yellow(" Already logged in. Use --force to overwrite."));
4023
+ console.log(
4024
+ chalk9.yellow(" Already logged in. Use --force to overwrite.")
4025
+ );
3885
4026
  console.log();
3886
4027
  return;
3887
4028
  }
@@ -3927,7 +4068,9 @@ async function runBrowserLogin() {
3927
4068
  } catch {
3928
4069
  spinner.stop();
3929
4070
  console.log();
3930
- console.log(chalk9.yellow(" Browser auto-open failed. Open this URL to continue:"));
4071
+ console.log(
4072
+ chalk9.yellow(" Browser auto-open failed. Open this URL to continue:")
4073
+ );
3931
4074
  console.log(chalk9.dim(` ${attempt.loginURL}`));
3932
4075
  console.log();
3933
4076
  spinner.start("Waiting for browser login...");
@@ -3935,8 +4078,11 @@ async function runBrowserLogin() {
3935
4078
  spinner.text = "Waiting for browser login...";
3936
4079
  const result = await attempt.waitForResult();
3937
4080
  spinner.succeed(`Logged in as ${chalk9.bold(result.me.user.email)}`);
4081
+ await continueFirstLoginFlow(result);
3938
4082
  } catch (error) {
3939
- spinner.fail(error instanceof Error ? error.message : "Browser login failed");
4083
+ spinner.fail(
4084
+ error instanceof Error ? error.message : "Browser login failed"
4085
+ );
3940
4086
  process.exit(1);
3941
4087
  } finally {
3942
4088
  await attempt?.close();
@@ -3958,6 +4104,73 @@ async function resolveAPIKeyInput(flagValue, readFromStdin) {
3958
4104
  }
3959
4105
  return Buffer.concat(chunks).toString("utf8").trim();
3960
4106
  }
4107
+ async function continueFirstLoginFlow(result) {
4108
+ const machineHandle = result.machineHandle?.trim();
4109
+ if (!machineHandle) {
4110
+ return;
4111
+ }
4112
+ console.log();
4113
+ console.log(
4114
+ chalk9.cyan(
4115
+ `Continuing first-time setup for ${chalk9.bold(machineHandle)}...
4116
+ `
4117
+ )
4118
+ );
4119
+ try {
4120
+ await runSelectedProvider(result.provider, machineHandle);
4121
+ if (result.autoSSH === false) {
4122
+ printNextStep(machineHandle);
4123
+ return;
4124
+ }
4125
+ const spinner = ora8(`Preparing SSH access for ${machineHandle}...`).start();
4126
+ try {
4127
+ const connection = await prepareSSHConnectionByIdentifier(machineHandle);
4128
+ spinner.succeed(`Connecting to ${chalk9.bold(machineHandle)}`);
4129
+ console.log(chalk9.dim(` ${connection.command}`));
4130
+ console.log();
4131
+ await openSSHConnection(connection);
4132
+ } catch (error) {
4133
+ spinner.fail(
4134
+ error instanceof Error ? error.message : "Failed to prepare SSH access"
4135
+ );
4136
+ throw error;
4137
+ }
4138
+ } catch (error) {
4139
+ const message = error instanceof Error ? error.message : "Failed to finish first-time setup";
4140
+ console.error(chalk9.red(`
4141
+ ${message}`));
4142
+ console.log();
4143
+ if (result.provider === "claude") {
4144
+ console.log(
4145
+ chalk9.dim(` computer claude-login --machine ${machineHandle}`)
4146
+ );
4147
+ } else if (result.provider === "codex") {
4148
+ console.log(
4149
+ chalk9.dim(` computer codex-login --machine ${machineHandle}`)
4150
+ );
4151
+ }
4152
+ console.log(chalk9.dim(` computer ssh ${machineHandle}`));
4153
+ console.log();
4154
+ process.exit(1);
4155
+ }
4156
+ }
4157
+ async function runSelectedProvider(provider, machineHandle) {
4158
+ if (provider === "claude") {
4159
+ await runClaudeLogin({ machine: machineHandle });
4160
+ return;
4161
+ }
4162
+ if (provider === "codex") {
4163
+ await runCodexLogin({ machine: machineHandle });
4164
+ return;
4165
+ }
4166
+ console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
4167
+ console.log();
4168
+ }
4169
+ function printNextStep(machineHandle) {
4170
+ console.log(chalk9.green(`Sandbox ${chalk9.bold(machineHandle)} is ready.`));
4171
+ console.log(chalk9.dim(` computer ssh ${machineHandle}`));
4172
+ console.log();
4173
+ }
3961
4174
 
3962
4175
  // src/commands/logout.ts
3963
4176
  import { Command as Command10 } from "commander";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aicomputer",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "Computer CLI - manage your Agent Computer fleet from the terminal",
5
5
  "homepage": "https://agentcomputer.ai",
6
6
  "repository": {