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.
- package/dist/commands/link.js +2 -1
- package/dist/commands/logout.js +46 -3
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +129 -36
- package/dist/index.js +2 -2
- package/dist/lib/device-session-auth.d.ts +12 -0
- package/dist/lib/device-session-auth.js +186 -0
- package/dist/lib/managed/config.d.ts +5 -1
- package/dist/lib/managed/config.js +42 -7
- package/package.json +1 -1
package/dist/commands/link.js
CHANGED
|
@@ -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
|
-
|
|
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}`);
|
package/dist/commands/logout.js
CHANGED
|
@@ -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
|
|
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(`-
|
|
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
|
|
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,
|
package/dist/commands/setup.d.ts
CHANGED
|
@@ -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>;
|
package/dist/commands/setup.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
856
|
-
|
|
857
|
-
|
|
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
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
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,
|
|
1922
|
+
resolved = await resolveModels(backendUrl, authCredential);
|
|
1849
1923
|
}
|
|
1850
1924
|
progress.update("Saving shared machine config");
|
|
1851
|
-
await (0, config_1.writeManagedConfig)({
|
|
1852
|
-
|
|
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
|
|
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(`-
|
|
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("
|
|
33
|
-
console.log("
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
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 {
|
|
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 (
|
|
94
|
-
|
|
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
|
-
|
|
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