theclawbay 0.4.1 → 0.5.0

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/README.md CHANGED
@@ -8,15 +8,17 @@ CLI for connecting Codex, Claude Code, Hermes Agent, Gemini-compatible apps, Con
8
8
  npm i -g theclawbay
9
9
  ```
10
10
 
11
- ## Setup
11
+ ## Usage
12
12
 
13
- Get your API key from `https://theclawbay.com/dashboard`.
13
+ Just run:
14
14
 
15
15
  ```sh
16
- theclawbay setup --api-key <apiKey>
16
+ theclawbay
17
17
  ```
18
18
 
19
- In an interactive terminal, setup walks you through three quick steps: pick the tools to connect, pick the models to enable (grouped by provider, including the Open Source catalog), and pick your default model.
19
+ That opens a menu set up / connect tools, switch your Codex account, check usage, or log out. You never need to remember a subcommand.
20
+
21
+ Setup walks you through three quick steps: pick the tools to connect, pick the models to enable (grouped by provider, including the Open Source catalog), and pick your default model. It signs you in through your browser; to use an API key from `https://theclawbay.com/dashboard` instead, run `theclawbay setup --api-key <apiKey>`.
20
22
  Toggle items with arrow keys plus `Enter`/`Space`, or press `1-9` and `0` to toggle tools directly.
21
23
  Every model you enable is registered in every tool that supports model switching (Codex, Continue, GSD, OpenClaw, OpenCode, Kilo, Roo Code, Hermes, Zo, Trae get named entries or profiles per model; Aider gets `/model <id>` aliases).
22
24
  Re-running setup also migrates legacy OpenCode and Kilo patches to the current reasoning-capable provider path and refreshes OpenClaw model metadata.
@@ -24,10 +26,6 @@ Setup also creates a local backup before changing config files, and you can choo
24
26
 
25
27
  ## Optional
26
28
 
27
- `theclawbay link --api-key <apiKey>`
28
-
29
- Saves the API key without writing local client config.
30
-
31
29
  `theclawbay logout`
32
30
 
33
31
  Removes The Claw Bay-managed local config from this machine.
