archbyte 0.3.5 → 0.4.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/dist/cli/auth.js CHANGED
@@ -4,6 +4,7 @@ import * as http from "http";
4
4
  import { spawn } from "child_process";
5
5
  import chalk from "chalk";
6
6
  import { CONFIG_DIR, CREDENTIALS_PATH, API_BASE, CLI_CALLBACK_PORT, OAUTH_TIMEOUT_MS, } from "./constants.js";
7
+ import { confirm, select, textInput } from "./ui.js";
7
8
  export async function handleLogin(provider) {
8
9
  console.log();
9
10
  console.log(chalk.bold.cyan("ArchByte Login"));
@@ -32,10 +33,39 @@ export async function handleLogin(provider) {
32
33
  // Offline — use cached tier
33
34
  }
34
35
  console.log(chalk.green(`Already logged in as ${chalk.bold(existing.email)} (${existing.tier} tier)`));
35
- console.log(chalk.gray("Run `archbyte logout` to sign out."));
36
+ console.log();
37
+ const addAnother = await confirm("Add a different account?");
38
+ if (!addAnother) {
39
+ console.log(chalk.gray("Run `archbyte logout` to sign out."));
40
+ return;
41
+ }
42
+ console.log();
43
+ }
44
+ // Pick provider: use explicit flag, or show interactive picker
45
+ let selectedProvider = provider ?? "github";
46
+ if (!provider) {
47
+ const providers = ["GitHub", "Google", "Email & Password"];
48
+ const idx = await select("Sign in with:", providers);
49
+ selectedProvider = ["github", "google", "email"][idx];
50
+ console.log();
51
+ }
52
+ // Email/password flow — direct API call, no browser
53
+ if (selectedProvider === "email") {
54
+ try {
55
+ const credentials = await emailPasswordFlow();
56
+ saveCredentials(credentials);
57
+ cacheVerifiedTier(credentials.tier === "premium" ? "premium" : "free", credentials.email);
58
+ console.log();
59
+ console.log(chalk.green(`Logged in as ${chalk.bold(credentials.email)} (${credentials.tier} tier)`));
60
+ console.log(chalk.gray(`Credentials saved to ${CREDENTIALS_PATH}`));
61
+ }
62
+ catch (err) {
63
+ console.error(chalk.red(`Login failed: ${err instanceof Error ? err.message : "Unknown error"}`));
64
+ process.exit(1);
65
+ }
36
66
  return;
37
67
  }
38
- const selectedProvider = provider ?? "github";
68
+ // OAuth flow (GitHub / Google)
39
69
  console.log(chalk.gray(`Opening browser for ${selectedProvider} sign-in...`));
40
70
  console.log();
41
71
  try {
@@ -92,26 +122,57 @@ export async function handleLoginWithToken(token) {
92
122
  }
93
123
  }
94
124
  // === Logout ===
