@brainbase-labs/cli 0.18.0 → 0.19.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.
Files changed (2) hide show
  1. package/dist/index.js +290 -133
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -32012,7 +32012,7 @@ var require_schemes = __commonJS((exports, module) => {
32012
32012
  urnComponent.nss = (uuidComponent.uuid || "").toLowerCase();
32013
32013
  return urnComponent;
32014
32014
  }
32015
- var http2 = {
32015
+ var http = {
32016
32016
  scheme: "http",
32017
32017
  domainHost: true,
32018
32018
  parse: httpParse,
@@ -32020,7 +32020,7 @@ var require_schemes = __commonJS((exports, module) => {
32020
32020
  };
32021
32021
  var https = {
32022
32022
  scheme: "https",
32023
- domainHost: http2.domainHost,
32023
+ domainHost: http.domainHost,
32024
32024
  parse: httpParse,
32025
32025
  serialize: httpSerialize
32026
32026
  };
@@ -32049,7 +32049,7 @@ var require_schemes = __commonJS((exports, module) => {
32049
32049
  skipNormalize: true
32050
32050
  };
32051
32051
  var SCHEMES = {
32052
- http: http2,
32052
+ http,
32053
32053
  https,
32054
32054
  ws: ws2,
32055
32055
  wss,
@@ -36008,7 +36008,7 @@ function padStart(s, n) {
36008
36008
  // package.json
36009
36009
  var package_default = {
36010
36010
  name: "@brainbase-labs/cli",
36011
- version: "0.18.0",
36011
+ version: "0.19.0",
36012
36012
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36013
36013
  type: "module",
36014
36014
  bin: {
@@ -40969,9 +40969,6 @@ function writeAuthUnlocked(s) {
40969
40969
  fs6.chmodSync(AUTH_FILE, 384);
40970
40970
  } catch {}
40971
40971
  }
40972
- function writeAuth(s) {
40973
- withAuthLock(() => writeAuthUnlocked(s));
40974
- }
40975
40972
  function clearAuth() {
40976
40973
  withAuthLock(() => {
40977
40974
  if (exists(AUTH_FILE))
@@ -61156,12 +61153,183 @@ function printSkillHelp() {
61156
61153
  }
61157
61154
 
61158
61155
  // src/cli/login.ts
61159
- import http from "node:http";
61160
- import net from "node:net";
61161
- import { randomBytes } from "node:crypto";
61162
61156
  import { spawn } from "node:child_process";
61163
61157
  var import_picocolors21 = __toESM(require_picocolors(), 1);
61164
61158
 
61159
+ // src/core/login-poll.ts
61160
+ var DEFAULT_LOGIN_TIMEOUT_MS = 10 * 60 * 1000;
61161
+ var MIN_LOGIN_TIMEOUT_MS = 30000;
61162
+ var DEFAULT_POLL_INTERVAL_MS = 2000;
61163
+ var MIN_POLL_INTERVAL_MS = 500;
61164
+ var REQUEST_TIMEOUT_MS = 1e4;
61165
+ var MAX_CONSECUTIVE_FAILURES = 20;
61166
+
61167
+ class LoginTimeoutError extends Error {
61168
+ constructor(timeoutMs, lastError) {
61169
+ super(`Login timed out after ${Math.round(timeoutMs / 1000)}s` + (lastError ? ` (last error: ${lastError})` : ""));
61170
+ this.name = "LoginTimeoutError";
61171
+ }
61172
+ }
61173
+ function sleep(ms2) {
61174
+ return new Promise((resolve) => setTimeout(resolve, ms2));
61175
+ }
61176
+ var MAX_POLL_INTERVAL_MS = 60000;
61177
+ function backoffInterval(res, current) {
61178
+ const header = Number(res.headers.get("Retry-After"));
61179
+ const suggested = Number.isFinite(header) && header > 0 ? header * 1000 : current * 2;
61180
+ return Math.min(Math.max(suggested, current), MAX_POLL_INTERVAL_MS);
61181
+ }
61182
+ function clamp2(value, min2, max2) {
61183
+ if (!Number.isFinite(value))
61184
+ return min2;
61185
+ return Math.min(Math.max(value, min2), max2);
61186
+ }
61187
+ function unsupportedDeployment(webBase) {
61188
+ return new Error(`${webBase} does not support device login. Update the Brainbase console, or point at another deployment with \`brainbase login --web <url>\`.`);
61189
+ }
61190
+ var UNSAFE_URI_CHARS = /[\s"'`$&|;<>^(){}\\!*]/;
61191
+ function resolveVerificationUri(webBase, uri) {
61192
+ const fallback = `${webBase}/cli/auth`;
61193
+ let base2;
61194
+ try {
61195
+ base2 = new URL(`${webBase}/`);
61196
+ } catch {
61197
+ return fallback;
61198
+ }
61199
+ let resolved;
61200
+ try {
61201
+ resolved = new URL(uri, base2);
61202
+ } catch {
61203
+ return fallback;
61204
+ }
61205
+ if (resolved.origin !== base2.origin)
61206
+ return fallback;
61207
+ if (resolved.protocol !== "http:" && resolved.protocol !== "https:") {
61208
+ return fallback;
61209
+ }
61210
+ const path73 = `${resolved.pathname}${resolved.search}${resolved.hash}`;
61211
+ if (UNSAFE_URI_CHARS.test(path73))
61212
+ return fallback;
61213
+ return resolved.toString();
61214
+ }
61215
+ async function readJson2(res) {
61216
+ try {
61217
+ const body = await res.json();
61218
+ return body && typeof body === "object" ? body : null;
61219
+ } catch {
61220
+ return null;
61221
+ }
61222
+ }
61223
+ var JSON_HEADERS = {
61224
+ "Content-Type": "application/json",
61225
+ Accept: "application/json"
61226
+ };
61227
+ async function startDeviceAuthorization(options) {
61228
+ const { webBase, fetchImpl = fetch } = options;
61229
+ const url = `${webBase}/api/cli/auth/device`;
61230
+ let res;
61231
+ try {
61232
+ res = await fetchImpl(url, {
61233
+ method: "POST",
61234
+ headers: JSON_HEADERS,
61235
+ body: "{}",
61236
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
61237
+ });
61238
+ } catch (err) {
61239
+ throw new Error(`Could not reach ${url}: ${err.message}`);
61240
+ }
61241
+ if (res.status === 404)
61242
+ throw unsupportedDeployment(webBase);
61243
+ if (!res.ok) {
61244
+ throw new Error(`Could not start login (${webBase} returned ${res.status}).`);
61245
+ }
61246
+ const body = await readJson2(res);
61247
+ if (!body?.device_code || !body.user_code) {
61248
+ throw unsupportedDeployment(webBase);
61249
+ }
61250
+ return {
61251
+ deviceCode: body.device_code,
61252
+ userCode: body.user_code,
61253
+ verificationUri: resolveVerificationUri(webBase, body.verification_uri ?? "/cli/auth"),
61254
+ intervalMs: clamp2(body.interval ? body.interval * 1000 : DEFAULT_POLL_INTERVAL_MS, MIN_POLL_INTERVAL_MS, MAX_POLL_INTERVAL_MS),
61255
+ expiresInMs: clamp2(body.expires_in ? body.expires_in * 1000 : DEFAULT_LOGIN_TIMEOUT_MS, MIN_LOGIN_TIMEOUT_MS, DEFAULT_LOGIN_TIMEOUT_MS)
61256
+ };
61257
+ }
61258
+ async function awaitDeviceSession(options) {
61259
+ const {
61260
+ webBase,
61261
+ authorization,
61262
+ timeoutMs = Math.min(authorization.expiresInMs, DEFAULT_LOGIN_TIMEOUT_MS),
61263
+ now: now2 = Date.now,
61264
+ wait = sleep,
61265
+ fetchImpl = fetch
61266
+ } = options;
61267
+ const url = `${webBase}/api/cli/auth/device/token`;
61268
+ const deadline = now2() + timeoutMs;
61269
+ let consecutiveFailures = 0;
61270
+ let intervalMs = authorization.intervalMs;
61271
+ let lastError;
61272
+ while (now2() < deadline) {
61273
+ let res;
61274
+ try {
61275
+ res = await fetchImpl(url, {
61276
+ method: "POST",
61277
+ headers: JSON_HEADERS,
61278
+ body: JSON.stringify({ device_code: authorization.deviceCode }),
61279
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
61280
+ });
61281
+ } catch (err) {
61282
+ lastError = err.message;
61283
+ if (++consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
61284
+ throw new Error(`Could not reach ${url}: ${lastError}`);
61285
+ }
61286
+ await wait(intervalMs);
61287
+ continue;
61288
+ }
61289
+ if (res.status === 410) {
61290
+ throw new Error("This login expired. Run `brainbase login` again.");
61291
+ }
61292
+ if (res.status === 409) {
61293
+ throw new Error("This login was already completed on another device. Run `brainbase login` again.");
61294
+ }
61295
+ if (res.status === 404)
61296
+ throw unsupportedDeployment(webBase);
61297
+ if (res.status === 429) {
61298
+ consecutiveFailures = 0;
61299
+ intervalMs = backoffInterval(res, intervalMs);
61300
+ lastError = "the server asked us to slow down";
61301
+ await wait(intervalMs);
61302
+ continue;
61303
+ }
61304
+ if (!res.ok) {
61305
+ lastError = `poll returned ${res.status}`;
61306
+ if (res.status >= 400 && res.status < 500) {
61307
+ const detail = (await readJson2(res))?.error;
61308
+ throw new Error(typeof detail === "string" ? detail : `The server rejected this login (${res.status}). Run \`brainbase login\` again.`);
61309
+ }
61310
+ if (++consecutiveFailures >= MAX_CONSECUTIVE_FAILURES) {
61311
+ throw new Error(`Could not reach ${url}: ${lastError}`);
61312
+ }
61313
+ await wait(intervalMs);
61314
+ continue;
61315
+ }
61316
+ consecutiveFailures = 0;
61317
+ const body = await readJson2(res);
61318
+ if (!body) {
61319
+ throw unsupportedDeployment(webBase);
61320
+ }
61321
+ if (body.status === "ready") {
61322
+ const session = body.session;
61323
+ if (!session) {
61324
+ throw new Error("The server said the login was ready but sent no session. Run `brainbase login` again.");
61325
+ }
61326
+ return session;
61327
+ }
61328
+ await wait(intervalMs);
61329
+ }
61330
+ throw new LoginTimeoutError(timeoutMs, lastError);
61331
+ }
61332
+
61165
61333
  // src/ui/ink/WelcomeCard.tsx
61166
61334
  var jsx_dev_runtime14 = __toESM(require_jsx_dev_runtime(), 1);
61167
61335
  function WelcomeCard(props) {
@@ -61240,143 +61408,132 @@ async function showWelcomeCard(props) {
61240
61408
 
61241
61409
  // src/cli/login.ts
61242
61410
  var DEFAULT_WEB_URL = "https://app.brainbaselabs.com";
61243
- var LOGIN_TIMEOUT_MS = 5 * 60 * 1000;
61244
61411
  function resolveLoginWebBase(web, envWeb = process.env.BRAINBASE_WEB_URL) {
61245
61412
  const override = web?.trim() || envWeb?.trim() || DEFAULT_WEB_URL;
61246
61413
  return override.replace(/\/+$/, "");
61247
61414
  }
61415
+ function isInsecureWebBase(webBase) {
61416
+ let url;
61417
+ try {
61418
+ url = new URL(webBase);
61419
+ } catch {
61420
+ return false;
61421
+ }
61422
+ if (url.protocol !== "http:")
61423
+ return false;
61424
+ return !["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
61425
+ }
61248
61426
  function openInBrowser(url) {
61249
- const platform2 = process.platform;
61250
- const cmd = platform2 === "darwin" ? "open" : platform2 === "win32" ? "cmd" : "xdg-open";
61251
- const args = platform2 === "win32" ? ["/c", "start", '""', url] : [url];
61252
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
61253
- }
61254
- function getFreePort() {
61255
- return new Promise((resolve, reject2) => {
61256
- const srv = net.createServer();
61257
- srv.unref();
61258
- srv.on("error", reject2);
61259
- srv.listen(0, "127.0.0.1", () => {
61260
- const addr = srv.address();
61261
- if (typeof addr === "object" && addr) {
61262
- const port = addr.port;
61263
- srv.close(() => resolve(port));
61264
- } else {
61265
- srv.close();
61266
- reject2(new Error("no port"));
61267
- }
61427
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
61428
+ try {
61429
+ const child = spawn(cmd, args, {
61430
+ stdio: "ignore",
61431
+ detached: true
61268
61432
  });
61269
- });
61433
+ child.on("error", () => {});
61434
+ child.unref();
61435
+ } catch {}
61270
61436
  }
61271
- async function readBody(req) {
61272
- return new Promise((resolve, reject2) => {
61273
- const chunks = [];
61274
- req.on("data", (c2) => chunks.push(c2));
61275
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
61276
- req.on("error", reject2);
61277
- });
61437
+ function displayUserCode(code) {
61438
+ return code.replace(/[^A-Za-z0-9-]/g, "");
61439
+ }
61440
+ function isLoopbackUrl(url) {
61441
+ return ["127.0.0.1", "localhost", "[::1]", "::1"].includes(url.hostname);
61442
+ }
61443
+ function assertNoTransportDowngrade(session, webBase) {
61444
+ let base2;
61445
+ try {
61446
+ base2 = new URL(webBase);
61447
+ } catch {
61448
+ return;
61449
+ }
61450
+ if (base2.protocol !== "https:")
61451
+ return;
61452
+ for (const [field, value] of [
61453
+ ["server", session.server],
61454
+ ["control_plane_url", session.control_plane_url],
61455
+ ["supabase_url", session.supabase_url]
61456
+ ]) {
61457
+ if (!value)
61458
+ continue;
61459
+ let url;
61460
+ try {
61461
+ url = new URL(value);
61462
+ } catch {
61463
+ throw new Error(`The server returned an unusable ${field} (${value}).`);
61464
+ }
61465
+ if (url.protocol !== "https:" && !isLoopbackUrl(url)) {
61466
+ throw new Error(`${webBase} returned a plaintext ${field} (${value}). Refusing to store it — every later command would send your token over an unencrypted connection.`);
61467
+ }
61468
+ }
61469
+ }
61470
+ function toAuthSession(session, webBase) {
61471
+ if (!session.access_token || !session.user_id) {
61472
+ throw new Error("The server returned an incomplete session.");
61473
+ }
61474
+ assertNoTransportDowngrade(session, webBase);
61475
+ const optional = (value) => value ?? undefined;
61476
+ const stored = {
61477
+ schemaVersion: 1,
61478
+ access_token: session.access_token,
61479
+ refresh_token: optional(session.refresh_token),
61480
+ expires_at: optional(session.expires_at),
61481
+ user_id: session.user_id,
61482
+ email: optional(session.email),
61483
+ server: optional(session.server),
61484
+ control_plane_url: normalizeControlPlaneUrl(optional(session.control_plane_url)),
61485
+ supabase_url: optional(session.supabase_url),
61486
+ supabase_anon_key: optional(session.supabase_anon_key),
61487
+ authedAt: new Date().toISOString()
61488
+ };
61489
+ const parsed = AuthSessionSchema.safeParse(stored);
61490
+ if (!parsed.success) {
61491
+ throw new Error(`The server returned a session this CLI cannot store (${parsed.error.issues[0]?.path.join(".") || "unknown field"}).`);
61492
+ }
61493
+ return parsed.data;
61494
+ }
61495
+ async function withCancelOnInterrupt(body) {
61496
+ const onInterrupt = () => {
61497
+ f2.warn("Cancelled. Nothing was saved.");
61498
+ process.exit(130);
61499
+ };
61500
+ process.prependListener("SIGINT", onInterrupt);
61501
+ try {
61502
+ return await body();
61503
+ } finally {
61504
+ process.removeListener("SIGINT", onInterrupt);
61505
+ }
61278
61506
  }
61279
61507
  async function runLogin(_cwd, args) {
61280
61508
  banner("login — connect this device to brainbase");
61281
61509
  const webBase = resolveLoginWebBase(args.web);
61282
- const expectedState = randomBytes(16).toString("hex");
61283
- const port = await getFreePort();
61284
- const callback = `http://127.0.0.1:${port}/cb`;
61285
- const url = `${webBase}/cli/auth?callback=${encodeURIComponent(callback)}&state=${expectedState}`;
61286
- let resolveResult;
61287
- let rejectResult;
61288
- const result2 = new Promise((res, rej) => {
61289
- resolveResult = res;
61290
- rejectResult = rej;
61291
- });
61292
- const corsOrigin = webBase;
61293
- const server = http.createServer(async (req, res) => {
61294
- const reqOrigin = req.headers.origin ?? corsOrigin;
61295
- res.setHeader("Access-Control-Allow-Origin", reqOrigin);
61296
- res.setHeader("Access-Control-Allow-Methods", "POST, OPTIONS");
61297
- res.setHeader("Access-Control-Allow-Headers", "Content-Type");
61298
- res.setHeader("Vary", "Origin");
61299
- if (req.method === "OPTIONS") {
61300
- res.writeHead(204).end();
61301
- return;
61302
- }
61303
- if (req.method === "GET" && (req.url === "/" || req.url === "/cb")) {
61304
- res.writeHead(200, { "Content-Type": "text/plain" });
61305
- res.end("brainbase login server is alive. Complete sign-in in your browser.");
61306
- return;
61307
- }
61308
- if (req.method === "POST" && req.url === "/cb") {
61309
- try {
61310
- const body = await readBody(req);
61311
- const payload = JSON.parse(body);
61312
- if (payload.state !== expectedState) {
61313
- res.writeHead(400, { "Content-Type": "application/json" });
61314
- res.end(JSON.stringify({ error: "state mismatch" }));
61315
- return;
61316
- }
61317
- if (!payload.access_token || !payload.user_id) {
61318
- res.writeHead(400, { "Content-Type": "application/json" });
61319
- res.end(JSON.stringify({ error: "missing fields" }));
61320
- return;
61321
- }
61322
- const session = {
61323
- schemaVersion: 1,
61324
- access_token: payload.access_token,
61325
- refresh_token: payload.refresh_token,
61326
- expires_at: payload.expires_at,
61327
- user_id: payload.user_id,
61328
- email: payload.email,
61329
- server: payload.server,
61330
- control_plane_url: normalizeControlPlaneUrl(payload.control_plane_url),
61331
- supabase_url: payload.supabase_url,
61332
- supabase_anon_key: payload.supabase_anon_key,
61333
- authedAt: new Date().toISOString()
61334
- };
61335
- writeAuth(session);
61336
- res.writeHead(200, { "Content-Type": "application/json" });
61337
- res.end(JSON.stringify({ ok: true }));
61338
- resolveResult(session);
61339
- } catch (err) {
61340
- res.writeHead(500, { "Content-Type": "application/json" });
61341
- res.end(JSON.stringify({ error: err.message }));
61342
- rejectResult(err);
61343
- }
61344
- return;
61345
- }
61346
- res.writeHead(404).end();
61347
- });
61348
- await new Promise((resolve, reject2) => {
61349
- server.once("error", reject2);
61350
- server.listen(port, "127.0.0.1", () => resolve());
61351
- });
61352
- f2.info(`Opening ${import_picocolors21.default.cyan(`${webBase}/cli/auth`)} in your browser…`);
61353
- f2.info(import_picocolors21.default.dim(`If it doesn't open, visit:`));
61354
- console.log(import_picocolors21.default.dim(" ") + url);
61355
- openInBrowser(url);
61356
61510
  const spinner = de();
61357
- spinner.start("Waiting for browser sign-in (Ctrl-C to cancel)");
61358
- const timeout = setTimeout(() => {
61359
- spinner.stop("Timed out.");
61360
- server.close();
61361
- rejectResult(new Error(`Login timed out after ${LOGIN_TIMEOUT_MS / 1000}s`));
61362
- }, LOGIN_TIMEOUT_MS);
61511
+ let spinning = false;
61512
+ if (isInsecureWebBase(webBase)) {
61513
+ f2.warn(import_picocolors21.default.yellow(`${webBase} is not HTTPS — your session would cross the network in the clear.`));
61514
+ }
61363
61515
  try {
61364
- const session = await result2;
61365
- clearTimeout(timeout);
61366
- server.close();
61516
+ const authorization = await startDeviceAuthorization({ webBase });
61517
+ f2.step(`Your code: ${import_picocolors21.default.bold(import_picocolors21.default.cyan(displayUserCode(authorization.userCode)))}`);
61518
+ f2.info(`Enter it at ${import_picocolors21.default.cyan(authorization.verificationUri)}`);
61519
+ f2.info(import_picocolors21.default.dim("Opening your browser…"));
61520
+ openInBrowser(authorization.verificationUri);
61521
+ spinner.start("Waiting for you to approve in the browser (Ctrl-C to cancel)");
61522
+ spinning = true;
61523
+ const session = await withCancelOnInterrupt(() => awaitDeviceSession({ webBase, authorization }));
61524
+ const stored = toAuthSession(session, webBase);
61367
61525
  spinner.stop("Authorized.");
61526
+ spinning = false;
61368
61527
  $e("Welcome to brainbase.");
61369
- if (session) {
61370
- await showWelcomeCard({
61371
- email: session.email ?? session.user_id,
61372
- controlPlaneUrl: controlPlaneBaseUrl(session),
61373
- expiresAt: session.expires_at,
61374
- hint: "brainbase template pack — bundle your first agent"
61375
- });
61376
- }
61528
+ await showWelcomeCard({
61529
+ email: stored.email ?? stored.user_id,
61530
+ controlPlaneUrl: controlPlaneBaseUrl(stored),
61531
+ expiresAt: stored.expires_at,
61532
+ hint: "brainbase template pack — bundle your first agent"
61533
+ });
61377
61534
  } catch (err) {
61378
- clearTimeout(timeout);
61379
- server.close();
61535
+ if (spinning)
61536
+ spinner.stop("Login failed.");
61380
61537
  f2.error(err.message);
61381
61538
  process.exit(1);
61382
61539
  }
@@ -74746,8 +74903,8 @@ async function random2(size2) {
74746
74903
  const evenDistCutoff = Math.pow(2, 8) - Math.pow(2, 8) % mask.length;
74747
74904
  let result2 = "";
74748
74905
  while (result2.length < size2) {
74749
- const randomBytes2 = await getRandomValues(size2 - result2.length);
74750
- for (const randomByte of randomBytes2) {
74906
+ const randomBytes = await getRandomValues(size2 - result2.length);
74907
+ for (const randomByte of randomBytes) {
74751
74908
  if (randomByte < evenDistCutoff) {
74752
74909
  result2 += mask[randomByte % mask.length];
74753
74910
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainbase-labs/cli",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
4
4
  "description": "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
5
5
  "type": "module",
6
6
  "bin": {