@@ -0,0 +1,25 @@
1
+ import { BaseCommand } from "../lib/base-command";
2
+ export default class CodexCommand extends BaseCommand {
3
+ static summary: string;
4
+ static description: string;
5
+ static examples: string[];
6
+ static args: {
7
+ action: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
8
+ name: import("@oclif/core/lib/interfaces").Arg<string | undefined, Record<string, unknown>>;
9
+ };
10
+ static flags: {
11
+ yes: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
12
+ };
13
+ run(): Promise<void>;
14
+ private describeActive;
15
+ private renderProfileLines;
16
+ private showStatus;
17
+ private showList;
18
+ private saveCurrent;
19
+ private detectRecoverable;
20
+ private recoverOfficial;
21
+ private switchTo;
22
+ private interactiveSwitch;
23
+ private performSwitch;
24
+ private removeOne;
25
+ }
@@ -0,0 +1,263 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const promises_1 = require("node:readline/promises");
5
+ const base_command_1 = require("../lib/base-command");
6
+ const errors_1 = require("../lib/errors");
7
+ const tui_1 = require("../lib/shared/tui");
8
+ const codex_profiles_1 = require("../lib/clients/codex-profiles");
9
+ const fsx_1 = require("../lib/shared/fsx");
10
+ const KNOWN_ACTIONS = new Set(["status", "list", "save", "use", "switch", "to", "remove", "rm", "delete", "recover"]);
11
+ function kindGlyph(profile) {
12
+ if (profile.kind === "official")
13
+ return "🅾";
14
+ if (profile.kind === "clawbay")
15
+ return tui_1.glyph.claw;
16
+ return tui_1.glyph.bullet;
17
+ }
18
+ class CodexCommand extends base_command_1.BaseCommand {
19
+ async run() {
20
+ await this.runSafe(async () => {
21
+ const { args, flags } = await this.parse(CodexCommand);
22
+ const nowIso = new Date().toISOString();
23
+ const rawAction = (args.action ?? "status").toLowerCase();
24
+ if (rawAction === "status")
25
+ return this.showStatus(nowIso, flags.yes);
26
+ if (rawAction === "list")
27
+ return this.showList(nowIso);
28
+ if (rawAction === "save")
29
+ return this.saveCurrent(args.name, nowIso);
30
+ if (rawAction === "recover")
31
+ return this.recoverOfficial(nowIso);
32
+ if (rawAction === "use" || rawAction === "switch" || rawAction === "to") {
33
+ return this.switchTo(args.name, nowIso, flags.yes);
34
+ }
35
+ if (rawAction === "remove" || rawAction === "rm" || rawAction === "delete") {
36
+ return this.removeOne(args.name, flags.yes);
37
+ }
38
+ if (!KNOWN_ACTIONS.has(rawAction)) {
39
+ // `theclawbay codex official` / `codex clawbay` — treat the token as a selector.
40
+ return this.switchTo(args.action, nowIso, flags.yes);
41
+ }
42
+ });
43
+ }
44
+ describeActive(active) {
45
+ if (!active.hasAuth)
46
+ return tui_1.tint.dim("No Codex login found in ~/.codex/auth.json");
47
+ return `${tui_1.tint.accentBold(active.label)} ${tui_1.tint.dim(`(${active.kind})`)}`;
48
+ }
49
+ renderProfileLines(profiles, activeSlug) {
50
+ if (profiles.length === 0) {
51
+ return [tui_1.tint.dim("No saved profiles yet. Run `theclawbay codex save` while logged in.")];
52
+ }
53
+ return profiles.map((profile) => {
54
+ const isActive = profile.slug === activeSlug;
55
+ const marker = isActive ? tui_1.tint.green(tui_1.glyph.radioOn) : tui_1.tint.dim(tui_1.glyph.radioOff);
56
+ const name = isActive ? tui_1.tint.bold(profile.label) : profile.label;
57
+ const suffix = isActive ? tui_1.tint.green(" ← active") : tui_1.tint.dim(` ${profile.slug}`);
58
+ return `${marker} ${kindGlyph(profile)} ${name}${suffix}`;
59
+ });
60
+ }
61
+ async showStatus(nowIso, skipPrompt) {
62
+ // Keep the active identity's snapshot fresh so switching back always works.
63
+ await (0, codex_profiles_1.ensureActiveIdentitySaved)(nowIso).catch(() => null);
64
+ const active = await (0, codex_profiles_1.readActiveIdentity)();
65
+ const profiles = await (0, codex_profiles_1.listProfiles)();
66
+ const activeSlug = active.hasAuth ? (0, codex_profiles_1.slugForIdentity)(active) : null;
67
+ const lines = [
68
+ (0, tui_1.statusLine)("info", `Active login: ${this.describeActive(active)}`),
69
+ "",
70
+ tui_1.tint.dim("Saved profiles:"),
71
+ ...this.renderProfileLines(profiles, activeSlug),
72
+ ];
73
+ const recoverable = await this.detectRecoverable();
74
+ if (recoverable) {
75
+ lines.push("");
76
+ lines.push((0, tui_1.statusLine)("warn", `A disabled ChatGPT login is sitting in auth.json${recoverable.email ? ` (${recoverable.email})` : ""}.`));
77
+ lines.push(tui_1.tint.dim(" Run `theclawbay codex recover` to save it as a profile."));
78
+ }
79
+ for (const line of (0, tui_1.renderBox)({ title: "Codex accounts", lines, footer: tui_1.tint.dim("Switch with: theclawbay codex use <name>") })) {
80
+ this.log(line);
81
+ }
82
+ const switchable = profiles.filter((p) => p.slug !== activeSlug);
83
+ if (skipPrompt || switchable.length === 0 || !process.stdin.isTTY || !process.stdout.isTTY)
84
+ return;
85
+ await this.interactiveSwitch(profiles, activeSlug, nowIso);
86
+ }
87
+ async showList(nowIso) {
88
+ const active = await (0, codex_profiles_1.readActiveIdentity)();
89
+ const profiles = await (0, codex_profiles_1.listProfiles)();
90
+ const activeSlug = active.hasAuth ? (0, codex_profiles_1.slugForIdentity)(active) : null;
91
+ if (profiles.length === 0) {
92
+ this.log(tui_1.tint.dim("No saved Codex profiles yet. Run `theclawbay codex save` while logged in."));
93
+ return;
94
+ }
95
+ for (const line of this.renderProfileLines(profiles, activeSlug))
96
+ this.log(line);
97
+ }
98
+ async saveCurrent(name, nowIso) {
99
+ const active = await (0, codex_profiles_1.readActiveIdentity)();
100
+ if (!active.hasAuth || active.authJson === null) {
101
+ throw new errors_1.ClawBayError("No Codex login found in ~/.codex/auth.json. Log into Codex first, then run `theclawbay codex save`.");
102
+ }
103
+ const profile = {
104
+ version: 1,
105
+ slug: (0, codex_profiles_1.slugForIdentity)(active),
106
+ kind: active.kind,
107
+ email: active.email,
108
+ providerId: active.providerId,
109
+ label: name?.trim() ? name.trim() : active.label,
110
+ authJson: active.authJson,
111
+ configToml: active.configToml,
112
+ savedAt: nowIso,
113
+ };
114
+ await (0, codex_profiles_1.saveProfile)(profile);
115
+ this.log((0, tui_1.statusLine)("ok", `Saved profile ${tui_1.tint.bold(profile.label)} ${tui_1.tint.dim(`(${profile.slug})`)}`));
116
+ }
117
+ async detectRecoverable() {
118
+ const raw = await (0, fsx_1.readFileIfExists)(codex_profiles_1.CODEX_AUTH_FILE);
119
+ const recovered = (0, codex_profiles_1.recoverCommentedOfficialAuth)(raw);
120
+ if (!recovered)
121
+ return null;
122
+ // Only worth surfacing if we don't already have an official profile saved.
123
+ const profiles = await (0, codex_profiles_1.listProfiles)();
124
+ if (profiles.some((p) => p.kind === "official"))
125
+ return null;
126
+ return { email: recovered.email };
127
+ }
128
+ async recoverOfficial(nowIso) {
129
+ const raw = await (0, fsx_1.readFileIfExists)(codex_profiles_1.CODEX_AUTH_FILE);
130
+ const recovered = (0, codex_profiles_1.recoverCommentedOfficialAuth)(raw);
131
+ if (!recovered) {
132
+ throw new errors_1.ClawBayError("No disabled ChatGPT login found in ~/.codex/auth.json to recover.");
133
+ }
134
+ const configToml = await (0, fsx_1.readFileIfExists)(codex_profiles_1.CODEX_CONFIG_FILE);
135
+ const profile = {
136
+ version: 1,
137
+ slug: recovered.email ? `official-${recovered.email.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")}` : "official",
138
+ kind: "official",
139
+ email: recovered.email,
140
+ providerId: "openai",
141
+ label: recovered.email ? `Official ChatGPT · ${recovered.email}` : "Official ChatGPT",
142
+ authJson: recovered.authJson,
143
+ configToml,
144
+ savedAt: nowIso,
145
+ };
146
+ await (0, codex_profiles_1.saveProfile)(profile);
147
+ this.log((0, tui_1.statusLine)("ok", `Recovered official login into profile ${tui_1.tint.bold(profile.label)}.`));
148
+ this.log(tui_1.tint.dim(`Switch to it with: theclawbay codex use ${profile.slug}`));
149
+ }
150
+ async switchTo(selector, nowIso, skipPrompt) {
151
+ const profiles = await (0, codex_profiles_1.listProfiles)();
152
+ if (profiles.length === 0) {
153
+ throw new errors_1.ClawBayError("No saved Codex profiles yet. Run `theclawbay codex save` while logged in, then switch.");
154
+ }
155
+ if (!selector || !selector.trim()) {
156
+ const active = await (0, codex_profiles_1.readActiveIdentity)();
157
+ const activeSlug = active.hasAuth ? (0, codex_profiles_1.slugForIdentity)(active) : null;
158
+ if (!skipPrompt && process.stdin.isTTY && process.stdout.isTTY) {
159
+ await this.interactiveSwitch(profiles, activeSlug, nowIso);
160
+ return;
161
+ }
162
+ throw new errors_1.ClawBayError("Which profile? Try `theclawbay codex use <name>` or `theclawbay codex list`.");
163
+ }
164
+ const matches = (0, codex_profiles_1.resolveProfileSelector)(selector, profiles);
165
+ if (matches.length === 0) {
166
+ if (["official", "chatgpt", "gpt", "openai"].includes(selector.trim().toLowerCase())) {
167
+ throw new errors_1.ClawBayError("No official ChatGPT profile saved yet. Log into Codex with your ChatGPT account (run `codex login`), then `theclawbay codex save` — or `theclawbay codex recover` if it's disabled in auth.json.");
168
+ }
169
+ throw new errors_1.ClawBayError(`No profile matched "${selector}". Run \`theclawbay codex list\` to see saved profiles.`);
170
+ }
171
+ if (matches.length > 1) {
172
+ const names = matches.map((m) => m.slug).join(", ");
173
+ throw new errors_1.ClawBayError(`"${selector}" matched multiple profiles: ${names}. Be more specific.`);
174
+ }
175
+ await this.performSwitch(matches[0], nowIso);
176
+ }
177
+ async interactiveSwitch(profiles, activeSlug, nowIso) {
178
+ const options = profiles.filter((p) => p.slug !== activeSlug);
179
+ if (options.length === 0)
180
+ return;
181
+ this.log("");
182
+ options.forEach((profile, index) => {
183
+ this.log(` ${tui_1.tint.accentBold(String(index + 1))}. ${kindGlyph(profile)} ${profile.label}`);
184
+ });
185
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
186
+ try {
187
+ const answer = await rl.question(tui_1.tint.dim("Switch to # (Enter to stay): "));
188
+ const choice = Number.parseInt(answer.trim(), 10);
189
+ if (!Number.isInteger(choice) || choice < 1 || choice > options.length)
190
+ return;
191
+ await this.performSwitch(options[choice - 1], nowIso);
192
+ }
193
+ finally {
194
+ rl.close();
195
+ process.stdin.resume();
196
+ }
197
+ }
198
+ async performSwitch(target, nowIso) {
199
+ const result = await (0, codex_profiles_1.switchToProfile)(target, nowIso);
200
+ if (result.alreadyActive) {
201
+ this.log((0, tui_1.statusLine)("info", `Already on ${tui_1.tint.bold(target.label)}.`));
202
+ return;
203
+ }
204
+ if (result.preserved) {
205
+ this.log((0, tui_1.statusLine)("ok", `Saved current login as ${tui_1.tint.dim(result.preserved.label)}.`));
206
+ }
207
+ this.log((0, tui_1.statusLine)("ok", `Switched Codex to ${tui_1.tint.accentBold(target.label)}.`));
208
+ this.log(tui_1.tint.dim("Restart Codex Desktop (or your terminal Codex session) to load this account's history."));
209
+ }
210
+ async removeOne(selector, skipPrompt) {
211
+ if (!selector || !selector.trim()) {
212
+ throw new errors_1.ClawBayError("Which profile? Try `theclawbay codex remove <name>`.");
213
+ }
214
+ const profiles = await (0, codex_profiles_1.listProfiles)();
215
+ const matches = (0, codex_profiles_1.resolveProfileSelector)(selector, profiles);
216
+ if (matches.length === 0)
217
+ throw new errors_1.ClawBayError(`No profile matched "${selector}".`);
218
+ if (matches.length > 1) {
219
+ throw new errors_1.ClawBayError(`"${selector}" matched multiple profiles: ${matches.map((m) => m.slug).join(", ")}.`);
220
+ }
221
+ const target = matches[0];
222
+ if (!skipPrompt && process.stdin.isTTY && process.stdout.isTTY) {
223
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
224
+ try {
225
+ const answer = await rl.question(`Forget saved profile "${target.label}"? [y/N] `);
226
+ const normalized = answer.trim().toLowerCase();
227
+ if (normalized !== "y" && normalized !== "yes") {
228
+ this.log(tui_1.tint.dim("Kept."));
229
+ return;
230
+ }
231
+ }
232
+ finally {
233
+ rl.close();
234
+ process.stdin.resume();
235
+ }
236
+ }
237
+ await (0, codex_profiles_1.removeProfile)(target.slug);
238
+ this.log((0, tui_1.statusLine)("ok", `Removed profile ${tui_1.tint.bold(target.label)}.`));
239
+ }
240
+ }
241
+ CodexCommand.summary = "Switch Codex between your The Claw Bay relay and official ChatGPT/OpenAI logins";
242
+ CodexCommand.description = "Save your Codex logins as named profiles and swap between them in one command. Each profile is a snapshot of ~/.codex/auth.json + config.toml, so switching restores that account's credentials and its own chat history. The active login is always snapshotted before a switch, so you never lose an account.";
243
+ CodexCommand.examples = [
244
+ "<%= config.bin %> codex # show the active login + saved profiles",
245
+ "<%= config.bin %> codex save # snapshot the current login as a profile",
246
+ "<%= config.bin %> codex use official # switch to your official ChatGPT login",
247
+ "<%= config.bin %> codex clawbay # switch back to The Claw Bay",
248
+ "<%= config.bin %> codex remove official # forget a saved profile",
249
+ ];
250
+ CodexCommand.args = {
251
+ action: core_1.Args.string({
252
+ required: false,
253
+ description: "status (default) | list | save | use | remove | recover — or a profile name to switch to",
254
+ }),
255
+ name: core_1.Args.string({
256
+ required: false,
257
+ description: "profile name/selector for use, save, or remove",
258
+ }),
259
+ };
260
+ CodexCommand.flags = {
261
+ yes: core_1.Flags.boolean({ char: "y", default: false, description: "skip confirmation prompts" }),
262
+ };
263
+ exports.default = CodexCommand;
@@ -12,6 +12,7 @@ const errors_1 = require("../lib/managed/errors");
12
12
  const api_key_1 = require("../lib/managed/api-key");
