Skip to content

Commit c3083f0

Browse files
committed
feat: add setup-token + token auth
1 parent 083877d commit c3083f0

9 files changed

Lines changed: 275 additions & 22 deletions

File tree

docs/cli/index.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,11 @@ Options:
177177
- `--workspace <dir>`
178178
- `--non-interactive`
179179
- `--mode <local|remote>`
180-
- `--auth-choice <oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
180+
- `--auth-choice <setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip>`
181+
- `--token-provider <id>` (non-interactive; used with `--auth-choice token`)
182+
- `--token <token>` (non-interactive; used with `--auth-choice token`)
183+
- `--token-profile-id <id>` (non-interactive; default: `<provider>:manual`)
184+
- `--token-expires-in <duration>` (non-interactive; e.g. `365d`, `12h`)
181185
- `--anthropic-api-key <key>`
182186
- `--openai-api-key <key>`
183187
- `--gemini-api-key <key>`

docs/gateway/authentication.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ clawdbot models status
2929
clawdbot doctor
3030
```
3131

32+
Alternative: run the wrapper (also updates Clawdbot config):
33+
34+
```bash
35+
clawdbot models auth setup-token --provider anthropic
36+
```
37+
38+
Manual token entry (any provider; writes `auth-profiles.json` + updates config):
39+
40+
```bash
41+
clawdbot models auth paste-token --provider anthropic
42+
clawdbot models auth paste-token --provider openrouter
43+
```
44+
3245
## Recommended: long‑lived Claude Code token
3346

3447
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
92105
2. **Clawdbot** syncs those into
93106
`~/.clawdbot/agents/<agentId>/agent/auth-profiles.json` when the auth store is
94107
loaded.
95-
3. OAuth refresh happens automatically on use if a token is expired.
108+
3. Refreshable OAuth profiles can be refreshed automatically on use. Static
109+
token profiles (including Claude CLI setup-token) are not refreshable by
110+
Clawdbot.
96111

97112
## Troubleshooting
98113

99114
### “No credentials found”
100115

101-
If the Anthropic OAuth profile is missing, run `claude setup-token` on the
116+
If the Anthropic token profile is missing, run `claude setup-token` on the
102117
**gateway host**, then re-check:
103118

