@bragduck/cli 2.38.2 → 2.39.1

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.
@@ -104,6 +104,14 @@ var init_constants = __esm({
104
104
  ORGANISATIONS: {
105
105
  LIST: "/v1/users/{userId}/organisations"
106
106
  },
107
+ STORIES: {
108
+ LIST: "/v1/users/{userId}/stories",
109
+ CREATE: "/v1/users/{userId}/stories",
110
+ DELETE: "/v1/users/{userId}/stories/{storyId}"
111
+ },
112
+ INTEGRATIONS: {
113
+ ACCESS_TOKEN: "/v1/integrations/{provider}/access-token"
114
+ },
107
115
  VERSION: "/v1/cli/version",
108
116
  /**
109
117
  * @deprecated Use BRAGS.REFINE instead
@@ -188,7 +196,7 @@ var init_env_loader = __esm({
188
196
  });
189
197
 
190
198
  // src/utils/errors.ts
191
- var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError, GoogleDocsError, SlackError;
199
+ var BragduckError, AuthenticationError, GitError, ApiError, NetworkError, ValidationError, OAuthError, TokenExpiredError, GitHubError, BitbucketError, GitLabError, JiraError, ConfluenceError, AtlassianError, GoogleDocsError, SlackError, IntegrationError;
192
200
  var init_errors = __esm({
193
201
  "src/utils/errors.ts"() {
194
202
  "use strict";
@@ -297,6 +305,12 @@ var init_errors = __esm({
297
305
  this.name = "SlackError";
298
306
  }
299
307
  };
308
+ IntegrationError = class extends BragduckError {
309
+ constructor(message, details) {
310
+ super(message, "INTEGRATION_ERROR", details);
311
+ this.name = "IntegrationError";
312
+ }
313
+ };
300
314
  }
301
315
  });
302
316
 
@@ -1295,8 +1309,8 @@ __export(version_exports, {
1295
1309
  import { readFileSync as readFileSync6 } from "fs";
1296
1310
  import { fileURLToPath as fileURLToPath7 } from "url";
1297
1311
  import { dirname as dirname6, join as join8 } from "path";
1298
- import chalk4 from "chalk";
1299
- import boxen2 from "boxen";
1312
+ import chalk2 from "chalk";
1313
+ import boxen from "boxen";
1300
1314
  function getCurrentVersion() {
1301
1315
  try {
1302
1316
  const packageJsonPath2 = join8(__dirname7, "../../package.json");
@@ -1356,16 +1370,16 @@ async function checkForUpdates(options = {}) {
1356
1370
  }
1357
1371
  }
1358
1372
  function displayUpdateNotification(currentVersion, latestVersion, critical = false) {
1359
- const urgency = critical ? chalk4.red.bold("CRITICAL UPDATE") : chalk4.yellow.bold("Update Available");
1373
+ const urgency = critical ? chalk2.red.bold("CRITICAL UPDATE") : chalk2.yellow.bold("Update Available");
1360
1374
  const message = `${urgency}
1361
1375
 
1362
- Current version: ${chalk4.dim(currentVersion)}
1363
- Latest version: ${chalk4.green(latestVersion)}
1376
+ Current version: ${chalk2.dim(currentVersion)}
1377
+ Latest version: ${chalk2.green(latestVersion)}
1364
1378
 
1365
- Update with: ${chalk4.cyan("npm install -g @bragduck/cli@latest")}`;
1379
+ Update with: ${chalk2.cyan("npm install -g @bragduck/cli@latest")}`;
1366
1380
  console.log("");
1367
1381
  console.log(
1368
- boxen2(message, {
1382
+ boxen(message, {
1369
1383
  padding: 1,
1370
1384
  margin: { top: 0, right: 1, bottom: 0, left: 1 },
1371
1385
  borderStyle: "round",
@@ -1417,7 +1431,7 @@ function getPlatformInfo() {
1417
1431
  const arch = process.arch;
1418
1432
  return `${platform}-${arch}`;
1419
1433
  }
1420
- var __filename8, __dirname8, ApiService, apiService;
1434
+ var __filename8, __dirname8, REFINE_BATCH_SIZE, CREATE_BATCH_SIZE, inBatches, ApiService, apiService;
1421
1435
  var init_api_service = __esm({
1422
1436
  "src/services/api.service.ts"() {
1423
1437
  "use strict";
@@ -1428,6 +1442,15 @@ var init_api_service = __esm({
1428
1442
  init_logger();
1429
1443
  __filename8 = fileURLToPath8(import.meta.url);
1430
1444
  __dirname8 = dirname7(__filename8);
1445
+ REFINE_BATCH_SIZE = 20;
1446
+ CREATE_BATCH_SIZE = 50;
1447
+ inBatches = (items, size) => {
1448
+ const batches = [];
1449
+ for (let start = 0; start < items.length; start += size) {
1450
+ batches.push(items.slice(start, start + size));
1451
+ }
1452
+ return batches;
1453
+ };
1431
1454
  ApiService = class {
1432
1455
  baseURL;
1433
1456
  client;
@@ -1543,10 +1566,23 @@ var init_api_service = __esm({
1543
1566
  };
1544
1567
  logger.debug(`Refining ${trimmedRequest.brags.length} brags`);
1545
1568
  try {
1546
- const response = await this.makeRequest(API_ENDPOINTS.BRAGS.REFINE, {
1547
- method: "POST",
1548
- body: trimmedRequest
1549
- });
1569
+ const response = {
1570
+ refined_brags: [],
1571
+ processing_time_ms: 0,
1572
+ tokens_used: 0
1573
+ };
1574
+ for (const batch of inBatches(trimmedRequest.brags, REFINE_BATCH_SIZE)) {
1575
+ const batchResponse = await this.makeRequest(
1576
+ API_ENDPOINTS.BRAGS.REFINE,
1577
+ {
1578
+ method: "POST",
1579
+ body: { ...trimmedRequest, brags: batch }
1580
+ }
1581
+ );
1582
+ response.refined_brags.push(...batchResponse.refined_brags);
1583
+ response.processing_time_ms += batchResponse.processing_time_ms;
1584
+ response.tokens_used += batchResponse.tokens_used;
1585
+ }
1550
1586
  logger.debug(`Successfully refined ${response.refined_brags.length} brags`);
1551
1587
  return response;
1552
1588
  } catch (_error) {
@@ -1580,10 +1616,18 @@ var init_api_service = __esm({
1580
1616
  logger.debug(`Creating ${request.brags.length} brags`);
1581
1617
  logger.debug(`Request body: ${JSON.stringify(request, null, 2)}`);
1582
1618
  try {
1583
- const response = await this.makeRequest(API_ENDPOINTS.BRAGS.CREATE, {
1584
- method: "POST",
1585
- body: request
1586
- });
1619
+ const response = { created: 0, brags: [] };
1620
+ for (const batch of inBatches(request.brags, CREATE_BATCH_SIZE)) {
1621
+ const batchResponse = await this.makeRequest(
1622
+ API_ENDPOINTS.BRAGS.CREATE,
1623
+ {
1624
+ method: "POST",
1625
+ body: { ...request, brags: batch }
1626
+ }
1627
+ );
1628
+ response.created += batchResponse.created;
1629
+ response.brags.push(...batchResponse.brags);
1630
+ }
1587
1631
  logger.debug(`Response: ${JSON.stringify(response, null, 2)}`);
1588
1632
  logger.debug(`Successfully created ${response.created} brags`);
1589
1633
  return response;
@@ -1622,6 +1666,70 @@ var init_api_service = __esm({
1622
1666
  throw _error;
1623
1667
  }
1624
1668
  }
1669
+ /**
1670
+ * List user's stories
1671
+ */
1672
+ async listStories(userId, params = {}) {
1673
+ const { page = 1, perPage = 20, search, competency, orgId } = params;
1674
+ logger.debug(`Listing stories: page=${page}, perPage=${perPage}`);
1675
+ try {
1676
+ const queryParams = new URLSearchParams6({
1677
+ page: page.toString(),
1678
+ perPage: perPage.toString()
1679
+ });
1680
+ if (search) queryParams.append("search", search);
1681
+ if (competency) queryParams.append("competency", competency);
1682
+ if (orgId) queryParams.append("orgId", orgId);
1683
+ const url = `${API_ENDPOINTS.STORIES.LIST.replace("{userId}", userId)}?${queryParams.toString()}`;
1684
+ const response = await this.makeRequest(url, {
1685
+ method: "GET"
1686
+ });
1687
+ logger.debug(
1688
+ `Successfully fetched ${response.items.length} stories (total: ${response.total})`
1689
+ );
1690
+ return response;
1691
+ } catch (_error) {
1692
+ logger.debug("Failed to list stories");
1693
+ throw _error;
1694
+ }
1695
+ }
1696
+ /**
1697
+ * Create a story
1698
+ */
1699
+ async createStory(userId, body) {
1700
+ logger.debug(`Creating story: ${body.title}`);
1701
+ try {
1702
+ const url = API_ENDPOINTS.STORIES.CREATE.replace("{userId}", userId);
1703
+ const response = await this.makeRequest(url, {
1704
+ method: "POST",
1705
+ body
1706
+ });
1707
+ logger.debug(`Successfully created story: ${response.id}`);
1708
+ return response;
1709
+ } catch (_error) {
1710
+ logger.debug("Failed to create story");
1711
+ throw _error;
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Delete a story (soft delete)
1716
+ */
1717
+ async deleteStory(userId, storyId) {
1718
+ logger.debug(`Deleting story: ${storyId}`);
1719
+ try {
1720
+ const url = API_ENDPOINTS.STORIES.DELETE.replace("{userId}", userId).replace(
1721
+ "{storyId}",
1722
+ storyId
1723
+ );
1724
+ await this.makeRequest(url, {
1725
+ method: "DELETE"
1726
+ });
1727
+ logger.debug(`Successfully deleted story: ${storyId}`);
1728
+ } catch (_error) {
1729
+ logger.debug("Failed to delete story");
1730
+ throw _error;
1731
+ }
1732
+ }
1625
1733
  /**
1626
1734
  * List user's organisations
1627
1735
  */
@@ -1660,6 +1768,18 @@ var init_api_service = __esm({
1660
1768
  throw error;
1661
1769
  }
1662
1770
  }
1771
+ /**
1772
+ * Get the access token of a tool connected on the BragDuck web app.
1773
+ * The server refreshes it; error statuses are not retried.
1774
+ */
1775
+ async getIntegrationAccessToken(provider) {
1776
+ logger.debug(`Fetching web-connected ${provider} access token`);
1777
+ const url = API_ENDPOINTS.INTEGRATIONS.ACCESS_TOKEN.replace("{provider}", provider);
1778
+ return this.makeRequest(url, {
1779
+ method: "GET",
1780
+ retry: 0
1781
+ });
1782
+ }
1663
1783
  /**
1664
1784
  * Check for CLI updates
1665
1785
  */
@@ -1721,8 +1841,8 @@ import { dirname as dirname8, join as join10 } from "path";
1721
1841
  // src/commands/auth.ts
1722
1842
  init_esm_shims();
1723
1843
  init_auth_service();
1724
- import boxen from "boxen";
1725
- import chalk3 from "chalk";
1844
+ import boxen2 from "boxen";
1845
+ import chalk4 from "chalk";
1726
1846
  import { input } from "@inquirer/prompts";
1727
1847
 
1728
1848
  // src/services/atlassian-auth.service.ts
@@ -2560,20 +2680,123 @@ var GitService = class {
2560
2680
  };
2561
2681
  var gitService = new GitService();
2562
2682
 
2683
+ // src/services/integrations.service.ts
2684
+ init_esm_shims();
2685
+ init_api_service();
2686
+ init_auth_service();
2687
+ init_storage_service();
2688
+ init_errors();
2689
+ init_logger();
2690
+ var WEB_APP_HOST = "app.bragduck.com";
2691
+ var EXPIRY_MARGIN_MS = 60 * 1e3;
2692
+ var PROVIDER_LABELS = {
2693
+ atlassian: "Atlassian",
2694
+ github: "GitHub",
2695
+ slack: "Slack",
2696
+ google: "Google Docs"
2697
+ };
2698
+ function toHost(url) {
2699
+ return url.trim().toLowerCase().replace(/^https?:\/\//, "").replace(/\/+$/, "");
2700
+ }
2701
+ var IntegrationsService = class {
2702
+ cache = /* @__PURE__ */ new Map();
2703
+ loggedSites = /* @__PURE__ */ new Set();
2704
+ /**
2705
+ * Get the access token for a web-connected tool.
2706
+ * Returns null when the user is not logged in to BragDuck or the tool is not connected.
2707
+ */
2708
+ async getConnectedAccessToken(provider) {
2709
+ const cached = this.cache.get(provider);
2710
+ if (cached && this.isFresh(cached)) {
2711
+ return cached;
2712
+ }
2713
+ if (!await authService.isAuthenticated()) {
2714
+ return null;
2715
+ }
2716
+ const label = PROVIDER_LABELS[provider];
2717
+ try {
2718
+ const connection = await apiService.getIntegrationAccessToken(provider);
2719
+ this.cache.set(provider, connection);
2720
+ logger.debug(`Using ${label} connected via web (${connection.accountName})`);
2721
+ return connection;
2722
+ } catch (error) {
2723
+ if (!(error instanceof ApiError)) {
2724
+ throw error;
2725
+ }
2726
+ if (error.statusCode === 404) {
2727
+ logger.debug(`${label} is not connected on the web`);
2728
+ return null;
2729
+ }
2730
+ if (error.statusCode === 409) {
2731
+ throw new IntegrationError(
2732
+ `Your ${label} connection has expired. Reconnect ${label} at ${WEB_APP_HOST}`,
2733
+ { hint: `Reconnect ${label} at https://${WEB_APP_HOST}` }
2734
+ );
2735
+ }
2736
+ if (error.statusCode === 502) {
2737
+ throw new IntegrationError(`Could not refresh your ${label} token. Try again shortly.`, {
2738
+ hint: `If this keeps happening, reconnect ${label} at https://${WEB_APP_HOST}`
2739
+ });
2740
+ }
2741
+ throw error;
2742
+ }
2743
+ }
2744
+ /**
2745
+ * Build Jira/Confluence OAuth credentials from the web-connected Atlassian account.
2746
+ * Uses the site matching the configured instance, otherwise the first site.
2747
+ */
2748
+ async getAtlassianCredentials(service) {
2749
+ const connection = await this.getConnectedAccessToken("atlassian");
2750
+ const sites = connection?.metadata.sites ?? [];
2751
+ if (!connection || sites.length === 0) {
2752
+ return null;
2753
+ }
2754
+ const configuredInstance = await storageService.getConfigWithHierarchy(`${service}Instance`);
2755
+ const matchingSite = configuredInstance ? sites.find((s) => toHost(s.url) === toHost(configuredInstance)) : void 0;
2756
+ const site = matchingSite ?? sites[0];
2757
+ if (sites.length > 1 && !this.loggedSites.has(site.id)) {
2758
+ this.loggedSites.add(site.id);
2759
+ logger.info(`Using Atlassian site ${site.name} (${site.url}) connected via web`);
2760
+ }
2761
+ return {
2762
+ accessToken: connection.accessToken,
2763
+ cloudId: site.id,
2764
+ instanceUrl: site.url,
2765
+ authMethod: "oauth"
2766
+ };
2767
+ }
2768
+ isFresh(connection) {
2769
+ if (!connection.expiresAt) {
2770
+ return true;
2771
+ }
2772
+ return new Date(connection.expiresAt).getTime() - EXPIRY_MARGIN_MS > Date.now();
2773
+ }
2774
+ };
2775
+ var integrationsService = new IntegrationsService();
2776
+
2563
2777
  // src/services/github.service.ts
2564
2778
  var execAsync2 = promisify2(exec2);
2565
2779
  var GitHubService = class {
2566
2780
  MAX_BODY_LENGTH = 5e3;
2567
2781
  PR_SEARCH_FIELDS = "number,title,body,author,mergedAt,additions,deletions,changedFiles,url,labels";
2782
+ // Set when gh has no login of its own and GitHub is connected on the BragDuck web app
2783
+ useWebConnection = false;
2568
2784
  /**
2569
2785
  * Execute a gh CLI command with clean environment
2570
2786
  * Unsets GITHUB_TOKEN and GH_TOKEN to prevent .envrc tokens from interfering
2571
- * with gh CLI's own authentication
2787
+ * with gh CLI's own authentication. When using the web connection, GH_TOKEN
2788
+ * is set to the server-held token instead.
2572
2789
  */
2573
2790
  async execGhCommand(command, cwd) {
2574
2791
  const env = { ...process.env };
2575
2792
  delete env.GITHUB_TOKEN;
2576
2793
  delete env.GH_TOKEN;
2794
+ if (this.useWebConnection) {
2795
+ const connection = await integrationsService.getConnectedAccessToken("github");
2796
+ if (connection) {
2797
+ env.GH_TOKEN = connection.accessToken;
2798
+ }
2799
+ }
2577
2800
  return execAsync2(command, { env, cwd: cwd || process.cwd() });
2578
2801
  }
2579
2802
  /**
@@ -2599,14 +2822,30 @@ var GitHubService = class {
2599
2822
  }
2600
2823
  }
2601
2824
  /**
2602
- * Check if user is authenticated with GitHub CLI
2825
+ * Check if user is authenticated with GitHub CLI, falling back to
2826
+ * the GitHub account connected on the BragDuck web app
2603
2827
  */
2604
2828
  async checkAuthentication() {
2605
2829
  try {
2606
2830
  await this.execGhCommand("command gh auth status");
2607
2831
  return true;
2608
2832
  } catch {
2609
- return false;
2833
+ return this.checkWebConnection();
2834
+ }
2835
+ }
2836
+ /**
2837
+ * Use the server-held GitHub token when GitHub is connected on the web
2838
+ */
2839
+ async checkWebConnection() {
2840
+ try {
2841
+ const connection = await integrationsService.getConnectedAccessToken("github");
2842
+ this.useWebConnection = connection !== null;
2843
+ return this.useWebConnection;
2844
+ } catch (error) {
2845
+ if (error instanceof IntegrationError) {
2846
+ throw new GitHubError(error.message, error.details);
2847
+ }
2848
+ throw error;
2610
2849
  }
2611
2850
  }
2612
2851
  /**
@@ -2630,6 +2869,9 @@ var GitHubService = class {
2630
2869
  return { installed: false, authenticated: false };
2631
2870
  }
2632
2871
  const authenticated = await this.checkAuthentication();
2872
+ if (this.useWebConnection) {
2873
+ return { installed, authenticated, connectedViaWeb: true };
2874
+ }
2633
2875
  return { installed, authenticated };
2634
2876
  }
2635
2877
  /**
@@ -2804,33 +3046,33 @@ init_logger();
2804
3046
 
2805
3047
  // src/ui/theme.ts
2806
3048
  init_esm_shims();
2807
- import chalk2 from "chalk";
3049
+ import chalk3 from "chalk";
2808
3050
  var colors = {
2809
3051
  // Primary colors for main actions and interactive elements
2810
- primary: chalk2.cyan,
3052
+ primary: chalk3.cyan,
2811
3053
  // Success states
2812
- success: chalk2.green,
2813
- successBold: chalk2.green.bold,
3054
+ success: chalk3.green,
3055
+ successBold: chalk3.green.bold,
2814
3056
  // Warning states
2815
- warning: chalk2.yellow,
2816
- warningBold: chalk2.yellow.bold,
3057
+ warning: chalk3.yellow,
3058
+ warningBold: chalk3.yellow.bold,
2817
3059
  // Error states
2818
- error: chalk2.red,
2819
- errorBold: chalk2.red.bold,
3060
+ error: chalk3.red,
3061
+ errorBold: chalk3.red.bold,
2820
3062
  // Info and metadata
2821
- info: chalk2.gray,
2822
- infoDim: chalk2.dim,
3063
+ info: chalk3.gray,
3064
+ infoDim: chalk3.dim,
2823
3065
  // Highlighted/important data
2824
- highlight: chalk2.yellow.bold,
2825
- highlightCyan: chalk2.cyan.bold,
3066
+ highlight: chalk3.yellow.bold,
3067
+ highlightCyan: chalk3.cyan.bold,
2826
3068
  // Text emphasis
2827
- bold: chalk2.bold,
2828
- dim: chalk2.dim,
3069
+ bold: chalk3.bold,
3070
+ dim: chalk3.dim,
2829
3071
  // Special purpose
2830
- white: chalk2.white,
2831
- gray: chalk2.gray,
3072
+ white: chalk3.white,
3073
+ gray: chalk3.gray,
2832
3074
  // Links and URLs
2833
- link: chalk2.blue.underline
3075
+ link: chalk3.blue.underline
2834
3076
  };
2835
3077
  var theme = {
2836
3078
  /**
@@ -2999,13 +3241,13 @@ async function authLogin() {
2999
3241
  const userInfo = authService.getUserInfo();
3000
3242
  if (userInfo) {
3001
3243
  logger.log(
3002
- boxen(
3003
- `${chalk3.yellow("Already authenticated!")}
3244
+ boxen2(
3245
+ `${chalk4.yellow("Already authenticated!")}
3004
3246
 
3005
- ${chalk3.gray("User:")} ${userInfo.name}
3006
- ${chalk3.gray("Email:")} ${userInfo.email}
3247
+ ${chalk4.gray("User:")} ${userInfo.name}
3248
+ ${chalk4.gray("Email:")} ${userInfo.email}
3007
3249
 
3008
- ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out")}`,
3250
+ ${chalk4.dim("Run")} ${chalk4.cyan("bragduck logout")} ${chalk4.dim("to sign out")}`,
3009
3251
  {
3010
3252
  padding: 1,
3011
3253
  margin: 1,
@@ -3022,13 +3264,13 @@ ${chalk3.dim("Run")} ${chalk3.cyan("bragduck logout")} ${chalk3.dim("to sign out
3022
3264
  const userInfo = await authService.login();
3023
3265
  logger.log("");
3024
3266
  logger.log(
3025
- boxen(
3026
- `${chalk3.green.bold("\u2713 Successfully authenticated!")}
3267
+ boxen2(
3268
+ `${chalk4.green.bold("\u2713 Successfully authenticated!")}
3027
3269
 
3028
- ${chalk3.gray("Welcome,")} ${chalk3.cyan(userInfo.name)}
3029
- ${chalk3.gray("Email:")} ${userInfo.email}
3270
+ ${chalk4.gray("Welcome,")} ${chalk4.cyan(userInfo.name)}
3271
+ ${chalk4.gray("Email:")} ${userInfo.email}
3030
3272
 
3031
- ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create brags")}`,
3273
+ ${chalk4.dim("Use")} ${chalk4.cyan("bragduck sync")} ${chalk4.dim("to create brags")}`,
3032
3274
  {
3033
3275
  padding: 1,
3034
3276
  margin: 1,
@@ -3042,7 +3284,7 @@ ${chalk3.dim("Use")} ${chalk3.cyan("bragduck sync")} ${chalk3.dim("to create bra
3042
3284
  const err = error;
3043
3285
  logger.log("");
3044
3286
  logger.log(
3045
- boxen(`${chalk3.red.bold("\u2717 Authentication Failed")}
3287
+ boxen2(`${chalk4.red.bold("\u2717 Authentication Failed")}
3046
3288
 
3047
3289
  ${err.message}`, {
3048
3290
  padding: 1,
@@ -3054,6 +3296,12 @@ ${err.message}`, {
3054
3296
  process.exit(1);
3055
3297
  }
3056
3298
  }
3299
+ async function getWebConnection(provider) {
3300
+ return integrationsService.getConnectedAccessToken(provider).catch((error) => {
3301
+ logger.debug(`Could not check web-connected ${provider}: ${error}`);
3302
+ return null;
3303
+ });
3304
+ }
3057
3305
  async function authStatus() {
3058
3306
  logger.log("");
3059
3307
  logger.info("Authentication Status:");
@@ -3069,9 +3317,14 @@ async function authStatus() {
3069
3317
  logger.log("");
3070
3318
  logger.info("Git Sources:");
3071
3319
  logger.log("");
3072
- const ghStatus = await githubService.getAuthStatus();
3320
+ const ghStatus = await githubService.getAuthStatus().catch((error) => {
3321
+ logger.debug(`Could not check GitHub auth status: ${error}`);
3322
+ return { installed: true, authenticated: false, connectedViaWeb: false };
3323
+ });
3073
3324
  if (ghStatus.installed) {
3074
- if (ghStatus.authenticated) {
3325
+ if (ghStatus.connectedViaWeb) {
3326
+ logger.info(`${colors.success("\u2713")} GitHub: connected via web`);
3327
+ } else if (ghStatus.authenticated) {
3075
3328
  logger.info(`${colors.success("\u2713")} GitHub: Authenticated`);
3076
3329
  } else {
3077
3330
  logger.info(`${colors.error("\u2717")} GitHub: Not authenticated`);
@@ -3116,8 +3369,15 @@ async function authStatus() {
3116
3369
  if (jiraAuth) logger.info(theme.secondary(" \xB7 Jira"));
3117
3370
  if (confluenceAuth) logger.info(theme.secondary(" \xB7 Confluence"));
3118
3371
  } else {
3119
- logger.info(`${colors.error("\u2717")} Atlassian (Jira + Confluence): Not connected`);
3120
- logger.info(theme.secondary(" Run: bragduck auth atlassian"));
3372
+ const webAtlassian = await getWebConnection("atlassian");
3373
+ if (webAtlassian) {
3374
+ logger.info(
3375
+ `${colors.success("\u2713")} Jira/Confluence: connected via web (${webAtlassian.accountName})`
3376
+ );
3377
+ } else {
3378
+ logger.info(`${colors.error("\u2717")} Atlassian (Jira + Confluence): Not connected`);
3379
+ logger.info(theme.secondary(" Run: bragduck auth atlassian"));
3380
+ }
3121
3381
  }
3122
3382
  logger.log("");
3123
3383
  logger.info("Document Sources:");
@@ -3126,8 +3386,15 @@ async function authStatus() {
3126
3386
  if (googleDocsAuth) {
3127
3387
  logger.info(`${colors.success("\u2713")} Google Docs (OAuth)`);
3128
3388
  } else {
3129
- logger.info(`${colors.error("\u2717")} Google Docs: Not connected`);
3130
- logger.info(theme.secondary(" Run: bragduck auth google"));
3389
+ const webGoogle = await getWebConnection("google");
3390
+ if (webGoogle) {
3391
+ logger.info(
3392
+ `${colors.success("\u2713")} Google Docs: connected via web (${webGoogle.accountName})`
3393
+ );
3394
+ } else {
3395
+ logger.info(`${colors.error("\u2717")} Google Docs: Not connected`);
3396
+ logger.info(theme.secondary(" Run: bragduck auth google"));
3397
+ }
3131
3398
  }
3132
3399
  const slackAuth = await storageService.isServiceAuthenticated("slack");
3133
3400
  if (slackAuth) {
@@ -3135,15 +3402,20 @@ async function authStatus() {
3135
3402
  const userLabel = creds?.username ? ` (user: ${creds.username})` : "";
3136
3403
  logger.info(`${colors.success("\u2713")} Slack: Connected${userLabel}`);
3137
3404
  } else {
3138
- logger.info(`${colors.error("\u2717")} Slack: Not connected`);
3139
- logger.info(theme.secondary(" Run: bragduck auth slack"));
3405
+ const webSlack = await getWebConnection("slack");
3406
+ if (webSlack) {
3407
+ logger.info(`${colors.success("\u2713")} Slack: connected via web (${webSlack.accountName})`);
3408
+ } else {
3409
+ logger.info(`${colors.error("\u2717")} Slack: Not connected`);
3410
+ logger.info(theme.secondary(" Run: bragduck auth slack"));
3411
+ }
3140
3412
  }
3141
3413
  logger.log("");
3142
3414
  }
3143
3415
  async function authBitbucket() {
3144
3416
  logger.log("");
3145
3417
  logger.log(
3146
- boxen(
3418
+ boxen2(
3147
3419
  theme.info("Bitbucket API Token Authentication") + "\n\nCreate an API Token at:\n" + colors.highlight("https://bitbucket.org/account/settings/api-token/new") + "\n\nRequired scopes:\n \u2022 pullrequest:read\n \u2022 repository:read\n \u2022 account:read\n\n" + theme.warning("Note: API tokens expire (max 1 year)"),
3148
3420
  boxStyles.info
3149
3421
  )
@@ -3174,7 +3446,7 @@ async function authBitbucket() {
3174
3446
  if (!response.ok) {
3175
3447
  logger.log("");
3176
3448
  logger.log(
3177
- boxen(
3449
+ boxen2(
3178
3450
  theme.error("\u2717 Authentication Failed") + "\n\nInvalid email or API token",
3179
3451
  boxStyles.error
3180
3452
  )
@@ -3195,7 +3467,7 @@ async function authBitbucket() {
3195
3467
  await storageService.setServiceCredentials("bitbucket", credentials);
3196
3468
  logger.log("");
3197
3469
  logger.log(
3198
- boxen(
3470
+ boxen2(
3199
3471
  theme.success("\u2713 Successfully authenticated with Bitbucket") + `
3200
3472
 
3201
3473
  Email: ${email}
@@ -3209,7 +3481,7 @@ User: ${user.display_name}
3209
3481
  const err = error;
3210
3482
  logger.log("");
3211
3483
  logger.log(
3212
- boxen(
3484
+ boxen2(
3213
3485
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3214
3486
  boxStyles.error
3215
3487
  )
@@ -3221,7 +3493,7 @@ User: ${user.display_name}
3221
3493
  async function authGitLab() {
3222
3494
  logger.log("");
3223
3495
  logger.log(
3224
- boxen(
3496
+ boxen2(
3225
3497
  theme.info("GitLab Personal Access Token Authentication") + "\n\nCreate a Personal Access Token at:\n" + colors.highlight("https://gitlab.com/-/profile/personal_access_tokens") + "\n\nRequired scopes:\n \u2022 api (full API access)\n \u2022 read_api (read API)\n \u2022 read_user (read user info)\n\n" + theme.warning("For self-hosted GitLab, check your instance docs"),
3226
3498
  boxStyles.info
3227
3499
  )
@@ -3243,7 +3515,7 @@ async function authGitLab() {
3243
3515
  if (!response.ok) {
3244
3516
  logger.log("");
3245
3517
  logger.log(
3246
- boxen(
3518
+ boxen2(
3247
3519
  theme.error("\u2717 Authentication Failed") + "\n\nInvalid instance URL or access token",
3248
3520
  boxStyles.error
3249
3521
  )
@@ -3259,7 +3531,7 @@ async function authGitLab() {
3259
3531
  await storageService.setServiceCredentials("gitlab", credentials);
3260
3532
  logger.log("");
3261
3533
  logger.log(
3262
- boxen(
3534
+ boxen2(
3263
3535
  theme.success("\u2713 Successfully authenticated with GitLab") + `
3264
3536
 
3265
3537
  Instance: ${instanceUrl}
@@ -3272,7 +3544,7 @@ User: ${user.name} (@${user.username})`,
3272
3544
  const err = error;
3273
3545
  logger.log("");
3274
3546
  logger.log(
3275
- boxen(
3547
+ boxen2(
3276
3548
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3277
3549
  boxStyles.error
3278
3550
  )
@@ -3289,7 +3561,7 @@ async function authAtlassian() {
3289
3561
  const result = await atlassianAuthService.login();
3290
3562
  logger.log("");
3291
3563
  logger.log(
3292
- boxen(
3564
+ boxen2(
3293
3565
  theme.success("\u2713 Successfully authenticated with Atlassian Cloud") + `
3294
3566
 
3295
3567
  Site: ${result.siteName}
@@ -3306,7 +3578,7 @@ Services configured:
3306
3578
  const err = error;
3307
3579
  logger.log("");
3308
3580
  logger.log(
3309
- boxen(
3581
+ boxen2(
3310
3582
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3311
3583
  boxStyles.error
3312
3584
  )
@@ -3323,7 +3595,7 @@ async function authGoogle() {
3323
3595
  const result = await googleAuthService.login();
3324
3596
  logger.log("");
3325
3597
  logger.log(
3326
- boxen(
3598
+ boxen2(
3327
3599
  theme.success("\u2713 Successfully authenticated with Google") + `
3328
3600
 
3329
3601
  Account: ${result.email}
@@ -3338,7 +3610,7 @@ Services configured:
3338
3610
  const err = error;
3339
3611
  logger.log("");
3340
3612
  logger.log(
3341
- boxen(
3613
+ boxen2(
3342
3614
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3343
3615
  boxStyles.error
3344
3616
  )
@@ -3355,7 +3627,7 @@ async function authSlack() {
3355
3627
  const result = await slackAuthService.login();
3356
3628
  logger.log("");
3357
3629
  logger.log(
3358
- boxen(
3630
+ boxen2(
3359
3631
  theme.success("\u2713 Successfully authenticated with Slack") + `
3360
3632
 
3361
3633
  Workspace: ${result.teamName}
@@ -3370,7 +3642,7 @@ Services configured:
3370
3642
  const err = error;
3371
3643
  logger.log("");
3372
3644
  logger.log(
3373
- boxen(
3645
+ boxen2(
3374
3646
  theme.error("\u2717 Authentication Failed") + "\n\n" + (err.message || "Unknown error"),
3375
3647
  boxStyles.error
3376
3648
  )
@@ -3553,8 +3825,8 @@ var SourceDetector = class {
3553
3825
  async checkAuthentication(type) {
3554
3826
  try {
3555
3827
  if (type === "github") {
3556
- await execAsync3("command gh auth status");
3557
- return true;
3828
+ const { authenticated } = await githubService.getAuthStatus();
3829
+ return authenticated;
3558
3830
  } else if (type === "bitbucket" || type === "atlassian") {
3559
3831
  return await storageService.isServiceAuthenticated("bitbucket");
3560
3832
  } else if (type === "gitlab") {
@@ -4303,6 +4575,10 @@ var JiraService = class {
4303
4575
  async getCredentials() {
4304
4576
  const creds = await storageService.getServiceCredentials("jira");
4305
4577
  if (!creds || !creds.accessToken) {
4578
+ const webCreds = await integrationsService.getAtlassianCredentials("jira");
4579
+ if (webCreds) {
4580
+ return webCreds;
4581
+ }
4306
4582
  throw new JiraError("Not authenticated with Jira", {
4307
4583
  hint: "Run: bragduck auth atlassian"
4308
4584
  });
@@ -4429,7 +4705,7 @@ var JiraService = class {
4429
4705
  try {
4430
4706
  await this.request("/rest/api/3/myself");
4431
4707
  } catch (error) {
4432
- if (error instanceof JiraError) {
4708
+ if (error instanceof JiraError || error instanceof IntegrationError) {
4433
4709
  throw error;
4434
4710
  }
4435
4711
  throw new JiraError("Could not access Jira instance via API", {
@@ -4563,7 +4839,8 @@ var JiraService = class {
4563
4839
  "reporter",
4564
4840
  "assignee",
4565
4841
  "status",
4566
- "issuetype"
4842
+ "issuetype",
4843
+ "project"
4567
4844
  ];
4568
4845
  const allIssues = [];
4569
4846
  const seenIssueKeys = /* @__PURE__ */ new Set();
@@ -4740,7 +5017,8 @@ ${truncatedDesc}`;
4740
5017
  externalId: issue.key,
4741
5018
  externalType: contribution?.type || "issue",
4742
5019
  externalSource: "jira",
4743
- externalUrl: url
5020
+ externalUrl: url,
5021
+ sourceName: issue.fields.project?.name || issue.fields.project?.key
4744
5022
  };
4745
5023
  }
4746
5024
  };
@@ -4802,6 +5080,10 @@ var ConfluenceService = class {
4802
5080
  async getCredentials() {
4803
5081
  const creds = await storageService.getServiceCredentials("confluence");
4804
5082
  if (!creds || !creds.accessToken) {
5083
+ const webCreds = await integrationsService.getAtlassianCredentials("confluence");
5084
+ if (webCreds) {
5085
+ return webCreds;
5086
+ }
4805
5087
  throw new ConfluenceError("Not authenticated with Confluence", {
4806
5088
  hint: "Run: bragduck auth atlassian"
4807
5089
  });
@@ -4922,7 +5204,7 @@ var ConfluenceService = class {
4922
5204
  try {
4923
5205
  await this.request("/wiki/rest/api/content?type=page&limit=1");
4924
5206
  } catch (error) {
4925
- if (error instanceof ConfluenceError) {
5207
+ if (error instanceof ConfluenceError || error instanceof IntegrationError) {
4926
5208
  throw error;
4927
5209
  }
4928
5210
  throw new ConfluenceError("Could not access Confluence instance via API", {
@@ -5046,7 +5328,7 @@ var ConfluenceService = class {
5046
5328
  cql,
5047
5329
  start: start.toString(),
5048
5330
  limit: limit.toString(),
5049
- expand: "version,history,children.comment,body.storage"
5331
+ expand: "version,history,children.comment,body.storage,space"
5050
5332
  };
5051
5333
  const queryString = Object.entries(params).map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`).join("&");
5052
5334
  const endpoint = `/wiki/rest/api/content/search?${queryString}`;
@@ -5190,7 +5472,8 @@ ${contribution.details}
5190
5472
  externalId: page.id,
5191
5473
  externalType: contribution?.type || "page",
5192
5474
  externalSource: "confluence",
5193
- externalUrl: url
5475
+ externalUrl: url,
5476
+ sourceName: page.space?.name || page.space?.key
5194
5477
  };
5195
5478
  }
5196
5479
  };
@@ -5251,6 +5534,10 @@ var GoogleDocsService = class {
5251
5534
  async getCredentials() {
5252
5535
  const creds = await storageService.getServiceCredentials("google-docs");
5253
5536
  if (!creds || !creds.accessToken) {
5537
+ const connection = await integrationsService.getConnectedAccessToken("google");
5538
+ if (connection) {
5539
+ return { accessToken: connection.accessToken };
5540
+ }
5254
5541
  throw new GoogleDocsError("Not authenticated with Google Docs", {
5255
5542
  hint: "Run: bragduck auth google"
5256
5543
  });
@@ -5349,7 +5636,7 @@ var GoogleDocsService = class {
5349
5636
  "https://www.googleapis.com/drive/v3/about?fields=user"
5350
5637
  );
5351
5638
  } catch (error) {
5352
- if (error instanceof GoogleDocsError) {
5639
+ if (error instanceof GoogleDocsError || error instanceof IntegrationError) {
5353
5640
  throw error;
5354
5641
  }
5355
5642
  throw new GoogleDocsError("Could not access Google Drive API", {
@@ -5506,6 +5793,8 @@ init_logger();
5506
5793
  init_storage_service();
5507
5794
  import { URLSearchParams as URLSearchParams8 } from "url";
5508
5795
  var SlackService = class {
5796
+ // Slack user ID of the web-connected account, looked up once per process
5797
+ webUserId = null;
5509
5798
  /**
5510
5799
  * Get stored Slack credentials.
5511
5800
  * Slack user tokens (xoxp-) don't expire by default — no expiry check needed.
@@ -5513,12 +5802,39 @@ var SlackService = class {
5513
5802
  async getCredentials() {
5514
5803
  const creds = await storageService.getServiceCredentials("slack");
5515
5804
  if (!creds || !creds.accessToken) {
5805
+ const webCreds = await this.getWebCredentials();
5806
+ if (webCreds) {
5807
+ return webCreds;
5808
+ }
5516
5809
  throw new SlackError("Not authenticated with Slack", {
5517
5810
  hint: "Run: bragduck auth slack"
5518
5811
  });
5519
5812
  }
5520
5813
  return creds;
5521
5814
  }
5815
+ /**
5816
+ * Build credentials from the Slack account connected on the BragDuck web app.
5817
+ * The server does not return the Slack user ID, so it is looked up via auth.test.
5818
+ */
5819
+ async getWebCredentials() {
5820
+ const connection = await integrationsService.getConnectedAccessToken("slack");
5821
+ if (!connection) {
5822
+ return null;
5823
+ }
5824
+ if (!this.webUserId) {
5825
+ const data = await this.authTest(connection.accessToken);
5826
+ if (!data.ok) {
5827
+ throw new SlackError(`Slack credentials invalid: ${data.error || "unknown error"}`, {
5828
+ hint: "Reconnect Slack at https://app.bragduck.com"
5829
+ });
5830
+ }
5831
+ this.webUserId = data.user_id;
5832
+ }
5833
+ return {
5834
+ accessToken: connection.accessToken,
5835
+ username: this.webUserId
5836
+ };
5837
+ }
5522
5838
  /**
5523
5839
  * Make authenticated GET request to Slack Web API
5524
5840
  */
@@ -5563,18 +5879,24 @@ var SlackService = class {
5563
5879
  return data;
5564
5880
  }
5565
5881
  /**
5566
- * Validate Slack credentials via auth.test
5882
+ * Call Slack auth.test with the given token
5567
5883
  */
5568
- async validateCredentials() {
5569
- const creds = await this.getCredentials();
5884
+ async authTest(accessToken) {
5570
5885
  const response = await fetch("https://slack.com/api/auth.test", {
5571
5886
  method: "POST",
5572
5887
  headers: {
5573
- Authorization: `Bearer ${creds.accessToken}`,
5888
+ Authorization: `Bearer ${accessToken}`,
5574
5889
  "Content-Type": "application/json"
5575
5890
  }
5576
5891
  });
5577
- const data = await response.json();
5892
+ return await response.json();
5893
+ }
5894
+ /**
5895
+ * Validate Slack credentials via auth.test
5896
+ */
5897
+ async validateCredentials() {
5898
+ const creds = await this.getCredentials();
5899
+ const data = await this.authTest(creds.accessToken);
5578
5900
  if (!data.ok) {
5579
5901
  throw new SlackError(`Slack credentials invalid: ${data.error || "unknown error"}`, {
5580
5902
  hint: "Run: bragduck auth slack"
@@ -5587,14 +5909,7 @@ var SlackService = class {
5587
5909
  async getCurrentUser() {
5588
5910
  try {
5589
5911
  const creds = await this.getCredentials();
5590
- const response = await fetch("https://slack.com/api/auth.test", {
5591
- method: "POST",
5592
- headers: {
5593
- Authorization: `Bearer ${creds.accessToken}`,
5594
- "Content-Type": "application/json"
5595
- }
5596
- });
5597
- const data = await response.json();
5912
+ const data = await this.authTest(creds.accessToken);
5598
5913
  return data.ok ? data.user : null;
5599
5914
  } catch {
5600
5915
  return null;
@@ -5667,7 +5982,9 @@ var SlackService = class {
5667
5982
  externalId: msg.ts,
5668
5983
  externalType: "message",
5669
5984
  externalSource: "slack",
5670
- externalUrl: msg.permalink
5985
+ externalUrl: msg.permalink,
5986
+ // Direct messages have no channel name to tag
5987
+ sourceName: msg.channel?.name || void 0
5671
5988
  };
5672
5989
  }
5673
5990
  };
@@ -5746,6 +6063,22 @@ var AdapterFactory = class {
5746
6063
  // src/commands/sync.ts
5747
6064
  init_logger();
5748
6065
 
6066
+ // src/utils/external-source.ts
6067
+ init_esm_shims();
6068
+ function getGitExternalSource(source) {
6069
+ switch (source) {
6070
+ case "github":
6071
+ return "github";
6072
+ case "gitlab":
6073
+ return "gitlab";
6074
+ case "bitbucket":
6075
+ case "atlassian":
6076
+ return "bitbucket";
6077
+ default:
6078
+ return void 0;
6079
+ }
6080
+ }
6081
+
5749
6082
  // src/utils/auth-helper.ts
5750
6083
  init_esm_shims();
5751
6084
  init_auth_service();
@@ -5970,6 +6303,12 @@ async function promptConfirm(message, defaultValue = true) {
5970
6303
  default: defaultValue
5971
6304
  });
5972
6305
  }
6306
+ async function promptInput(message, defaultValue) {
6307
+ return await input2({
6308
+ message,
6309
+ default: defaultValue
6310
+ });
6311
+ }
5973
6312
  async function promptDaysToScan(defaultDays = 30) {
5974
6313
  const choices = [
5975
6314
  { name: "1 day", value: "1", description: "Last 24 hours" },
@@ -6217,6 +6556,13 @@ function createFetchSpinner(text) {
6217
6556
  spinner: "bouncingBar"
6218
6557
  });
6219
6558
  }
6559
+ function createWriteSpinner(text) {
6560
+ return ora({
6561
+ text,
6562
+ color: "cyan",
6563
+ spinner: "dots"
6564
+ });
6565
+ }
6220
6566
  function createValidateSpinner(text) {
6221
6567
  return ora({
6222
6568
  text,
@@ -6239,6 +6585,15 @@ function updateStepSpinner(spinner, currentStep, totalSteps, text) {
6239
6585
  function fetchingBragsSpinner() {
6240
6586
  return createFetchSpinner("Fetching your brags...");
6241
6587
  }
6588
+ function fetchingStoriesSpinner() {
6589
+ return createFetchSpinner("Fetching your stories...");
6590
+ }
6591
+ function creatingStorySpinner() {
6592
+ return createWriteSpinner("Creating story...");
6593
+ }
6594
+ function deletingStorySpinner() {
6595
+ return createWriteSpinner("Deleting story...");
6596
+ }
6242
6597
  function succeedSpinner(spinner, text) {
6243
6598
  if (text) {
6244
6599
  spinner.succeed(colors.success(text));
@@ -6381,6 +6736,17 @@ init_api_service();
6381
6736
  init_auth_service();
6382
6737
  init_storage_service();
6383
6738
  init_logger();
6739
+
6740
+ // src/utils/source-tag.ts
6741
+ init_esm_shims();
6742
+ var MAX_SOURCE_TAG_LENGTH = 50;
6743
+ function withSourceTag(sourceName, tags) {
6744
+ const tag = sourceName?.replace(/,/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_SOURCE_TAG_LENGTH).trim();
6745
+ if (!tag) return tags;
6746
+ return [tag, ...tags.filter((existing) => existing.toLowerCase() !== tag.toLowerCase())];
6747
+ }
6748
+
6749
+ // src/sync/multi-repo-sync.ts
6384
6750
  async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, options) {
6385
6751
  const TOTAL_STEPS = 5;
6386
6752
  logger.debug(`Syncing repository: ${repo.name} at ${repo.path}`);
@@ -6535,10 +6901,10 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6535
6901
  );
6536
6902
  createSpinner.start();
6537
6903
  const createRequest = {
6904
+ mode: options.turbo ? "turbo" : "manual",
6538
6905
  brags: acceptedBrags.map((refined, index) => {
6539
6906
  const originalCommit = selectedCommits[index];
6540
- const repoTag = repo.name;
6541
- const tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
6907
+ const tagsWithRepo = withSourceTag(repo.name, refined.suggested_tags);
6542
6908
  return {
6543
6909
  commit_sha: originalCommit?.sha || `brag-${index}`,
6544
6910
  title: refined.refined_title,
@@ -6557,7 +6923,7 @@ async function syncSingleRepository(repo, days, sortOption, scanMode, orgId, opt
6557
6923
  // External fields for non-git sources
6558
6924
  externalId: originalCommit?.externalId,
6559
6925
  externalType: originalCommit?.externalType,
6560
- externalSource: originalCommit?.externalSource,
6926
+ externalSource: originalCommit?.externalSource ?? getGitExternalSource(repo.service),
6561
6927
  externalUrl: originalCommit?.externalUrl
6562
6928
  };
6563
6929
  })
@@ -6963,14 +7329,14 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
6963
7329
  );
6964
7330
  createSpinner.start();
6965
7331
  const createRequest = {
7332
+ mode: options.turbo ? "turbo" : "manual",
6966
7333
  brags: acceptedBrags.map((refined, index) => {
6967
7334
  const originalCommit = selectedCommits[index];
6968
7335
  const isGitSource = sourceType === "github" || sourceType === "gitlab" || sourceType === "bitbucket" || sourceType === "atlassian";
6969
- let tagsWithRepo = refined.suggested_tags;
6970
- if (isGitSource && repoInfo.name) {
6971
- const repoTag = repoInfo.name;
6972
- tagsWithRepo = refined.suggested_tags.includes(repoTag) ? refined.suggested_tags : [repoTag, ...refined.suggested_tags];
6973
- }
7336
+ const tagsWithRepo = withSourceTag(
7337
+ isGitSource ? repoInfo.name : originalCommit?.sourceName,
7338
+ refined.suggested_tags
7339
+ );
6974
7340
  return {
6975
7341
  commit_sha: originalCommit?.sha || `brag-${index}`,
6976
7342
  title: refined.refined_title,
@@ -6988,7 +7354,7 @@ async function syncSingleService(sourceType, options, TOTAL_STEPS, sharedDays, s
6988
7354
  // External fields for non-git sources (Jira, Confluence, etc.)
6989
7355
  externalId: originalCommit?.externalId,
6990
7356
  externalType: originalCommit?.externalType,
6991
- externalSource: originalCommit?.externalSource,
7357
+ externalSource: originalCommit?.externalSource ?? getGitExternalSource(sourceType),
6992
7358
  externalUrl: originalCommit?.externalUrl
6993
7359
  };
6994
7360
  })
@@ -7368,7 +7734,8 @@ To authenticate:
7368
7734
  sourceType = selectedSource;
7369
7735
  }
7370
7736
  if (sourceType === "jira" || sourceType === "confluence") {
7371
- const creds = await storageService.getServiceCredentials(sourceType);
7737
+ const localCreds = await storageService.getServiceCredentials(sourceType);
7738
+ const creds = localCreds?.accessToken ? localCreds : await integrationsService.getAtlassianCredentials(sourceType);
7372
7739
  const envInstance = loadEnvConfig()[`${sourceType}Instance`];
7373
7740
  const projectConfig = await findProjectConfig();
7374
7741
  const configInstance = projectConfig?.[`${sourceType}Instance`];
@@ -7471,6 +7838,9 @@ function getErrorHint(error, sourceType) {
7471
7838
  }
7472
7839
  return 'Run "bragduck auth login" to login again';
7473
7840
  }
7841
+ if (error.name === "IntegrationError" && error.details?.hint) {
7842
+ return error.details.hint;
7843
+ }
7474
7844
  if (error.name === "GoogleDocsError") {
7475
7845
  return 'Run "bragduck auth google" to re-authenticate with Google';
7476
7846
  }
@@ -7680,13 +8050,15 @@ function formatBragsTable(brags) {
7680
8050
  return table.toString() + (links ? "\n\n" + links : "");
7681
8051
  }
7682
8052
  function formatBragLinks(brags) {
7683
- const lines = brags.filter((brag) => brag.url || brag.goalIds && brag.goalIds.length > 0).map((brag) => {
8053
+ const lines = brags.filter((brag) => brag.url || brag.keyResultIds && brag.keyResultIds.length > 0).map((brag) => {
7684
8054
  const parts = [` ${colors.white(brag.title)}`];
7685
8055
  if (brag.url) {
7686
8056
  parts.push(` ${colors.info(brag.url)}`);
7687
8057
  }
7688
- if (brag.goalIds && brag.goalIds.length > 0) {
7689
- parts.push(` ${colors.dim("Linked to key result:")} ${colors.primary(brag.goalIds[0])}`);
8058
+ if (brag.keyResultIds && brag.keyResultIds.length > 0) {
8059
+ parts.push(
8060
+ ` ${colors.dim("Linked to key result:")} ${colors.primary(brag.keyResultIds[0])}`
8061
+ );
7690
8062
  }
7691
8063
  return parts.join("\n");
7692
8064
  });
@@ -7698,9 +8070,9 @@ function formatBragsOneline(brags) {
7698
8070
  const date = formatDate(brag.date);
7699
8071
  const urlLine = brag.url ? `
7700
8072
  ${colors.info(brag.url)}` : "";
7701
- const goalLine = brag.goalIds && brag.goalIds.length > 0 ? `
7702
- ${colors.dim("Linked to key result:")} ${colors.primary(brag.goalIds[0])}` : "";
7703
- return `${colors.highlight(date)} ${colors.white(brag.title)}${urlLine}${goalLine}`;
8073
+ const keyResultLine = brag.keyResultIds && brag.keyResultIds.length > 0 ? `
8074
+ ${colors.dim("Linked to key result:")} ${colors.primary(brag.keyResultIds[0])}` : "";
8075
+ return `${colors.highlight(date)} ${colors.white(brag.title)}${urlLine}${keyResultLine}`;
7704
8076
  }).join("\n");
7705
8077
  }
7706
8078
  function truncateText(text, maxLength) {
@@ -7730,14 +8102,338 @@ function getErrorHint2(error) {
7730
8102
  return "Try running with DEBUG=* for more information";
7731
8103
  }
7732
8104
 
8105
+ // src/commands/story.ts
8106
+ init_esm_shims();
8107
+ init_api_service();
8108
+ init_auth_service();
8109
+ import boxen8 from "boxen";
8110
+ import Table3 from "cli-table3";
8111
+ init_logger();
8112
+ async function storyCommand(subcommand, idOrOptions, _options) {
8113
+ switch (subcommand) {
8114
+ case "list":
8115
+ case "ls":
8116
+ await storyListCommand(idOrOptions || {});
8117
+ break;
8118
+ case "create":
8119
+ case "add":
8120
+ await storyCreateCommand(idOrOptions || {});
8121
+ break;
8122
+ case "delete":
8123
+ case "rm":
8124
+ if (typeof idOrOptions === "string") {
8125
+ await storyDeleteCommand(idOrOptions);
8126
+ } else {
8127
+ logger.log("");
8128
+ logger.log(
8129
+ boxen8(
8130
+ formatErrorMessage("Missing story ID", "Usage: bragduck story delete <id>"),
8131
+ boxStyles.error
8132
+ )
8133
+ );
8134
+ process.exit(1);
8135
+ }
8136
+ break;
8137
+ default:
8138
+ logger.log("");
8139
+ logger.log(
8140
+ boxen8(
8141
+ colors.primary("Story Commands") + `
8142
+
8143
+ ${theme.command("bragduck story list")} List your interview stories
8144
+ ${theme.command("bragduck story create")} Create a new STAR story
8145
+ ${theme.command("bragduck story delete")} Delete a story by ID
8146
+
8147
+ ` + colors.dim("Aliases: ls, add, rm"),
8148
+ { ...boxStyles.info, padding: 1 }
8149
+ )
8150
+ );
8151
+ break;
8152
+ }
8153
+ }
8154
+ async function storyListCommand(options) {
8155
+ logger.log("");
8156
+ try {
8157
+ const isAuthenticated = await ensureAuthenticated();
8158
+ if (!isAuthenticated) {
8159
+ process.exit(1);
8160
+ }
8161
+ const subSpinner = createValidateSpinner("Checking subscription...");
8162
+ subSpinner.start();
8163
+ let subscriptionStatus;
8164
+ try {
8165
+ subscriptionStatus = await apiService.getSubscriptionStatus();
8166
+ } catch (error) {
8167
+ failSpinner(subSpinner, "Failed to check subscription");
8168
+ throw error;
8169
+ }
8170
+ if (subscriptionStatus.tier === "FREE") {
8171
+ failSpinner(subSpinner, "Free plan \u2014 upgrade to use the CLI");
8172
+ showUpgradeMessage();
8173
+ return;
8174
+ }
8175
+ subSpinner.stop();
8176
+ const userInfo = authService.getUserInfo();
8177
+ if (!userInfo) {
8178
+ logger.log(boxen8(formatErrorMessage("Could not retrieve user info"), boxStyles.error));
8179
+ process.exit(1);
8180
+ }
8181
+ const perPage = options.limit || 20;
8182
+ const spinner = fetchingStoriesSpinner();
8183
+ spinner.start();
8184
+ const response = await apiService.listStories(userInfo.id, {
8185
+ perPage,
8186
+ search: options.search,
8187
+ competency: options.competency
8188
+ });
8189
+ if (response.items.length === 0) {
8190
+ failSpinner(spinner, "No stories found");
8191
+ logger.log("");
8192
+ if (options.search || options.competency) {
8193
+ logger.info("Try adjusting your filters or run without filters to see all stories");
8194
+ } else {
8195
+ logger.info(
8196
+ theme.secondary("Run ") + theme.command("bragduck story create") + theme.secondary(" to create your first interview story")
8197
+ );
8198
+ }
8199
+ return;
8200
+ }
8201
+ succeedSpinner(spinner, `Found ${response.total} stor${response.total > 1 ? "ies" : "y"}`);
8202
+ showCommandHeader(userInfo);
8203
+ logger.log("");
8204
+ logger.log(formatStoriesTable(response.items));
8205
+ logger.log("");
8206
+ if (response.total > response.items.length) {
8207
+ logger.info(`Showing ${response.items.length} of ${response.total} total stories`);
8208
+ logger.log("");
8209
+ }
8210
+ if (options.search || options.competency) {
8211
+ const filterInfo = [];
8212
+ if (options.search) filterInfo.push(`search: "${options.search}"`);
8213
+ if (options.competency) filterInfo.push(`competency: ${options.competency}`);
8214
+ logger.info(theme.info(`Filters applied: ${filterInfo.join(", ")}`));
8215
+ logger.log("");
8216
+ }
8217
+ } catch (error) {
8218
+ const err = error;
8219
+ logger.log("");
8220
+ logger.log(boxen8(formatErrorMessage(err.message, getErrorHint3(err)), boxStyles.error));
8221
+ process.exit(1);
8222
+ }
8223
+ }
8224
+ async function storyCreateCommand(options) {
8225
+ logger.log("");
8226
+ try {
8227
+ const isAuthenticated = await ensureAuthenticated();
8228
+ if (!isAuthenticated) {
8229
+ process.exit(1);
8230
+ }
8231
+ const subSpinner = createValidateSpinner("Checking subscription...");
8232
+ subSpinner.start();
8233
+ let subscriptionStatus;
8234
+ try {
8235
+ subscriptionStatus = await apiService.getSubscriptionStatus();
8236
+ } catch (error) {
8237
+ failSpinner(subSpinner, "Failed to check subscription");
8238
+ throw error;
8239
+ }
8240
+ if (subscriptionStatus.tier === "FREE") {
8241
+ failSpinner(subSpinner, "Free plan \u2014 upgrade to use the CLI");
8242
+ showUpgradeMessage();
8243
+ return;
8244
+ }
8245
+ subSpinner.stop();
8246
+ const userInfo = authService.getUserInfo();
8247
+ if (!userInfo) {
8248
+ logger.log(boxen8(formatErrorMessage("Could not retrieve user info"), boxStyles.error));
8249
+ process.exit(1);
8250
+ }
8251
+ logger.log(
8252
+ boxen8(
8253
+ colors.primary("Create Interview Story") + "\n\n" + colors.dim("Use the STAR format to structure your story:\n") + colors.dim("Situation, Task, Action, Result"),
8254
+ { ...boxStyles.info, padding: 1 }
8255
+ )
8256
+ );
8257
+ logger.log("");
8258
+ const title = options.title || await promptInput("Story title (short summary):");
8259
+ if (!title.trim()) {
8260
+ logger.log(theme.error("Title is required"));
8261
+ process.exit(1);
8262
+ }
8263
+ const situation = await promptInput("Situation (context and background):");
8264
+ if (!situation.trim()) {
8265
+ logger.log(theme.error("Situation is required"));
8266
+ process.exit(1);
8267
+ }
8268
+ const task = await promptInput("Task (your responsibility or challenge):");
8269
+ if (!task.trim()) {
8270
+ logger.log(theme.error("Task is required"));
8271
+ process.exit(1);
8272
+ }
8273
+ const action = await promptInput("Action (what you did):");
8274
+ if (!action.trim()) {
8275
+ logger.log(theme.error("Action is required"));
8276
+ process.exit(1);
8277
+ }
8278
+ const result = await promptInput("Result (outcome and impact):");
8279
+ if (!result.trim()) {
8280
+ logger.log(theme.error("Result is required"));
8281
+ process.exit(1);
8282
+ }
8283
+ const competenciesInput = await promptInput(
8284
+ "Competencies (comma-separated, e.g. leadership, system design):",
8285
+ ""
8286
+ );
8287
+ const competencies = competenciesInput ? competenciesInput.split(",").map((c) => c.trim()).filter(Boolean) : [];
8288
+ let orgId;
8289
+ try {
8290
+ const orgsResponse = await apiService.listUserOrganisations(userInfo.id);
8291
+ if (orgsResponse.items.length > 0) {
8292
+ const selectedOrg = await promptSelectOrganisationWithDefault(orgsResponse.items);
8293
+ if (selectedOrg) {
8294
+ orgId = selectedOrg;
8295
+ }
8296
+ }
8297
+ } catch {
8298
+ logger.debug("Failed to fetch organisations, skipping org selection");
8299
+ }
8300
+ logger.log("");
8301
+ logger.log(
8302
+ boxen8(
8303
+ colors.primary("Preview") + `
8304
+
8305
+ ${theme.label("Title")} ${colors.white(title)}
8306
+
8307
+ ${theme.label("Situation")} ${colors.white(situation)}
8308
+
8309
+ ${theme.label("Task")} ${colors.white(task)}
8310
+
8311
+ ${theme.label("Action")} ${colors.white(action)}
8312
+
8313
+ ${theme.label("Result")} ${colors.white(result)}
8314
+
8315
+ ${theme.label("Competencies")} ${competencies.length > 0 ? colors.primary(competencies.join(", ")) : colors.dim("none")}`,
8316
+ { ...boxStyles.info, padding: 1 }
8317
+ )
8318
+ );
8319
+ logger.log("");
8320
+ const shouldCreate = await promptConfirm("Create this story?", true);
8321
+ if (!shouldCreate) {
8322
+ logger.log(theme.secondary("Story creation cancelled"));
8323
+ return;
8324
+ }
8325
+ const spinner = creatingStorySpinner();
8326
+ spinner.start();
8327
+ const body = {
8328
+ title,
8329
+ situation,
8330
+ task,
8331
+ action,
8332
+ result,
8333
+ competencies,
8334
+ source: "cli"
8335
+ };
8336
+ if (orgId) {
8337
+ body.orgId = orgId;
8338
+ }
8339
+ const story = await apiService.createStory(userInfo.id, body);
8340
+ succeedSpinner(spinner, `Story created: "${story.title}"`);
8341
+ logger.log("");
8342
+ } catch (error) {
8343
+ const err = error;
8344
+ logger.log("");
8345
+ logger.log(boxen8(formatErrorMessage(err.message, getErrorHint3(err)), boxStyles.error));
8346
+ process.exit(1);
8347
+ }
8348
+ }
8349
+ async function storyDeleteCommand(storyId) {
8350
+ logger.log("");
8351
+ try {
8352
+ const isAuthenticated = await ensureAuthenticated();
8353
+ if (!isAuthenticated) {
8354
+ process.exit(1);
8355
+ }
8356
+ const userInfo = authService.getUserInfo();
8357
+ if (!userInfo) {
8358
+ logger.log(boxen8(formatErrorMessage("Could not retrieve user info"), boxStyles.error));
8359
+ process.exit(1);
8360
+ }
8361
+ const shouldDelete = await promptConfirm(`Delete story ${storyId}?`, false);
8362
+ if (!shouldDelete) {
8363
+ logger.log(theme.secondary("Deletion cancelled"));
8364
+ return;
8365
+ }
8366
+ const spinner = deletingStorySpinner();
8367
+ spinner.start();
8368
+ await apiService.deleteStory(userInfo.id, storyId);
8369
+ succeedSpinner(spinner, "Story deleted");
8370
+ logger.log("");
8371
+ } catch (error) {
8372
+ const err = error;
8373
+ logger.log("");
8374
+ logger.log(boxen8(formatErrorMessage(err.message, getErrorHint3(err)), boxStyles.error));
8375
+ process.exit(1);
8376
+ }
8377
+ }
8378
+ function formatStoriesTable(stories) {
8379
+ const table = new Table3({
8380
+ head: [
8381
+ colors.primary("ID"),
8382
+ colors.primary("Title"),
8383
+ colors.primary("Competencies"),
8384
+ colors.primary("Updated")
8385
+ ],
8386
+ colWidths: [12, 35, 30, 14],
8387
+ wordWrap: true,
8388
+ style: tableStyles.default.style
8389
+ });
8390
+ stories.forEach((story) => {
8391
+ const competencies = story.competencies.length > 0 ? story.competencies.join(", ") : colors.dim("none");
8392
+ const updated = formatDate(story.updatedAt);
8393
+ table.push([
8394
+ colors.dim(story.id),
8395
+ colors.white(story.title),
8396
+ colors.primary(competencies),
8397
+ colors.highlight(updated)
8398
+ ]);
8399
+ });
8400
+ return table.toString();
8401
+ }
8402
+ function showUpgradeMessage() {
8403
+ logger.log("");
8404
+ logger.log(
8405
+ boxen8(
8406
+ theme.warning("CLI Access Requires Subscription") + "\n\nThe BragDuck CLI is available for Plus and Pro subscribers.\nUpgrade now to unlock:\n\n \u2022 List and manage your interview stories\n \u2022 Create STAR-format stories from the terminal\n \u2022 Automatic work item scanning\n\n" + colors.highlight("Upgrade at: https://bragduck.com/app/settings/plans"),
8407
+ {
8408
+ ...boxStyles.warning,
8409
+ padding: 1,
8410
+ margin: 1
8411
+ }
8412
+ )
8413
+ );
8414
+ logger.log("");
8415
+ }
8416
+ function getErrorHint3(error) {
8417
+ if (error.name === "TokenExpiredError" || error.name === "AuthenticationError") {
8418
+ return 'Run "bragduck auth login" to login again';
8419
+ }
8420
+ if (error.name === "NetworkError") {
8421
+ return "Check your internet connection and try again";
8422
+ }
8423
+ if (error.name === "ApiError") {
8424
+ return "The server might be experiencing issues. Try again later";
8425
+ }
8426
+ return "Try running with DEBUG=* for more information";
8427
+ }
8428
+
7733
8429
  // src/commands/config.ts
7734
8430
  init_esm_shims();
7735
8431
  init_storage_service();
7736
8432
  init_logger();
7737
8433
  init_constants();
7738
- import boxen8 from "boxen";
8434
+ import boxen9 from "boxen";
7739
8435
  import chalk6 from "chalk";
7740
- import Table3 from "cli-table3";
8436
+ import Table4 from "cli-table3";
7741
8437
  init_errors();
7742
8438
  var VALID_CONFIG_KEYS = Object.values(CONFIG_KEYS);
7743
8439
  async function configCommand(subcommand, key, value) {
@@ -7771,7 +8467,7 @@ async function configCommand(subcommand, key, value) {
7771
8467
  const err = error;
7772
8468
  logger.log("");
7773
8469
  logger.log(
7774
- boxen8(formatErrorMessage(err.message, getConfigHint(err)), {
8470
+ boxen9(formatErrorMessage(err.message, getConfigHint(err)), {
7775
8471
  padding: 1,
7776
8472
  margin: 1,
7777
8473
  borderStyle: "round",
@@ -7791,7 +8487,7 @@ async function handleListConfig() {
7791
8487
  confluenceInstance: storageService.getConfig("confluenceInstance"),
7792
8488
  gitlabInstance: storageService.getConfig("gitlabInstance")
7793
8489
  };
7794
- const table = new Table3({
8490
+ const table = new Table4({
7795
8491
  head: [chalk6.cyan("Key"), chalk6.cyan("Value"), chalk6.cyan("Default")],
7796
8492
  colWidths: [25, 40, 40],
7797
8493
  wordWrap: true,
@@ -7865,7 +8561,7 @@ async function handleSetConfig(key, value) {
7865
8561
  const typedValue = validateAndConvertValue(key, value);
7866
8562
  storageService.setConfig(key, typedValue);
7867
8563
  logger.log(
7868
- boxen8(
8564
+ boxen9(
7869
8565
  `${chalk6.green.bold("\u2713 Configuration updated")}
7870
8566
 
7871
8567
  ${chalk6.white(key)}: ${chalk6.yellow(String(typedValue))}`,
@@ -7997,6 +8693,18 @@ program.command("list").description("List your existing brags").option("-l, --li
7997
8693
  process.exit(1);
7998
8694
  }
7999
8695
  });
8696
+ program.command("story [subcommand] [id]").description("Manage interview stories (subcommands: list, create, delete)").option("-l, --limit <number>", "Number of stories to display", (val) => parseInt(val, 10), 20).option("-s, --search <query>", "Search stories by keyword").option("-c, --competency <competency>", "Filter by competency").option("-t, --title <title>", "Story title (for create)").action(async (subcommand, id, options) => {
8697
+ try {
8698
+ if (subcommand === "delete" || subcommand === "rm") {
8699
+ await storyCommand(subcommand, id, options);
8700
+ } else {
8701
+ await storyCommand(subcommand, options);
8702
+ }
8703
+ } catch (error) {
8704
+ console.error(error);
8705
+ process.exit(1);
8706
+ }
8707
+ });
8000
8708
  program.command("logout").description("Clear stored credentials").action(async () => {
8001
8709
  try {
8002
8710
  await logoutCommand();