13
13
  const urls_1 = require("../lib/shared/urls");
14
14
  const codex_1 = require("../lib/clients/codex");
15
+ const codex_profiles_ui_1 = require("../lib/clients/codex-profiles-ui");
15
16
  const claude_1 = require("../lib/clients/claude");
16
17
  const continue_1 = require("../lib/clients/continue");
17
18
  const cline_1 = require("../lib/clients/cline");
@@ -96,9 +97,18 @@ class SetupCommand extends base_command_1.BaseCommand {
96
97
  if (!(error instanceof errors_1.ManagedConfigMissingError))
97
98
  throw error;
98
99
  }
99
- const explicitApiKey = (flags["api-key"] ?? "").trim();
100
+ let explicitApiKey = (flags["api-key"] ?? "").trim();
100
101
  const managedCredential = (managed?.credential ?? "").trim();
101
102
  const managedAuthType = managed?.authType ?? null;
103
+ // Fresh interactive setup: ask how they want to sign in. Picking "API key"
104
+ // prompts for it here so it flows through the same path as --api-key; an
105
+ // empty entry (or picking "browser") falls through to device-session below.
106
+ if (!explicitApiKey && !managedCredential && !flags.yes && process.stdin.isTTY && process.stdout.isTTY) {
107
+ const method = await (0, prompts_1.promptAuthMethod)();
108
+ if (method === "api-key") {
109
+ explicitApiKey = await (0, prompts_1.promptApiKey)();
110
+ }
111
+ }
102
112
  let authType = explicitApiKey
103
113
  ? "api-key"
104
114
  : managedAuthType === "api-key"
@@ -470,6 +480,10 @@ class SetupCommand extends base_command_1.BaseCommand {
470
480
  }