104119
```bash

src/cli/program.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,7 +240,23 @@ export function buildProgram() {
240240
.option("--mode <mode>", "Wizard mode: local|remote")
241241
.option(
242242
"--auth-choice <choice>",
243-
"Auth: oauth|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
243+
"Auth: setup-token|claude-cli|token|openai-codex|openai-api-key|codex-cli|antigravity|gemini-api-key|apiKey|minimax-cloud|minimax|skip",
244+
)
245+
.option(
246+
"--token-provider <id>",
247+
"Token provider id (non-interactive; used with --auth-choice token)",
248+
)
249+
.option(
250+
"--token <token>",
251+
"Token value (non-interactive; used with --auth-choice token)",
252+
)
253+
.option(
254+
"--token-profile-id <id>",
255+
"Auth profile id (non-interactive; default: <provider>:manual)",
256+
)
257+
.option(
258+
"--token-expires-in <duration>",
259+
"Optional token expiry duration (e.g. 365d, 12h)",
244260
)
245261
.option("--anthropic-api-key <key>", "Anthropic API key")
246262
.option("--openai-api-key <key>", "OpenAI API key")
@@ -270,6 +286,7 @@ export function buildProgram() {
270286
mode: opts.mode as "local" | "remote" | undefined,
271287
authChoice: opts.authChoice as
272288
| "oauth"
289+
| "setup-token"
273290
| "claude-cli"
274291
| "token"
275292
| "openai-codex"
@@ -282,6 +299,10 @@ export function buildProgram() {
282299
| "minimax"
283300
| "skip"
284301
| undefined,
302+
tokenProvider: opts.tokenProvider as string | undefined,
303+
token: opts.token as string | undefined,
304+
tokenProfileId: opts.tokenProfileId as string | undefined,
305+
tokenExpiresIn: opts.tokenExpiresIn as string | undefined,
285306
anthropicApiKey: opts.anthropicApiKey as string | undefined,
286307
openaiApiKey: opts.openaiApiKey as string | undefined,
287308
geminiApiKey: opts.geminiApiKey as string | undefined,

src/commands/auth-choice-options.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,17 +64,23 @@ export function buildAuthChoiceOptions(params: {
6464
if (claudeCli?.type === "oauth" || claudeCli?.type === "token") {
6565
options.push({
6666
value: "claude-cli",
67-
label: "Anthropic OAuth (Claude CLI)",
67+
label: "Anthropic token (Claude CLI)",
6868
hint: formatOAuthHint(claudeCli.expires),
6969
});
7070
} else if (params.includeClaudeCliIfMissing && platform === "darwin") {
7171
options.push({
7272
value: "claude-cli",
73-
label: "Anthropic OAuth (Claude CLI)",
73+
label: "Anthropic token (Claude CLI)",
7474
hint: "requires Keychain access",
7575
});
7676
}
7777

78+
options.push({
79+
value: "setup-token",
80+
label: "Anthropic token (run setup-token)",
81+
hint: "Runs `claude setup-token`",
82+
});
83+
7884
options.push({
7985
value: "token",
8086
label: "Anthropic token (paste setup-token)",

src/commands/auth-choice.ts

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,68 @@ export async function applyAuthChoice(params: {
216216
provider: "anthropic",
217217
mode: "token",
218218
});
219-
} else if (params.authChoice === "token" || params.authChoice === "oauth") {
219+
} else if (
220+
params.authChoice === "setup-token" ||
221+
params.authChoice === "oauth"
222+
) {
223+
await params.prompter.note(
224+
[
225+
"This will run `claude setup-token` to create a long-lived Anthropic token.",
226+
"Requires an interactive TTY and a Claude Pro/Max subscription.",
227+
].join("\n"),
228+
"Anthropic setup-token",
229+
);
230+
231+
if (!process.stdin.isTTY) {
232+
await params.prompter.note(
233+
"`claude setup-token` requires an interactive TTY.",
234+
"Anthropic setup-token",
235+
);
236+
return { config: nextConfig, agentModelOverride };
237+
}
238+
239+
const proceed = await params.prompter.confirm({
240+
message: "Run `claude setup-token` now?",
241+
initialValue: true,
242+
});
243+
if (!proceed) return { config: nextConfig, agentModelOverride };
244+
245+
const res = await (async () => {
246+
const { spawnSync } = await import("node:child_process");
247+
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
248+
})();
249+
if (res.error) {
250+
await params.prompter.note(
251+
`Failed to run claude: ${String(res.error)}`,
252+
"Anthropic setup-token",
253+
);
254+
return { config: nextConfig, agentModelOverride };
255+
}
256+
if (typeof res.status === "number" && res.status !== 0) {
257+
await params.prompter.note(
258+
`claude setup-token failed (exit ${res.status})`,
259+
"Anthropic setup-token",
260+
);
261+
return { config: nextConfig, agentModelOverride };
262+
}
263+
264+
const store = ensureAuthProfileStore(params.agentDir, {
265+
allowKeychainPrompt: true,
266+
});
267+
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
268+
await params.prompter.note(
269+
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
270+
"Anthropic setup-token",
271+
);
272+
return { config: nextConfig, agentModelOverride };
273+
}
274+
275+
nextConfig = applyAuthProfileConfig(nextConfig, {
276+
profileId: CLAUDE_CLI_PROFILE_ID,
277+
provider: "anthropic",
278+
mode: "token",
279+
});
280+
} else if (params.authChoice === "token") {
220281
const provider = (await params.prompter.select({
221282
message: "Token provider",
222283
options: [{ value: "anthropic", label: "Anthropic (only supported)" }],

src/commands/configure.ts

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,7 @@ async function promptAuthConfig(
352352
runtime,
353353
) as
354354
| "oauth"
355+
| "setup-token"
355356
| "claude-cli"
356357
| "token"
357358
| "openai-codex"
@@ -403,7 +404,68 @@ async function promptAuthConfig(
403404
provider: "anthropic",
404405
mode: "token",
405406
});
406-
} else if (authChoice === "token" || authChoice === "oauth") {
407+
} else if (authChoice === "setup-token" || authChoice === "oauth") {
408+
note(
409+
[
410+
"This will run `claude setup-token` to create a long-lived Anthropic token.",
411+
"Requires an interactive TTY and a Claude Pro/Max subscription.",
412+
].join("\n"),
413+
"Anthropic setup-token",
414+
);
415+
416+
if (!process.stdin.isTTY) {
417+
note(
418+
"`claude setup-token` requires an interactive TTY.",
419+
"Anthropic setup-token",
420+
);
421+
return next;
422+
}
423+
424+
const runNow = guardCancel(
425+
await confirm({
426+
message: "Run `claude setup-token` now?",
427+
initialValue: true,
428+
}),
429+
runtime,
430+
);
431+
if (!runNow) return next;
432+
433+
const res = await (async () => {
434+
const { spawnSync } = await import("node:child_process");
435+
return spawnSync("claude", ["setup-token"], { stdio: "inherit" });
436+
})();
437+
if (res.error) {
438+
note(
439+
`Failed to run claude: ${String(res.error)}`,
440+
"Anthropic setup-token",
441+
);
442+
return next;
443+
}
444+
if (typeof res.status === "number" && res.status !== 0) {
445+
note(
446+
`claude setup-token failed (exit ${res.status})`,
447+
"Anthropic setup-token",
448+
);
449+
return next;
450+
}
451+
452+
const store = ensureAuthProfileStore(undefined, {
453+
allowKeychainPrompt: true,
454+
});
455+
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
456+
note(
457+
`No Claude CLI credentials found after setup-token. Expected ${CLAUDE_CLI_PROFILE_ID}.`,
458+
"Anthropic setup-token",
459+
);
460+
return next;
461+
}
462+
463+
next = applyAuthProfileConfig(next, {
464+
profileId: CLAUDE_CLI_PROFILE_ID,
465+
provider: "anthropic",
466+
mode: "token",
467+
});
468+
} else if (authChoice === "token") {
407469
const provider = guardCancel(
408470
await select({
409471
message: "Token provider",
@@ -726,6 +788,7 @@ async function promptAuthConfig(
726788
: (next.agents?.defaults?.model?.primary ?? "");
727789
const preferAnthropic =
728790
authChoice === "claude-cli" ||
791+
authChoice === "setup-token" ||
729792
authChoice === "token" ||
730793
authChoice === "oauth" ||
731794
authChoice === "apiKey";

src/commands/onboard-non-interactive.ts

Lines changed: 79 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1+
import { spawnSync } from "node:child_process";
12
import path from "node:path";
23
import {
34
CLAUDE_CLI_PROFILE_ID,
45
CODEX_CLI_PROFILE_ID,
56
ensureAuthProfileStore,
7+
upsertAuthProfile,
68
} from "../agents/auth-profiles.js";
79
import { resolveEnvApiKey } from "../agents/model-auth.js";
10+
import { normalizeProviderId } from "../agents/model-selection.js";
11+
import { parseDurationMs } from "../cli/parse-duration.js";
812
import {
913
type ClawdbotConfig,
1014
CONFIG_PATH_CLAWDBOT,
@@ -206,18 +210,82 @@ export async function runNonInteractiveOnboarding(
206210
nextConfig = applyOpenAICodexModelDefault(nextConfig).next;
207211
} else if (authChoice === "minimax") {
208212
nextConfig = applyMinimaxConfig(nextConfig);
209-
} else if (
210-
authChoice === "token" ||
211-
authChoice === "oauth" ||
212-
authChoice === "openai-codex" ||
213-
authChoice === "antigravity"
214-
) {
213+
} else if (authChoice === "setup-token" || authChoice === "oauth") {
214+
if (!process.stdin.isTTY) {
215+
runtime.error("`claude setup-token` requires an interactive TTY.");
216+
runtime.exit(1);
217+
return;
218+
}
219+
220+
const res = spawnSync("claude", ["setup-token"], { stdio: "inherit" });
221+
if (res.error) throw res.error;
222+
if (typeof res.status === "number" && res.status !== 0) {
223+
runtime.error(`claude setup-token failed (exit ${res.status})`);
224+
runtime.exit(1);
225+
return;
226+
}
227+
228+
const store = ensureAuthProfileStore(undefined, {
229+
allowKeychainPrompt: true,
230+
});
231+
if (!store.profiles[CLAUDE_CLI_PROFILE_ID]) {
232+
runtime.error(
233+
`No Claude CLI credentials found after setup-token. Expected auth profile ${CLAUDE_CLI_PROFILE_ID}.`,
234+
);
235+
runtime.exit(1);
236+
return;
237+
}
238+
239+
nextConfig = applyAuthProfileConfig(nextConfig, {
240+
profileId: CLAUDE_CLI_PROFILE_ID,
241+
provider: "anthropic",
242+
mode: "token",
243+
});
244+
} else if (authChoice === "token") {
245+
const providerRaw = opts.tokenProvider?.trim();
246+
const tokenRaw = opts.token?.trim();
247+
if (!providerRaw) {
248+
runtime.error(
249+
"Missing --token-provider (required for --auth-choice token).",
250+
);
251+
runtime.exit(1);
252+
return;
253+
}
254+
if (!tokenRaw) {
255+
runtime.error("Missing --token (required for --auth-choice token).");
256+
runtime.exit(1);
257+
return;
258+
}
259+
260+
const provider = normalizeProviderId(providerRaw);
261+
const profileId = (
262+
opts.tokenProfileId?.trim() || `${provider}:manual`
263+
).trim();
264+
const expires =
265+
opts.tokenExpiresIn?.trim() && opts.tokenExpiresIn.trim().length > 0
266+
? Date.now() +
267+
parseDurationMs(String(opts.tokenExpiresIn).trim(), {
268+
defaultUnit: "d",
269+
})
270+
: undefined;
271+
272+
upsertAuthProfile({
273+
profileId,
274+
credential: {
275+
type: "token",
276+
provider,
277+
token: tokenRaw,
278+
...(expires ? { expires } : {}),
279+
},
280+
});
281+
nextConfig = applyAuthProfileConfig(nextConfig, {
282+
profileId,
283+
provider,
284+
mode: "token",
285+
});
286+
} else if (authChoice === "openai-codex" || authChoice === "antigravity") {
215287
const label =
216-
authChoice === "antigravity"
217-
? "Antigravity"
218-
: authChoice === "token"
219-
? "Token"
220-
: "OAuth";
288+
authChoice === "antigravity" ? "Antigravity" : "OpenAI Codex OAuth";
221289
runtime.error(`${label} requires interactive mode.`);
222290
runtime.exit(1);
223291
return;

0 commit comments

Comments
 (0)