@brainbase-labs/cli 0.47.0 → 0.48.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 (3) hide show
  1. package/README.md +12 -16
  2. package/dist/index.js +305 -137
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -336,7 +336,7 @@ Playbook files carry a small YAML frontmatter (`title`, `description`) — the C
336
336
  | `BRAINBASE_API_URL` | Legacy KLS host override. Control requests use `/api/cli`; it is also the fallback for model-proxy and registry traffic. |
337
337
  | `BRAINBASE_PROXY_URL` | Override the model-proxy host used when enabling harness tracking. |
338
338
  | `BRAINBASE_REGISTRY_URL` | Override the registry API host. |
339
- | `BRAINBASE_TOKEN` | Use a PAT instead of the stored login session. The only way a PAT authenticates a control-plane command — see [Auth](#auth). |
339
+ | `BRAINBASE_TOKEN` | Use a PAT instead of the stored login session. Outranks both the session and the PAT saved in `token.json`. See [Auth](#auth). |
340
340
  | `BRAINBASE_HOME` | Where local state lives (default: `~/.brainbase`) |
341
341
 
342
342
  Model-proxy and registry traffic fall back through `BRAINBASE_API_URL`,
@@ -344,30 +344,26 @@ the server captured at login, and finally `https://api.v1.brainbaselabs.com`.
344
344
 
345
345
  ## Auth
346
346
 
347
- Three credentials, and which ones a command considers depends on the service
348
- it talks to:
347
+ Every command tries three credentials, in this order:
349
348
 
350
- | Commands | Credentials tried, in order |
351
- |-|-|
352
- | `agent`, `orchestration`, `link`, `unlink`, `sync`, `status`, `team` | `BRAINBASE_TOKEN`, then the `auth.json` session |
353
- | `template`, `skill`, `token` | `BRAINBASE_TOKEN`, then the session, then `token.json` |
354
- | `task` commands | `BRAINBASE_TOKEN`, then the session, then `token.json` — the last only when no session is configured |
355
- | `benchmark` control-plane commands | `BRAINBASE_TOKEN`, then the session |
349
+ 1. `BRAINBASE_TOKEN`
350
+ 2. the `auth.json` login session, renewed first if it has expired
351
+ 3. the PAT that `brainbase token create` saved in `~/.brainbase/token.json`,
352
+ when there is no session or it could not be renewed. A PAT minted by a
353
+ different account than the session is never used in its place.
356
354
 
357
- `brainbase whoami` (or `--json`) reports which one is active.
355
+ `brainbase whoami` (or `--json`) reports which one is active. It renews an
356
+ expired session itself, so what it reports is what the next command will use.
358
357
 
359
- **For CI:** `brainbase token create` saves the PAT to `~/.brainbase/token.json`,
360
- and control-plane commands never read that file — `brainbase agent push` needs
361
- the token in `BRAINBASE_TOKEN`:
358
+ **For CI:** a runner has no `token.json`, so hand it the PAT as a secret:
362
359
 
363
360
  ```bash
364
361
  export BRAINBASE_TOKEN=bbpat_…
365
362
  brainbase agent push
366
363
  ```
367
364
 
368
- Do not bake a `~/.brainbase` directory into a build image: a leftover
369
- `auth.json` outranks the stored PAT, and every control-plane command starts
370
- failing once its session expires.
365
+ Do not bake a `~/.brainbase` directory into a build image. Ship
366
+ `BRAINBASE_TOKEN` as a secret instead.
371
367
 
372
368
  An explicit PAT does not inherit routing from a stored login. Set
373
369
  `BRAINBASE_CONTROL_PLANE_URL` when using a PAT against a non-default MAS host.
package/dist/index.js CHANGED
@@ -36091,7 +36091,7 @@ function padStart(s, n) {
36091
36091
  // package.json
36092
36092
  var package_default = {
36093
36093
  name: "@brainbase-labs/cli",
36094
- version: "0.47.0",
36094
+ version: "0.48.0",
36095
36095
  description: "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
36096
36096
  type: "module",
36097
36097
  bin: {
@@ -40994,7 +40994,8 @@ var AuthSessionSchema = exports_external.object({
40994
40994
  supabase_url: exports_external.string().optional(),
40995
40995
  supabase_anon_key: exports_external.string().optional(),
40996
40996
  web_url: exports_external.string().optional(),
40997
- authedAt: exports_external.string()
40997
+ authedAt: exports_external.string(),
40998
+ refresh_rejected: exports_external.object({ at: exports_external.string(), code: exports_external.string() }).optional()
40998
40999
  });
40999
41000
  function readAuth() {
41000
41001
  if (!exists(AUTH_FILE))
@@ -41141,6 +41142,47 @@ function authStatus() {
41141
41142
  return { ok: false, session: s, reason: "session expired" };
41142
41143
  return { ok: true, session: s };
41143
41144
  }
41145
+ function isSessionRefreshable(session) {
41146
+ return Boolean(session.refresh_token && session.supabase_url && session.supabase_anon_key && !session.refresh_rejected);
41147
+ }
41148
+ var lastRefreshFailure = null;
41149
+ function refreshFailureFor(session) {
41150
+ if (session.refresh_rejected) {
41151
+ return { kind: "rejected", detail: session.refresh_rejected.code };
41152
+ }
41153
+ if (lastRefreshFailure && lastRefreshFailure.refreshToken === session.refresh_token) {
41154
+ return lastRefreshFailure.failure;
41155
+ }
41156
+ return null;
41157
+ }
41158
+ function noteRefreshFailure(session, failure) {
41159
+ lastRefreshFailure = { refreshToken: session.refresh_token ?? "", failure };
41160
+ }
41161
+ async function rejectionCode(res) {
41162
+ if (![400, 401, 403].includes(res.status))
41163
+ return null;
41164
+ try {
41165
+ const body = await res.json();
41166
+ for (const key of ["error_code", "error"]) {
41167
+ const value = body[key];
41168
+ if (typeof value === "string" && value.trim())
41169
+ return value.trim();
41170
+ }
41171
+ } catch {}
41172
+ return null;
41173
+ }
41174
+ function markRefreshRejected(session, code) {
41175
+ withAuthLock(() => {
41176
+ const current = readAuth();
41177
+ if (!current || !isSameAuthIdentity(current, session) || current.access_token !== session.access_token || current.refresh_token !== session.refresh_token) {
41178
+ return;
41179
+ }
41180
+ writeAuthUnlocked({
41181
+ ...current,
41182
+ refresh_rejected: { at: new Date().toISOString(), code }
41183
+ });
41184
+ });
41185
+ }
41144
41186
  var refreshInFlight = null;
41145
41187
  function validatedRefreshResult(expectedSession, result) {
41146
41188
  const current = readAuth();
@@ -41162,10 +41204,11 @@ function adoptRefreshedLineage(session) {
41162
41204
  return isAuthValid(current) ? current : null;
41163
41205
  }
41164
41206
  async function refreshSession(session) {
41165
- if (!session.refresh_token)
41207
+ if (!isSessionRefreshable(session))
41166
41208
  return null;
41167
- if (!session.supabase_url || !session.supabase_anon_key)
41209
+ if (!session.refresh_token || !session.supabase_url || !session.supabase_anon_key) {
41168
41210
  return null;
41211
+ }
41169
41212
  const releaseRefreshLock = acquireLock(REFRESH_LOCK_FILE, REFRESH_LOCK_TIMEOUT_MS);
41170
41213
  if (!releaseRefreshLock) {
41171
41214
  const adopted = adoptRefreshedLineage(session);
@@ -41173,12 +41216,24 @@ async function refreshSession(session) {
41173
41216
  return adopted;
41174
41217
  if (isAuthValid(session))
41175
41218
  return session;
41219
+ noteRefreshFailure(session, {
41220
+ kind: "unavailable",
41221
+ detail: "timed out waiting for another command to renew it"
41222
+ });
41176
41223
  throw new AuthRefreshLockTimeoutError;
41177
41224
  }
41178
41225
  try {
41179
41226
  const adopted = adoptRefreshedLineage(session);
41180
41227
  if (adopted !== undefined)
41181
41228
  return adopted;
41229
+ const onDisk = readAuth();
41230
+ if (onDisk?.refresh_rejected) {
41231
+ noteRefreshFailure(session, {
41232
+ kind: "rejected",
41233
+ detail: onDisk.refresh_rejected.code
41234
+ });
41235
+ return null;
41236
+ }
41182
41237
  const url = `${session.supabase_url.replace(/\/+$/, "")}/auth/v1/token?grant_type=refresh_token`;
41183
41238
  let res;
41184
41239
  try {
@@ -41192,19 +41247,43 @@ async function refreshSession(session) {
41192
41247
  },
41193
41248
  body: JSON.stringify({ refresh_token: session.refresh_token })
41194
41249
  });
41195
- } catch {
41250
+ } catch (error) {
41251
+ noteRefreshFailure(session, {
41252
+ kind: "unavailable",
41253
+ detail: `could not reach ${new URL(url).host}: ${error.message}`
41254
+ });
41196
41255
  return null;
41197
41256
  }
41198
- if (!res.ok)
41257
+ if (!res.ok) {
41258
+ const code = await rejectionCode(res);
41259
+ if (code) {
41260
+ markRefreshRejected(session, code);
41261
+ noteRefreshFailure(session, { kind: "rejected", detail: code });
41262
+ } else {
41263
+ noteRefreshFailure(session, {
41264
+ kind: "unavailable",
41265
+ detail: `the auth server answered HTTP ${res.status}`
41266
+ });
41267
+ }
41199
41268
  return null;
41269
+ }
41200
41270
  let data;
41201
41271
  try {
41202
41272
  data = await res.json();
41203
41273
  } catch {
41274
+ noteRefreshFailure(session, {
41275
+ kind: "unavailable",
41276
+ detail: "the auth server sent an unreadable response"
41277
+ });
41204
41278
  return null;
41205
41279
  }
41206
- if (!data.access_token)
41280
+ if (!data.access_token) {
41281
+ noteRefreshFailure(session, {
41282
+ kind: "unavailable",
41283
+ detail: "the auth server sent no access token"
41284
+ });
41207
41285
  return null;
41286
+ }
41208
41287
  const now = Math.floor(Date.now() / 1000);
41209
41288
  const expires_at = data.expires_at ?? (data.expires_in ? now + data.expires_in : undefined);
41210
41289
  const updated = {
@@ -41224,6 +41303,7 @@ async function refreshSession(session) {
41224
41303
  if (adopted2 !== undefined)
41225
41304
  return adopted2;
41226
41305
  writeAuthUnlocked(updated);
41306
+ lastRefreshFailure = null;
41227
41307
  return updated;
41228
41308
  });
41229
41309
  } finally {
@@ -41263,7 +41343,7 @@ async function refreshSessionAfterUnauthorized(rejectedSession) {
41263
41343
  return current;
41264
41344
  }
41265
41345
  const refreshed = await tryRefreshSession(rejectedSession);
41266
- if (refreshed) {
41346
+ if (refreshed && refreshed.access_token !== rejectedSession.access_token) {
41267
41347
  if (!isSameAuthIdentity(refreshed, rejectedSession)) {
41268
41348
  throw new AuthSessionChangedError;
41269
41349
  }
@@ -41276,8 +41356,21 @@ async function refreshSessionAfterUnauthorized(rejectedSession) {
41276
41356
  if (latest.access_token !== rejectedSession.access_token && isAuthValid(latest)) {
41277
41357
  return latest;
41278
41358
  }
41359
+ expireRejectedAccessToken(rejectedSession);
41279
41360
  return null;
41280
41361
  }
41362
+ function expireRejectedAccessToken(session) {
41363
+ withAuthLock(() => {
41364
+ const current = readAuth();
41365
+ if (!current || !isSameAuthIdentity(current, session) || current.access_token !== session.access_token || !isAuthValid(current)) {
41366
+ return;
41367
+ }
41368
+ writeAuthUnlocked({
41369
+ ...current,
41370
+ expires_at: Math.floor(Date.now() / 1000) - 1
41371
+ });
41372
+ });
41373
+ }
41281
41374
  async function ensureFreshSession() {
41282
41375
  const s = readAuth();
41283
41376
  if (!s)
@@ -54875,7 +54968,8 @@ var StoredTokenSchema = exports_external.object({
54875
54968
  token: exports_external.string(),
54876
54969
  prefix: exports_external.string(),
54877
54970
  name: exports_external.string().optional(),
54878
- createdAt: exports_external.string()
54971
+ createdAt: exports_external.string(),
54972
+ user_id: exports_external.string().optional()
54879
54973
  });
54880
54974
  function withTokenLock(operation) {
54881
54975
  const release = acquireLock(TOKEN_LOCK_FILE, TOKEN_LOCK_TIMEOUT_MS);
@@ -54897,7 +54991,7 @@ function readToken() {
54897
54991
  return null;
54898
54992
  }
54899
54993
  }
54900
- function writeToken(token, name) {
54994
+ function writeToken(token, name, userId) {
54901
54995
  if (!isValidTokenFormat(token)) {
54902
54996
  throw new Error("refusing to store token: invalid format");
54903
54997
  }
@@ -54906,7 +55000,8 @@ function writeToken(token, name) {
54906
55000
  token,
54907
55001
  prefix: tokenPrefix(token),
54908
55002
  name,
54909
- createdAt: new Date().toISOString()
55003
+ createdAt: new Date().toISOString(),
55004
+ ...userId ? { user_id: userId } : {}
54910
55005
  };
54911
55006
  withTokenLock(() => writeJsonAtomic(TOKEN_FILE, stored));
54912
55007
  return stored;
@@ -54940,8 +55035,6 @@ function clearTokenIfMatches(prefix) {
54940
55035
  }
54941
55036
 
54942
55037
  // src/core/credentials.ts
54943
- var REGISTRY_COMMANDS = "template, skill, token";
54944
- var CONTROL_PLANE_COMMANDS = "agent, orchestration, link, unlink, sync, status, team";
54945
55038
  var NOT_A_PAT = "(not a bbpat_… token)";
54946
55039
  function maskPat(token) {
54947
55040
  if (!isValidTokenFormat(token))
@@ -54954,58 +55047,89 @@ function readEnvPat() {
54954
55047
  return null;
54955
55048
  return { prefix: maskPat(value), malformed: !isValidTokenFormat(value) };
54956
55049
  }
54957
- function storedPatIgnoredHint(client) {
54958
- const setEnv = `Set BRAINBASE_TOKEN to that token's value to authenticate with it here, ` + "or run `brainbase login` to refresh the session";
54959
- switch (client) {
54960
- case "control-plane":
54961
- return `The PAT saved in ${TOKEN_FILE} is never used by control-plane ` + `commands (${CONTROL_PLANE_COMMANDS}). ${setEnv}.`;
54962
- case "managed-task":
54963
- return `The PAT saved in ${TOKEN_FILE} is not used while a login session is ` + `configured. ${setEnv} — or \`brainbase logout\`, which leaves the ` + "PAT as the only credential.";
54964
- default: {
54965
- const exhaustive = client;
54966
- throw new Error(`unhandled credential client: ${String(exhaustive)}`);
54967
- }
55050
+ function storedPatFor(session) {
55051
+ const stored = readToken();
55052
+ if (!stored)
55053
+ return null;
55054
+ if (session && stored.user_id && stored.user_id !== session.user_id) {
55055
+ return null;
54968
55056
  }
55057
+ return stored;
54969
55058
  }
54970
- function withStoredPatHint(reason, client) {
54971
- if (!readToken())
54972
- return reason;
54973
- const trimmed = reason.trimEnd();
54974
- const separator = /[.!?:]$/.test(trimmed) ? " " : ". ";
54975
- return `${trimmed}${separator}${storedPatIgnoredHint(client)}`;
55059
+ var announcedFallback = false;
55060
+ function announceStoredPatFallback(session, stored) {
55061
+ if (!session || announcedFallback)
55062
+ return;
55063
+ announcedFallback = true;
55064
+ console.error(`Login session ${sessionProblem(session)}; using the PAT saved in ${TOKEN_FILE} ` + `(${stored.prefix}…). Run \`brainbase login\` to switch back.`);
55065
+ }
55066
+ function sessionProblem(session) {
55067
+ const failure = refreshFailureFor(session);
55068
+ if (failure?.kind === "rejected") {
55069
+ return `expired and could not be renewed (the auth server rejected its refresh token: ${failure.detail})`;
55070
+ }
55071
+ if (failure?.kind === "unavailable") {
55072
+ return `expired and could not be renewed (${failure.detail})`;
55073
+ }
55074
+ if (!isSessionRefreshable(session)) {
55075
+ return "expired and cannot be renewed (no refresh token was saved with it)";
55076
+ }
55077
+ return "expired";
55078
+ }
55079
+ function noUsableCredentialMessage() {
55080
+ const session = readAuth();
55081
+ if (!session) {
55082
+ return "Not logged in. Run `brainbase login` to connect this device.";
55083
+ }
55084
+ const failure = refreshFailureFor(session);
55085
+ const retry = failure?.kind === "unavailable" ? " Check your connection and retry, or run `brainbase login`." : " Run `brainbase login` to sign in again.";
55086
+ const stored = readToken();
55087
+ const mismatch = stored && !storedPatFor(session) ? ` The PAT saved in ${TOKEN_FILE} belongs to a different account, so it was not used.` : "";
55088
+ return `Your login session ${sessionProblem(session)}.${retry}${mismatch}`;
54976
55089
  }
54977
- function withRejectedSessionHint(message, source, client) {
55090
+ function withRejectedSessionHint(message, source) {
54978
55091
  if (source !== "session")
54979
55092
  return message;
54980
- return withStoredPatHint(message, client);
54981
- }
54982
- function isRefreshable(session) {
54983
- return Boolean(session.refresh_token && session.supabase_url && session.supabase_anon_key);
55093
+ const trimmed = message.trimEnd();
55094
+ const separator = /[.!?:]$/.test(trimmed) ? " " : ". ";
55095
+ const session = readAuth();
55096
+ const renewable = session ? isSessionRefreshable(session) : false;
55097
+ if (session && !renewable && storedPatFor(session)) {
55098
+ return `${trimmed}${separator}The login session could not be renewed; ` + `retry to use the PAT saved in ${TOKEN_FILE}, or run \`brainbase login\`.`;
55099
+ }
55100
+ return `${trimmed}${separator}The login session could not be renewed. Run \`brainbase login\` to sign in again.`;
54984
55101
  }
54985
55102
  function describeCredentials() {
54986
55103
  const env3 = readEnvPat();
54987
55104
  const status = authStatus();
54988
- const stored = readToken();
54989
- const refreshable = Boolean(status.session && !status.ok && isRefreshable(status.session));
55105
+ const storedToken = readToken();
55106
+ const storedUsable = storedToken ? storedPatFor(status.session) : null;
55107
+ const refreshable = Boolean(status.session && !status.ok && isSessionRefreshable(status.session));
55108
+ const failure = status.session ? refreshFailureFor(status.session) : null;
54990
55109
  const session = status.session ? {
54991
55110
  email: status.session.email ?? null,
54992
55111
  userId: status.session.user_id,
54993
55112
  expiresAt: status.session.expires_at ?? null,
54994
55113
  expired: !status.ok,
54995
- refreshable
55114
+ refreshable,
55115
+ refreshError: failure ? failure.kind === "rejected" ? `rejected: ${failure.detail}` : failure.detail : null
54996
55116
  } : null;
54997
55117
  let active = null;
54998
55118
  if (env3)
54999
55119
  active = "env_pat";
55000
- else if (status.ok || refreshable)
55120
+ else if (status.ok || refreshable && !failure)
55001
55121
  active = "session";
55002
- else if (stored)
55122
+ else if (storedUsable)
55003
55123
  active = "stored_pat";
55004
55124
  return {
55005
55125
  active,
55006
55126
  envPat: env3,
55007
55127
  session,
55008
- storedPat: stored ? { name: stored.name ?? null, prefix: stored.prefix } : null
55128
+ storedPat: storedToken ? {
55129
+ name: storedToken.name ?? null,
55130
+ prefix: storedToken.prefix,
55131
+ usable: Boolean(storedUsable)
55132
+ } : null
55009
55133
  };
55010
55134
  }
55011
55135
 
@@ -55092,7 +55216,7 @@ function legacyScheduleError() {
55092
55216
  function legacyAgentConfigError() {
55093
55217
  return new ApiError("Declarative machine/model config requires the MAS control plane. Set BRAINBASE_CONTROL_PLANE_URL or unset the legacy BRAINBASE_API_URL override.", 400);
55094
55218
  }
55095
- async function resolveCredential(storedPatHint = "control-plane") {
55219
+ async function resolveCredential() {
55096
55220
  const envToken = process.env.BRAINBASE_TOKEN;
55097
55221
  if (envToken && envToken.trim()) {
55098
55222
  return {
@@ -55101,43 +55225,39 @@ async function resolveCredential(storedPatHint = "control-plane") {
55101
55225
  source: "env_pat"
55102
55226
  };
55103
55227
  }
55104
- const session = await ensureFreshSession();
55105
- if (session) {
55106
- return {
55107
- bearer: session.access_token,
55108
- session,
55109
- source: "session"
55110
- };
55111
- }
55112
- const status = authStatus();
55113
- const reason = status.reason ?? "not logged in";
55114
- throw new ApiError(status.ok ? "CLI authentication changed while this command was starting; retry it" : storedPatHint ? withStoredPatHint(reason, storedPatHint) : reason, 401);
55115
- }
55116
- async function resolveMasCredential() {
55117
- const configuredSession = readAuth();
55228
+ const configured = readAuth();
55229
+ let session;
55118
55230
  try {
55119
- return await resolveCredential(null);
55231
+ session = await ensureFreshSession();
55120
55232
  } catch (error) {
55121
- if (!(error instanceof ApiError && error.status === 401)) {
55233
+ if (!(error instanceof AuthRefreshLockTimeoutError))
55122
55234
  throw error;
55123
- }
55124
- if (configuredSession || readAuth()) {
55125
- if (readToken()) {
55126
- throw new ApiError(`${error.message}. ${storedPatIgnoredHint("managed-task")}`, 401);
55127
- }
55235
+ const current2 = readAuth();
55236
+ const stored2 = storedPatFor(current2);
55237
+ if (!stored2 || (configured?.user_id ?? null) !== (current2?.user_id ?? null)) {
55128
55238
  throw error;
55129
55239
  }
55240
+ announceStoredPatFallback(current2, stored2);
55241
+ return { bearer: stored2.token, session: null, source: "stored_pat" };
55130
55242
  }
55131
- const storedToken = readToken();
55132
- if (storedToken) {
55243
+ if (session) {
55133
55244
  return {
55134
- bearer: storedToken.token,
55135
- session: null,
55136
- source: "stored_pat"
55245
+ bearer: session.access_token,
55246
+ session,
55247
+ source: "session"
55137
55248
  };
55138
55249
  }
55139
55250
  const status = authStatus();
55140
- throw new ApiError(status.ok ? "CLI authentication changed while this command was starting; retry it" : status.reason ?? "not logged in", 401);
55251
+ const current = readAuth();
55252
+ if (status.ok || (configured?.user_id ?? null) !== (current?.user_id ?? null)) {
55253
+ throw new ApiError("CLI authentication changed while this command was starting; retry it", 401);
55254
+ }
55255
+ const stored = storedPatFor(current);
55256
+ if (stored) {
55257
+ announceStoredPatFallback(current, stored);
55258
+ return { bearer: stored.token, session: null, source: "stored_pat" };
55259
+ }
55260
+ throw new ApiError(noUsableCredentialMessage(), 401);
55141
55261
  }
55142
55262
  function networkErrorCode(error) {
55143
55263
  if (!error || typeof error !== "object")
@@ -55225,7 +55345,7 @@ async function request(pathname, init = {}) {
55225
55345
  } catch {}
55226
55346
  if (!res.ok) {
55227
55347
  const message = apiErrorMessage(body, res.status);
55228
- throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "control-plane") : message, res.status, body);
55348
+ throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : message, res.status, body);
55229
55349
  }
55230
55350
  return body;
55231
55351
  }
@@ -55278,7 +55398,7 @@ function parseMasTaskCreateResponse(body) {
55278
55398
  };
55279
55399
  }
55280
55400
  async function masRequest(pathname, init) {
55281
- const credential = await resolveMasCredential();
55401
+ const credential = await resolveCredential();
55282
55402
  let currentSession = credential.session;
55283
55403
  let ambiguousNetworkRetriesRemaining = 1;
55284
55404
  let requestMayHaveSucceeded = false;
@@ -55369,7 +55489,7 @@ async function masRequest(pathname, init) {
55369
55489
  }
55370
55490
  if (!res.ok) {
55371
55491
  const message = masApiErrorMessage(body, res.status);
55372
- throw new ApiError(res.status >= 500 ? `${message} Task creation may still be processing; check your tasks before running this command again.` : res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : message, res.status, body);
55492
+ throw new ApiError(res.status >= 500 ? `${message} Task creation may still be processing; check your tasks before running this command again.` : res.status === 401 ? withRejectedSessionHint(message, credential.source) : message, res.status, body);
55373
55493
  }
55374
55494
  return body;
55375
55495
  }
@@ -55383,7 +55503,7 @@ function masNamedRecord(body, what, nameField) {
55383
55503
  return { id: record.id, name };
55384
55504
  }
55385
55505
  async function masReadRequest(pathname, callerSignal) {
55386
- const credential = await resolveMasCredential();
55506
+ const credential = await resolveCredential();
55387
55507
  let currentSession = credential.session;
55388
55508
  let refreshSessionAvailable = credential.source === "session";
55389
55509
  let res;
@@ -55421,7 +55541,7 @@ async function masReadRequest(pathname, callerSignal) {
55421
55541
  } catch {}
55422
55542
  if (!res.ok) {
55423
55543
  const message = masApiErrorMessage(body, res.status);
55424
- throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
55544
+ throw new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
55425
55545
  }
55426
55546
  return body;
55427
55547
  }
@@ -55440,7 +55560,7 @@ function withRequestDeadline(callerSignal, timeoutMs) {
55440
55560
  return combined.signal;
55441
55561
  }
55442
55562
  async function masResourceRequest(pathname, init = {}, onHeaders) {
55443
- const credential = await resolveMasCredential();
55563
+ const credential = await resolveCredential();
55444
55564
  let currentSession = credential.session;
55445
55565
  const res = await sendWithAuthRetry(credential.source === "session" ? currentSession : null, async (refreshed) => {
55446
55566
  if (refreshed)
@@ -55462,7 +55582,7 @@ async function masResourceRequest(pathname, init = {}, onHeaders) {
55462
55582
  } catch {}
55463
55583
  if (!res.ok) {
55464
55584
  const message = masApiErrorMessage(body, res.status);
55465
- const failure = new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source, "managed-task") : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
55585
+ const failure = new ApiError(res.status === 401 ? withRejectedSessionHint(message, credential.source) : res.status === 404 && isUnroutedPath(body) ? `This control plane does not serve ${pathname} yet. Update the server, or use the web app.` : message, res.status, body);
55466
55586
  const retryAfter = res.headers.get("Retry-After");
55467
55587
  if (retryAfter)
55468
55588
  failure.retryAfter = retryAfter;
@@ -55475,12 +55595,12 @@ async function masResourceRequest(pathname, init = {}, onHeaders) {
55475
55595
  class StreamHostMovedError extends ApiError {
55476
55596
  }
55477
55597
  async function masStreamTarget(pathname) {
55478
- const credential = await resolveMasCredential();
55598
+ const credential = await resolveCredential();
55479
55599
  const base2 = masApiBase(credential.session);
55480
55600
  return {
55481
55601
  url: `${base2}${pathname}`,
55482
55602
  resolveBearer: async () => {
55483
- const fresh = await resolveMasCredential();
55603
+ const fresh = await resolveCredential();
55484
55604
  const freshBase = masApiBase(fresh.session);
55485
55605
  if (freshBase !== base2) {
55486
55606
  throw new StreamHostMovedError(`The control plane moved from ${base2} to ${freshBase} while this stream was open, so the current credential was not sent. Re-run the command to follow against the new host.`);
@@ -55934,18 +56054,26 @@ async function resolveAuth() {
55934
56054
  } catch (error) {
55935
56055
  if (!(error instanceof AuthRefreshLockTimeoutError))
55936
56056
  throw error;
55937
- const pat2 = readToken();
55938
- if (pat2)
56057
+ const session = readAuth();
56058
+ const pat2 = storedPatFor(session);
56059
+ if (pat2) {
56060
+ announceStoredPatFallback(session, pat2);
55939
56061
  return patAuth(pat2.token);
56062
+ }
55940
56063
  throw error;
55941
56064
  }
55942
56065
  if (freshSession)
55943
56066
  return jwtAuth(freshSession);
55944
56067
  const status = authStatus();
55945
- const pat = readToken();
55946
- if (pat)
56068
+ if (status.ok) {
56069
+ throw new ApiError("CLI authentication changed while this command was starting; retry it", 401);
56070
+ }
56071
+ const pat = storedPatFor(status.session);
56072
+ if (pat) {
56073
+ announceStoredPatFallback(status.session, pat);
55947
56074
  return patAuth(pat.token);
55948
- throw new ApiError(status.ok ? "CLI authentication changed while this command was starting; retry it" : status.reason ?? "not logged in", 401);
56075
+ }
56076
+ throw new ApiError(noUsableCredentialMessage(), 401);
55949
56077
  }
55950
56078
  function mergeHeaders(auth, overrides) {
55951
56079
  const headers = new Headers(auth);
@@ -63396,13 +63524,62 @@ async function runLogout() {
63396
63524
  }
63397
63525
 
63398
63526
  // src/cli/whoami.ts
63399
- function usableForControlPlane(report) {
63527
+ var PAT_LOOKUP_TIMEOUT_MS = 5000;
63528
+ async function settleSession() {
63529
+ if (process.env.BRAINBASE_TOKEN?.trim())
63530
+ return;
63531
+ if (!readAuth())
63532
+ return;
63533
+ try {
63534
+ await ensureFreshSession();
63535
+ } catch {}
63536
+ }
63537
+ function activePat(report) {
63538
+ if (report.active === "env_pat") {
63539
+ return report.envPat?.malformed ? null : process.env.BRAINBASE_TOKEN?.trim() ?? null;
63540
+ }
63541
+ if (report.active === "stored_pat")
63542
+ return readToken()?.token ?? null;
63543
+ return null;
63544
+ }
63545
+ async function lookupPat(token) {
63546
+ let res;
63547
+ try {
63548
+ res = await fetch(`${baseUrl(null)}/v1/registry/me`, {
63549
+ headers: {
63550
+ Authorization: `Bearer ${token}`,
63551
+ "X-Auth-Source": "pat",
63552
+ Accept: "application/json"
63553
+ },
63554
+ signal: AbortSignal.timeout(PAT_LOOKUP_TIMEOUT_MS)
63555
+ });
63556
+ } catch {
63557
+ return { kind: "unknown" };
63558
+ }
63559
+ let body = null;
63560
+ try {
63561
+ body = await res.json();
63562
+ } catch {}
63563
+ const detail = body && typeof body === "object" && typeof body.detail === "string" ? body.detail : null;
63564
+ if (res.status === 401 && detail)
63565
+ return { kind: "rejected", message: detail };
63566
+ if (!res.ok || !body || typeof body !== "object")
63567
+ return { kind: "unknown" };
63568
+ const obj = body;
63569
+ return {
63570
+ kind: "ok",
63571
+ userId: typeof obj.id === "string" ? obj.id : null,
63572
+ email: typeof obj.email === "string" ? obj.email : null
63573
+ };
63574
+ }
63575
+ function usableForControlPlane(report, pat) {
63400
63576
  switch (report.active) {
63401
63577
  case "env_pat":
63402
- return !report.envPat?.malformed;
63578
+ return !report.envPat?.malformed && pat.kind !== "rejected";
63579
+ case "stored_pat":
63580
+ return pat.kind !== "rejected";
63403
63581
  case "session":
63404
63582
  return true;
63405
- case "stored_pat":
63406
63583
  case null:
63407
63584
  return false;
63408
63585
  default: {
@@ -63411,41 +63588,44 @@ function usableForControlPlane(report) {
63411
63588
  }
63412
63589
  }
63413
63590
  }
63414
- function exitCodeFor(report) {
63415
- return usableForControlPlane(report) ? 0 : 1;
63416
- }
63417
- function reasonFor(report) {
63591
+ function reasonFor(report, pat) {
63592
+ if (pat.kind === "rejected") {
63593
+ const which = report.active === "env_pat" ? "the PAT in BRAINBASE_TOKEN" : "the PAT saved in token.json";
63594
+ return `The server rejected ${which} (${pat.message}). ` + "Mint a new one with `brainbase token create`, or run `brainbase login`.";
63595
+ }
63418
63596
  switch (report.active) {
63419
63597
  case "env_pat":
63420
63598
  return report.envPat?.malformed ? "BRAINBASE_TOKEN is set to something that is not a `bbpat_…` token, " + "and it outranks every other credential — so every request will fail " + "until it is corrected or unset. Mint one with `brainbase token create`." : null;
63421
63599
  case "session":
63422
63600
  return null;
63423
63601
  case "stored_pat":
63424
- return `The stored PAT authenticates registry commands (${REGISTRY_COMMANDS}) only. ` + `Set BRAINBASE_TOKEN to its value for ${CONTROL_PLANE_COMMANDS}, ` + "or run `brainbase login`.";
63602
+ return report.session ? noUsableCredentialMessage() : null;
63425
63603
  case null:
63426
- return report.session?.expired ? "Session expired. Run `brainbase login` to connect this device." : "Not logged in. Run `brainbase login` to connect this device.";
63604
+ return noUsableCredentialMessage();
63427
63605
  default: {
63428
63606
  const exhaustive = report.active;
63429
63607
  throw new Error(`unhandled credential: ${String(exhaustive)}`);
63430
63608
  }
63431
63609
  }
63432
63610
  }
63433
- function toJson(report, reason) {
63611
+ function toJson(report, pat, reason) {
63434
63612
  const usingSession = report.active === "session";
63613
+ const patIdentity = pat.kind === "ok" ? pat : null;
63435
63614
  return {
63436
63615
  credential: report.active,
63437
- authenticated: report.active !== null && !report.envPat?.malformed,
63438
- control_plane: usableForControlPlane(report),
63439
- email: usingSession ? report.session?.email ?? null : null,
63440
- user_id: usingSession ? report.session?.userId ?? null : null,
63616
+ authenticated: report.active !== null && !report.envPat?.malformed && pat.kind !== "rejected",
63617
+ control_plane: usableForControlPlane(report, pat),
63618
+ email: usingSession ? report.session?.email ?? null : patIdentity?.email ?? null,
63619
+ user_id: usingSession ? report.session?.userId ?? null : patIdentity?.userId ?? null,
63441
63620
  expires_at: usingSession ? report.session?.expiresAt ?? null : null,
63442
63621
  token_prefix: report.active === "env_pat" ? report.envPat?.prefix ?? null : report.active === "stored_pat" ? report.storedPat?.prefix ?? null : null,
63443
63622
  control_plane_url: controlPlaneBaseUrl(usingSession ? readAuth() : null),
63444
63623
  session: report.session ? {
63445
63624
  present: true,
63446
63625
  expired: report.session.expired,
63447
- refreshable: report.session.refreshable
63448
- } : { present: false, expired: false, refreshable: false },
63626
+ refreshable: report.session.refreshable,
63627
+ refresh_error: report.session.refreshError
63628
+ } : { present: false, expired: false, refreshable: false, refresh_error: null },
63449
63629
  stored_pat: report.storedPat ? { present: true, name: report.storedPat.name } : { present: false, name: null },
63450
63630
  reason
63451
63631
  };
@@ -63463,13 +63643,10 @@ function cardRows(report) {
63463
63643
  }
63464
63644
  break;
63465
63645
  case "session":
63466
- rows.push([
63467
- "credential",
63468
- report.session?.refreshable ? "login session (auth.json) — expired, renews on next use" : "login session (auth.json)"
63469
- ]);
63646
+ rows.push(["credential", "login session (auth.json)"]);
63470
63647
  break;
63471
63648
  case "stored_pat":
63472
- rows.push(["credential", "stored PAT (token.json)"]);
63649
+ rows.push(["credential", "saved PAT (token.json)"]);
63473
63650
  rows.push([
63474
63651
  "token",
63475
63652
  report.storedPat?.name ? `${displayPrefix(report.storedPat.prefix)} (${report.storedPat.name})` : displayPrefix(report.storedPat?.prefix ?? "?")
@@ -63483,26 +63660,30 @@ function cardRows(report) {
63483
63660
  }
63484
63661
  }
63485
63662
  if (report.session && report.active !== "session") {
63663
+ const state = !report.session.expired ? "not used, env PAT wins" : report.session.refreshError ? `expired, renewal failed: ${report.session.refreshError}` : "expired";
63486
63664
  rows.push([
63487
63665
  "session",
63488
- `${report.session.email ?? report.session.userId} (${report.session.expired ? "expired" : "not used — env PAT wins"})`
63666
+ `${report.session.email ?? report.session.userId} (${state})`
63489
63667
  ]);
63490
63668
  }
63491
63669
  return rows;
63492
63670
  }
63493
63671
  async function runWhoami(args = {}) {
63672
+ await settleSession();
63494
63673
  const report = describeCredentials();
63495
- const reason = reasonFor(report);
63496
- process.exitCode = exitCodeFor(report);
63674
+ const token = activePat(report);
63675
+ const pat = token ? await lookupPat(token) : { kind: "unknown" };
63676
+ const reason = reasonFor(report, pat);
63677
+ process.exitCode = usableForControlPlane(report, pat) ? 0 : 1;
63497
63678
  if (args.json) {
63498
- console.log(JSON.stringify(toJson(report, reason), null, 2));
63679
+ console.log(JSON.stringify(toJson(report, pat, reason), null, 2));
63499
63680
  return;
63500
63681
  }
63501
63682
  const signedIn = report.active !== null;
63502
63683
  await showIdentityCard({
63503
63684
  title: signedIn ? "WHOAMI" : "NOT SIGNED IN",
63504
63685
  tone: process.exitCode === 0 ? "ok" : "warn",
63505
- email: report.active === "session" ? report.session?.email ?? report.session?.userId ?? null : null,
63686
+ email: report.active === "session" ? report.session?.email ?? report.session?.userId ?? null : pat.kind === "ok" ? pat.email ?? pat.userId : null,
63506
63687
  controlPlaneUrl: signedIn ? controlPlaneBaseUrl(report.active === "session" ? readAuth() : null) : null,
63507
63688
  expiresAt: report.active === "session" ? report.session?.expiresAt : null,
63508
63689
  rows: cardRows(report),
@@ -75158,22 +75339,13 @@ function TokenCreatedCard(props) {
75158
75339
  children: [
75159
75340
  "saved to ",
75160
75341
  props.storedAt,
75161
- " (mode 0600) — covers ",
75162
- REGISTRY_COMMANDS
75342
+ " (mode 0600), used whenever no login session is usable"
75163
75343
  ]
75164
75344
  }, undefined, true, undefined, this),
75165
75345
  /* @__PURE__ */ jsx_dev_runtime17.jsxDEV(Text, {
75166
75346
  dimColor: true,
75167
- children: [
75168
- CONTROL_PLANE_COMMANDS,
75169
- " do ",
75170
- /* @__PURE__ */ jsx_dev_runtime17.jsxDEV(Text, {
75171
- bold: true,
75172
- children: "not"
75173
- }, undefined, false, undefined, this),
75174
- " read that file — export it instead:"
75175
- ]
75176
- }, undefined, true, undefined, this)
75347
+ children: "for CI or another machine, export it instead:"
75348
+ }, undefined, false, undefined, this)
75177
75349
  ]
75178
75350
  }, undefined, true, undefined, this)
75179
75351
  ]
@@ -75363,7 +75535,7 @@ function sessionWouldBeUsed() {
75363
75535
  const { ok, session } = authStatus();
75364
75536
  if (ok)
75365
75537
  return true;
75366
- return Boolean(session?.refresh_token && session.supabase_url && session.supabase_anon_key);
75538
+ return Boolean(session && isSessionRefreshable(session));
75367
75539
  }
75368
75540
  function withLoginHint(error) {
75369
75541
  if (!(error instanceof ApiError) || error.status !== 403)
@@ -75397,6 +75569,7 @@ async function runTokenCreate(args) {
75397
75569
  } else {
75398
75570
  scopes = args.scopes;
75399
75571
  }
75572
+ const minter = readAuth()?.user_id;
75400
75573
  const spinner2 = de();
75401
75574
  spinner2.start("Creating token…");
75402
75575
  let created;
@@ -75411,7 +75584,10 @@ async function runTokenCreate(args) {
75411
75584
  }
75412
75585
  spinner2.stop("Token created.");
75413
75586
  try {
75414
- writeToken(created.token, name);
75587
+ if (readAuth()?.user_id !== minter) {
75588
+ throw new Error("the login changed while it was being created");
75589
+ }
75590
+ writeToken(created.token, name, minter);
75415
75591
  } catch (error) {
75416
75592
  throw new Error(`Created token ${created.id}, but could not save it locally: ${error.message}
75417
75593
  ` + `This is the only time it is shown — copy it now:
@@ -96053,11 +96229,6 @@ var COMMANDS3 = [
96053
96229
  "help",
96054
96230
  "version"
96055
96231
  ];
96056
- var STORED_PAT_COMMANDS = new Set([
96057
- "template",
96058
- "skill",
96059
- "token"
96060
- ]);
96061
96232
  var SUBCOMMAND_OWNED_FLAGS = {
96062
96233
  token: ["--scope", "--name"]
96063
96234
  };
@@ -96244,7 +96415,7 @@ function help() {
96244
96415
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_API_URL")} legacy KLS host override (uses /api/cli; proxy/registry fallback)`);
96245
96416
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_PROXY_URL")} override the model-proxy URL used by harness traffic (default https://api.v1.brainbaselabs.com)`);
96246
96417
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_REGISTRY_URL")} override the registry API URL (default https://api.v1.brainbaselabs.com)`);
96247
- out.push(` ${import_picocolors80.default.dim("BRAINBASE_TOKEN")} long-lived CLI PAT; the only PAT control-plane commands accept (token.json is not read there)`);
96418
+ out.push(` ${import_picocolors80.default.dim("BRAINBASE_TOKEN")} long-lived CLI PAT; outranks the login session and the PAT saved in token.json`);
96248
96419
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_SKIP_AUTH")} bypass the auth gate for development`);
96249
96420
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_NON_INTERACTIVE")} force non-interactive mode — skip/auto-default prompts (CI & agents)`);
96250
96421
  out.push(` ${import_picocolors80.default.dim("BRAINBASE_RUN_ENTRYPOINT")} =1 → agent pull executes the agent entrypoint (sandbox boots; or pass --run-entrypoint)`);
@@ -96397,12 +96568,12 @@ async function requireAuth(cmd) {
96397
96568
  if (envToken && envToken.trim())
96398
96569
  return;
96399
96570
  let status = authStatus();
96400
- if (!status.ok && status.session?.refresh_token) {
96571
+ if (!status.ok && status.session) {
96401
96572
  let refreshed;
96402
96573
  try {
96403
96574
  refreshed = await tryRefreshSession(status.session);
96404
96575
  } catch (error2) {
96405
- if (error2 instanceof AuthRefreshLockTimeoutError && STORED_PAT_COMMANDS.has(cmd) && readToken()) {
96576
+ if (error2 instanceof AuthRefreshLockTimeoutError && storedPatFor(status.session)) {
96406
96577
  return;
96407
96578
  }
96408
96579
  throw error2;
@@ -96413,16 +96584,13 @@ async function requireAuth(cmd) {
96413
96584
  }
96414
96585
  if (status.ok)
96415
96586
  return;
96416
- if (STORED_PAT_COMMANDS.has(cmd) && readToken())
96587
+ if (storedPatFor(status.session))
96417
96588
  return;
96418
96589
  console.error("");
96419
96590
  console.error(` ${brandTint("◆")} ${import_picocolors80.default.bold("brainbase")}`);
96420
96591
  console.error("");
96421
96592
  console.error(` ${import_picocolors80.default.red("✗")} You need to sign in to use ${import_picocolors80.default.bold("brainbase " + cmd)}.`);
96422
- if (status.reason)
96423
- console.error(` ${import_picocolors80.default.dim(status.reason)}`);
96424
- console.error("");
96425
- console.error(` Run ${import_picocolors80.default.cyan("brainbase login")} to connect this device.`);
96593
+ console.error(` ${import_picocolors80.default.dim(noUsableCredentialMessage())}`);
96426
96594
  console.error("");
96427
96595
  process14.exit(1);
96428
96596
  }
@@ -96706,7 +96874,7 @@ ${didYouMean(cmd, COMMANDS3, "brainbase")}`);
96706
96874
  } catch (err) {
96707
96875
  console.error(import_picocolors80.default.red(`
96708
96876
  ${err.message}`));
96709
- if (err instanceof ApiError && err.status === 401) {
96877
+ if (err instanceof ApiError && err.status === 401 && !err.message.includes("brainbase login")) {
96710
96878
  console.error(` Run ${import_picocolors80.default.cyan("brainbase login")} to connect this device.`);
96711
96879
  }
96712
96880
  if (process14.env.BRAINBASE_DEBUG)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brainbase-labs/cli",
3
- "version": "0.47.0",
3
+ "version": "0.48.0",
4
4
  "description": "Pack, share, and install agent templates across harnesses (Claude Code, Codex, ...).",
5
5
  "type": "module",
6
6
  "bin": {