archbyte 0.3.4 → 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/README.md +42 -0
- package/bin/archbyte.js +32 -14
- package/dist/agents/pipeline/merger.js +16 -11
- package/dist/agents/providers/claude-sdk.d.ts +7 -0
- package/dist/agents/providers/claude-sdk.js +59 -0
- package/dist/agents/providers/router.d.ts +5 -0
- package/dist/agents/providers/router.js +23 -1
- package/dist/agents/runtime/types.d.ts +2 -2
- package/dist/agents/runtime/types.js +5 -0
- package/dist/cli/analyze.js +8 -4
- package/dist/cli/auth.d.ts +11 -2
- package/dist/cli/auth.js +312 -73
- package/dist/cli/config.d.ts +1 -0
- package/dist/cli/config.js +51 -15
- package/dist/cli/constants.js +4 -1
- package/dist/cli/export.js +64 -2
- package/dist/cli/license-gate.d.ts +1 -1
- package/dist/cli/license-gate.js +3 -2
- package/dist/cli/mcp.js +8 -12
- package/dist/cli/setup.js +166 -35
- package/dist/cli/ui.d.ts +14 -0
- package/dist/cli/ui.js +98 -14
- package/dist/cli/utils.d.ts +23 -0
- package/dist/cli/utils.js +52 -0
- package/dist/server/src/index.js +59 -5
- package/package.json +4 -1
- package/ui/dist/assets/index-DmO1qYan.js +70 -0
- package/ui/dist/index.html +1 -1
- package/ui/dist/assets/index-Bdr9FnaA.js +0 -70
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(
|
|
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
|
-
|
|
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
|
|
98
|
-
if (!
|
|
127
|
+
const store = loadStore();
|
|
128
|
+
if (!store) {
|
|
99
129
|
console.log(chalk.gray("Not logged in."));
|
|
100
130
|
return;
|
|
101
131
|
}
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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();
|
|
111
166
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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);
|
|
174
|
+
}
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
-
|
|
300
|
+
return null;
|
|
165
301
|
}
|
|
166
|
-
return null;
|
|
167
302
|
}
|
|
168
|
-
function
|
|
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(
|
|
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,8 +312,41 @@ 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
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Check if credentials are expired. Treats invalid/unparseable dates
|
|
341
|
+
* as expired (fail-closed) to prevent corrupted files from bypassing
|
|
342
|
+
* expiry checks.
|
|
343
|
+
*/
|
|
181
344
|
function isExpired(creds) {
|
|
182
|
-
|
|
345
|
+
const expiry = new Date(creds.expiresAt);
|
|
346
|
+
// Invalid Date — treat as expired (fail-closed)
|
|
347
|
+
if (isNaN(expiry.getTime()))
|
|
348
|
+
return true;
|
|
349
|
+
return expiry < new Date();
|
|
183
350
|
}
|
|
184
351
|
function parseJWTPayload(token) {
|
|
185
352
|
try {
|
|
@@ -268,6 +435,9 @@ const OFFLINE_MAX_FREE = 0; // Free users: 0 offline actions (must verify online
|
|
|
268
435
|
/**
|
|
269
436
|
* Check if an offline action is allowed. Returns true if within limits.
|
|
270
437
|
* Increments the counter when allowed.
|
|
438
|
+
*
|
|
439
|
+
* Uses atomic write-to-temp-then-rename to prevent race conditions
|
|
440
|
+
* between concurrent CLI invocations.
|
|
271
441
|
*/
|
|
272
442
|
export function checkOfflineAction() {
|
|
273
443
|
const tier = getVerifiedTier();
|
|
@@ -293,13 +463,15 @@ export function checkOfflineAction() {
|
|
|
293
463
|
reason: `Offline action limit reached (${maxActions}/${maxActions}). Reconnect to the license server to continue.`,
|
|
294
464
|
};
|
|
295
465
|
}
|
|
296
|
-
// Increment and save
|
|
466
|
+
// Increment and save atomically (write to temp file, then rename)
|
|
297
467
|
data.count++;
|
|
298
|
-
|
|
468
|
+
const tmpPath = OFFLINE_ACTIONS_PATH + `.${process.pid}.tmp`;
|
|
469
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data), "utf-8");
|
|
299
470
|
try {
|
|
300
|
-
fs.chmodSync(
|
|
471
|
+
fs.chmodSync(tmpPath, 0o600);
|
|
301
472
|
}
|
|
302
473
|
catch { /* Windows */ }
|
|
474
|
+
fs.renameSync(tmpPath, OFFLINE_ACTIONS_PATH);
|
|
303
475
|
return { allowed: true };
|
|
304
476
|
}
|
|
305
477
|
catch {
|
|
@@ -319,54 +491,121 @@ export function resetOfflineActions() {
|
|
|
319
491
|
// Non-critical
|
|
320
492
|
}
|
|
321
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
|
+
}
|
|
322
546
|
// === OAuth Flow ===
|
|
323
547
|
function startOAuthFlow(provider = "github") {
|
|
324
548
|
return new Promise((resolve, reject) => {
|
|
549
|
+
let resolved = false;
|
|
325
550
|
const timeout = setTimeout(() => {
|
|
326
551
|
server.close();
|
|
327
|
-
|
|
552
|
+
if (!resolved) {
|
|
553
|
+
resolved = true;
|
|
554
|
+
reject(new Error("Login timed out (60s). Try again or use --token."));
|
|
555
|
+
}
|
|
328
556
|
}, OAUTH_TIMEOUT_MS);
|
|
329
557
|
const server = http.createServer(async (req, res) => {
|
|
330
558
|
const url = new URL(req.url ?? "/", `http://localhost:${CLI_CALLBACK_PORT}`);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
res.end("<h1>Login failed</h1><p>No token received. Close this window and try again.</p>");
|
|
340
|
-
return;
|
|
341
|
-
}
|
|
559
|
+
// Only handle /callback — return 404 for everything else
|
|
560
|
+
if (url.pathname !== "/callback") {
|
|
561
|
+
res.writeHead(404, { "Content-Type": "text/html" });
|
|
562
|
+
res.end("<h1>Not found</h1><p>This server only handles OAuth callbacks.</p>");
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
// Prevent double-processing (e.g. browser retry, double-click)
|
|
566
|
+
if (resolved) {
|
|
342
567
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
343
|
-
res.end("<h1>
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
reject(
|
|
568
|
+
res.end("<h1>Already processed</h1><p>You can close this window.</p>");
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
// Extract token from raw query string, not URLSearchParams
|
|
572
|
+
// (URLSearchParams decodes '+' as space per x-www-form-urlencoded, corrupting JWT signatures)
|
|
573
|
+
const rawQuery = (req.url ?? "").split("?")[1] ?? "";
|
|
574
|
+
const tokenMatch = rawQuery.match(/(?:^|&)token=([^&]+)/);
|
|
575
|
+
const token = tokenMatch ? decodeURIComponent(tokenMatch[1]) : null;
|
|
576
|
+
if (!token) {
|
|
577
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
578
|
+
res.end("<h1>Login failed</h1><p>No token received. Close this window and try again.</p>");
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
582
|
+
res.end("<h1>Login successful!</h1><p>You can close this window and return to your terminal.</p>");
|
|
583
|
+
resolved = true;
|
|
584
|
+
clearTimeout(timeout);
|
|
585
|
+
server.close();
|
|
586
|
+
// Fetch user info with the token
|
|
587
|
+
try {
|
|
588
|
+
const meRes = await fetch(`${API_BASE}/api/v1/me`, {
|
|
589
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
590
|
+
});
|
|
591
|
+
if (!meRes.ok) {
|
|
592
|
+
const errBody = await meRes.text().catch(() => "");
|
|
593
|
+
reject(new Error(`Failed to fetch user info (${meRes.status}: ${errBody})`));
|
|
594
|
+
return;
|
|
369
595
|
}
|
|
596
|
+
const { user } = (await meRes.json());
|
|
597
|
+
const payload = parseJWTPayload(token);
|
|
598
|
+
resolve({
|
|
599
|
+
token,
|
|
600
|
+
email: user.email,
|
|
601
|
+
tier: user.tier,
|
|
602
|
+
expiresAt: payload?.exp
|
|
603
|
+
? new Date(payload.exp * 1000).toISOString()
|
|
604
|
+
: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
catch (err) {
|
|
608
|
+
reject(err);
|
|
370
609
|
}
|
|
371
610
|
});
|
|
372
611
|
server.listen(CLI_CALLBACK_PORT, "127.0.0.1", () => {
|
package/dist/cli/config.d.ts
CHANGED
package/dist/cli/config.js
CHANGED
|
@@ -1,7 +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";
|
|
6
|
+
const VALID_PROVIDERS = ["anthropic", "openai", "google", "claude-sdk"];
|
|
5
7
|
export async function handleConfig(options) {
|
|
6
8
|
const [action, key, value] = options.args;
|
|
7
9
|
if (!action || action === "show") {
|
|
@@ -22,7 +24,7 @@ export async function handleConfig(options) {
|
|
|
22
24
|
console.error(chalk.red("Usage: archbyte config get <key>"));
|
|
23
25
|
process.exit(1);
|
|
24
26
|
}
|
|
25
|
-
getConfig(key);
|
|
27
|
+
getConfig(key, options.raw);
|
|
26
28
|
return;
|
|
27
29
|
}
|
|
28
30
|
if (action === "path") {
|
|
@@ -52,6 +54,13 @@ function saveConfig(config) {
|
|
|
52
54
|
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
53
55
|
}
|
|
54
56
|
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
57
|
+
// Restrict permissions — config contains API keys in profiles
|
|
58
|
+
try {
|
|
59
|
+
fs.chmodSync(CONFIG_PATH, 0o600);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// Windows doesn't support chmod
|
|
63
|
+
}
|
|
55
64
|
}
|
|
56
65
|
function showConfig() {
|
|
57
66
|
const config = loadConfig();
|
|
@@ -66,7 +75,8 @@ function showConfig() {
|
|
|
66
75
|
console.log(chalk.gray(" archbyte init"));
|
|
67
76
|
return;
|
|
68
77
|
}
|
|
69
|
-
|
|
78
|
+
const providerDisplay = config.provider === "claude-sdk" ? "Claude Code (SDK)" : config.provider;
|
|
79
|
+
console.log(` ${chalk.bold("provider")}: ${providerDisplay ?? chalk.gray("not set")}`);
|
|
70
80
|
// Show profiles
|
|
71
81
|
if (configured.length > 0) {
|
|
72
82
|
console.log();
|
|
@@ -100,8 +110,10 @@ function setConfig(key, value) {
|
|
|
100
110
|
process.exit(1);
|
|
101
111
|
}
|
|
102
112
|
config.provider = value;
|
|
103
|
-
|
|
104
|
-
|
|
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) {
|
|
105
117
|
console.log(chalk.green(`Switched to ${value} (credentials on file)`));
|
|
106
118
|
}
|
|
107
119
|
else {
|
|
@@ -149,7 +161,7 @@ function setConfig(key, value) {
|
|
|
149
161
|
console.log(chalk.green(`Set ${key} = ${key.includes("key") ? maskKey(value) : value}`));
|
|
150
162
|
}
|
|
151
163
|
}
|
|
152
|
-
function getConfig(key) {
|
|
164
|
+
function getConfig(key, raw = false) {
|
|
153
165
|
const config = loadConfig();
|
|
154
166
|
const profiles = (config.profiles ?? {});
|
|
155
167
|
const active = config.provider;
|
|
@@ -160,9 +172,13 @@ function getConfig(key) {
|
|
|
160
172
|
break;
|
|
161
173
|
case "api-key":
|
|
162
174
|
case "apiKey":
|
|
163
|
-
case "key":
|
|
164
|
-
|
|
175
|
+
case "key": {
|
|
176
|
+
const apiKey = profile?.apiKey ?? config.apiKey ?? "";
|
|
177
|
+
// Mask by default to prevent accidental exposure in logs/recordings.
|
|
178
|
+
// Use `archbyte config get api-key --raw` for the unmasked value.
|
|
179
|
+
console.log(raw ? apiKey : (apiKey ? maskKey(apiKey) : ""));
|
|
165
180
|
break;
|
|
181
|
+
}
|
|
166
182
|
case "model":
|
|
167
183
|
console.log(profile?.model ?? config.model ?? "");
|
|
168
184
|
break;
|
|
@@ -171,11 +187,6 @@ function getConfig(key) {
|
|
|
171
187
|
process.exit(1);
|
|
172
188
|
}
|
|
173
189
|
}
|
|
174
|
-
function maskKey(key) {
|
|
175
|
-
if (key.length <= 8)
|
|
176
|
-
return "****";
|
|
177
|
-
return key.slice(0, 6) + "..." + key.slice(-4);
|
|
178
|
-
}
|
|
179
190
|
/**
|
|
180
191
|
* Resolve the full ArchByteConfig from config file + env vars.
|
|
181
192
|
* Supports profiles (new) and legacy flat config (backward compat).
|
|
@@ -184,8 +195,11 @@ function maskKey(key) {
|
|
|
184
195
|
export function resolveConfig() {
|
|
185
196
|
const config = loadConfig();
|
|
186
197
|
const provider = process.env.ARCHBYTE_PROVIDER ?? config.provider;
|
|
187
|
-
// Reject unknown providers
|
|
198
|
+
// Reject unknown providers with a helpful message
|
|
188
199
|
if (provider && !VALID_PROVIDERS.includes(provider)) {
|
|
200
|
+
if (process.env.ARCHBYTE_PROVIDER) {
|
|
201
|
+
console.error(chalk.red(`Invalid ARCHBYTE_PROVIDER="${provider}". Must be: ${VALID_PROVIDERS.join(", ")}`));
|
|
202
|
+
}
|
|
189
203
|
return null;
|
|
190
204
|
}
|
|
191
205
|
// Resolve API key + model from profiles first, then legacy flat keys
|
|
@@ -193,8 +207,20 @@ export function resolveConfig() {
|
|
|
193
207
|
const profile = provider ? profiles[provider] : undefined;
|
|
194
208
|
const apiKey = process.env.ARCHBYTE_API_KEY ?? profile?.apiKey ?? config.apiKey;
|
|
195
209
|
const model = process.env.ARCHBYTE_MODEL ?? profile?.model ?? config.model;
|
|
196
|
-
//
|
|
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
|
|
197
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
|
|
198
224
|
if (process.env.ANTHROPIC_API_KEY) {
|
|
199
225
|
return { provider: "anthropic", apiKey: process.env.ANTHROPIC_API_KEY };
|
|
200
226
|
}
|
|
@@ -217,3 +243,13 @@ export function resolveConfig() {
|
|
|
217
243
|
model: model,
|
|
218
244
|
};
|
|
219
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
|
+
}
|
package/dist/cli/constants.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// Shared constants for the ArchByte CLI.
|
|
2
2
|
// Single source of truth for URLs, ports, paths, and timeouts.
|
|
3
3
|
import * as path from "path";
|
|
4
|
+
import { resolveHome } from "./utils.js";
|
|
4
5
|
// ─── API ───
|
|
5
6
|
export const API_BASE = process.env.ARCHBYTE_API_URL ?? "https://api.heartbyte.io";
|
|
6
7
|
export const SITE_URL = "https://archbyte.heartbyte.io";
|
|
@@ -8,7 +9,9 @@ export const SITE_URL = "https://archbyte.heartbyte.io";
|
|
|
8
9
|
export const DEFAULT_PORT = 3847;
|
|
9
10
|
export const CLI_CALLBACK_PORT = 19274;
|
|
10
11
|
// ─── Paths ───
|
|
11
|
-
|
|
12
|
+
// resolveHome() throws if HOME/USERPROFILE is unset (e.g. in bare containers),
|
|
13
|
+
// giving a clear error instead of silently writing to "./.archbyte".
|
|
14
|
+
export const CONFIG_DIR = path.join(resolveHome(), ".archbyte");
|
|
12
15
|
export const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
13
16
|
export const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
|
|
14
17
|
/** Project-local .archbyte directory name */
|