feat: add setup-token + token auth
This commit is contained in:
@@ -177,7 +177,11 @@ Options:
|
||||
- `--workspace <dir>`
|
||||
- `--non-interactive`
|
||||
- `--mode <local|remote>`
|
||||
- `--auth-choice <oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
|
||||
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
|
||||
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
|
||||
- `--token <token>` (non-interactive; used with `--auth-choice token`)
|
||||
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
|
||||
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
|
||||
- `--anthropic-api-key <key>`
|
||||
- `--openai-api-key <key>`
|
||||
- `--gemini-api-key <key>`
|
||||
|
||||
@@ -29,6 +29,19 @@ clawdbot models status
|
||||
clawdbot doctor
|
||||
```
|
||||
|
||||
Alternative: run the wrapper (also updates Clawdbot config):
|
||||
|
||||
```bash
|
||||
clawdbot models auth setup-token --provider anthropic
|
||||
```
|
||||
|
||||
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
|
||||
|
||||
```bash
|
||||
clawdbot models auth paste-token --provider anthropic
|
||||
clawdbot models auth paste-token --provider openrouter
|
||||
```
|
||||
|
||||
## Recommended: long‑lived Claude Code token
|
||||
|
||||
Run this on the **gateway host** (the machine running the Gateway):
|
||||
@@ -92,13 +105,15 @@ Use `--agent <id>` to target a specific agent; omit it to use the configured def
|
||||
2. **Clawdbot** syncs those into
|
||||
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
|
||||
loaded.
|
||||
3. OAuth refresh happens automatically on use if a token is expired.
|
||||
3. Refreshable OAuth profiles can be refreshed automatically on use. Static
|
||||
token profiles (including Claude CLI setup-token) are not refreshable by
|
||||
Clawdbot.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### “No credentials found”
|
||||
|
||||
If the Anthropic OAuth profile is missing, run `claude setup-token` on the
|
||||
If the Anthropic token profile is missing, run `claude setup-token` on the
|
||||
**gateway host**, then re-check:
|
||||
|
||||
```bash
|
||||
|
||||
@@ -240,7 +240,23 @@ export function buildProgram() {
|
||||
.option("--mode <mode>", "Wizard mode: local|remote")
|
||||
.option(
|
||||
"--auth-choice <choice>",
|
||||
"Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
|
||||
"Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
|
||||
)
|
||||
.option(
|
||||
"--token-provider <id>",
|
||||
"Token provider id (non-interactive; used with --auth-choice token)",
|
||||
)
|
||||
.option(
|
||||
"--token <token>",
|
||||
"Token value (non-interactive; used with --auth-choice token)",
|
||||
)
|
||||
.option(
|
||||
"--token-profile-id <id>",
|
||||
"Auth profile id (non-interactive; default: <provider>:manual)",
|
||||
)
|
||||
.option(
|
||||
"--token-expires-in <duration>",
|
||||
"Optional token expiry duration (e.g. 365d, 12h)",
|
||||
)
|
||||
.option("--anthropic-api-key <key>", "Anthropic API key")
|
||||
.option("--openai-api-key <key>", "OpenAI API key")
|
||||
@@ -270,6 +286,7 @@ export function buildProgram() {
|
||||
mode: opts.mode as "local" | "remote" | undefined,
|
||||
authChoice: opts.authChoice as
|
||||
| "oauth"
|
||||
| "setup-token"
|
||||
| "claude-cli"
|
||||
| "token"
|
||||
| "openai-codex"
|
||||
@@ -282,6 +299,10 @@ export function buildProgram() {
|
||||
| "minimax"
|
||||
| "skip"
|
||||
| undefined,
|
||||
tokenProvider: opts.tokenProvider as string | undefined,
|
||||
token: opts.token as string | undefined,
|
||||
tokenProfileId: opts.tokenProfileId as string | undefined,
|
||||
tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
|
||||
anthropicApiKey: opts.anthropicApiKey as string | undefined,
|
||||
openaiApiKey: opts.openaiApiKey as string | undefined,
|
||||
geminiApiKey: opts.geminiApiKey as string | undefined,
|
||||
|
||||
@@ -64,17 +64,23 @@ export function buildAuthChoiceOptions(params: {
|
||||
if (claudeCli?.type === "oauth" || claudeCli?.type === "token") {
|
||||
options.push({
|
||||
value: "claude-cli",
|
||||
label: "Anthropic OAuth (Claude CLI)",
|
||||
label: "Anthropic token (Claude CLI)",
|
||||
hint: formatOAuthHint(claudeCli.expires),
|
||||
});
|
||||
} else if (params.includeClaudeCliIfMissing && platform === "darwin") {
|
||||
options.push({
|
||||
value: "claude-cli",
|
||||
label: "Anthropic OAuth (Claude CLI)",
|
||||
label: "Anthropic token (Claude CLI)",
|
||||
hint: "requires Keychain access",
|
||||
});
|
||||
}
|
||||
|
||||
options.push({
|
||||
value: "setup-token",
|
||||
label: "Anthropic token (run setup-token)",
|
||||
hint: "Runs `claude setup-token`",
|
||||
});
|
||||
|
||||
options.push({
|
||||
value: "token",
|
||||
label: "Anthropic token (paste setup-token)",
|
||||
|
||||
@@ -216,7 +216,68 @@ export async function applyAuthChoice(params: {
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (params.authChoice === "token" || params.authChoice === "oauth") {
|
||||
} else if (
|
||||
params.authChoice === "setup-token" ||
|
||||
params.authChoice === "oauth"
|
||||
) {
|
||||
await params.prompter.note(
|
||||
[
|
||||
"This will run `claude setup-token` to create a long-lived Anthropic token.",
|
||||
"Requires an interactive TTY and a Claude Pro/Max subscription.",
|
||||
].join("\n"),
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
await params.prompter.note(
|
||||
"`claude setup-token` requires an interactive TTY.",
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
const proceed = await params.prompter.confirm({
|
||||
message: "Run `claude setup-token` now?",
|
||||
initialValue: true,
|
||||
});
|
||||
if (!proceed) return { config: nextConfig, agentModelOverride };
|
||||
|
||||
const res = await (async () => {
|
||||
const { spawnSync } = await import("node:child_process");
|
||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
||||
})();
|
||||
if (res.error) {
|
||||
await params.prompter.note(
|
||||
`Failed to run claude: ${String(res.error)}`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
await params.prompter.note(
|
||||
`claude setup-token failed (exit ${res.status})`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir, {
|
||||
allowKeychainPrompt: true,
|
||||
});
|
||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||
await params.prompter.note(
|
||||
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return { config: nextConfig, agentModelOverride };
|
||||
}
|
||||
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (params.authChoice === "token") {
|
||||
const provider = (await params.prompter.select({
|
||||
message: "Token provider",
|
||||
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],
|
||||
|
||||
@@ -352,6 +352,7 @@ async function promptAuthConfig(
|
||||
runtime,
|
||||
) as
|
||||
| "oauth"
|
||||
| "setup-token"
|
||||
| "claude-cli"
|
||||
| "token"
|
||||
| "openai-codex"
|
||||
@@ -403,7 +404,68 @@ async function promptAuthConfig(
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "token" || authChoice === "oauth") {
|
||||
} else if (authChoice === "setup-token" || authChoice === "oauth") {
|
||||
note(
|
||||
[
|
||||
"This will run `claude setup-token` to create a long-lived Anthropic token.",
|
||||
"Requires an interactive TTY and a Claude Pro/Max subscription.",
|
||||
].join("\n"),
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
|
||||
if (!process.stdin.isTTY) {
|
||||
note(
|
||||
"`claude setup-token` requires an interactive TTY.",
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
const runNow = guardCancel(
|
||||
await confirm({
|
||||
message: "Run `claude setup-token` now?",
|
||||
initialValue: true,
|
||||
}),
|
||||
runtime,
|
||||
);
|
||||
if (!runNow) return next;
|
||||
|
||||
const res = await (async () => {
|
||||
const { spawnSync } = await import("node:child_process");
|
||||
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
||||
})();
|
||||
if (res.error) {
|
||||
note(
|
||||
`Failed to run claude: ${String(res.error)}`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
note(
|
||||
`claude setup-token failed (exit ${res.status})`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: true,
|
||||
});
|
||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||
note(
|
||||
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
|
||||
"Anthropic setup-token",
|
||||
);
|
||||
return next;
|
||||
}
|
||||
|
||||
next = applyAuthProfileConfig(next, {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "token") {
|
||||
const provider = guardCancel(
|
||||
await select({
|
||||
message: "Token provider",
|
||||
@@ -726,6 +788,7 @@ async function promptAuthConfig(
|
||||
: (next.agents?.defaults?.model?.primary ?? "");
|
||||
const preferAnthropic =
|
||||
authChoice === "claude-cli" ||
|
||||
authChoice === "setup-token" ||
|
||||
authChoice === "token" ||
|
||||
authChoice === "oauth" ||
|
||||
authChoice === "apiKey";
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import {
|
||||
CLAUDE_CLI_PROFILE_ID,
|
||||
CODEX_CLI_PROFILE_ID,
|
||||
ensureAuthProfileStore,
|
||||
upsertAuthProfile,
|
||||
} from "../agents/auth-profiles.js";
|
||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||
import { parseDurationMs } from "../cli/parse-duration.js";
|
||||
import {
|
||||
type ClawdbotConfig,
|
||||
CONFIG_PATH_CLAWDBOT,
|
||||
@@ -206,18 +210,82 @@ export async function runNonInteractiveOnboarding(
|
||||
nextConfig = applyOpenAICodexModelDefault(nextConfig).next;
|
||||
} else if (authChoice === "minimax") {
|
||||
nextConfig = applyMinimaxConfig(nextConfig);
|
||||
} else if (
|
||||
authChoice === "token" ||
|
||||
authChoice === "oauth" ||
|
||||
authChoice === "openai-codex" ||
|
||||
authChoice === "antigravity"
|
||||
) {
|
||||
} else if (authChoice === "setup-token" || authChoice === "oauth") {
|
||||
if (!process.stdin.isTTY) {
|
||||
runtime.error("`claude setup-token` requires an interactive TTY.");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" });
|
||||
if (res.error) throw res.error;
|
||||
if (typeof res.status === "number" && res.status !== 0) {
|
||||
runtime.error(`claude setup-token failed (exit ${res.status})`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const store = ensureAuthProfileStore(undefined, {
|
||||
allowKeychainPrompt: true,
|
||||
});
|
||||
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
|
||||
runtime.error(
|
||||
`No Claude CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`,
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: CLAUDE_CLI_PROFILE_ID,
|
||||
provider: "anthropic",
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "token") {
|
||||
const providerRaw = opts.tokenProvider?.trim();
|
||||
const tokenRaw = opts.token?.trim();
|
||||
if (!providerRaw) {
|
||||
runtime.error(
|
||||
"Missing --token-provider (required for --auth-choice token).",
|
||||
);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
if (!tokenRaw) {
|
||||
runtime.error("Missing --token (required for --auth-choice token).");
|
||||
runtime.exit(1);
|
||||
return;
|
||||
}
|
||||
|
||||
const provider = normalizeProviderId(providerRaw);
|
||||
const profileId = (
|
||||
opts.tokenProfileId?.trim() || `${provider}:manual`
|
||||
).trim();
|
||||
const expires =
|
||||
opts.tokenExpiresIn?.trim() && opts.tokenExpiresIn.trim().length > 0
|
||||
? Date.now() +
|
||||
parseDurationMs(String(opts.tokenExpiresIn).trim(), {
|
||||
defaultUnit: "d",
|
||||
})
|
||||
: undefined;
|
||||
|
||||
upsertAuthProfile({
|
||||
profileId,
|
||||
credential: {
|
||||
type: "token",
|
||||
provider,
|
||||
token: tokenRaw,
|
||||
...(expires ? { expires } : {}),
|
||||
},
|
||||
});
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
provider,
|
||||
mode: "token",
|
||||
});
|
||||
} else if (authChoice === "openai-codex" || authChoice === "antigravity") {
|
||||
const label =
|
||||
authChoice === "antigravity"
|
||||
? "Antigravity"
|
||||
: authChoice === "token"
|
||||
? "Token"
|
||||
: "OAuth";
|
||||
authChoice === "antigravity" ? "Antigravity" : "OpenAI Codex OAuth";
|
||||
runtime.error(`${label} requires interactive mode.`);
|
||||
runtime.exit(1);
|
||||
return;
|
||||
|
||||
@@ -3,7 +3,9 @@ import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||
|
||||
export type OnboardMode = "local" | "remote";
|
||||
export type AuthChoice =
|
||||
// Legacy alias for `setup-token` (kept for backwards CLI compatibility).
|
||||
| "oauth"
|
||||
| "setup-token"
|
||||
| "claude-cli"
|
||||
| "token"
|
||||
| "openai-codex"
|
||||
@@ -27,6 +29,14 @@ export type OnboardOptions = {
|
||||
workspace?: string;
|
||||
nonInteractive?: boolean;
|
||||
authChoice?: AuthChoice;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
tokenProvider?: string;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
token?: string;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
tokenProfileId?: string;
|
||||
/** Used when `authChoice=token` in non-interactive mode. */
|
||||
tokenExpiresIn?: string;
|
||||
anthropicApiKey?: string;
|
||||
openaiApiKey?: string;
|
||||
geminiApiKey?: string;
|
||||
|
||||
@@ -10,6 +10,11 @@ export async function onboardCommand(
|
||||
runtime: RuntimeEnv = defaultRuntime,
|
||||
) {
|
||||
assertSupportedRuntime(runtime);
|
||||
const authChoice =
|
||||
opts.authChoice === "oauth" ? ("setup-token" as const) : opts.authChoice;
|
||||
const normalizedOpts =
|
||||
authChoice === opts.authChoice ? opts : { ...opts, authChoice };
|
||||
|
||||
if (process.platform === "win32") {
|
||||
runtime.log(
|
||||
[
|
||||
@@ -20,12 +25,12 @@ export async function onboardCommand(
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.nonInteractive) {
|
||||
await runNonInteractiveOnboarding(opts, runtime);
|
||||
if (normalizedOpts.nonInteractive) {
|
||||
await runNonInteractiveOnboarding(normalizedOpts, runtime);
|
||||
return;
|
||||
}
|
||||
|
||||
await runInteractiveOnboarding(opts, runtime);
|
||||
await runInteractiveOnboarding(normalizedOpts, runtime);
|
||||
}
|
||||
|
||||
export type { OnboardOptions } from "./onboard-types.js";
|
||||
|
||||
Reference in New Issue
Block a user