@braingrid/cli 0.2.25 → 0.2.26

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/CHANGELOG.md CHANGED
@@ -7,6 +7,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.26] - 2025-01-20
11
+
12
+ ### Added
13
+
14
+ - **BRAINGRID_API_TOKEN environment variable support**
15
+ - Enables authentication via JWT token for sandbox/CI environments
16
+ - Allows CLI usage without interactive OAuth login flow
17
+ - Useful for automated pipelines and testing scenarios
18
+
19
+ ### Changed
20
+
21
+ - **README documentation updates**
22
+ - Added `requirement create-branch` and `requirement review` commands to docs
23
+ - Updated shell completion subcommands list
24
+
10
25
  ## [0.2.25] - 2025-12-22
11
26
 
12
27
  ### Added
package/README.md CHANGED
@@ -213,6 +213,8 @@ braingrid requirement update [id] [--status IDEA|PLANNED|IN_PROGRESS|REVIEW|COMP
213
213
  braingrid requirement delete [id] [--force]
214
214
  braingrid requirement breakdown [id]
215
215
  braingrid requirement build [id] [--format markdown|json|xml]
216
+ braingrid requirement create-branch [id] [--name <branch-name>] [--base <branch>]
217
+ braingrid requirement review [id] [--pr <number>]
216
218
 
217
219
  # Working with a different project:
218
220
  braingrid requirement list -p PROJ-456 [--status PLANNED]
@@ -226,6 +228,10 @@ braingrid requirement create -p PROJ-456 --name "Description"
226
228
  > **Note:** The `-r`/`--requirement` parameter is optional and accepts formats like `REQ-456`, `req-456`, or `456`. The CLI will automatically detect the requirement ID from your git branch name (e.g., `feature/REQ-123-description` or `REQ-123-fix-bug`) if it is not provided.
227
229
  >
228
230
  > **Note:** The `requirement list` command displays requirements with their status, name, branch (if assigned), and progress percentage.
231
+ >
232
+ > **Note:** The `create-branch` command creates a GitHub branch for a requirement. It auto-generates a branch name in the format `{username}/REQ-123-slug` if not provided.
233
+ >
234
+ > **Note:** The `review` command runs an AI-powered acceptance review on a pull request, validating it against the requirement's acceptance criteria. It auto-detects the PR number from the current branch if not provided.
229
235
 
230
236
  ### Task Commands
231
237
 
@@ -309,7 +315,7 @@ eval "$(braingrid completion zsh)"
309
315
  ### What Gets Completed
310
316
 
311
317
  - **Commands**: `login`, `logout`, `project`, `requirement`, `task`, etc.
312
- - **Subcommands**: `list`, `show`, `create`, `update`, `delete`, `breakdown`, `build`
318
+ - **Subcommands**: `list`, `show`, `create`, `update`, `delete`, `breakdown`, `build`, `create-branch`, `review`
313
319
  - **Options**: `--help`, `--format`, `--status`, `--project`, `--requirement`
314
320
  - **Values**: Status values (`IDEA`, `PLANNED`, `IN_PROGRESS`, etc.), format options (`table`, `json`, `xml`, `markdown`)
315
321
 
package/dist/cli.js CHANGED
@@ -23,6 +23,81 @@ import chalk6 from "chalk";
23
23
  // src/utils/axios-with-auth.ts
24
24
  import axios from "axios";
25
25
 
26
+ // src/build-config.ts
27
+ var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
28
+ var CLI_VERSION = true ? "0.2.26" : "0.0.0-test";
29
+ var PRODUCTION_CONFIG = {
30
+ apiUrl: "https://app.braingrid.ai",
31
+ workosAuthUrl: "https://auth.braingrid.ai",
32
+ workosClientId: "client_01K6H010C9K69HSDPM9CQM85S7"
33
+ };
34
+ var DEVELOPMENT_CONFIG = {
35
+ apiUrl: "https://app.dev.braingrid.ai",
36
+ workosAuthUrl: "https://balanced-celebration-78-staging.authkit.app",
37
+ workosClientId: "client_01K6H04GF21T4JXNS3JDQM3YNE"
38
+ };
39
+
40
+ // src/utils/config.ts
41
+ var BRAINGRID_API_TOKEN = process.env.BRAINGRID_API_TOKEN;
42
+ function getConfig() {
43
+ const baseConfig = BUILD_ENV === "production" ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG;
44
+ let apiUrl = baseConfig.apiUrl;
45
+ if (BUILD_ENV === "development") {
46
+ if (process.env.NODE_ENV === "local" || process.env.NODE_ENV === "test") {
47
+ apiUrl = "http://localhost:3377";
48
+ } else if (process.env.NODE_ENV === "development") {
49
+ apiUrl = DEVELOPMENT_CONFIG.apiUrl;
50
+ }
51
+ }
52
+ const getWorkOSAuthUrl = () => {
53
+ if (process.env.WORKOS_AUTH_URL) {
54
+ return process.env.WORKOS_AUTH_URL;
55
+ }
56
+ if (BUILD_ENV === "production") {
57
+ return PRODUCTION_CONFIG.workosAuthUrl;
58
+ }
59
+ const env = process.env.NODE_ENV || "development";
60
+ if (env === "local" || env === "test" || env === "development" || env === "staging") {
61
+ return DEVELOPMENT_CONFIG.workosAuthUrl;
62
+ }
63
+ return PRODUCTION_CONFIG.workosAuthUrl;
64
+ };
65
+ const getOAuthClientId = () => {
66
+ if (process.env.WORKOS_CLIENT_ID) {
67
+ return process.env.WORKOS_CLIENT_ID;
68
+ }
69
+ if (BUILD_ENV === "production") {
70
+ return PRODUCTION_CONFIG.workosClientId;
71
+ }
72
+ const env = process.env.NODE_ENV || "development";
73
+ if (env === "local" || env === "test" || env === "development" || env === "staging") {
74
+ return DEVELOPMENT_CONFIG.workosClientId;
75
+ }
76
+ return PRODUCTION_CONFIG.workosClientId;
77
+ };
78
+ const getWebAppUrl = () => {
79
+ if (process.env.BRAINGRID_WEB_URL) {
80
+ return process.env.BRAINGRID_WEB_URL;
81
+ }
82
+ if (BUILD_ENV === "production") {
83
+ return PRODUCTION_CONFIG.apiUrl;
84
+ }
85
+ const env = process.env.NODE_ENV || "development";
86
+ if (env === "local" || env === "test") {
87
+ return "http://localhost:3377";
88
+ }
89
+ return DEVELOPMENT_CONFIG.apiUrl;
90
+ };
91
+ return {
92
+ apiUrl: process.env.BRAINGRID_API_URL || apiUrl,
93
+ organizationId: process.env.BRAINGRID_ORG_ID,
94
+ clientId: process.env.BRAINGRID_CLIENT_ID || "braingrid-cli",
95
+ oauthClientId: getOAuthClientId(),
96
+ getWorkOSAuthUrl,
97
+ getWebAppUrl
98
+ };
99
+ }
100
+
26
101
  // src/utils/logger.ts
27
102
  import fs from "fs";
28
103
  import path from "path";
@@ -211,6 +286,10 @@ function createAuthenticatedAxios(auth) {
211
286
  });
