theclawbay 0.3.40 → 0.3.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,8 @@ class LinkCommand extends base_command_1.BaseCommand {
16
16
  const backendUrl = backendUrlFlag ?? inferredBackendUrl ?? DEFAULT_BACKEND_URL;
17
17
  await (0, config_1.writeManagedConfig)({
18
18
  backendUrl,
19
- apiKey,
19
+ authType: "api-key",
20
+ credential: apiKey,
20
21
  });
21
22
  this.log(`Linked. Managed config written to ${paths_1.managedConfigPath}`);
22
23
  this.log(`Backend: ${backendUrl}`);
@@ -12,6 +12,8 @@ const base_command_1 = require("../lib/base-command");
12
12
  const codex_auth_seeding_1 = require("../lib/codex-auth-seeding");
13
13
  const codex_history_migration_1 = require("../lib/codex-history-migration");
14
14
  const codex_model_cache_migration_1 = require("../lib/codex-model-cache-migration");
15
+ const config_1 = require("../lib/managed/config");
16
+ const errors_1 = require("../lib/managed/errors");
15
17
  const supported_models_1 = require("../lib/supported-models");
16
18
  const codex_vscode_patch_1 = require("../lib/codex-vscode-patch");
17
19
  const paths_1 = require("../lib/config/paths");
@@ -58,7 +60,7 @@ function logLogoutCompactSummary(params) {
58
60
  params.log("Reverted: no local tool changes were found.");
59
61
  }
60
62
  if (params.sharedConfigChanged) {
61
- params.log("Shared config: removed your API key env and managed machine setup.");
63
+ params.log("Shared config: removed your local credential env and managed machine setup.");
62
64
  }
63
65
  else {
64
66
  params.log("Shared config: nothing managed needed cleanup.");
@@ -710,6 +712,14 @@ class LogoutCommand extends base_command_1.BaseCommand {
710
712
  await this.runSafe(async () => {
711
713
  const { flags } = await this.parse(LogoutCommand);
712
714
  const debugOutput = flags["debug-output"];
715
+ let managed = null;
716
+ try {
717
+ managed = await (0, config_1.readManagedConfig)();
718
+ }
719
+ catch (error) {
720
+ if (!(error instanceof errors_1.ManagedConfigMissingError))
721
+ throw error;
722
+ }
713
723
  const progress = this.createProgressHandle(!debugOutput);
714
724
  let deletedManagedPaths = 0;
715
725
  let removedEnvFile = false;
@@ -729,7 +739,31 @@ class LogoutCommand extends base_command_1.BaseCommand {
729
739
  let authSeedCleanup;
730
740
  let modelCacheCleanup;
731
741
  let codexVsCodePatchCleanup;
742
+ let remoteRevokeWarning = null;
732
743
  try {
744
+ if (managed?.authType === "device-session" && managed.credential.trim()) {
745
+ progress.update("Revoking linked device");
746
+ try {
747
+ const response = await fetch(`${managed.backendUrl}/api/setup/device-session/revoke`, {
748
+ method: "POST",
749
+ headers: {
750
+ Authorization: `Bearer ${managed.credential}`,
751
+ Accept: "application/json",
752
+ },
753
+ signal: AbortSignal.timeout(10000),
754
+ });
755
+ if (!response.ok && response.status !== 401) {
756
+ const body = (await response.json().catch(() => ({})));
757
+ remoteRevokeWarning =
758
+ body.error ??
759
+ `remote device revoke failed with HTTP ${response.status}`;
760
+ }
761
+ }
762
+ catch (error) {
763
+ remoteRevokeWarning =
764
+ error.message || "failed to revoke linked device";
765
+ }
766
+ }
733
767
  progress.update("Removing shared machine config");
734
768
  deletedManagedPaths = (await Promise.all([
735
769
  removeFileIfExists(paths_1.managedConfigPath),
@@ -826,6 +860,9 @@ class LogoutCommand extends base_command_1.BaseCommand {
826
860
  summaryNotes.add(codexVsCodePatchCleanup.warning);
827
861
  if (modelCacheCleanup.warning)
828
862
  summaryNotes.add(modelCacheCleanup.warning);
863
+ if (remoteRevokeWarning) {
864
+ summaryNotes.add(`Device revoke warning: ${remoteRevokeWarning}`);
865
+ }
829
866
  if (!debugOutput) {
830
867
  logLogoutCompactSummary({
831
868
  log: (message) => this.log(message),
@@ -836,7 +873,13 @@ class LogoutCommand extends base_command_1.BaseCommand {
836
873
  return;
837
874
  }
838
875
  this.log(`- Managed configs removed: ${deletedManagedPaths}`);
839
- this.log(`- API key env file removed: ${removedEnvFile ? "yes" : "no"}`);
876
+ this.log(`- Local credential env file removed: ${removedEnvFile ? "yes" : "no"}`);
877
+ if (remoteRevokeWarning) {
878
+ this.log(`- Remote device revoke: ${remoteRevokeWarning}`);
879
+ }
880
+ else if (managed?.authType === "device-session") {
881
+ this.log("- Remote device revoke: completed.");
882
+ }
840
883
  this.log(`- Shell profiles updated: ${updatedShellFiles.length ? updatedShellFiles.join(", ") : "none"}`);
841
884
  this.log(`- VS Code env hooks updated: ${updatedVsCodeHooks.length ? updatedVsCodeHooks.join(", ") : "none"}`);
842
885
  this.log(`- Codex config cleaned: ${updatedCodexConfig ? "yes" : "no"}`);
@@ -909,7 +952,7 @@ class LogoutCommand extends base_command_1.BaseCommand {
909
952
  });
910
953
  }
911
954
  }
912
- LogoutCommand.description = "Log out from local The Claw Bay API-key auth on this machine";
955
+ LogoutCommand.description = "Log out from local The Claw Bay auth on this machine";
913
956
  LogoutCommand.flags = {
914
957
  "debug-output": core_1.Flags.boolean({
915
958
  required: false,
@@ -4,6 +4,7 @@ export default class SetupCommand extends BaseCommand {
4
4
  static flags: {
5
5
  backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
6
6
  "api-key": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
+ "device-name": import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
7
8
  clients: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
8
9
  yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
9
10
  "codex-usage-ui": import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
@@ -22,6 +22,7 @@ const api_key_1 = require("../lib/managed/api-key");
22
22
  const config_1 = require("../lib/managed/config");
23
23
  const errors_1 = require("../lib/managed/errors");
24
24
  const supported_models_1 = require("../lib/supported-models");
25
+ const device_session_auth_1 = require("../lib/device-session-auth");
25
26
  const DEFAULT_BACKEND_URL = "https://theclawbay.com";
26
27
  const DEFAULT_PROVIDER_ID = "theclawbay";
27
28
  const CLI_HTTP_USER_AGENT = "theclawbay-cli";
@@ -163,7 +164,7 @@ function logSetupCompactSummary(params) {
163
164
  else {
164
165
  params.log("Configured: no local tools selected.");
165
166
  }
166
- params.log("Shared config: saved your API key and managed machine setup.");
167
+ params.log("Shared config: saved your The Claw Bay credential and managed machine setup.");
167
168
  if (restartTargets.length > 0) {
168
169
  params.log(`Restart if already open: ${formatSummaryList(restartTargets)}`);
169
170
  }
@@ -687,7 +688,7 @@ async function promptForSetupClients(clients) {
687
688
  let settled = false;
688
689
  let hint = clients.some((client) => client.detected)
689
690
  ? "Choose the tools you want to configure."
690
- : "No supported local clients are detected yet. Apply setup will just save your managed config and API key env.";
691
+ : "No supported local clients are detected yet. Apply setup will just save your managed config and local credential env.";
691
692
  const stdin = process.stdin;
692
693
  const stdout = process.stdout;
693
694
  const wasRaw = stdin.isRaw;
@@ -751,7 +752,6 @@ async function promptForSetupClients(clients) {
751
752
  stdin.off("keypress", onKeypress);
752
753
  if (stdin.isTTY)
753
754
  stdin.setRawMode(Boolean(wasRaw));
754
- stdin.pause();
755
755
  stdout.write("\x1b[?25h");
756
756
  clearScreen();
757
757
  const finalSelection = new Set(options.filter((option) => option.checked).map((option) => option.id));
@@ -852,19 +852,38 @@ function resolveSetupClientSelection(params) {
852
852
  async function promptForCodexUsageUiPatch() {
853
853
  if (!process.stdin.isTTY || !process.stdout.isTTY)
854
854
  return false;
855
- const rl = (0, promises_2.createInterface)({
856
- input: process.stdin,
857
- output: process.stdout,
855
+ const stdin = process.stdin;
856
+ const stdout = process.stdout;
857
+ const wasRaw = stdin.isRaw;
858
+ stdin.resume();
859
+ return new Promise((resolve) => {
860
+ const finish = (enabled) => {
861
+ stdin.off("data", onData);
862
+ if (stdin.isTTY)
863
+ stdin.setRawMode(Boolean(wasRaw));
864
+ stdout.write("\n");
865
+ resolve(enabled);
866
+ };
867
+ const onData = (chunk) => {
868
+ const key = chunk.toString("utf8");
869
+ if (key === "\u0003") {
870
+ stdout.write("\x1b[?25h");
871
+ process.exit(130);
872
+ }
873
+ const normalized = key.trim().toLowerCase();
874
+ if (normalized === "y" || normalized === "yes") {
875
+ finish(true);
876
+ return;
877
+ }
878
+ if (!normalized || normalized === "n" || normalized === "no") {
879
+ finish(false);
880
+ }
881
+ };
882
+ process.stdout.write("\nPatch Codex to show The Claw Bay usage remaining? [y/N] ");
883
+ if (stdin.isTTY)
884
+ stdin.setRawMode(true);
885
+ stdin.on("data", onData);
858
886
  });
859
- try {
860
- process.stdout.write("\n");
861
- process.stdout.write("Patch Codex to show The Claw Bay usage remaining? [y/N] ");
862
- const answer = (await rl.question("")).trim().toLowerCase();
863
- return answer === "y" || answer === "yes";
864
- }
865
- finally {
866
- rl.close();
867
- }
868
887
  }
869
888
  async function resolveCodexUsageUiSelection(params) {
870
889
  if (!params.codexSelected)
@@ -875,6 +894,26 @@ async function resolveCodexUsageUiSelection(params) {
875
894
  return false;
876
895
  return promptForCodexUsageUiPatch();
877
896
  }
897
+ async function resolveDeviceLabel(params) {
898
+ const fallback = node_os_1.default.hostname().trim() || "This device";
899
+ const preferred = (params.flagValue ?? params.managedValue ?? "").trim() || fallback;
900
+ if (params.flagValue?.trim())
901
+ return preferred;
902
+ if (params.skipPrompt || !process.stdin.isTTY || !process.stdout.isTTY) {
903
+ return preferred;
904
+ }
905
+ const rl = (0, promises_2.createInterface)({
906
+ input: process.stdin,
907
+ output: process.stdout,
908
+ });
909
+ try {
910
+ const answer = await rl.question(`\nName this device [${preferred}] `);
911
+ return answer.trim() || preferred;
912
+ }
913
+ finally {
914
+ rl.close();
915
+ }
916
+ }
878
917
  async function fetchBackendModelIds(backendUrl, apiKey) {
879
918
  const url = `${trimTrailingSlash(backendUrl)}/api/codex-auth/v1/proxy/v1/models`;
880
919
  try {
@@ -1700,10 +1739,27 @@ class SetupCommand extends base_command_1.BaseCommand {
1700
1739
  if (!(error instanceof errors_1.ManagedConfigMissingError))
1701
1740
  throw error;
1702
1741
  }
1703
- const apiKey = (flags["api-key"] ?? managed?.apiKey ?? "").trim();
1704
- if (!apiKey)
1705
- throw new Error('API key is required. Run "theclawbay setup --api-key <key>".');
1706
- const backendRaw = flags.backend ?? (0, api_key_1.tryInferBackendUrlFromApiKey)(apiKey) ?? managed?.backendUrl ?? DEFAULT_BACKEND_URL;
1742
+ const explicitApiKey = (flags["api-key"] ?? "").trim();
1743
+ const managedCredential = (managed?.credential ?? "").trim();
1744
+ const managedAuthType = managed?.authType ?? null;
1745
+ let authType = explicitApiKey
1746
+ ? "api-key"
1747
+ : managedAuthType === "api-key"
1748
+ ? "api-key"
1749
+ : "device-session";
1750
+ let authCredential = explicitApiKey || managedCredential;
1751
+ let deviceSessionId = managedAuthType === "device-session"
1752
+ ? managed?.deviceSessionId ?? null
1753
+ : null;
1754
+ let deviceLabel = managedAuthType === "device-session"
1755
+ ? managed?.deviceLabel ?? null
1756
+ : null;
1757
+ const backendRaw = flags.backend ??
1758
+ (explicitApiKey || managedAuthType === "api-key"
1759
+ ? (0, api_key_1.tryInferBackendUrlFromApiKey)(explicitApiKey || managedCredential)
1760
+ : null) ??
1761
+ managed?.backendUrl ??
1762
+ DEFAULT_BACKEND_URL;
1707
1763
  const backendUrl = normalizeUrl(backendRaw, "--backend");
1708
1764
  const [codexDetected, continueDetected, clineDetected, kiloDetected, rooDetected, traeDetected, aiderDetected, zoDetected] = await Promise.all([
1709
1765
  detectCodexClient(),
@@ -1820,6 +1876,24 @@ class SetupCommand extends base_command_1.BaseCommand {
1820
1876
  if (flags["codex-usage-ui"] && !selectedSetupClients.has("codex")) {
1821
1877
  throw new Error("--codex-usage-ui requires Codex to be selected for setup.");
1822
1878
  }
1879
+ if (!authCredential) {
1880
+ deviceLabel = await resolveDeviceLabel({
1881
+ flagValue: flags["device-name"],
1882
+ managedValue: managed?.deviceLabel,
1883
+ skipPrompt: flags.yes,
1884
+ });
1885
+ const browserAuth = await (0, device_session_auth_1.createBrowserLinkedDeviceSession)({
1886
+ backendUrl,
1887
+ deviceLabel,
1888
+ selectedClients: Array.from(selectedSetupClients),
1889
+ usagePatchRequested: codexUsageUiEnabled,
1890
+ log: (message) => this.log(message),
1891
+ });
1892
+ authType = browserAuth.authType;
1893
+ authCredential = browserAuth.credential;
1894
+ deviceSessionId = browserAuth.deviceSessionId;
1895
+ deviceLabel = browserAuth.deviceLabel;
1896
+ }
1823
1897
  const progress = this.createProgressHandle(!debugOutput);
1824
1898
  let resolved = null;
1825
1899
  let updatedShellFiles = [];
@@ -1845,17 +1919,23 @@ class SetupCommand extends base_command_1.BaseCommand {
1845
1919
  try {
1846
1920
  if (selectedSetupClients.size > 0) {
1847
1921
  progress.update("Resolving supported models");
1848
- resolved = await resolveModels(backendUrl, apiKey);
1922
+ resolved = await resolveModels(backendUrl, authCredential);
1849
1923
  }
1850
1924
  progress.update("Saving shared machine config");
1851
- await (0, config_1.writeManagedConfig)({ backendUrl, apiKey });
1852
- updatedShellFiles = await persistApiKeyEnv(apiKey);
1925
+ await (0, config_1.writeManagedConfig)({
1926
+ backendUrl,
1927
+ authType,
1928
+ credential: authCredential,
1929
+ deviceSessionId,
1930
+ deviceLabel,
1931
+ });
1932
+ updatedShellFiles = await persistApiKeyEnv(authCredential);
1853
1933
  if (selectedSetupClients.has("codex")) {
1854
1934
  progress.update("Configuring Codex");
1855
1935
  codexConfigPath = await writeCodexConfig({
1856
1936
  backendUrl,
1857
1937
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1858
- apiKey,
1938
+ apiKey: authCredential,
1859
1939
  enableUsageUiPatch: codexUsageUiEnabled,
1860
1940
  });
1861
1941
  updatedVsCodeEnvFiles = await persistVsCodeServerEnvSource();
@@ -1867,7 +1947,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1867
1947
  if (codexUsageUiEnabled) {
1868
1948
  authSeed = await (0, codex_auth_seeding_1.ensureCodexUsageLimitAuth)({
1869
1949
  codexHome: paths_1.codexDir,
1870
- apiKey,
1950
+ apiKey: authCredential,
1871
1951
  });
1872
1952
  codexVsCodePatch = await (0, codex_vscode_patch_1.patchCodexVsCodeExtensions)({
1873
1953
  logoutCommandPath: resolveCommandPath("theclawbay"),
@@ -1879,7 +1959,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1879
1959
  });
1880
1960
  authSeed = await (0, codex_auth_seeding_1.ensureCodexApiKeyAuth)({
1881
1961
  codexHome: paths_1.codexDir,
1882
- apiKey,
1962
+ apiKey: authCredential,
1883
1963
  });
1884
1964
  codexVsCodeCleanup = await (0, codex_vscode_patch_1.cleanupCodexVsCodeExtensions)();
1885
1965
  }
@@ -1897,7 +1977,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1897
1977
  continueConfigPath = await writeContinueConfig({
1898
1978
  backendUrl,
1899
1979
  model: resolved?.model ?? DEFAULT_CONTINUE_MODEL,
1900
- apiKey,
1980
+ apiKey: authCredential,
1901
1981
  });
1902
1982
  }
1903
1983
  if (selectedSetupClients.has("cline")) {
@@ -1905,7 +1985,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1905
1985
  clineConfigPaths = await writeClineConfig({
1906
1986
  backendUrl,
1907
1987
  model: resolved?.model ?? DEFAULT_CLINE_MODEL,
1908
- apiKey,
1988
+ apiKey: authCredential,
1909
1989
  });
1910
1990
  }
1911
1991
  if (selectedSetupClients.has("openclaw")) {
@@ -1914,7 +1994,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1914
1994
  backendUrl,
1915
1995
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1916
1996
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
1917
- apiKey,
1997
+ apiKey: authCredential,
1918
1998
  });
1919
1999
  openClawConfigPath = openClawResult.configPath;
1920
2000
  openClawCliWarning = openClawResult.cliWarning ?? null;
@@ -1925,7 +2005,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1925
2005
  backendUrl,
1926
2006
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1927
2007
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
1928
- apiKey,
2008
+ apiKey: authCredential,
1929
2009
  });
1930
2010
  }
1931
2011
  if (selectedSetupClients.has("kilo")) {
@@ -1934,7 +2014,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1934
2014
  backendUrl,
1935
2015
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1936
2016
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
1937
- apiKey,
2017
+ apiKey: authCredential,
1938
2018
  });
1939
2019
  }
1940
2020
  if (selectedSetupClients.has("roo")) {
@@ -1942,7 +2022,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1942
2022
  rooConfigPaths = await writeRooConfig({
1943
2023
  backendUrl,
1944
2024
  model: resolved?.model ?? DEFAULT_ROO_MODEL,
1945
- apiKey,
2025
+ apiKey: authCredential,
1946
2026
  });
1947
2027
  }
1948
2028
  if (selectedSetupClients.has("trae")) {
@@ -1950,7 +2030,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1950
2030
  traeBundlePathPatched = await patchTraeBundle({
1951
2031
  backendUrl,
1952
2032
  model: resolved?.model ?? DEFAULT_CODEX_MODEL,
1953
- apiKey,
2033
+ apiKey: authCredential,
1954
2034
  models: resolved?.models ?? [{ id: DEFAULT_CODEX_MODEL, name: modelDisplayName(DEFAULT_CODEX_MODEL) }],
1955
2035
  });
1956
2036
  }
@@ -1959,7 +2039,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1959
2039
  aiderConfigPath = await writeAiderConfig({
1960
2040
  backendUrl,
1961
2041
  model: resolved?.model ?? DEFAULT_AIDER_MODEL,
1962
- apiKey,
2042
+ apiKey: authCredential,
1963
2043
  });
1964
2044
  }
1965
2045
  if (selectedSetupClients.has("zo")) {
@@ -1967,7 +2047,7 @@ class SetupCommand extends base_command_1.BaseCommand {
1967
2047
  zoConfigName = await configureZoByok({
1968
2048
  backendUrl,
1969
2049
  model: resolved?.model ?? DEFAULT_ZO_MODEL,
1970
- apiKey,
2050
+ apiKey: authCredential,
1971
2051
  });
1972
2052
  }
1973
2053
  }
@@ -2016,6 +2096,9 @@ class SetupCommand extends base_command_1.BaseCommand {
2016
2096
  else if (codexUsageUiEnabled && codexVsCodePatch?.action === "none" && !codexVsCodePatch.warning) {
2017
2097
  summaryNotes.add("No installed Codex VS Code extension build was found to patch locally.");
2018
2098
  }
2099
+ if (authType === "device-session" && deviceLabel) {
2100
+ summaryNotes.add(`This machine is linked as "${deviceLabel}". You can revoke it from the dashboard Devices section or with \`theclawbay logout\`.`);
2101
+ }
2019
2102
  if (!debugOutput) {
2020
2103
  logSetupCompactSummary({
2021
2104
  log: (message) => this.log(message),
@@ -2027,12 +2110,17 @@ class SetupCommand extends base_command_1.BaseCommand {
2027
2110
  }
2028
2111
  this.log(`- Managed config: ${paths_1.managedConfigPath}`);
2029
2112
  this.log(`- Backend: ${backendUrl}`);
2113
+ this.log(`- Auth mode: ${authType}`);
2114
+ if (authType === "device-session") {
2115
+ this.log(`- Device session id: ${deviceSessionId ?? "n/a"}`);
2116
+ this.log(`- Device label: ${deviceLabel ?? "n/a"}`);
2117
+ }
2030
2118
  if (selectedSetupClients.size === 0) {
2031
- this.log("- Local clients: none selected; saved managed config and API key env only.");
2119
+ this.log("- Local clients: none selected; saved managed config and local credential env only.");
2032
2120
  }
2033
2121
  if (resolved?.note)
2034
2122
  this.log(resolved.note);
2035
- this.log(`- API key env: ${ENV_FILE}`);
2123
+ this.log(`- Local credential env: ${ENV_FILE}`);
2036
2124
  this.log(`- Shell profiles updated: ${updatedShellFiles.join(", ")}`);
2037
2125
  if (selectedSetupClients.has("codex")) {
2038
2126
  this.log(`- Codex: configured (${codexConfigPath})`);
@@ -2250,6 +2338,11 @@ SetupCommand.flags = {
2250
2338
  aliases: ["apiKey"],
2251
2339
  description: "API key issued by your The Claw Bay dashboard",
2252
2340
  }),
2341
+ "device-name": core_1.Flags.string({
2342
+ required: false,
2343
+ aliases: ["deviceName"],
2344
+ description: "Device name to save when browser login links this machine",
2345
+ }),
2253
2346
  clients: core_1.Flags.string({
2254
2347
  required: false,
2255
2348
  description: "Detected local clients to configure: codex, continue, cline, openclaw, opencode, kilo, roo, trae, aider, zo",
package/dist/index.js CHANGED
@@ -29,8 +29,8 @@ async function maybePrintBareInvocationHint() {
29
29
  currentVersion,
30
30
  log: (message) => console.log(message),
31
31
  }).catch(() => { });
32
- console.log("Get your API key at: https://theclawbay.com/dashboard");
33
- console.log("Then run: theclawbay setup --api-key <apiKey>");
32
+ console.log("Run: theclawbay setup");
33
+ console.log("Optional manual path: theclawbay setup --api-key <apiKey>");
34
34
  console.log("");
35
35
  }
36
36
  async function main() {
@@ -0,0 +1,12 @@
1
+ export declare function createBrowserLinkedDeviceSession(params: {
2
+ backendUrl: string;
3
+ deviceLabel?: string | null;
4
+ selectedClients: string[];
5
+ usagePatchRequested: boolean;
6
+ log: (message: string) => void;
7
+ }): Promise<{
8
+ credential: string;
9
+ authType: "device-session";
10
+ deviceSessionId: string;
11
+ deviceLabel: string;
12
+ }>;
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createBrowserLinkedDeviceSession = createBrowserLinkedDeviceSession;
7
+ const node_crypto_1 = __importDefault(require("node:crypto"));
8
+ const node_http_1 = __importDefault(require("node:http"));
9
+ const node_os_1 = __importDefault(require("node:os"));
10
+ const node_child_process_1 = require("node:child_process");
11
+ function preferredBrowserCallbackPort() {
12
+ const parsed = Number(process.env.THECLAWBAY_CALLBACK_PORT?.trim() || "");
13
+ if (Number.isFinite(parsed) && parsed > 0 && parsed < 65536) {
14
+ return Math.round(parsed);
15
+ }
16
+ return 4589;
17
+ }
18
+ function openUrlInBrowser(url) {
19
+ try {
20
+ if (process.platform === "darwin") {
21
+ const child = (0, node_child_process_1.spawn)("open", [url], {
22
+ detached: true,
23
+ stdio: "ignore",
24
+ });
25
+ child.unref();
26
+ return true;
27
+ }
28
+ if (process.platform === "win32") {
29
+ const child = (0, node_child_process_1.spawn)("cmd", ["/c", "start", "", url], {
30
+ detached: true,
31
+ stdio: "ignore",
32
+ });
33
+ child.unref();
34
+ return true;
35
+ }
36
+ const child = (0, node_child_process_1.spawn)("xdg-open", [url], {
37
+ detached: true,
38
+ stdio: "ignore",
39
+ });
40
+ child.unref();
41
+ return true;
42
+ }
43
+ catch {
44
+ return false;
45
+ }
46
+ }
47
+ function htmlPage(title, message) {
48
+ return `<!doctype html>
49
+ <html lang="en">
50
+ <head>
51
+ <meta charset="utf-8" />
52
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
53
+ <title>${title}</title>
54
+ <style>
55
+ body { font-family: ui-sans-serif, system-ui, sans-serif; background: #09111f; color: #f5f7fb; margin: 0; padding: 40px 20px; }
56
+ .card { max-width: 620px; margin: 0 auto; padding: 28px; border-radius: 24px; background: linear-gradient(180deg, rgba(18,24,38,0.94), rgba(11,16,28,0.92)); border: 1px solid rgba(255,255,255,0.08); }
57
+ h1 { font-size: 28px; margin: 0 0 12px; }
58
+ p { color: rgba(255,255,255,0.75); line-height: 1.7; margin: 0; }
59
+ </style>
60
+ </head>
61
+ <body>
62
+ <div class="card">
63
+ <h1>${title}</h1>
64
+ <p>${message}</p>
65
+ </div>
66
+ </body>
67
+ </html>`;
68
+ }
69
+ async function createLocalCallbackServer(expectedState) {
70
+ const preferredPort = preferredBrowserCallbackPort();
71
+ let resolveCallback;
72
+ let rejectCallback;
73
+ const callbackPromise = new Promise((resolve, reject) => {
74
+ resolveCallback = resolve;
75
+ rejectCallback = reject;
76
+ });
77
+ const server = node_http_1.default.createServer((req, res) => {
78
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
79
+ if (url.pathname !== "/auth/callback") {
80
+ res.writeHead(404, { "content-type": "text/html; charset=utf-8" });
81
+ res.end(htmlPage("Not found", "This local setup callback path is invalid."));
82
+ return;
83
+ }
84
+ const state = url.searchParams.get("state")?.trim() ?? "";
85
+ const sessionId = url.searchParams.get("sessionId")?.trim() ?? "";
86
+ const code = url.searchParams.get("code")?.trim() ?? "";
87
+ if (!sessionId || !code || state !== expectedState) {
88
+ res.writeHead(400, { "content-type": "text/html; charset=utf-8" });
89
+ res.end(htmlPage("Connection failed", "The local setup callback did not match the expected state. Return to your terminal and run setup again."));
90
+ rejectCallback(new Error("browser callback state mismatch"));
91
+ return;
92
+ }
93
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
94
+ res.end(htmlPage("Device linked", "This device is now connected to The Claw Bay. You can return to your terminal."));
95
+ resolveCallback({ sessionId, state, code });
96
+ });
97
+ const actualPort = await new Promise((resolve, reject) => {
98
+ const listen = (port) => {
99
+ server.once("error", (error) => {
100
+ const err = error;
101
+ if (port !== 0 && err.code === "EADDRINUSE") {
102
+ listen(0);
103
+ return;
104
+ }
105
+ reject(error);
106
+ });
107
+ server.listen(port, "127.0.0.1", () => {
108
+ const address = server.address();
109
+ if (!address || typeof address === "string") {
110
+ reject(new Error("failed to determine local callback port"));
111
+ return;
112
+ }
113
+ resolve(address.port);
114
+ });
115
+ };
116
+ listen(preferredPort);
117
+ });
118
+ const callbackUrl = `http://127.0.0.1:${actualPort}/auth/callback`;
119
+ return {
120
+ callbackUrl,
121
+ waitForCallback: async () => callbackPromise,
122
+ close: async () => await new Promise((resolve) => {
123
+ server.close(() => resolve());
124
+ }),
125
+ };
126
+ }
127
+ async function createBrowserLinkedDeviceSession(params) {
128
+ const label = (params.deviceLabel ?? "").trim() || node_os_1.default.hostname() || "This device";
129
+ const state = node_crypto_1.default.randomUUID();
130
+ const callbackServer = await createLocalCallbackServer(state);
131
+ try {
132
+ const startResponse = await fetch(`${params.backendUrl}/api/setup/device-session/start`, {
133
+ method: "POST",
134
+ headers: { "Content-Type": "application/json" },
135
+ body: JSON.stringify({
136
+ callbackUrl: callbackServer.callbackUrl,
137
+ state,
138
+ deviceLabel: label,
139
+ selectedClients: params.selectedClients,
140
+ usagePatchRequested: params.usagePatchRequested,
141
+ }),
142
+ signal: AbortSignal.timeout(20000),
143
+ });
144
+ const startBody = (await startResponse.json().catch(() => ({})));
145
+ if (!startResponse.ok || !startBody.authUrl || !startBody.sessionId) {
146
+ throw new Error(startBody.error ?? `failed to start browser auth (HTTP ${startResponse.status})`);
147
+ }
148
+ const opened = openUrlInBrowser(startBody.authUrl);
149
+ params.log(opened
150
+ ? `Open browser auth if it did not appear automatically: ${startBody.authUrl}`
151
+ : `Open this URL to continue setup: ${startBody.authUrl}`);
152
+ const callback = await Promise.race([
153
+ callbackServer.waitForCallback(),
154
+ new Promise((_, reject) => {
155
+ setTimeout(() => reject(new Error("browser setup timed out")), 15 * 60000);
156
+ }),
157
+ ]);
158
+ const exchangeResponse = await fetch(`${params.backendUrl}/api/setup/device-session/exchange`, {
159
+ method: "POST",
160
+ headers: { "Content-Type": "application/json" },
161
+ body: JSON.stringify({
162
+ sessionId: callback.sessionId,
163
+ state: callback.state,
164
+ code: callback.code,
165
+ }),
166
+ signal: AbortSignal.timeout(20000),
167
+ });
168
+ const exchangeBody = (await exchangeResponse.json().catch(() => ({})));
169
+ if (!exchangeResponse.ok ||
170
+ exchangeBody.authType !== "device-session" ||
171
+ !exchangeBody.credential ||
172
+ !exchangeBody.deviceSessionId) {
173
+ throw new Error(exchangeBody.error ??
174
+ `failed to exchange browser setup session (HTTP ${exchangeResponse.status})`);
175
+ }
176
+ return {
177
+ credential: exchangeBody.credential,
178
+ authType: "device-session",
179
+ deviceSessionId: exchangeBody.deviceSessionId,
180
+ deviceLabel: exchangeBody.deviceLabel?.trim() || label,
181
+ };
182
+ }
183
+ finally {
184
+ await callbackServer.close().catch(() => { });
185
+ }
186
+ }
@@ -1,6 +1,10 @@
1
+ export type ManagedAuthType = "api-key" | "device-session";
1
2
  export type ManagedConfig = {
2
3
  backendUrl: string;
3
- apiKey: string;
4
+ authType: ManagedAuthType;
5
+ credential: string;
6
+ deviceSessionId?: string | null;
7
+ deviceLabel?: string | null;
4
8
  };
5
9
  export declare function readManagedConfig(): Promise<ManagedConfig>;
6
10
  export declare function writeManagedConfig(config: ManagedConfig): Promise<void>;
@@ -72,31 +72,66 @@ async function readManagedConfig() {
72
72
  throw new errors_1.ManagedConfigInvalidError("backendUrl is required");
73
73
  }
74
74
  const backendUrl = normalizeAndValidateBackendUrl(obj.backendUrl);
75
- if (obj.authType === "seat") {
75
+ const rawAuthType = obj.authType;
76
+ if (typeof rawAuthType === "string" && rawAuthType === "seat") {
76
77
  throw new errors_1.ManagedConfigInvalidError('legacy seat-mode config is no longer supported; run "theclawbay link --api-key <key>" to relink');
77
78
  }
78
- if (typeof obj.apiKey !== "string" || !obj.apiKey.trim()) {
79
- throw new errors_1.ManagedConfigInvalidError("apiKey is required");
79
+ const authType = rawAuthType === "device-session" ? "device-session" : "api-key";
80
+ const rawCredential = typeof obj.credential === "string" && obj.credential.trim()
81
+ ? obj.credential.trim()
82
+ : typeof obj.apiKey === "string" && obj.apiKey.trim()
83
+ ? obj.apiKey.trim()
84
+ : "";
85
+ if (!rawCredential) {
86
+ throw new errors_1.ManagedConfigInvalidError("credential is required");
80
87
  }
88
+ const deviceSessionId = typeof obj.deviceSessionId === "string" && obj.deviceSessionId.trim()
89
+ ? obj.deviceSessionId.trim()
90
+ : null;
91
+ const deviceLabel = typeof obj.deviceLabel === "string" && obj.deviceLabel.trim()
92
+ ? obj.deviceLabel.trim()
93
+ : null;
81
94
  if (loadedFromPath !== paths_1.managedConfigPath) {
82
95
  // Best-effort migration to the new config filename.
83
96
  await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
84
97
  await promises_1.default.writeFile(paths_1.managedConfigPath, raw, "utf8").catch(() => { });
85
98
  }
86
- return { backendUrl, apiKey: obj.apiKey.trim() };
99
+ return {
100
+ backendUrl,
101
+ authType,
102
+ credential: rawCredential,
103
+ deviceSessionId,
104
+ deviceLabel,
105
+ };
87
106
  }
88
107
  async function writeManagedConfig(config) {
89
108
  if (typeof config.backendUrl !== "string" || !config.backendUrl.trim()) {
90
109
  throw new errors_1.ManagedConfigInvalidError("backendUrl is required");
91
110
  }
92
111
  const backendUrl = normalizeAndValidateBackendUrl(config.backendUrl);
93
- if (typeof config.apiKey !== "string" || !config.apiKey.trim()) {
94
- throw new errors_1.ManagedConfigInvalidError("apiKey is required");
112
+ if (config.authType !== "api-key" &&
113
+ config.authType !== "device-session") {
114
+ throw new errors_1.ManagedConfigInvalidError("authType must be api-key or device-session");
115
+ }
116
+ if (typeof config.credential !== "string" || !config.credential.trim()) {
117
+ throw new errors_1.ManagedConfigInvalidError("credential is required");
95
118
  }
96
119
  await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
120
+ const deviceSessionId = typeof config.deviceSessionId === "string" && config.deviceSessionId.trim()
121
+ ? config.deviceSessionId.trim()
122
+ : null;
123
+ const deviceLabel = typeof config.deviceLabel === "string" && config.deviceLabel.trim()
124
+ ? config.deviceLabel.trim()
125
+ : null;
97
126
  const contents = JSON.stringify({
98
127
  backendUrl,
99
- apiKey: config.apiKey.trim(),
128
+ authType: config.authType,
129
+ credential: config.credential.trim(),
130
+ ...(config.authType === "api-key"
131
+ ? { apiKey: config.credential.trim() }
132
+ : {}),
133
+ ...(deviceSessionId ? { deviceSessionId } : {}),
134
+ ...(deviceLabel ? { deviceLabel } : {}),
100
135
  }, null, 2);
101
136
  await promises_1.default.writeFile(paths_1.managedConfigPath, `${contents}\n`, "utf8");
102
137
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.3.40",
3
+ "version": "0.3.42",
4
4
  "description": "CLI for connecting Codex, Continue, Cline, OpenClaw, OpenCode, Kilo, Roo Code, Aider, experimental Trae, and experimental Zo to The Claw Bay.",
5
5
  "license": "MIT",
6
6
  "bin": {