95
- export async function handleLogout() {
125
+ export async function handleLogout(options) {
96
126
  console.log();
97
- const creds = loadCredentials();
98
- if (!creds) {
127
+ const store = loadStore();
128
+ if (!store) {
99
129
  console.log(chalk.gray("Not logged in."));
100
130
  return;
101
131
  }
102
- try {
103
- fs.unlinkSync(CREDENTIALS_PATH);
132
+ const emails = Object.keys(store.accounts);
133
+ // Logout all accounts
134
+ if (options?.all) {
135
+ try {
136
+ fs.unlinkSync(CREDENTIALS_PATH);
137
+ }
138
+ catch { /* Ignore */ }
139
+ try {
140
+ fs.unlinkSync(TIER_CACHE_PATH);
141
+ }
142
+ catch { /* Ignore */ }
143
+ resetOfflineActions();
144
+ console.log(chalk.green(`Logged out of ${emails.length} account${emails.length === 1 ? "" : "s"}.`));
145
+ return;
104
146
  }
105
- catch {
106
- // Ignore
147
+ // Determine which account to remove
148
+ const targetEmail = options?.email ?? store.active;
149
+ if (!store.accounts[targetEmail]) {
150
+ console.log(chalk.yellow(`No account found for ${targetEmail}.`));
151
+ return;
107
152
  }
108
- // Clean up tier cache and offline action tracker
109
- try {
110
- fs.unlinkSync(TIER_CACHE_PATH);
153
+ delete store.accounts[targetEmail];
154
+ const remaining = Object.keys(store.accounts);
155
+ if (remaining.length === 0) {
156
+ // Last account — delete the file entirely
157
+ try {
158
+ fs.unlinkSync(CREDENTIALS_PATH);
159
+ }
160
+ catch { /* Ignore */ }
161
+ try {
162
+ fs.unlinkSync(TIER_CACHE_PATH);
163
+ }
164
+ catch { /* Ignore */ }
165
+ resetOfflineActions();
166
+ }
167
+ else {
168
+ // Auto-switch if we removed the active account
169
+ if (store.active === targetEmail) {
170
+ store.active = remaining[0];
171
+ console.log(chalk.gray(`Switched active account to ${store.active}`));
172
+ }
173
+ saveStore(store);
111
174
  }
112
- catch { /* Ignore */ }
113
- resetOfflineActions();
114
- console.log(chalk.green(`Logged out (was ${creds.email}).`));
175
+ console.log(chalk.green(`Logged out (was ${targetEmail}).`));
115
176
  }
116
177
  // === Status ===
117
178
  export async function handleStatus() {
@@ -145,32 +206,105 @@ export async function handleStatus() {
145
206
  }
146
207
  console.log();
147
208
  }
209
+ // === Accounts ===
210
+ export async function handleAccounts() {
211
+ console.log();
212
+ console.log(chalk.bold.cyan("ArchByte Accounts"));
213
+ console.log();
214
+ const store = loadStore();
215
+ if (!store || Object.keys(store.accounts).length === 0) {
216
+ console.log(chalk.yellow("No accounts. Run `archbyte login` to sign in."));
217
+ return;
218
+ }
219
+ for (const [email, creds] of Object.entries(store.accounts)) {
220
+ const isActive = email === store.active;
221
+ const marker = isActive ? chalk.green("*") : " ";
222
+ const tier = creds.tier === "premium" ? chalk.green("Pro") : "Basic";
223
+ const expired = isExpired(creds) ? chalk.red(" (expired)") : "";
224
+ console.log(` ${marker} ${chalk.bold(email)} ${tier}${expired}`);
225
+ }
226
+ console.log();
227
+ console.log(chalk.gray("* = active account"));
228
+ console.log(chalk.gray("Switch: archbyte accounts switch [email]"));
229
+ console.log();
230
+ }
231
+ export async function handleAccountSwitch(email) {
232
+ console.log();
233
+ const store = loadStore();
234
+ if (!store || Object.keys(store.accounts).length === 0) {
235
+ console.log(chalk.yellow("No accounts. Run `archbyte login` to sign in."));
236
+ return;
237
+ }
238
+ const emails = Object.keys(store.accounts);
239
+ if (emails.length === 1) {
240
+ console.log(chalk.gray(`Only one account: ${emails[0]}`));
241
+ return;
242
+ }
243
+ let target = email;
244
+ if (!target) {
245
+ // Interactive selection
246
+ const idx = await select("Switch to account:", emails.map((e) => {
247
+ const active = e === store.active ? chalk.green(" (active)") : "";
248
+ const tier = store.accounts[e].tier === "premium" ? " Pro" : "";
249
+ return `${e}${tier}${active}`;
250
+ }));
251
+ target = emails[idx];
252
+ }
253
+ if (!store.accounts[target]) {
254
+ console.log(chalk.red(`Account not found: ${target}`));
255
+ console.log(chalk.gray(`Available: ${emails.join(", ")}`));
256
+ return;
257
+ }
258
+ if (store.active === target) {
259
+ console.log(chalk.gray(`Already active: ${target}`));
260
+ return;
261
+ }
262
+ store.active = target;
263
+ saveStore(store);
264
+ console.log(chalk.green(`Switched to ${chalk.bold(target)}`));
265
+ }
148
266
  // === Credential Helpers (exported for license gate) ===
149
- export function loadCredentials() {
267
+ function isMultiAccountStore(data) {
268
+ return (typeof data === "object" &&
269
+ data !== null &&
270
+ data.version === 2 &&
271
+ typeof data.active === "string" &&
272
+ typeof data.accounts === "object" &&
273
+ data.accounts !== null);
274
+ }
275
+ function loadStore() {
150
276
  try {
151
- if (fs.existsSync(CREDENTIALS_PATH)) {
152
- const data = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
153
- // Validate structure reject malformed credentials
154
- if (typeof data?.token !== "string" ||
155
- typeof data?.email !== "string" ||
156
- typeof data?.tier !== "string" ||
157
- typeof data?.expiresAt !== "string") {
158
- return null;
159
- }
160
- return data;
277
+ if (!fs.existsSync(CREDENTIALS_PATH))
278
+ return null;
279
+ const raw = JSON.parse(fs.readFileSync(CREDENTIALS_PATH, "utf-8"));
280
+ // Already multi-account format
281
+ if (isMultiAccountStore(raw))
282
+ return raw;
283
+ // Auto-migrate old single-account format
284
+ if (typeof raw?.token === "string" &&
285
+ typeof raw?.email === "string" &&
286
+ typeof raw?.tier === "string" &&
287
+ typeof raw?.expiresAt === "string") {
288
+ const creds = raw;
289
+ const store = {
290
+ version: 2,
291
+ active: creds.email,
292
+ accounts: { [creds.email]: creds },
293
+ };
294
+ saveStore(store);
295
+ return store;
161
296
  }
297
+ return null;
162
298
  }
163
299
  catch {
164
- // Ignore — corrupt file treated as no credentials
300
+ return null;
165
301
  }
166
- return null;
167
302
  }
168
- function saveCredentials(creds) {
303
+ function saveStore(store) {
169
304
  if (!fs.existsSync(CONFIG_DIR)) {
170
305
  fs.mkdirSync(CONFIG_DIR, { recursive: true });
171
306
  }
172
- fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(creds, null, 2), "utf-8");
173
- // Restrict permissions (owner-only read/write)
307
+ fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(store, null, 2), "utf-8");
174
308
  try {
175
309
  fs.chmodSync(CREDENTIALS_PATH, 0o600);
176
310
  }
@@ -178,6 +312,30 @@ function saveCredentials(creds) {
178
312
  // Windows doesn't support chmod
179
313
  }
180
314
  }
315
+ export function loadCredentials() {
316
+ const store = loadStore();
317
+ if (!store)
318
+ return null;
319
+ const creds = store.accounts[store.active];
320
+ if (!creds ||
321
+ typeof creds.token !== "string" ||
322
+ typeof creds.email !== "string" ||
323
+ typeof creds.tier !== "string" ||
324
+ typeof creds.expiresAt !== "string") {
325
+ return null;
326
+ }
327
+ return creds;
328
+ }
329
+ function saveCredentials(creds) {
330
+ const store = loadStore() ?? {
331
+ version: 2,
332
+ active: creds.email,
333
+ accounts: {},
334
+ };
335
+ store.accounts[creds.email] = creds;
336
+ store.active = creds.email;
337
+ saveStore(store);
338
+ }
181
339
  /**
182
340
  * Check if credentials are expired. Treats invalid/unparseable dates
183
341
  * as expired (fail-closed) to prevent corrupted files from bypassing
@@ -333,6 +491,58 @@ export function resetOfflineActions() {
333
491
  // Non-critical
334
492
  }
335
493
  }
494
+ // === Email/Password Flow ===
495
+ async function emailPasswordFlow() {
496
+ const email = await textInput("Email");
497
+ if (!email)
498
+ throw new Error("Email is required.");
499
+ const password = await textInput("Password", { mask: true });
500
+ if (!password)
501
+ throw new Error("Password is required.");
502
+ const res = await fetch(`${API_BASE}/api/v1/auth/login`, {
503
+ method: "POST",
504
+ headers: { "Content-Type": "application/json" },
505
+ body: JSON.stringify({ email, password }),
506
+ });
507
+ if (res.status === 401) {
508
+ console.log();
509
+ console.log(chalk.yellow("No account found with those credentials."));
510
+ const shouldSignUp = await confirm("Create a new account?");
511
+ if (!shouldSignUp)
512
+ throw new Error("Login cancelled.");
513
+ return emailSignupFlow(email, password);
514
+ }
515
+ if (!res.ok) {
516
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
517
+ throw new Error(body.error ?? `Login failed (${res.status})`);
518
+ }
519
+ return parseTokenResponse(await res.json());
520
+ }
521
+ async function emailSignupFlow(email, password) {
522
+ const name = await textInput("Name (optional)");
523
+ const res = await fetch(`${API_BASE}/api/v1/auth/signup`, {
524
+ method: "POST",
525
+ headers: { "Content-Type": "application/json" },
526
+ body: JSON.stringify({ email, password, name: name || undefined }),
527
+ });
528
+ if (!res.ok) {
529
+ const body = await res.json().catch(() => ({ error: "Unknown error" }));
530
+ throw new Error(body.error ?? `Signup failed (${res.status})`);
531
+ }
532
+ return parseTokenResponse(await res.json());
533
+ }
534
+ function parseTokenResponse(data) {
535
+ const { token, user } = data;
536
+ const payload = parseJWTPayload(token);
537
+ return {
538
+ token,
539
+ email: user.email,
540
+ tier: user.tier,
541
+ expiresAt: payload?.exp
542
+ ? new Date(payload.exp * 1000).toISOString()
543
+ : new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
544
+ };
545
+ }
336
546
  // === OAuth Flow ===
337
547
  function startOAuthFlow(provider = "github") {
338
548
  return new Promise((resolve, reject) => {
@@ -1,8 +1,9 @@
1
1
  import * as fs from "fs";
2
+ import { execSync } from "child_process";
2
3
  import chalk from "chalk";
3
4
  import { CONFIG_DIR, CONFIG_PATH } from "./constants.js";
4
5
  import { maskKey } from "./utils.js";
5
- const VALID_PROVIDERS = ["anthropic", "openai", "google"];
6
+ const VALID_PROVIDERS = ["anthropic", "openai", "google", "claude-sdk"];
6
7
  export async function handleConfig(options) {
7
8
  const [action, key, value] = options.args;
8
9
  if (!action || action === "show") {
@@ -74,7 +75,8 @@ function showConfig() {
74
75
  console.log(chalk.gray(" archbyte init"));
75
76
  return;
76
77
  }
77
- console.log(` ${chalk.bold("provider")}: ${config.provider ?? chalk.gray("not set")}`);
78
+ const providerDisplay = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
79
+ console.log(` ${chalk.bold("provider")}: ${providerDisplay ?? chalk.gray("not set")}`);
78
80
  // Show profiles
79
81
  if (configured.length > 0) {
80
82
  console.log();
@@ -108,8 +110,10 @@ function setConfig(key, value) {
108
110
  process.exit(1);
109
111
  }
110
112
  config.provider = value;
111
- // Check if this provider has a stored profile
112
- if (profiles[value]?.apiKey) {
113
+ if (value === "claude-sdk") {
114
+ console.log(chalk.green("Switched to Claude Code (SDK) — no API key needed."));
115
+ }
116
+ else if (profiles[value]?.apiKey) {
113
117
  console.log(chalk.green(`Switched to ${value} (credentials on file)`));
114
118
  }
115
119
  else {
@@ -203,8 +207,20 @@ export function resolveConfig() {
203
207
  const profile = provider ? profiles[provider] : undefined;
204
208
  const apiKey = process.env.ARCHBYTE_API_KEY ?? profile?.apiKey ?? config.apiKey;
205
209
  const model = process.env.ARCHBYTE_MODEL ?? profile?.model ?? config.model;
206
- // Auto-detect from known env vars if nothing explicit
210
+ // claude-sdk provider doesn't need an API key
211
+ if (provider === "claude-sdk") {
212
+ return {
213
+ provider,
214
+ model: model,
215
+ };
216
+ }
217
+ // Auto-detect if nothing explicitly configured
207
218
  if (!provider && !apiKey) {
219
+ // Claude Code on PATH → zero-config, preferred
220
+ if (isClaudeCodeOnPath()) {
221
+ return { provider: "claude-sdk" };
222
+ }
223
+ // Fall back to API key env vars
208
224
  if (process.env.ANTHROPIC_API_KEY) {
209
225
  return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
210
226
  }
@@ -227,3 +243,13 @@ export function resolveConfig() {
227
243
  model: model,
228
244
  };
229
245
  }
246
+ function isClaudeCodeOnPath() {
247
+ try {
248
+ const cmd = process.platform === "win32" ? "where claude" : "which claude";
249
+ execSync(cmd, { stdio: "pipe" });
250
+ return true;
251
+ }
252
+ catch {
253
+ return false;
254
+ }
255
+ }
@@ -1,4 +1,5 @@
1
1
  import chalk from "chalk";
2
+ import { fileURLToPath } from "url";
2
3
  import { resolveArchitecturePath, loadArchitectureFile } from "./shared.js";
3
4
  /**
4
5
  * Export architecture diagram to Mermaid, Markdown, or JSON format.
@@ -7,11 +8,24 @@ export async function handleExport(options) {
7
8
  const diagramPath = resolveArchitecturePath(options);
8
9
  const arch = loadArchitectureFile(diagramPath);
9
10
  const format = options.format || "mermaid";
10
- const SUPPORTED_FORMATS = ["mermaid", "markdown", "json", "plantuml", "dot"];
11
+ const SUPPORTED_FORMATS = ["mermaid", "markdown", "json", "plantuml", "dot", "html"];
11
12
  if (!SUPPORTED_FORMATS.includes(format)) {
12
- console.error(chalk.red(`Unknown format: "${format}". Supported: ${SUPPORTED_FORMATS.join(", ")}`));
13
+ const formatList = SUPPORTED_FORMATS.map(f => f === "html" ? `html ${chalk.yellow("[Pro]")}` : f).join(", ");
14
+ console.error(chalk.red(`Unknown format: "${format}". Supported: ${formatList}`));
13
15
  process.exit(1);
14
16
  }
17
+ // HTML export requires Pro tier (skip in dev/local builds)
18
+ if (format === "html" && !process.env.ARCHBYTE_DEV) {
19
+ const { loadCredentials } = await import("./auth.js");
20
+ const creds = loadCredentials();
21
+ if (!creds || creds.tier !== "premium") {
22
+ console.error();
23
+ console.error(chalk.red("HTML export requires a Pro subscription."));
24
+ console.error(chalk.gray("Upgrade at https://heartbyte.io/archbyte"));
25
+ console.error();
26
+ process.exit(1);
27
+ }
28
+ }
15
29
  let output;
16
30
  switch (format) {
17
31
  case "mermaid":
@@ -26,6 +40,9 @@ export async function handleExport(options) {
26
40
  case "dot":
27
41
  output = exportDot(arch);
28
42
  break;
43
+ case "html":
44
+ output = await exportHtml(arch);
45
+ break;
29
46
  default:
30
47
  output = exportMarkdown(arch);
31
48
  break;
@@ -306,6 +323,51 @@ function exportDot(arch) {
306
323
  lines.push("}");
307
324
  return lines.join("\n");
308
325
  }
326
+ /**
327
+ * Export architecture as a self-contained interactive HTML file.
328
+ * Reads the pre-built UI assets from ui/dist/ and inlines them
329
+ * with the architecture data injected as window.__ARCHBYTE_DATA__.
330
+ */
331
+ async function exportHtml(arch) {
332
+ const fs = await import("fs");
333
+ const pathMod = await import("path");
334
+ const __filename = fileURLToPath(import.meta.url);
335
+ const __dirname = pathMod.dirname(__filename);
336
+ const uiDist = pathMod.resolve(__dirname, "../../ui/dist");
337
+ if (!fs.existsSync(uiDist)) {
338
+ console.error(chalk.red("UI build not found at " + uiDist));
339
+ console.error(chalk.gray("Run: npm run build:ui"));
340
+ process.exit(1);
341
+ }
342
+ // Read CSS and JS assets
343
+ const assetsDir = pathMod.join(uiDist, "assets");
344
+ const assetFiles = fs.readdirSync(assetsDir);
345
+ let cssContent = "";
346
+ for (const f of assetFiles.filter((f) => f.endsWith(".css"))) {
347
+ cssContent += fs.readFileSync(pathMod.join(assetsDir, f), "utf-8") + "\n";
348
+ }
349
+ let jsContent = "";
350
+ for (const f of assetFiles.filter((f) => f.endsWith(".js"))) {
351
+ jsContent += fs.readFileSync(pathMod.join(assetsDir, f), "utf-8") + "\n";
352
+ }
353
+ const projectName = process.cwd().split("/").pop() || "Architecture";
354
+ // Build HTML from scratch (avoids fragile regex on Vite template)
355
+ return `<!DOCTYPE html>
356
+ <html lang="en" data-theme="dark">
357
+ <head>
358
+ <meta charset="UTF-8">
359
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
360
+ <title>${projectName} - ArchByte Architecture</title>
361
+ <style>${cssContent}</style>
362
+ </head>
363
+ <body>
364
+ <script>try{if(localStorage.getItem('archbyte-theme')==='light')document.documentElement.setAttribute('data-theme','light')}catch(e){}</script>
365
+ <div id="root"></div>
366
+ <script>window.__ARCHBYTE_DATA__ = ${JSON.stringify(arch)};</script>
367
+ <script type="module">${jsContent}</script>
368
+ </body>
369
+ </html>`;
370
+ }
309
371
  /**
310
372
  * Sanitize an ID for use in PlantUML diagrams.
311
373
  */
package/dist/cli/setup.js CHANGED
@@ -15,6 +15,12 @@ const PROVIDERS = [
15
15
  { name: "google", label: "Google", hint: "Gemini 2.5 Pro / Flash" },
16
16
  ];
17
17
  const PROVIDER_MODELS = {
18
+ "claude-sdk": [
19
+ { id: "", label: "Default (recommended)", hint: "Sonnet for all agents" },
20
+ { id: "opus", label: "Claude Opus 4.6", hint: "Most capable" },
21
+ { id: "sonnet", label: "Claude Sonnet 4.5", hint: "Fast, great quality" },
22
+ { id: "haiku", label: "Claude Haiku 4.5", hint: "Fastest, cheapest" },
23
+ ],
18
24
  anthropic: [
19
25
  { id: "", label: "Default (recommended)", hint: "Opus for all agents" },
20
26
  { id: "claude-opus-4-6", label: "Claude Opus 4.6", hint: "Most capable" },
@@ -209,12 +215,6 @@ export async function handleSetup() {
209
215
  const hasClaude = isInPath("claude");
210
216
  const codexDir = path.join(CONFIG_DIR, "../.codex");
211
217
  const hasCodex = fs.existsSync(codexDir);
212
- if (hasClaude || hasCodex) {
213
- const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
214
- console.log(chalk.cyan(` Detected ${tools} on this machine.`));
215
- console.log(chalk.white(` After setup, run `) + chalk.bold.cyan(`archbyte mcp install`) + chalk.white(` to use ArchByte from your AI tool.`));
216
- console.log();
217
- }
218
218
  const config = loadConfig();
219
219
  const profiles = getProfiles(config);
220
220
  // Migrate legacy flat config → profiles
@@ -247,6 +247,79 @@ export async function handleSetup() {
247
247
  }
248
248
  console.log();
249
249
  }
250
+ // Detect AI coding tools and offer zero-config options
251
+ if (hasClaude || hasCodex) {
252
+ const tools = [hasClaude && "Claude Code", hasCodex && "Codex CLI"].filter(Boolean).join(" and ");
253
+ console.log(chalk.cyan(` Detected ${tools} on this machine.\n`));
254
+ // Build options based on what's detected
255
+ const toolOptions = [];
256
+ if (hasClaude) {
257
+ toolOptions.push({
258
+ label: `Claude Code (SDK) ${chalk.gray("zero config — uses your Claude Code subscription")}`,
259
+ value: "claude-sdk",
260
+ });
261
+ }
262
+ if (hasCodex) {
263
+ toolOptions.push({
264
+ label: `Codex CLI ${chalk.gray("zero config — uses your Codex subscription")}`,
265
+ value: "codex",
266
+ });
267
+ }
268
+ toolOptions.push({
269
+ label: `Bring your own API key ${chalk.gray("Anthropic, OpenAI, or Google")}`,
270
+ value: "byok",
271
+ });
272
+ const toolIdx = await select("How do you want to run ArchByte?", toolOptions.map((o) => o.label));
273
+ const choice = toolOptions[toolIdx].value;
274
+ if (choice === "claude-sdk") {
275
+ config.provider = "claude-sdk";
276
+ // Model selection
277
+ const models = PROVIDER_MODELS["claude-sdk"];
278
+ const modelIdx = await select("\n Choose a model:", models.map((m) => `${m.label} ${chalk.gray(m.hint)}`));
279
+ const chosenModel = models[modelIdx];
280
+ if (chosenModel.id) {
281
+ config.model = chosenModel.id;
282
+ console.log(chalk.green(` ✓ Model: ${chosenModel.label}`));
283
+ }
284
+ else {
285
+ delete config.model;
286
+ console.log(chalk.green(` ✓ Model: Sonnet (default)`));
287
+ }
288
+ config.profiles = profiles;
289
+ delete config.apiKey;
290
+ saveConfig(config);
291
+ const dim = chalk.gray;
292
+ const sep = dim(" ───");
293
+ console.log();
294
+ console.log(chalk.bold.green(" ✓ Setup complete — using Claude Code (SDK)"));
295
+ console.log();
296
+ console.log(sep);
297
+ console.log();
298
+ console.log(dim(" No API key needed. ArchByte uses your Claude Code subscription."));
299
+ console.log(dim(" All model calls go through Claude Code on this machine."));
300
+ console.log();
301
+ console.log(sep);
302
+ console.log();
303
+ console.log(" " + chalk.bold("Next steps"));
304
+ console.log();
305
+ console.log(" " + chalk.cyan("archbyte run") + " Analyze your codebase");
306
+ if (hasCodex) {
307
+ console.log(" " + chalk.cyan("archbyte mcp install") + " Use from Codex CLI");
308
+ }
309
+ console.log(" " + chalk.cyan("archbyte status") + " Check account and usage");
310
+ console.log(" " + chalk.cyan("archbyte --help") + " See all commands");
311
+ console.log();
312
+ return;
313
+ }
314
+ if (choice === "codex") {
315
+ // TODO: Add Codex SDK provider when available
316
+ console.log(chalk.yellow("\n Codex SDK provider coming soon. Setting up with API key for now."));
317
+ console.log(chalk.gray(" In the meantime, use archbyte mcp install to run ArchByte from Codex.\n"));
318
+ }
319
+ // User chose BYOK — continue to normal provider selection below
320
+ if (choice === "byok")
321
+ console.log();
322
+ }
250
323
  // Step 1: Choose provider
251
324
  const idx = await select("Choose your model provider:", PROVIDERS.map((p) => {
252
325
  const active = config.provider === p.name ? chalk.green(" (active)") : "";
package/dist/cli/ui.d.ts CHANGED
@@ -30,4 +30,13 @@ export declare function progressBar(totalSteps: number): ProgressBar;
30
30
  * (arrow keys, etc.) to prevent accidental confirmation.
31
31
  */
32
32
  export declare function confirm(prompt: string): Promise<boolean>;
33
+ /**
34
+ * Text input prompt. Returns the entered string.
35
+ * Non-TTY fallback: returns empty string.
36
+ *
37
+ * @param mask - If true, replaces each character with * (for passwords).
38
+ */
39
+ export declare function textInput(prompt: string, opts?: {
40
+ mask?: boolean;
41
+ }): Promise<string>;
33
42
  export {};