471
481
  if (selectedSetupClients.has("codex")) {
472
482
  progress.update("Configuring Codex");
483
+ // If they're signed into their own Codex/ChatGPT account, offer to save
484
+ // it before we overwrite config.toml, so they can switch back later via
485
+ // `theclawbay codex`. Best-effort — never block setup.
486
+ await (0, codex_profiles_ui_1.preserveDetectedCodexLogin)({ skipPrompt: flags.yes, log: (m) => this.log(m) }).catch(() => undefined);
473
487
  codexConfigPath = await (0, codex_1.writeCodexConfig)({
474
488
  backendUrl,
475
489
  model: resolved?.model ?? models_1.DEFAULT_CODEX_MODEL,
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ const node_fs_1 = require("node:fs");
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  const core_1 = require("@oclif/core");
10
10
  const update_check_1 = require("./lib/update-check");
11
+ const main_menu_1 = require("./lib/main-menu");
11
12
  function detectPackageVersion() {
12
13
  try {
13
14
  const packageJsonPath = node_path_1.default.join(__dirname, "..", "package.json");
@@ -34,6 +35,17 @@ async function maybePrintBareInvocationHint() {
34
35
  console.log("");
35
36
  }
36
37
  async function main() {
38
+ const argv = process.argv.slice(2);
39
+ // Bare `theclawbay` on a terminal opens a friendly menu instead of dumping help.
40
+ if (argv.length === 0 && process.stdin.isTTY && process.stdout.isTTY) {
41
+ const choice = await (0, main_menu_1.promptMainMenu)().catch(() => null);
42
+ if (choice && choice.argv.length > 0) {
43
+ await (0, core_1.run)(choice.argv).then(() => (0, core_1.flush)());
44
+ return;
45
+ }
46
+ await (0, core_1.flush)();
47
+ return;
48
+ }
37
49
  await maybePrintBareInvocationHint();
38
50
  await (0, core_1.run)().then(() => (0, core_1.flush)());
39
51
  }
@@ -0,0 +1,4 @@
1
+ export declare function preserveDetectedCodexLogin(params: {
2
+ skipPrompt: boolean;
3
+ log: (message: string) => void;
4
+ }): Promise<void>;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.preserveDetectedCodexLogin = preserveDetectedCodexLogin;
4
+ const promises_1 = require("node:readline/promises");
5
+ const codex_profiles_1 = require("./codex-profiles");
6
+ // Called from `setup`, right before we overwrite Codex's config with the relay.
7
+ // If the machine is signed into an official ChatGPT account, ask whether to keep
8
+ // it (so the user can switch back later); anything else is snapshotted silently.
9
+ // With --yes / no TTY we preserve the login without asking — losing it would be
10
+ // worse than an unprompted save.
11
+ async function preserveDetectedCodexLogin(params) {
12
+ const active = await (0, codex_profiles_1.readActiveIdentity)();
13
+ if (!active.hasAuth)
14
+ return;
15
+ const profile = (0, codex_profiles_1.buildProfileFromActive)(active, new Date().toISOString());
16
+ if (!profile)
17
+ return;
18
+ const interactive = !params.skipPrompt && process.stdin.isTTY && process.stdout.isTTY;
19
+ if (active.kind === "official" && interactive) {
20
+ const who = active.email ? ` as ${active.email}` : "";
21
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
22
+ let keep = true;
23
+ try {
24
+ const answer = await rl.question(`\nYou're signed into Codex${who}. Save this login so you can switch back to it anytime? [Y/n] `);
25
+ const normalized = answer.trim().toLowerCase();
26
+ keep = normalized === "" || normalized === "y" || normalized === "yes";
27
+ }
28
+ finally {
29
+ rl.close();
30
+ process.stdin.resume();
31
+ }
32
+ if (!keep)
33
+ return;
34
+ }
35
+ await (0, codex_profiles_1.saveProfile)(profile);
36
+ params.log(active.kind === "official"
37
+ ? `Saved your ChatGPT login — switch back anytime with "theclawbay codex".`
38
+ : `Saved your current Codex login for quick switching ("theclawbay codex").`);
39
+ }
@@ -0,0 +1,50 @@
1
+ export declare const CODEX_AUTH_FILE: string;
2
+ export declare const CODEX_CONFIG_FILE: string;
3
+ export type CodexIdentityKind = "official" | "clawbay" | "other";
4
+ export type CodexIdentity = {
5
+ kind: CodexIdentityKind;
6
+ email: string | null;
7
+ providerId: string | null;
8
+ label: string;
9
+ };
10
+ export type CodexProfile = CodexIdentity & {
11
+ version: 1;
12
+ slug: string;
13
+ authJson: string;
14
+ configToml: string | null;
15
+ savedAt: string;
16
+ };
17
+ export type CodexActiveIdentity = CodexIdentity & {
18
+ hasAuth: boolean;
19
+ authJson: string | null;
20
+ configToml: string | null;
21
+ };
22
+ export declare function decodeJwtClaims(token: unknown): Record<string, unknown> | null;
23
+ export declare function emailFromIdToken(idToken: unknown): string | null;
24
+ export declare function parseAuthJsonLoose(raw: string | null): Record<string, unknown> | null;
25
+ export declare function providerIdFromConfig(configToml: string | null): string | null;
26
+ export declare function detectIdentity(params: {
27
+ authJson: string | null;
28
+ configToml: string | null;
29
+ }): CodexIdentity;
30
+ export declare function slugForIdentity(identity: CodexIdentity): string;
31
+ export declare function readActiveIdentity(): Promise<CodexActiveIdentity>;
32
+ export declare function isValidProfile(value: unknown): value is CodexProfile;
33
+ export declare function saveProfile(profile: CodexProfile): Promise<void>;
34
+ export declare function listProfiles(): Promise<CodexProfile[]>;
35
+ export declare function getProfile(slug: string): Promise<CodexProfile | null>;
36
+ export declare function removeProfile(slug: string): Promise<boolean>;
37
+ export declare function buildProfileFromActive(active: CodexActiveIdentity, nowIso: string): CodexProfile | null;
38
+ export declare function ensureActiveIdentitySaved(nowIso: string): Promise<CodexProfile | null>;
39
+ export declare function applyProfile(profile: CodexProfile): Promise<void>;
40
+ export type CodexSwitchResult = {
41
+ target: CodexProfile;
42
+ preserved: CodexProfile | null;
43
+ alreadyActive: boolean;
44
+ };
45
+ export declare function switchToProfile(target: CodexProfile, nowIso: string): Promise<CodexSwitchResult>;
46
+ export declare function recoverCommentedOfficialAuth(raw: string | null): {
47
+ authJson: string;
48
+ email: string | null;
49
+ } | null;
50
+ export declare function resolveProfileSelector(selector: string, profiles: CodexProfile[]): CodexProfile[];
@@ -0,0 +1,366 @@
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.CODEX_CONFIG_FILE = exports.CODEX_AUTH_FILE = void 0;
7
+ exports.decodeJwtClaims = decodeJwtClaims;
8
+ exports.emailFromIdToken = emailFromIdToken;
9
+ exports.parseAuthJsonLoose = parseAuthJsonLoose;
10
+ exports.providerIdFromConfig = providerIdFromConfig;
11
+ exports.detectIdentity = detectIdentity;
12
+ exports.slugForIdentity = slugForIdentity;
13
+ exports.readActiveIdentity = readActiveIdentity;
14
+ exports.isValidProfile = isValidProfile;
15
+ exports.saveProfile = saveProfile;
16
+ exports.listProfiles = listProfiles;
17
+ exports.getProfile = getProfile;
18
+ exports.removeProfile = removeProfile;
19
+ exports.buildProfileFromActive = buildProfileFromActive;
20
+ exports.ensureActiveIdentitySaved = ensureActiveIdentitySaved;
21
+ exports.applyProfile = applyProfile;
22
+ exports.switchToProfile = switchToProfile;
23
+ exports.recoverCommentedOfficialAuth = recoverCommentedOfficialAuth;
24
+ exports.resolveProfileSelector = resolveProfileSelector;
25
+ const promises_1 = __importDefault(require("node:fs/promises"));
26
+ const node_path_1 = __importDefault(require("node:path"));
27
+ const paths_1 = require("../config/paths");
28
+ const providers_1 = require("../shared/providers");
29
+ const fsx_1 = require("../shared/fsx");
30
+ // Codex stores a machine's active identity in exactly two files under ~/.codex:
31
+ // auth.json — OpenAI login (ChatGPT OAuth tokens *or* an API key)
32
+ // config.toml — active model_provider + provider tables (our managed relay block)
33
+ // A "profile" is a verbatim snapshot of both files plus a little detected metadata,
34
+ // so switching identities never requires understanding Codex's auth internals: we
35
+ // simply restore exactly the bytes that were known to work.
36
+ exports.CODEX_AUTH_FILE = node_path_1.default.join(paths_1.codexDir, "auth.json");
37
+ exports.CODEX_CONFIG_FILE = node_path_1.default.join(paths_1.codexDir, "config.toml");
38
+ function base64UrlDecode(segment) {
39
+ try {
40
+ const normalized = segment.replace(/-/g, "+").replace(/_/g, "/");
41
+ const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4);
42
+ return Buffer.from(padded, "base64").toString("utf8");
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ // Best-effort JWT claim reader. We never verify the signature — we only want the
49
+ // email the token was minted for, purely to label the profile for the human.
50
+ function decodeJwtClaims(token) {
51
+ if (typeof token !== "string")
52
+ return null;
53
+ const parts = token.split(".");
54
+ if (parts.length < 2)
55
+ return null;
56
+ const json = base64UrlDecode(parts[1] ?? "");
57
+ if (json === null)
58
+ return null;
59
+ try {
60
+ const parsed = JSON.parse(json);
61
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
62
+ return parsed;
63
+ }
64
+ return null;
65
+ }
66
+ catch {
67
+ return null;
68
+ }
69
+ }
70
+ function emailFromIdToken(idToken) {
71
+ const claims = decodeJwtClaims(idToken);
72
+ if (!claims)
73
+ return null;
74
+ const direct = claims.email;
75
+ if (typeof direct === "string" && direct.includes("@"))
76
+ return direct;
77
+ // ChatGPT id tokens nest profile info under an OpenAI-namespaced claim.
78
+ for (const key of Object.keys(claims)) {
79
+ if (!key.startsWith("https://api.openai.com/"))
80
+ continue;
81
+ const value = claims[key];
82
+ if (value && typeof value === "object" && !Array.isArray(value)) {
83
+ const nested = value.email;
84
+ if (typeof nested === "string" && nested.includes("@"))
85
+ return nested;
86
+ }
87
+ }
88
+ return null;
89
+ }
90
+ // Codex's auth.json is plain JSON, but hand-edited files (e.g. someone commenting
91
+ // out the `tokens` block with `//` to force API-key mode) are common in the wild.
92
+ // Parse strictly first; fall back to stripping `//` line comments so we can still
93
+ // read the *active* identity out of a lightly-mangled file.
94
+ function parseAuthJsonLoose(raw) {
95
+ if (raw === null || !raw.trim())
96
+ return null;
97
+ const attempt = (text) => {
98
+ try {
99
+ const parsed = JSON.parse(text);
100
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
101
+ return parsed;
102
+ }
103
+ return null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ };
109
+ return (attempt(raw) ??
110
+ attempt(raw
111
+ .split(/\r?\n/)
112
+ .filter((line) => !line.trim().startsWith("//"))
113
+ .join("\n")));
114
+ }
115
+ function providerIdFromConfig(configToml) {
116
+ if (!configToml)
117
+ return null;
118
+ const match = /^\s*model_provider\s*=\s*"([^"]+)"/m.exec(configToml);
119
+ return match ? (match[1] ?? null) : null;
120
+ }
121
+ function tokensBlock(auth) {
122
+ if (!auth)
123
+ return null;
124
+ const tokens = auth.tokens;
125
+ if (tokens && typeof tokens === "object" && !Array.isArray(tokens)) {
126
+ return tokens;
127
+ }
128
+ return null;
129
+ }
130
+ function detectIdentity(params) {
131
+ const auth = parseAuthJsonLoose(params.authJson);
132
+ const providerId = providerIdFromConfig(params.configToml);
133
+ const tokens = tokensBlock(auth);
134
+ // A live ChatGPT OAuth session (id_token present) is the official account.
135
+ if (tokens && typeof tokens.id_token === "string" && tokens.id_token.trim()) {
136
+ const email = emailFromIdToken(tokens.id_token);
137
+ return {
138
+ kind: "official",
139
+ email,
140
+ providerId: providerId ?? providers_1.OPENAI_PROVIDER_ID,
141
+ label: email ? `Official ChatGPT · ${email}` : "Official ChatGPT",
142
+ };
143
+ }
144
+ const usesClawbayProvider = providerId === providers_1.DEFAULT_PROVIDER_ID || (params.configToml?.includes(`[model_providers.${providers_1.DEFAULT_PROVIDER_ID}]`) ?? false);
145
+ const hasApiKey = typeof auth?.OPENAI_API_KEY === "string" && auth.OPENAI_API_KEY.trim().length > 0;
146
+ if (usesClawbayProvider) {
147
+ return { kind: "clawbay", email: null, providerId: providerId ?? providers_1.DEFAULT_PROVIDER_ID, label: "The Claw Bay" };
148
+ }
149
+ if (hasApiKey || providerId) {
150
+ return {
151
+ kind: "other",
152
+ email: null,
153
+ providerId: providerId ?? providers_1.OPENAI_PROVIDER_ID,
154
+ label: providerId ? `OpenAI API key · ${providerId}` : "OpenAI API key",
155
+ };
156
+ }
157
+ return { kind: "other", email: null, providerId, label: "Unknown Codex login" };
158
+ }
159
+ function slugForIdentity(identity) {
160
+ if (identity.kind === "clawbay")
161
+ return "clawbay";
162
+ const sanitize = (value) => value
163
+ .toLowerCase()
164
+ .replace(/[^a-z0-9]+/g, "-")
165
+ .replace(/^-+|-+$/g, "")
166
+ .slice(0, 48);
167
+ if (identity.kind === "official") {
168
+ return identity.email ? `official-${sanitize(identity.email)}` : "official";
169
+ }
170
+ return identity.providerId ? `other-${sanitize(identity.providerId)}` : "other";
171
+ }
172
+ async function readActiveIdentity() {
173
+ const [authJson, configToml] = await Promise.all([
174
+ (0, fsx_1.readFileIfExists)(exports.CODEX_AUTH_FILE),
175
+ (0, fsx_1.readFileIfExists)(exports.CODEX_CONFIG_FILE),
176
+ ]);
177
+ const identity = detectIdentity({ authJson, configToml });
178
+ return {
179
+ ...identity,
180
+ hasAuth: authJson !== null && authJson.trim().length > 0,
181
+ authJson,
182
+ configToml,
183
+ };
184
+ }
185
+ function profilePath(slug) {
186
+ return node_path_1.default.join(paths_1.codexProfilesDir, `${slug}.json`);
187
+ }
188
+ function isValidProfile(value) {
189
+ if (!value || typeof value !== "object" || Array.isArray(value))
190
+ return false;
191
+ const record = value;
192
+ return (record.version === 1 &&
193
+ typeof record.slug === "string" &&
194
+ typeof record.authJson === "string" &&
195
+ (record.kind === "official" || record.kind === "clawbay" || record.kind === "other"));
196
+ }
197
+ async function saveProfile(profile) {
198
+ await promises_1.default.mkdir(paths_1.codexProfilesDir, { recursive: true });
199
+ const target = profilePath(profile.slug);
200
+ await promises_1.default.writeFile(target, `${JSON.stringify(profile, null, 2)}\n`, "utf8");
201
+ // auth.json holds live credentials — keep the snapshot owner-only.
202
+ await promises_1.default.chmod(target, 0o600).catch(() => undefined);
203
+ }
204
+ async function listProfiles() {
205
+ let entries = [];
206
+ try {
207
+ entries = await promises_1.default.readdir(paths_1.codexProfilesDir);
208
+ }
209
+ catch (error) {
210
+ const err = error;
211
+ if (err.code === "ENOENT")
212
+ return [];
213
+ throw error;
214
+ }
215
+ const profiles = [];
216
+ for (const entry of entries) {
217
+ if (!entry.endsWith(".json"))
218
+ continue;
219
+ const raw = await (0, fsx_1.readFileIfExists)(node_path_1.default.join(paths_1.codexProfilesDir, entry));
220
+ if (raw === null)
221
+ continue;
222
+ try {
223
+ const parsed = JSON.parse(raw);
224
+ if (isValidProfile(parsed))
225
+ profiles.push(parsed);
226
+ }
227
+ catch {
228
+ // ignore corrupt snapshot files
229
+ }
230
+ }
231
+ profiles.sort((a, b) => a.label.localeCompare(b.label));
232
+ return profiles;
233
+ }
234
+ async function getProfile(slug) {
235
+ const raw = await (0, fsx_1.readFileIfExists)(profilePath(slug));
236
+ if (raw === null)
237
+ return null;
238
+ try {
239
+ const parsed = JSON.parse(raw);
240
+ return isValidProfile(parsed) ? parsed : null;
241
+ }
242
+ catch {
243
+ return null;
244
+ }
245
+ }
246
+ async function removeProfile(slug) {
247
+ try {
248
+ await promises_1.default.unlink(profilePath(slug));
249
+ return true;
250
+ }
251
+ catch (error) {
252
+ const err = error;
253
+ if (err.code === "ENOENT")
254
+ return false;
255
+ throw error;
256
+ }
257
+ }
258
+ // Capture whatever identity is currently live in ~/.codex as a profile snapshot.
259
+ // Returns null when there is no auth.json worth preserving (fresh machine).
260
+ function buildProfileFromActive(active, nowIso) {
261
+ if (!active.hasAuth || active.authJson === null)
262
+ return null;
263
+ return {
264
+ version: 1,
265
+ slug: slugForIdentity(active),
266
+ kind: active.kind,
267
+ email: active.email,
268
+ providerId: active.providerId,
269
+ label: active.label,
270
+ authJson: active.authJson,
271
+ configToml: active.configToml,
272
+ savedAt: nowIso,
273
+ };
274
+ }
275
+ // Refresh (or create) the snapshot for the currently-active identity so switching
276
+ // back to it always restores the freshest working credentials.
277
+ async function ensureActiveIdentitySaved(nowIso) {
278
+ const active = await readActiveIdentity();
279
+ const profile = buildProfileFromActive(active, nowIso);
280
+ if (!profile)
281
+ return null;
282
+ await saveProfile(profile);
283
+ return profile;
284
+ }
285
+ async function applyProfile(profile) {
286
+ await promises_1.default.mkdir(paths_1.codexDir, { recursive: true });
287
+ await promises_1.default.writeFile(exports.CODEX_AUTH_FILE, profile.authJson, "utf8");
288
+ await promises_1.default.chmod(exports.CODEX_AUTH_FILE, 0o600).catch(() => undefined);
289
+ if (profile.configToml !== null) {
290
+ await promises_1.default.writeFile(exports.CODEX_CONFIG_FILE, profile.configToml, "utf8");
291
+ }
292
+ }
293
+ async function switchToProfile(target, nowIso) {
294
+ const active = await readActiveIdentity();
295
+ const activeSlug = active.hasAuth ? slugForIdentity(active) : null;
296
+ if (activeSlug === target.slug) {
297
+ // Re-apply anyway (config may have drifted) but report it was already current.
298
+ await applyProfile(target);
299
+ return { target, preserved: null, alreadyActive: true };
300
+ }
301
+ // Preserve the outgoing identity before we overwrite it.
302
+ const preserved = buildProfileFromActive(active, nowIso);
303
+ if (preserved)
304
+ await saveProfile(preserved);
305
+ await applyProfile(target);
306
+ return { target, preserved, alreadyActive: false };
307
+ }
308
+ // Some machines have a hand-disabled ChatGPT login: the `tokens` block is left in
309
+ // auth.json but commented out with `//` so Codex falls back to an API key. Rebuild a
310
+ // usable official auth.json from those commented lines so the login can be rescued
311
+ // into a profile without a fresh `codex login`. Read-only: never touches ~/.codex.
312
+ function recoverCommentedOfficialAuth(raw) {
313
+ if (raw === null || !raw.trim())
314
+ return null;
315
+ if (!/\/\/\s*"(?:tokens|id_token)"/.test(raw))
316
+ return null;
317
+ const uncommented = raw
318
+ .split(/\r?\n/)
319
+ .map((line) => {
320
+ const match = /^(\s*)\/\/\s?(.*)$/.exec(line);
321
+ return match ? `${match[1]}${match[2]}` : line;
322
+ })
323
+ .join("\n");
324
+ const attempts = [uncommented, uncommented.replace(/,(\s*[}\]])/g, "$1")];
325
+ for (const candidate of attempts) {
326
+ let parsed = null;
327
+ try {
328
+ const value = JSON.parse(candidate);
329
+ if (value && typeof value === "object" && !Array.isArray(value)) {
330
+ parsed = value;
331
+ }
332
+ }
333
+ catch {
334
+ parsed = null;
335
+ }
336
+ const tokens = tokensBlock(parsed);
337
+ if (!tokens || typeof tokens.id_token !== "string" || !tokens.id_token.trim())
338
+ continue;
339
+ // Force ChatGPT mode and drop any lingering API key so the restored file is
340
+ // unambiguously the official login.
341
+ const rebuilt = { ...parsed };
342
+ rebuilt.auth_mode = "chatgpt";
343
+ delete rebuilt.OPENAI_API_KEY;
344
+ return { authJson: `${JSON.stringify(rebuilt, null, 2)}\n`, email: emailFromIdToken(tokens.id_token) };
345
+ }
346
+ return null;
347
+ }
348
+ // Match a user-typed selector against saved profiles: exact slug, a kind keyword
349
+ // ("official" / "clawbay" / "gpt" / "openai"), or an email substring.
350
+ function resolveProfileSelector(selector, profiles) {
351
+ const needle = selector.trim().toLowerCase();
352
+ if (!needle)
353
+ return [];
354
+ const exact = profiles.filter((p) => p.slug.toLowerCase() === needle);
355
+ if (exact.length > 0)
356
+ return exact;
357
+ if (needle === "official" || needle === "chatgpt" || needle === "gpt" || needle === "openai") {
358
+ return profiles.filter((p) => p.kind === "official");
359
+ }
360
+ if (needle === "clawbay" || needle === "claw" || needle === "relay") {
361
+ return profiles.filter((p) => p.kind === "clawbay");
362
+ }
363
+ return profiles.filter((p) => p.slug.toLowerCase().includes(needle) ||
364
+ (p.email ?? "").toLowerCase().includes(needle) ||
365
+ p.label.toLowerCase().includes(needle));
366
+ }
@@ -1,6 +1,7 @@
1
1
  export declare const codexDir: string;
2
2
  export declare const theclawbayConfigDir: string;
3
3
  export declare const theclawbayStateDir: string;
4
+ export declare const codexProfilesDir: string;
4
5
  export declare const theclawbayBackupDir: string;
5
6
  export declare const managedConfigPath: string;
6
7
  export declare const updateCheckStatePath: string;
@@ -3,12 +3,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.legacyManagedConfigPathCodexAuth = exports.legacyManagedConfigPathClayBay = exports.updateCheckStatePath = exports.managedConfigPath = exports.theclawbayBackupDir = exports.theclawbayStateDir = exports.theclawbayConfigDir = exports.codexDir = void 0;
6
+ exports.legacyManagedConfigPathCodexAuth = exports.legacyManagedConfigPathClayBay = exports.updateCheckStatePath = exports.managedConfigPath = exports.theclawbayBackupDir = exports.codexProfilesDir = exports.theclawbayStateDir = exports.theclawbayConfigDir = exports.codexDir = void 0;
7
7
  const node_os_1 = __importDefault(require("node:os"));
8
8
  const node_path_1 = __importDefault(require("node:path"));
9
9
  exports.codexDir = node_path_1.default.join(node_os_1.default.homedir(), ".codex");
10
10
  exports.theclawbayConfigDir = node_path_1.default.join(node_os_1.default.homedir(), ".config", "theclawbay");
11
11
  exports.theclawbayStateDir = node_path_1.default.join(exports.theclawbayConfigDir, "state");
12
+ exports.codexProfilesDir = node_path_1.default.join(exports.theclawbayConfigDir, "codex-profiles");
12
13
  exports.theclawbayBackupDir = node_path_1.default.join(node_os_1.default.homedir(), ".config", "theclawbay-backups");
13
14
  exports.managedConfigPath = node_path_1.default.join(exports.codexDir, "theclawbay.managed.json");
14
15
  exports.updateCheckStatePath = node_path_1.default.join(exports.codexDir, "theclawbay.update-check.json");
@@ -0,0 +1,4 @@
1
+ export type MenuChoice = {
2
+ argv: string[];
3
+ } | null;
4
+ export declare function promptMainMenu(): Promise<MenuChoice>;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptMainMenu = promptMainMenu;
4
+ const prompts_1 = require("./shared/prompts");
5
+ const tui_1 = require("./shared/tui");
6
+ const ITEMS = [
7
+ { icon: tui_1.glyph.claw, label: "Set up / connect tools", desc: "Point Codex, Claude & others at The Claw Bay", argv: ["setup"] },
8
+ { icon: "🔀", label: "Switch Codex account", desc: "Flip between The Claw Bay and your official ChatGPT login", argv: ["codex"] },
9
+ { icon: "📊", label: "Usage", desc: "See how much of your plan is left", argv: ["usage"] },
10
+ { icon: "🚪", label: "Log out", desc: "Revert every local change on this machine", argv: ["logout"] },
11
+ ];
12
+ async function promptMainMenu() {
13
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
14
+ return null;
15
+ let cursor = 0;
16
+ const quitIndex = ITEMS.length;
17
+ const total = ITEMS.length + 1;
18
+ const render = () => {
19
+ const rows = ITEMS.map((item, index) => {
20
+ const active = index === cursor;
21
+ const pointer = active ? tui_1.tint.accentBold(tui_1.glyph.pointer) : " ";
22
+ const number = tui_1.tint.dim(String(index + 1));
23
+ const head = `${item.icon} ${item.label}`;
24
+ const label = active ? tui_1.tint.accentBold(head) : head;
25
+ return `${pointer} ${number} ${label} ${tui_1.tint.dim(`— ${item.desc}`)}`;
26
+ });
27
+ const quitActive = cursor === quitIndex;
28
+ const quitRow = `${quitActive ? tui_1.tint.accentBold(tui_1.glyph.pointer) : " "} ${quitActive ? tui_1.tint.accentBold("Quit") : tui_1.tint.dim("Quit")}`;
29
+ return [
30
+ "",
31
+ ...(0, tui_1.renderBox)({
32
+ title: "The Claw Bay",
33
+ lines: [
34
+ tui_1.tint.dim("What would you like to do?"),
35
+ "",
36
+ ...rows,
37
+ "",
38
+ quitRow,
39
+ ],
40
+ footer: (0, tui_1.keyHints)([
41
+ ["↑↓", "move"],
42
+ ["1-4", "jump"],
43
+ ["enter", "select"],
44
+ ["q", "quit"],
45
+ ]),
46
+ }),
47
+ ];
48
+ };
49
+ return (0, prompts_1.runKeyLoop)({
50
+ render,
51
+ onKey: (key, finish, repaint) => {
52
+ if (key.name === "up" || key.name === "k") {
53
+ cursor = (cursor - 1 + total) % total;
54
+ repaint();
55
+ return;
56
+ }
57
+ if (key.name === "down" || key.name === "j") {
58
+ cursor = (cursor + 1) % total;
59
+ repaint();
60
+ return;
61
+ }
62
+ const direct = /^[1-9]$/.test(key.sequence ?? "") ? Number.parseInt(key.sequence ?? "", 10) : Number.NaN;
63
+ if (Number.isInteger(direct) && direct >= 1 && direct <= ITEMS.length) {
64
+ finish({ argv: ITEMS[direct - 1].argv });
65
+ return;
66
+ }
67
+ if (key.name === "q" || key.name === "escape") {
68
+ finish(null);
69
+ return;
70
+ }
71
+ if (key.name === "return" || key.name === "enter") {
72
+ finish(cursor === quitIndex ? null : { argv: ITEMS[cursor].argv });
73
+ }
74
+ },
75
+ });
76
+ }
@@ -74,7 +74,7 @@ async function readManagedConfig() {
74
74
  const backendUrl = normalizeAndValidateBackendUrl(obj.backendUrl);
75
75
  const rawAuthType = obj.authType;
76
76
  if (typeof rawAuthType === "string" && rawAuthType === "seat") {
77
- throw new errors_1.ManagedConfigInvalidError('legacy seat-mode config is no longer supported; run "theclawbay link --api-key <key>" to relink');
77
+ throw new errors_1.ManagedConfigInvalidError('legacy seat-mode config is no longer supported; run "theclawbay setup" to relink');
78
78
  }
79
79
  const authType = rawAuthType === "device-session" ? "device-session" : "api-key";
80
80
  const rawCredential = typeof obj.credential === "string" && obj.credential.trim()
@@ -5,7 +5,7 @@ const errors_1 = require("../errors");
5
5
  class ManagedConfigMissingError extends errors_1.ClawBayError {
6
6
  constructor(configPath) {
7
7
  super(`No managed config found at ${configPath}. ` +
8
- `Run "theclawbay link --api-key <key>" first.`);
8
+ `Run "theclawbay" and choose Set up first (or "theclawbay setup --api-key <key>").`);
9
9
  }
10
10
  }
11
11
  exports.ManagedConfigMissingError = ManagedConfigMissingError;
@@ -33,7 +33,11 @@ function powerShellProfilePaths() {
33
33
  const addDocumentsProfiles = (documentsDir) => {
34
34
  if (!documentsDir)
35
35
  return;
36
+ // Must mirror the setup-side list in env-persist.ts: setup writes managed
37
+ // blocks into profile.ps1 as well, so logout has to clean all four paths.
38
+ candidates.add(node_path_1.default.join(documentsDir, "PowerShell", "profile.ps1"));
36
39
  candidates.add(node_path_1.default.join(documentsDir, "PowerShell", "Microsoft.PowerShell_profile.ps1"));
40
+ candidates.add(node_path_1.default.join(documentsDir, "WindowsPowerShell", "profile.ps1"));
37
41
  candidates.add(node_path_1.default.join(documentsDir, "WindowsPowerShell", "Microsoft.PowerShell_profile.ps1"));
38
42
  };
39
43
  if (node_os_1.default.platform() === "win32") {
@@ -1,7 +1,19 @@
1
1
  import { SetupClient, SetupClientId } from "./client-registry";
2
2
  import { ConfiguredModelSelection, ModelResolution } from "./models";
3
+ export type SetupAuthMethod = "device-session" | "api-key";
4
+ export declare function promptAuthMethod(): Promise<SetupAuthMethod>;
5
+ export declare function promptApiKey(): Promise<string>;
3
6
  export declare function formatSetupClientLabel(client: SetupClient, hyperlinksEnabled: boolean): string;
4
7
  export declare function parseSetupClientFlags(raw: string | undefined): Set<SetupClientId> | null;
8
+ type KeypressInfo = {
9
+ name?: string;
10
+ sequence?: string;
11
+ ctrl?: boolean;
12
+ };
13
+ export declare function runKeyLoop<T>(params: {
14
+ render: () => string[];
15
+ onKey: (key: KeypressInfo, finish: (value: T) => void, repaint: () => void) => void;
16
+ }): Promise<T>;
5
17
  export declare function promptForSetupClients(clients: SetupClient[]): Promise<Set<SetupClientId>>;
6
18
  export declare function resolveSetupClientSelection(params: {
7
19
  setupClients: SetupClient[];
@@ -27,3 +39,4 @@ export declare function resolveConfiguredModelSelection(params: {
27
39
  requestedModels: string | undefined;
28
40
  skipPrompt: boolean;
29
41
  }): Promise<ConfiguredModelSelection>;
42
+ export {};
@@ -1,7 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.promptAuthMethod = promptAuthMethod;
4
+ exports.promptApiKey = promptApiKey;
3
5
  exports.formatSetupClientLabel = formatSetupClientLabel;
4
6
  exports.parseSetupClientFlags = parseSetupClientFlags;
7
+ exports.runKeyLoop = runKeyLoop;
5
8
  exports.promptForSetupClients = promptForSetupClients;
6
9
  exports.resolveSetupClientSelection = resolveSetupClientSelection;
7
10
  exports.promptForGroupedModelSelection = promptForGroupedModelSelection;
@@ -10,9 +13,81 @@ exports.resolveConfiguredModelSelection = resolveConfiguredModelSelection;
10
13
  const client_registry_1 = require("./client-registry");
11
14
  const format_1 = require("./format");
12
15
  const node_readline_1 = require("node:readline");
16
+ const promises_1 = require("node:readline/promises");
13
17
  const fsx_1 = require("./fsx");
14
18
  const models_1 = require("./models");
15
19
  const tui_1 = require("./tui");
20
+ // Ask how the user wants to authenticate this machine. Browser sign-in is the
21
+ // recommended default; an API key is offered for people who prefer to paste one.
22
+ async function promptAuthMethod() {
23
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
24
+ return "device-session";
25
+ const items = [
26
+ { value: "device-session", label: "Sign in with your browser", desc: "Recommended — opens theclawbay.com to authorize this machine" },
27
+ { value: "api-key", label: "Paste an API key", desc: "Use a key from your dashboard instead" },
28
+ ];
29
+ let cursor = 0;
30
+ const render = () => {
31
+ const rows = items.map((item, index) => {
32
+ const active = index === cursor;
33
+ const pointer = active ? tui_1.tint.accentBold(tui_1.glyph.pointer) : " ";
34
+ const number = tui_1.tint.dim(String(index + 1));
35
+ const label = active ? tui_1.tint.accentBold(item.label) : item.label;
36
+ return `${pointer} ${number} ${label} ${tui_1.tint.dim(`— ${item.desc}`)}`;
37
+ });
38
+ return [
39
+ "",
40
+ ...(0, tui_1.renderBox)({
41
+ title: "How do you want to sign in?",
42
+ lines: rows,
43
+ footer: (0, tui_1.keyHints)([
44
+ ["↑↓", "move"],
45
+ ["1-2", "jump"],
46
+ ["enter", "select"],
47
+ ]),
48
+ }),
49
+ ];
50
+ };
51
+ return runKeyLoop({
52
+ render,
53
+ onKey: (key, finish, repaint) => {
54
+ if (key.name === "up" || key.name === "k") {
55
+ cursor = (cursor - 1 + items.length) % items.length;
56
+ repaint();
57
+ return;
58
+ }
59
+ if (key.name === "down" || key.name === "j") {
60
+ cursor = (cursor + 1) % items.length;
61
+ repaint();
62
+ return;
63
+ }
64
+ if ((key.sequence ?? "") === "1") {
65
+ finish("device-session");
66
+ return;
67
+ }
68
+ if ((key.sequence ?? "") === "2") {
69
+ finish("api-key");
70
+ return;
71
+ }
72
+ if (key.name === "return" || key.name === "enter") {
73
+ finish(items[cursor].value);
74
+ }
75
+ },
76
+ });
77
+ }
78
+ async function promptApiKey() {
79
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
80
+ return "";
81
+ const rl = (0, promises_1.createInterface)({ input: process.stdin, output: process.stdout });
82
+ try {
83
+ const answer = await rl.question("Paste your The Claw Bay API key (or press Enter to sign in with your browser): ");
84
+ return answer.trim();
85
+ }
86
+ finally {
87
+ rl.close();
88
+ process.stdin.resume();
89
+ }
90
+ }
16
91
  function formatSetupClientLabel(client, hyperlinksEnabled) {
17
92
  const iconAndLabel = `${client.icon} ${client.label}`;
18
93
  if (hyperlinksEnabled) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theclawbay",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "CLI for connecting Codex, Hermes Agent, Gemini-compatible apps, Continue, Cline, GSD, OpenClaw, OpenCode, Kilo, Roo Code, Aider, experimental Trae, and experimental Zo to The Claw Bay.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -99,16 +99,6 @@
99
99
  "cachedInputPer1M": 0.05,
100
100
  "outputPer1M": 3.0
101
101
  },
102
- {
103
- "id": "gemini-3.5-flash",
104
- "label": "Gemini 3.5 Flash",
105
- "note": "Fast Gemini 3.5 lane for lower-latency multimodal and coding requests.",
106
- "tone": "ink",
107
- "provider": "google",
108
- "inputPer1M": 1.5,
109
- "cachedInputPer1M": 0.15,
110
- "outputPer1M": 9.0
111
- },
112
102
  {
113
103
  "id": "deepseek-v4-flash",
114
104
  "label": "DeepSeek V4 Flash",
@@ -1,9 +0,0 @@
1
- import { BaseCommand } from "../lib/base-command";
2
- export default class LinkCommand extends BaseCommand {
3
- static description: string;
4
- static flags: {
5
- backend: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
6
- "api-key": import("@oclif/core/lib/interfaces").OptionFlag<string, import("@oclif/core/lib/interfaces").CustomOptions>;
7
- };
8
- run(): Promise<void>;
9
- }
@@ -1,40 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const core_1 = require("@oclif/core");
4
- const base_command_1 = require("../lib/base-command");
5
- const config_1 = require("../lib/managed/config");
6
- const paths_1 = require("../lib/config/paths");
7
- const api_key_1 = require("../lib/managed/api-key");
8
- const DEFAULT_BACKEND_URL = "https://theclawbay.com";
9
- class LinkCommand extends base_command_1.BaseCommand {
10
- async run() {
11
- await this.runSafe(async () => {
12
- const { flags } = await this.parse(LinkCommand);
13
- const apiKey = flags["api-key"];
14
- const backendUrlFlag = flags.backend;
15
- const inferredBackendUrl = (0, api_key_1.tryInferBackendUrlFromApiKey)(apiKey);
16
- const backendUrl = backendUrlFlag ?? inferredBackendUrl ?? DEFAULT_BACKEND_URL;
17
- await (0, config_1.writeManagedConfig)({
18
- backendUrl,
19
- authType: "api-key",
20
- credential: apiKey,
21
- });
22
- this.log(`Linked. Managed config written to ${paths_1.managedConfigPath}`);
23
- this.log(`Backend: ${backendUrl}`);
24
- this.log('Run "theclawbay setup" to configure Codex and any detected OpenClaw, OpenCode, Kilo, experimental Trae, or experimental Zo installs on this machine.');
25
- });
26
- }
27
- }
28
- LinkCommand.description = "Link this machine to your The Claw Bay API backend using an API key";
29
- LinkCommand.flags = {
30
- backend: core_1.Flags.string({
31
- required: false,
32
- description: "Backend base URL override (default: https://theclawbay.com)",
33
- }),
34
- "api-key": core_1.Flags.string({
35
- required: true,
36
- aliases: ["apiKey"],
37
- description: "API key issued by your The Claw Bay dashboard",
38
- }),
39
- };
40
- exports.default = LinkCommand;