212
287
  instance.interceptors.request.use(
213
288
  async (config) => {
289
+ if (BRAINGRID_API_TOKEN) {
290
+ config.headers.Authorization = `Bearer ${BRAINGRID_API_TOKEN}`;
291
+ return config;
292
+ }
214
293
  const session = await auth.getStoredSession();
215
294
  if (session) {
216
295
  config.headers.Authorization = `Bearer ${session.sealed_session}`;
@@ -251,6 +330,11 @@ function createAuthenticatedAxios(auth) {
251
330
  const isRedirectToAuth = isAuthRedirect(error);
252
331
  if ((is401 || isRedirectToAuth) && !originalRequest._retry) {
253
332
  originalRequest._retry = true;
333
+ if (BRAINGRID_API_TOKEN) {
334
+ logger.warn("[AUTH] Sandbox token expired or invalid");
335
+ const sandboxError = new Error(auth.getSandboxExpiredMessage());
336
+ return Promise.reject(sandboxError);
337
+ }
254
338
  if (isRedirectToAuth) {
255
339
  logger.debug("[AUTH] Received redirect to auth endpoint - token likely expired");
256
340
  } else {
@@ -422,82 +506,6 @@ import { createHash, randomBytes } from "crypto";
422
506
  import { createServer } from "http";
423
507
  import open from "open";
424
508
  import axios3, { AxiosError as AxiosError2 } from "axios";
425
-
426
- // src/build-config.ts
427
- var BUILD_ENV = true ? "production" : process.env.NODE_ENV === "test" ? "development" : "production";
428
- var CLI_VERSION = true ? "0.2.25" : "0.0.0-test";
429
- var PRODUCTION_CONFIG = {
430
- apiUrl: "https://app.braingrid.ai",
431
- workosAuthUrl: "https://auth.braingrid.ai",
432
- workosClientId: "client_01K6H010C9K69HSDPM9CQM85S7"
433
- };
434
- var DEVELOPMENT_CONFIG = {
435
- apiUrl: "https://app.dev.braingrid.ai",
436
- workosAuthUrl: "https://balanced-celebration-78-staging.authkit.app",
437
- workosClientId: "client_01K6H04GF21T4JXNS3JDQM3YNE"
438
- };
439
-
440
- // src/utils/config.ts
441
- function getConfig() {
442
- const baseConfig = BUILD_ENV === "production" ? PRODUCTION_CONFIG : DEVELOPMENT_CONFIG;
443
- let apiUrl = baseConfig.apiUrl;
444
- if (BUILD_ENV === "development") {
445
- if (process.env.NODE_ENV === "local" || process.env.NODE_ENV === "test") {
446
- apiUrl = "http://localhost:3377";
447
- } else if (process.env.NODE_ENV === "development") {
448
- apiUrl = DEVELOPMENT_CONFIG.apiUrl;
449
- }
450
- }
451
- const getWorkOSAuthUrl = () => {
452
- if (process.env.WORKOS_AUTH_URL) {
453
- return process.env.WORKOS_AUTH_URL;
454
- }
455
- if (BUILD_ENV === "production") {
456
- return PRODUCTION_CONFIG.workosAuthUrl;
457
- }
458
- const env = process.env.NODE_ENV || "development";
459
- if (env === "local" || env === "test" || env === "development" || env === "staging") {
460
- return DEVELOPMENT_CONFIG.workosAuthUrl;
461
- }
462
- return PRODUCTION_CONFIG.workosAuthUrl;
463
- };
464
- const getOAuthClientId = () => {
465
- if (process.env.WORKOS_CLIENT_ID) {
466
- return process.env.WORKOS_CLIENT_ID;
467
- }
468
- if (BUILD_ENV === "production") {
469
- return PRODUCTION_CONFIG.workosClientId;
470
- }
471
- const env = process.env.NODE_ENV || "development";
472
- if (env === "local" || env === "test" || env === "development" || env === "staging") {
473
- return DEVELOPMENT_CONFIG.workosClientId;
474
- }
475
- return PRODUCTION_CONFIG.workosClientId;
476
- };
477
- const getWebAppUrl = () => {
478
- if (process.env.BRAINGRID_WEB_URL) {
479
- return process.env.BRAINGRID_WEB_URL;
480
- }
481
- if (BUILD_ENV === "production") {
482
- return PRODUCTION_CONFIG.apiUrl;
483
- }
484
- const env = process.env.NODE_ENV || "development";
485
- if (env === "local" || env === "test") {
486
- return "http://localhost:3377";
487
- }
488
- return DEVELOPMENT_CONFIG.apiUrl;
489
- };
490
- return {
491
- apiUrl: process.env.BRAINGRID_API_URL || apiUrl,
492
- organizationId: process.env.BRAINGRID_ORG_ID,
493
- clientId: process.env.BRAINGRID_CLIENT_ID || "braingrid-cli",
494
- oauthClientId: getOAuthClientId(),
495
- getWorkOSAuthUrl,
496
- getWebAppUrl
497
- };
498
- }
499
-
500
- // src/services/oauth2-auth.ts
501
509
  var logger2 = getLogger();
502
510
  var OAuth2Handler = class {
503
511
  /**
@@ -1000,6 +1008,7 @@ var KEYCHAIN_SERVICE = "braingrid-cli";
1000
1008
  var KEYCHAIN_ACCOUNT = "session";
1001
1009
  var GITHUB_KEYCHAIN_ACCOUNT = "github-token";
1002
1010
  var BraingridAuth = class {
1011
+ // Cached session from env token
1003
1012
  constructor(baseUrl) {
1004
1013
  this.lastValidationTime = 0;
1005
1014
  this.lastValidationResult = false;
@@ -1011,11 +1020,103 @@ var BraingridAuth = class {
1011
1020
  this.oauthHandler = null;
1012
1021
  // Store refresh token separately
1013
1022
  this.logger = getLogger();
1023
+ this.envTokenSession = null;
1014
1024
  const config = getConfig();
1015
1025
  this.baseUrl = baseUrl || config.apiUrl || "https://app.braingrid.ai";
1016
1026
  }
1027
+ /**
1028
+ * Check if CLI is using BRAINGRID_API_TOKEN environment variable
1029
+ * This is used in sandbox environments where tokens are injected
1030
+ */
1031
+ isUsingEnvToken() {
1032
+ return Boolean(BRAINGRID_API_TOKEN);
1033
+ }
1034
+ /**
1035
+ * Get the env token value (for use in axios interceptor)
1036
+ */
1037
+ getEnvToken() {
1038
+ return BRAINGRID_API_TOKEN;
1039
+ }
1040
+ /**
1041
+ * Fetch user profile from server using the env token
1042
+ * This is used to populate session data when using BRAINGRID_API_TOKEN
1043
+ */
1044
+ async fetchProfileFromServer() {
1045
+ if (!BRAINGRID_API_TOKEN) {
1046
+ return null;
1047
+ }
1048
+ try {
1049
+ this.logger.debug("[AUTH] Fetching profile using BRAINGRID_API_TOKEN");
1050
+ const response = await axiosWithRetry(
1051
+ {
1052
+ url: `${this.baseUrl}/api/v1/profile`,
1053
+ method: "POST",
1054
+ headers: {
1055
+ "Content-Type": "application/json",
1056
+ Authorization: `Bearer ${BRAINGRID_API_TOKEN}`
1057
+ },
1058
+ maxRedirects: 0,
1059
+ validateStatus: (status) => status < 500
1060
+ },
1061
+ {
1062
+ maxRetries: 2,
1063
+ initialDelay: 500
1064
+ }
1065
+ );
1066
+ if (response.status !== 200) {
1067
+ this.logger.warn(`[AUTH] Profile fetch failed with status ${response.status}`);
1068
+ return null;
1069
+ }
1070
+ const profileData = response.data;
1071
+ if (profileData.error || !profileData.user || !profileData.organization) {
1072
+ this.logger.warn("[AUTH] Profile response missing user or organization data");
1073
+ return null;
1074
+ }
1075
+ const user = {
1076
+ object: "user",
1077
+ id: profileData.user.id,
1078
+ email: profileData.user.email,
1079
+ emailVerified: true,
1080
+ firstName: profileData.user.firstName || "",
1081
+ lastName: profileData.user.lastName || "",
1082
+ profilePictureUrl: profileData.user.avatar || "",
1083
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
1084
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1085
+ lastSignInAt: (/* @__PURE__ */ new Date()).toISOString(),
1086
+ externalId: null,
1087
+ metadata: {
1088
+ username: profileData.user.username,
1089
+ organizationId: profileData.organization.id,
1090
+ organizationName: profileData.organization.name
1091
+ }
1092
+ };
1093
+ const session = {
1094
+ user,
1095
+ sealed_session: BRAINGRID_API_TOKEN,
1096
+ organization_id: profileData.organization.id,
1097
+ created_at: /* @__PURE__ */ new Date(),
1098
+ updated_at: /* @__PURE__ */ new Date(),
1099
+ login_time: /* @__PURE__ */ new Date()
1100
+ };
1101
+ this.envTokenSession = session;
1102
+ this.logger.debug("[AUTH] Successfully fetched profile from env token");
1103
+ return session;
1104
+ } catch (error) {
1105
+ this.logger.error("[AUTH] Error fetching profile with env token:", { error });
1106
+ return null;
1107
+ }
1108
+ }
1017
1109
  async isAuthenticated(forceValidation = false) {
1018
1110
  try {
1111
+ if (BRAINGRID_API_TOKEN) {
1112
+ this.logger.debug("[AUTH] Using BRAINGRID_API_TOKEN (sandbox mode)");
1113
+ if (isJWTExpired(BRAINGRID_API_TOKEN)) {
1114
+ this.logger.warn("[AUTH] Sandbox session expired");
1115
+ return false;
1116
+ }
1117
+ const session2 = await this.getStoredSession();
1118
+ return session2 !== null;
1119
+ }
1019
1120
  const session = await this.getStoredSession();
1020
1121
  if (!session) {
1021
1122
  this.logger.debug("[AUTH] No stored session found");
@@ -1149,6 +1250,12 @@ var BraingridAuth = class {
1149
1250
  }
1150
1251
  }
1151
1252
  async getStoredSession() {
1253
+ if (BRAINGRID_API_TOKEN) {
1254
+ if (this.envTokenSession) {
1255
+ return this.envTokenSession;
1256
+ }
1257
+ return await this.fetchProfileFromServer();
1258
+ }
1152
1259
  try {
1153
1260
  const sessionData = await credentialStore.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
1154
1261
  if (!sessionData) return null;
@@ -1177,12 +1284,23 @@ var BraingridAuth = class {
1177
1284
  this.loginTime = nowMs;
1178
1285
  }
1179
1286
  async clearSession() {
1287
+ if (BRAINGRID_API_TOKEN) {
1288
+ this.logger.debug("[AUTH] Session managed by sandbox environment - clear is no-op");
1289
+ this.envTokenSession = null;
1290
+ return;
1291
+ }
1180
1292
  await credentialStore.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
1181
1293
  await credentialStore.deletePassword(KEYCHAIN_SERVICE, "refresh-token");
1182
1294
  this.refreshTokenValue = void 0;
1183
1295
  this.lastValidationTime = 0;
1184
1296
  this.lastValidationResult = false;
1185
1297
  }
1298
+ /**
1299
+ * Get sandbox-specific error message for expired/invalid tokens
1300
+ */
1301
+ getSandboxExpiredMessage() {
1302
+ return "Sandbox session expired. Your sandbox environment has a 5-hour limit.\nPlease create a new sandbox to continue.";
1303
+ }
1186
1304
  async handleAuthenticationError() {
1187
1305
  this.lastValidationTime = 0;
1188
1306
  this.lastValidationResult = false;
@@ -5317,6 +5435,14 @@ function getAuth() {
5317
5435
  async function handleLogin() {
5318
5436
  try {
5319
5437
  const auth = getAuth();
5438
+ if (BRAINGRID_API_TOKEN) {
5439
+ return {
5440
+ success: true,
5441
+ message: chalk9.blue(
5442
+ "\u2139\uFE0F Using BRAINGRID_API_TOKEN - already authenticated via sandbox environment."
5443
+ )
5444
+ };
5445
+ }
5320
5446
  console.log(chalk9.blue("\u{1F510} Starting OAuth2 authentication flow..."));
5321
5447
  console.log(chalk9.dim("Your browser will open to complete authentication.\n"));
5322
5448
  const gitUser = await getGitUser();
@@ -5350,6 +5476,12 @@ async function handleLogin() {
5350
5476
  async function handleLogout() {
5351
5477
  try {
5352
5478
  const auth = getAuth();
5479
+ if (BRAINGRID_API_TOKEN) {
5480
+ return {
5481
+ success: true,
5482
+ message: chalk9.blue("\u2139\uFE0F Session managed by sandbox environment - logout not required.")
5483
+ };
5484
+ }
5353
5485
  await auth.clearSession();
5354
5486
  return {
5355
5487
  success: true,
@@ -5367,6 +5499,12 @@ async function handleWhoami() {
5367
5499
  const auth = getAuth();
5368
5500
  const isAuthenticated = await auth.isAuthenticated();
5369
5501
  if (!isAuthenticated) {
5502
+ if (BRAINGRID_API_TOKEN) {
5503
+ return {
5504
+ success: false,
5505
+ message: chalk9.red(auth.getSandboxExpiredMessage())
5506
+ };
5507
+ }
5370
5508
  return {
5371
5509
  success: false,
5372
5510
  message: chalk9.yellow("\u26A0\uFE0F Not logged in. Run `braingrid login` to authenticate.")
@@ -5389,8 +5527,13 @@ async function handleWhoami() {
5389
5527
  `;
5390
5528
  output += `${chalk9.bold("Org ID:")} ${session.organization_id}
5391
5529
  `;
5392
- output += `${chalk9.bold("Session:")} ${new Date(session.created_at).toLocaleString()}
5530
+ if (BRAINGRID_API_TOKEN) {
5531
+ output += `${chalk9.bold("Auth:")} ${chalk9.cyan("Sandbox API Token")}
5393
5532
  `;
5533
+ } else {
5534
+ output += `${chalk9.bold("Session:")} ${new Date(session.created_at).toLocaleString()}
5535
+ `;
5536
+ }
5394
5537
  return {
5395
5538
  success: true,
5396
5539
  message: output,