@browserstack/mcp-server 1.2.14 → 1.2.15-beta.2

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 (170) hide show
  1. package/dist/lib/percy-api/auth.d.ts +41 -0
  2. package/dist/lib/percy-api/auth.js +96 -0
  3. package/dist/lib/percy-api/cache.d.ts +28 -0
  4. package/dist/lib/percy-api/cache.js +48 -0
  5. package/dist/lib/percy-api/client.d.ts +69 -0
  6. package/dist/lib/percy-api/client.js +275 -0
  7. package/dist/lib/percy-api/errors.d.ts +15 -0
  8. package/dist/lib/percy-api/errors.js +52 -0
  9. package/dist/lib/percy-api/formatter.d.ts +16 -0
  10. package/dist/lib/percy-api/formatter.js +344 -0
  11. package/dist/lib/percy-api/percy-auth.d.ts +43 -0
  12. package/dist/lib/percy-api/percy-auth.js +137 -0
  13. package/dist/lib/percy-api/percy-error-handler.d.ts +24 -0
  14. package/dist/lib/percy-api/percy-error-handler.js +302 -0
  15. package/dist/lib/percy-api/percy-session.d.ts +42 -0
  16. package/dist/lib/percy-api/percy-session.js +87 -0
  17. package/dist/lib/percy-api/polling.d.ts +26 -0
  18. package/dist/lib/percy-api/polling.js +42 -0
  19. package/dist/lib/percy-api/types.d.ts +56 -0
  20. package/dist/lib/percy-api/types.js +76 -0
  21. package/dist/server-factory.js +4 -0
  22. package/dist/tools/percy-mcp/advanced/branchline-operations.d.ts +16 -0
  23. package/dist/tools/percy-mcp/advanced/branchline-operations.js +81 -0
  24. package/dist/tools/percy-mcp/advanced/manage-variants.d.ts +16 -0
  25. package/dist/tools/percy-mcp/advanced/manage-variants.js +155 -0
  26. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.d.ts +16 -0
  27. package/dist/tools/percy-mcp/advanced/manage-visual-monitoring.js +171 -0
  28. package/dist/tools/percy-mcp/auth/auth-status.d.ts +3 -0
  29. package/dist/tools/percy-mcp/auth/auth-status.js +131 -0
  30. package/dist/tools/percy-mcp/core/approve-build.d.ts +14 -0
  31. package/dist/tools/percy-mcp/core/approve-build.js +97 -0
  32. package/dist/tools/percy-mcp/core/get-build-items.d.ts +13 -0
  33. package/dist/tools/percy-mcp/core/get-build-items.js +65 -0
  34. package/dist/tools/percy-mcp/core/get-build.d.ts +10 -0
  35. package/dist/tools/percy-mcp/core/get-build.js +16 -0
  36. package/dist/tools/percy-mcp/core/get-comparison.d.ts +11 -0
  37. package/dist/tools/percy-mcp/core/get-comparison.js +59 -0
  38. package/dist/tools/percy-mcp/core/get-snapshot.d.ts +10 -0
  39. package/dist/tools/percy-mcp/core/get-snapshot.js +40 -0
  40. package/dist/tools/percy-mcp/core/list-builds.d.ts +14 -0
  41. package/dist/tools/percy-mcp/core/list-builds.js +45 -0
  42. package/dist/tools/percy-mcp/core/list-projects.d.ts +12 -0
  43. package/dist/tools/percy-mcp/core/list-projects.js +51 -0
  44. package/dist/tools/percy-mcp/creation/create-app-snapshot.d.ts +12 -0
  45. package/dist/tools/percy-mcp/creation/create-app-snapshot.js +29 -0
  46. package/dist/tools/percy-mcp/creation/create-build.d.ts +19 -0
  47. package/dist/tools/percy-mcp/creation/create-build.js +68 -0
  48. package/dist/tools/percy-mcp/creation/create-comparison.d.ts +18 -0
  49. package/dist/tools/percy-mcp/creation/create-comparison.js +90 -0
  50. package/dist/tools/percy-mcp/creation/create-snapshot.d.ts +17 -0
  51. package/dist/tools/percy-mcp/creation/create-snapshot.js +99 -0
  52. package/dist/tools/percy-mcp/creation/finalize-build.d.ts +12 -0
  53. package/dist/tools/percy-mcp/creation/finalize-build.js +33 -0
  54. package/dist/tools/percy-mcp/creation/finalize-comparison.d.ts +10 -0
  55. package/dist/tools/percy-mcp/creation/finalize-comparison.js +16 -0
  56. package/dist/tools/percy-mcp/creation/finalize-snapshot.d.ts +12 -0
  57. package/dist/tools/percy-mcp/creation/finalize-snapshot.js +33 -0
  58. package/dist/tools/percy-mcp/creation/upload-resource.d.ts +15 -0
  59. package/dist/tools/percy-mcp/creation/upload-resource.js +43 -0
  60. package/dist/tools/percy-mcp/creation/upload-tile.d.ts +11 -0
  61. package/dist/tools/percy-mcp/creation/upload-tile.js +53 -0
  62. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.d.ts +13 -0
  63. package/dist/tools/percy-mcp/diagnostics/analyze-logs-realtime.js +65 -0
  64. package/dist/tools/percy-mcp/diagnostics/get-build-logs.d.ts +17 -0
  65. package/dist/tools/percy-mcp/diagnostics/get-build-logs.js +74 -0
  66. package/dist/tools/percy-mcp/diagnostics/get-network-logs.d.ts +5 -0
  67. package/dist/tools/percy-mcp/diagnostics/get-network-logs.js +21 -0
  68. package/dist/tools/percy-mcp/diagnostics/get-suggestions.d.ts +7 -0
  69. package/dist/tools/percy-mcp/diagnostics/get-suggestions.js +24 -0
  70. package/dist/tools/percy-mcp/index.d.ts +36 -0
  71. package/dist/tools/percy-mcp/index.js +1137 -0
  72. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.d.ts +15 -0
  73. package/dist/tools/percy-mcp/intelligence/get-ai-analysis.js +166 -0
  74. package/dist/tools/percy-mcp/intelligence/get-ai-quota.d.ts +9 -0
  75. package/dist/tools/percy-mcp/intelligence/get-ai-quota.js +73 -0
  76. package/dist/tools/percy-mcp/intelligence/get-build-summary.d.ts +11 -0
  77. package/dist/tools/percy-mcp/intelligence/get-build-summary.js +78 -0
  78. package/dist/tools/percy-mcp/intelligence/get-rca.d.ts +6 -0
  79. package/dist/tools/percy-mcp/intelligence/get-rca.js +153 -0
  80. package/dist/tools/percy-mcp/intelligence/suggest-prompt.d.ts +15 -0
  81. package/dist/tools/percy-mcp/intelligence/suggest-prompt.js +86 -0
  82. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.d.ts +16 -0
  83. package/dist/tools/percy-mcp/intelligence/trigger-ai-recompute.js +64 -0
  84. package/dist/tools/percy-mcp/management/create-project.d.ts +14 -0
  85. package/dist/tools/percy-mcp/management/create-project.js +52 -0
  86. package/dist/tools/percy-mcp/management/get-usage-stats.d.ts +12 -0
  87. package/dist/tools/percy-mcp/management/get-usage-stats.js +61 -0
  88. package/dist/tools/percy-mcp/management/manage-browser-targets.d.ts +12 -0
  89. package/dist/tools/percy-mcp/management/manage-browser-targets.js +136 -0
  90. package/dist/tools/percy-mcp/management/manage-comments.d.ts +14 -0
  91. package/dist/tools/percy-mcp/management/manage-comments.js +147 -0
  92. package/dist/tools/percy-mcp/management/manage-ignored-regions.d.ts +18 -0
  93. package/dist/tools/percy-mcp/management/manage-ignored-regions.js +182 -0
  94. package/dist/tools/percy-mcp/management/manage-project-settings.d.ts +16 -0
  95. package/dist/tools/percy-mcp/management/manage-project-settings.js +97 -0
  96. package/dist/tools/percy-mcp/management/manage-tokens.d.ts +14 -0
  97. package/dist/tools/percy-mcp/management/manage-tokens.js +90 -0
  98. package/dist/tools/percy-mcp/management/manage-webhooks.d.ts +15 -0
  99. package/dist/tools/percy-mcp/management/manage-webhooks.js +180 -0
  100. package/dist/tools/percy-mcp/v2/auth-status.d.ts +3 -0
  101. package/dist/tools/percy-mcp/v2/auth-status.js +80 -0
  102. package/dist/tools/percy-mcp/v2/clone-build.d.ts +24 -0
  103. package/dist/tools/percy-mcp/v2/clone-build.js +539 -0
  104. package/dist/tools/percy-mcp/v2/create-app-build.d.ts +28 -0
  105. package/dist/tools/percy-mcp/v2/create-app-build.js +442 -0
  106. package/dist/tools/percy-mcp/v2/create-build.d.ts +16 -0
  107. package/dist/tools/percy-mcp/v2/create-build.js +601 -0
  108. package/dist/tools/percy-mcp/v2/create-project.d.ts +8 -0
  109. package/dist/tools/percy-mcp/v2/create-project.js +33 -0
  110. package/dist/tools/percy-mcp/v2/discover-urls.d.ts +7 -0
  111. package/dist/tools/percy-mcp/v2/discover-urls.js +38 -0
  112. package/dist/tools/percy-mcp/v2/figma-baseline.d.ts +7 -0
  113. package/dist/tools/percy-mcp/v2/figma-baseline.js +18 -0
  114. package/dist/tools/percy-mcp/v2/figma-build.d.ts +7 -0
  115. package/dist/tools/percy-mcp/v2/figma-build.js +39 -0
  116. package/dist/tools/percy-mcp/v2/figma-link.d.ts +6 -0
  117. package/dist/tools/percy-mcp/v2/figma-link.js +27 -0
  118. package/dist/tools/percy-mcp/v2/get-ai-summary.d.ts +5 -0
  119. package/dist/tools/percy-mcp/v2/get-ai-summary.js +109 -0
  120. package/dist/tools/percy-mcp/v2/get-build-detail.d.ts +22 -0
  121. package/dist/tools/percy-mcp/v2/get-build-detail.js +567 -0
  122. package/dist/tools/percy-mcp/v2/get-builds.d.ts +8 -0
  123. package/dist/tools/percy-mcp/v2/get-builds.js +63 -0
  124. package/dist/tools/percy-mcp/v2/get-comparison.d.ts +5 -0
  125. package/dist/tools/percy-mcp/v2/get-comparison.js +94 -0
  126. package/dist/tools/percy-mcp/v2/get-devices.d.ts +5 -0
  127. package/dist/tools/percy-mcp/v2/get-devices.js +33 -0
  128. package/dist/tools/percy-mcp/v2/get-insights.d.ts +7 -0
  129. package/dist/tools/percy-mcp/v2/get-insights.js +52 -0
  130. package/dist/tools/percy-mcp/v2/get-projects.d.ts +6 -0
  131. package/dist/tools/percy-mcp/v2/get-projects.js +41 -0
  132. package/dist/tools/percy-mcp/v2/get-snapshot.d.ts +5 -0
  133. package/dist/tools/percy-mcp/v2/get-snapshot.js +96 -0
  134. package/dist/tools/percy-mcp/v2/get-test-case-history.d.ts +5 -0
  135. package/dist/tools/percy-mcp/v2/get-test-case-history.js +20 -0
  136. package/dist/tools/percy-mcp/v2/get-test-cases.d.ts +6 -0
  137. package/dist/tools/percy-mcp/v2/get-test-cases.js +36 -0
  138. package/dist/tools/percy-mcp/v2/index.d.ts +35 -0
  139. package/dist/tools/percy-mcp/v2/index.js +544 -0
  140. package/dist/tools/percy-mcp/v2/list-integrations.d.ts +5 -0
  141. package/dist/tools/percy-mcp/v2/list-integrations.js +41 -0
  142. package/dist/tools/percy-mcp/v2/manage-domains.d.ts +8 -0
  143. package/dist/tools/percy-mcp/v2/manage-domains.js +33 -0
  144. package/dist/tools/percy-mcp/v2/manage-insights-email.d.ts +8 -0
  145. package/dist/tools/percy-mcp/v2/manage-insights-email.js +49 -0
  146. package/dist/tools/percy-mcp/v2/manage-usage-alerts.d.ts +10 -0
  147. package/dist/tools/percy-mcp/v2/manage-usage-alerts.js +43 -0
  148. package/dist/tools/percy-mcp/v2/migrate-integrations.d.ts +6 -0
  149. package/dist/tools/percy-mcp/v2/migrate-integrations.js +20 -0
  150. package/dist/tools/percy-mcp/v2/preview-comparison.d.ts +5 -0
  151. package/dist/tools/percy-mcp/v2/preview-comparison.js +17 -0
  152. package/dist/tools/percy-mcp/v2/search-build-items.d.ts +12 -0
  153. package/dist/tools/percy-mcp/v2/search-build-items.js +45 -0
  154. package/dist/tools/percy-mcp/workflows/auto-triage.d.ts +7 -0
  155. package/dist/tools/percy-mcp/workflows/auto-triage.js +82 -0
  156. package/dist/tools/percy-mcp/workflows/clone-build.d.ts +22 -0
  157. package/dist/tools/percy-mcp/workflows/clone-build.js +414 -0
  158. package/dist/tools/percy-mcp/workflows/create-percy-build.d.ts +32 -0
  159. package/dist/tools/percy-mcp/workflows/create-percy-build.js +434 -0
  160. package/dist/tools/percy-mcp/workflows/debug-failed-build.d.ts +5 -0
  161. package/dist/tools/percy-mcp/workflows/debug-failed-build.js +122 -0
  162. package/dist/tools/percy-mcp/workflows/diff-explain.d.ts +6 -0
  163. package/dist/tools/percy-mcp/workflows/diff-explain.js +147 -0
  164. package/dist/tools/percy-mcp/workflows/pr-visual-report.d.ts +8 -0
  165. package/dist/tools/percy-mcp/workflows/pr-visual-report.js +184 -0
  166. package/dist/tools/percy-mcp/workflows/run-tests.d.ts +17 -0
  167. package/dist/tools/percy-mcp/workflows/run-tests.js +107 -0
  168. package/dist/tools/percy-mcp/workflows/snapshot-urls.d.ts +18 -0
  169. package/dist/tools/percy-mcp/workflows/snapshot-urls.js +197 -0
  170. package/package.json +4 -3
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Percy API authentication module.
3
+ * Resolves Percy tokens via environment variables or BrowserStack credential fallback.
4
+ *
5
+ * SECURITY: Token values are NEVER logged or included in error messages.
6
+ * Masked format (****<last4>) is used when referencing tokens in diagnostics.
7
+ */
8
+ import { BrowserStackConfig } from "../types.js";
9
+ type TokenScope = "project" | "org" | "auto";
10
+ interface ResolveTokenOptions {
11
+ projectName?: string;
12
+ scope?: TokenScope;
13
+ }
14
+ /**
15
+ * Masks a token for safe display in error messages.
16
+ * Shows only the last 4 characters.
17
+ */
18
+ export declare function maskToken(token: string): string;
19
+ /**
20
+ * Resolves a Percy token using the following priority:
21
+ *
22
+ * 1. `process.env.PERCY_TOKEN` (for project or auto scope)
23
+ * 2. `process.env.PERCY_ORG_TOKEN` (for org scope)
24
+ * 3. Fallback: fetch via BrowserStack API using `fetchPercyToken()`
25
+ * 4. If nothing works, throws an enriched error with guidance
26
+ */
27
+ export declare function resolvePercyToken(config: BrowserStackConfig, options?: ResolveTokenOptions): Promise<string>;
28
+ /**
29
+ * Returns headers for Percy API requests.
30
+ * Includes Authorization, Content-Type, and User-Agent.
31
+ */
32
+ export declare function getPercyHeaders(config: BrowserStackConfig, options?: {
33
+ scope?: TokenScope;
34
+ projectName?: string;
35
+ }): Promise<Record<string, string>>;
36
+ /**
37
+ * Returns the Percy API base URL.
38
+ * Defaults to `https://percy.io/api/v1`, overridable via `PERCY_API_URL` env var.
39
+ */
40
+ export declare function getPercyApiBaseUrl(): string;
41
+ export {};
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Percy API authentication module.
3
+ * Resolves Percy tokens via environment variables or BrowserStack credential fallback.
4
+ *
5
+ * SECURITY: Token values are NEVER logged or included in error messages.
6
+ * Masked format (****<last4>) is used when referencing tokens in diagnostics.
7
+ */
8
+ import { fetchPercyToken } from "../../tools/sdk-utils/percy-web/fetchPercyToken.js";
9
+ /**
10
+ * Masks a token for safe display in error messages.
11
+ * Shows only the last 4 characters.
12
+ */
13
+ export function maskToken(token) {
14
+ if (token.length <= 4) {
15
+ return "****";
16
+ }
17
+ return `****${token.slice(-4)}`;
18
+ }
19
+ /**
20
+ * Resolves a Percy token using the following priority:
21
+ *
22
+ * 1. `process.env.PERCY_TOKEN` (for project or auto scope)
23
+ * 2. `process.env.PERCY_ORG_TOKEN` (for org scope)
24
+ * 3. Fallback: fetch via BrowserStack API using `fetchPercyToken()`
25
+ * 4. If nothing works, throws an enriched error with guidance
26
+ */
27
+ export async function resolvePercyToken(config, options = {}) {
28
+ const { projectName, scope = "auto" } = options;
29
+ // For project or auto scope, check PERCY_TOKEN first
30
+ if (scope === "project" || scope === "auto") {
31
+ const envToken = process.env.PERCY_TOKEN;
32
+ if (envToken) {
33
+ return envToken;
34
+ }
35
+ }
36
+ // For org scope, check PERCY_ORG_TOKEN
37
+ if (scope === "org") {
38
+ const orgToken = process.env.PERCY_ORG_TOKEN;
39
+ if (orgToken) {
40
+ return orgToken;
41
+ }
42
+ }
43
+ // For auto scope, also check PERCY_ORG_TOKEN as secondary
44
+ if (scope === "auto") {
45
+ const orgToken = process.env.PERCY_ORG_TOKEN;
46
+ if (orgToken) {
47
+ return orgToken;
48
+ }
49
+ }
50
+ // Fallback: fetch via BrowserStack credentials
51
+ const username = config["browserstack-username"];
52
+ const accessKey = config["browserstack-access-key"];
53
+ if (username && accessKey) {
54
+ const auth = `${username}:${accessKey}`;
55
+ const resolvedProjectName = projectName || "default";
56
+ try {
57
+ const token = await fetchPercyToken(resolvedProjectName, auth, {});
58
+ return token;
59
+ }
60
+ catch (error) {
61
+ const message = error instanceof Error ? error.message : String(error);
62
+ throw new Error(`Failed to fetch Percy token via BrowserStack API: ${message}. ` +
63
+ `Set PERCY_TOKEN or PERCY_ORG_TOKEN environment variable as an alternative.`);
64
+ }
65
+ }
66
+ // Nothing worked — provide actionable guidance
67
+ if (scope === "project") {
68
+ throw new Error("Percy project token not available. Set PERCY_TOKEN environment variable, " +
69
+ "or provide BrowserStack credentials to fetch a token automatically.");
70
+ }
71
+ if (scope === "org") {
72
+ throw new Error("Percy org token not available. Set PERCY_ORG_TOKEN environment variable.");
73
+ }
74
+ throw new Error("Percy token not available. Set PERCY_TOKEN (project) or PERCY_ORG_TOKEN (org) " +
75
+ "environment variable, or provide BrowserStack credentials (browserstack-username " +
76
+ "and browserstack-access-key) to fetch a token automatically.");
77
+ }
78
+ /**
79
+ * Returns headers for Percy API requests.
80
+ * Includes Authorization, Content-Type, and User-Agent.
81
+ */
82
+ export async function getPercyHeaders(config, options = {}) {
83
+ const token = await resolvePercyToken(config, options);
84
+ return {
85
+ Authorization: `Token token=${token}`,
86
+ "Content-Type": "application/json",
87
+ "User-Agent": "browserstack-mcp-server",
88
+ };
89
+ }
90
+ /**
91
+ * Returns the Percy API base URL.
92
+ * Defaults to `https://percy.io/api/v1`, overridable via `PERCY_API_URL` env var.
93
+ */
94
+ export function getPercyApiBaseUrl() {
95
+ return process.env.PERCY_API_URL || "https://percy.io/api/v1";
96
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Simple in-memory cache with per-entry TTL.
3
+ *
4
+ * Used to cache Percy API responses and avoid redundant network calls
5
+ * within short time windows (e.g., multiple tools querying the same build).
6
+ */
7
+ export declare class PercyCache {
8
+ private store;
9
+ /**
10
+ * Returns the cached value if it exists and has not expired.
11
+ * Expired entries are deleted on access.
12
+ */
13
+ get<T>(key: string): T | null;
14
+ /**
15
+ * Stores a value with an optional TTL (defaults to 30 seconds).
16
+ */
17
+ set(key: string, value: unknown, ttlMs?: number): void;
18
+ /**
19
+ * Removes all entries from the cache.
20
+ */
21
+ clear(): void;
22
+ /**
23
+ * Removes a single entry from the cache.
24
+ */
25
+ delete(key: string): void;
26
+ }
27
+ /** Singleton cache instance shared across Percy API tools. */
28
+ export declare const percyCache: PercyCache;
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Simple in-memory cache with per-entry TTL.
3
+ *
4
+ * Used to cache Percy API responses and avoid redundant network calls
5
+ * within short time windows (e.g., multiple tools querying the same build).
6
+ */
7
+ const DEFAULT_TTL_MS = 30_000; // 30 seconds
8
+ export class PercyCache {
9
+ store = new Map();
10
+ /**
11
+ * Returns the cached value if it exists and has not expired.
12
+ * Expired entries are deleted on access.
13
+ */
14
+ get(key) {
15
+ const entry = this.store.get(key);
16
+ if (!entry) {
17
+ return null;
18
+ }
19
+ if (Date.now() > entry.expiresAt) {
20
+ this.store.delete(key);
21
+ return null;
22
+ }
23
+ return entry.value;
24
+ }
25
+ /**
26
+ * Stores a value with an optional TTL (defaults to 30 seconds).
27
+ */
28
+ set(key, value, ttlMs = DEFAULT_TTL_MS) {
29
+ this.store.set(key, {
30
+ value,
31
+ expiresAt: Date.now() + ttlMs,
32
+ });
33
+ }
34
+ /**
35
+ * Removes all entries from the cache.
36
+ */
37
+ clear() {
38
+ this.store.clear();
39
+ }
40
+ /**
41
+ * Removes a single entry from the cache.
42
+ */
43
+ delete(key) {
44
+ this.store.delete(key);
45
+ }
46
+ }
47
+ /** Singleton cache instance shared across Percy API tools. */
48
+ export const percyCache = new PercyCache();
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Percy API HTTP client.
3
+ *
4
+ * Uses native `fetch` (consistent with existing Percy tools in this repo).
5
+ * Handles JSON:API deserialization, rate limiting, and error enrichment.
6
+ *
7
+ * SECURITY: Token values are NEVER logged or exposed in error messages.
8
+ */
9
+ import { BrowserStackConfig } from "../types.js";
10
+ type TokenScope = "project" | "org" | "auto";
11
+ interface ClientOptions {
12
+ scope?: TokenScope;
13
+ projectName?: string;
14
+ }
15
+ interface JsonApiResource {
16
+ id: string;
17
+ type: string;
18
+ attributes?: Record<string, unknown>;
19
+ relationships?: Record<string, {
20
+ data: {
21
+ id: string;
22
+ type: string;
23
+ } | Array<{
24
+ id: string;
25
+ type: string;
26
+ }> | null;
27
+ }>;
28
+ }
29
+ interface JsonApiEnvelope {
30
+ data: JsonApiResource | JsonApiResource[] | null;
31
+ included?: JsonApiResource[];
32
+ meta?: Record<string, unknown>;
33
+ }
34
+ /**
35
+ * Deserializes a JSON:API envelope into plain objects.
36
+ *
37
+ * - `data: null` → returns `null`
38
+ * - `data: []` → returns `[]`
39
+ * - `data: { ... }` → returns a single deserialized object
40
+ * - `data: [{ ... }, ...]` → returns an array of deserialized objects
41
+ */
42
+ export declare function deserialize(envelope: JsonApiEnvelope): {
43
+ data: Record<string, unknown> | Record<string, unknown>[] | null;
44
+ meta?: Record<string, unknown>;
45
+ };
46
+ export declare class PercyClient {
47
+ private config;
48
+ private options;
49
+ constructor(config: BrowserStackConfig, options?: ClientOptions);
50
+ /**
51
+ * GET request with optional query params and JSON:API `include`.
52
+ */
53
+ get<T = Record<string, unknown>>(path: string, params?: Record<string, string>, includes?: string[]): Promise<T>;
54
+ /**
55
+ * POST request with an optional JSON body.
56
+ */
57
+ post<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
58
+ /**
59
+ * PATCH request with an optional JSON body.
60
+ */
61
+ patch<T = Record<string, unknown>>(path: string, body?: unknown): Promise<T>;
62
+ /**
63
+ * DELETE request.
64
+ */
65
+ del(path: string): Promise<void>;
66
+ private buildUrl;
67
+ private request;
68
+ }
69
+ export {};
@@ -0,0 +1,275 @@
1
+ /**
2
+ * Percy API HTTP client.
3
+ *
4
+ * Uses native `fetch` (consistent with existing Percy tools in this repo).
5
+ * Handles JSON:API deserialization, rate limiting, and error enrichment.
6
+ *
7
+ * SECURITY: Token values are NEVER logged or exposed in error messages.
8
+ */
9
+ import { getPercyHeaders, getPercyApiBaseUrl } from "./auth.js";
10
+ import { enrichPercyError } from "./errors.js";
11
+ // ---------------------------------------------------------------------------
12
+ // Helpers – kebab-case to camelCase
13
+ // ---------------------------------------------------------------------------
14
+ function kebabToCamel(str) {
15
+ return str.replace(/-([a-z0-9])/g, (_, char) => char.toUpperCase());
16
+ }
17
+ function camelCaseKeys(obj) {
18
+ if (obj === null || obj === undefined) {
19
+ return obj;
20
+ }
21
+ if (Array.isArray(obj)) {
22
+ return obj.map(camelCaseKeys);
23
+ }
24
+ if (typeof obj === "object") {
25
+ const result = {};
26
+ for (const [key, value] of Object.entries(obj)) {
27
+ const camelKey = kebabToCamel(key);
28
+ result[camelKey] =
29
+ value !== null && typeof value === "object"
30
+ ? camelCaseKeys(value)
31
+ : value;
32
+ }
33
+ return result;
34
+ }
35
+ return obj;
36
+ }
37
+ // ---------------------------------------------------------------------------
38
+ // JSON:API Deserializer
39
+ // ---------------------------------------------------------------------------
40
+ /**
41
+ * Builds a lookup index of included resources keyed by `type:id`.
42
+ */
43
+ function buildIncludedIndex(included) {
44
+ const index = new Map();
45
+ for (const resource of included) {
46
+ const flattened = flattenResource(resource);
47
+ index.set(`${resource.type}:${resource.id}`, flattened);
48
+ }
49
+ return index;
50
+ }
51
+ /**
52
+ * Flattens a single JSON:API resource — merges `attributes` into the top
53
+ * level alongside `id` and `type`, converting keys to camelCase.
54
+ */
55
+ function flattenResource(resource) {
56
+ const attrs = resource.attributes
57
+ ? camelCaseKeys(resource.attributes)
58
+ : {};
59
+ return {
60
+ id: resource.id,
61
+ type: resource.type,
62
+ ...attrs,
63
+ };
64
+ }
65
+ /**
66
+ * Resolves relationships for a resource against the included index.
67
+ * Returns the resolved object(s) or the raw { id, type } ref when not found.
68
+ */
69
+ function resolveRelationships(resource, index) {
70
+ if (!resource.relationships) {
71
+ return {};
72
+ }
73
+ const resolved = {};
74
+ for (const [relName, relValue] of Object.entries(resource.relationships)) {
75
+ const camelName = kebabToCamel(relName);
76
+ const { data } = relValue;
77
+ if (data === null || data === undefined) {
78
+ resolved[camelName] = null;
79
+ }
80
+ else if (Array.isArray(data)) {
81
+ resolved[camelName] = data.map((ref) => index.get(`${ref.type}:${ref.id}`) ?? { id: ref.id, type: ref.type });
82
+ }
83
+ else {
84
+ resolved[camelName] = index.get(`${data.type}:${data.id}`) ?? {
85
+ id: data.id,
86
+ type: data.type,
87
+ };
88
+ }
89
+ }
90
+ return resolved;
91
+ }
92
+ /**
93
+ * Deserializes a JSON:API envelope into plain objects.
94
+ *
95
+ * - `data: null` → returns `null`
96
+ * - `data: []` → returns `[]`
97
+ * - `data: { ... }` → returns a single deserialized object
98
+ * - `data: [{ ... }, ...]` → returns an array of deserialized objects
99
+ */
100
+ export function deserialize(envelope) {
101
+ const included = envelope.included ?? [];
102
+ const index = buildIncludedIndex(included);
103
+ if (envelope.data === null || envelope.data === undefined) {
104
+ return { data: null, meta: envelope.meta };
105
+ }
106
+ if (Array.isArray(envelope.data)) {
107
+ const records = envelope.data.map((resource) => ({
108
+ ...flattenResource(resource),
109
+ ...resolveRelationships(resource, index),
110
+ }));
111
+ return { data: records, meta: envelope.meta };
112
+ }
113
+ const record = {
114
+ ...flattenResource(envelope.data),
115
+ ...resolveRelationships(envelope.data, index),
116
+ };
117
+ return { data: record, meta: envelope.meta };
118
+ }
119
+ // ---------------------------------------------------------------------------
120
+ // Rate Limit / Retry
121
+ // ---------------------------------------------------------------------------
122
+ const MAX_RETRIES = 3;
123
+ const BASE_RETRY_DELAY_MS = 1_000;
124
+ async function sleep(ms) {
125
+ return new Promise((resolve) => setTimeout(resolve, ms));
126
+ }
127
+ // ---------------------------------------------------------------------------
128
+ // PercyClient
129
+ // ---------------------------------------------------------------------------
130
+ export class PercyClient {
131
+ config;
132
+ options;
133
+ constructor(config, options) {
134
+ this.config = config;
135
+ this.options = options ?? {};
136
+ }
137
+ // -----------------------------------------------------------------------
138
+ // Public HTTP methods
139
+ // -----------------------------------------------------------------------
140
+ /**
141
+ * GET request with optional query params and JSON:API `include`.
142
+ */
143
+ async get(path, params, includes) {
144
+ const url = this.buildUrl(path, params, includes);
145
+ return this.request("GET", url);
146
+ }
147
+ /**
148
+ * POST request with an optional JSON body.
149
+ */
150
+ async post(path, body) {
151
+ const url = this.buildUrl(path);
152
+ return this.request("POST", url, body);
153
+ }
154
+ /**
155
+ * PATCH request with an optional JSON body.
156
+ */
157
+ async patch(path, body) {
158
+ const url = this.buildUrl(path);
159
+ return this.request("PATCH", url, body);
160
+ }
161
+ /**
162
+ * DELETE request.
163
+ */
164
+ async del(path) {
165
+ const url = this.buildUrl(path);
166
+ await this.request("DELETE", url);
167
+ }
168
+ // -----------------------------------------------------------------------
169
+ // Internal
170
+ // -----------------------------------------------------------------------
171
+ buildUrl(path, params, includes) {
172
+ const base = getPercyApiBaseUrl();
173
+ // Ensure no double slashes between base and path
174
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
175
+ const url = new URL(`${base}${normalizedPath}`);
176
+ if (params) {
177
+ for (const [key, value] of Object.entries(params)) {
178
+ url.searchParams.set(key, value);
179
+ }
180
+ }
181
+ if (includes && includes.length > 0) {
182
+ url.searchParams.set("include", includes.join(","));
183
+ }
184
+ return url.toString();
185
+ }
186
+ async request(method, url, body) {
187
+ const headers = await getPercyHeaders(this.config, {
188
+ scope: this.options.scope,
189
+ projectName: this.options.projectName,
190
+ });
191
+ let lastError;
192
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
193
+ const fetchOptions = {
194
+ method,
195
+ headers,
196
+ };
197
+ if (body !== undefined) {
198
+ fetchOptions.body = JSON.stringify(body);
199
+ }
200
+ let response;
201
+ try {
202
+ response = await fetch(url, fetchOptions);
203
+ }
204
+ catch (networkError) {
205
+ lastError =
206
+ networkError instanceof Error
207
+ ? networkError
208
+ : new Error(String(networkError));
209
+ // Network errors are not retryable via the rate-limit path,
210
+ // but we still respect the retry loop for consistency.
211
+ if (attempt < MAX_RETRIES) {
212
+ await sleep(BASE_RETRY_DELAY_MS * Math.pow(2, attempt));
213
+ continue;
214
+ }
215
+ throw lastError;
216
+ }
217
+ // 204 No Content
218
+ if (response.status === 204) {
219
+ return undefined;
220
+ }
221
+ // Rate limited — retry with backoff
222
+ if (response.status === 429) {
223
+ const retryAfter = response.headers.get("Retry-After");
224
+ const delayMs = retryAfter
225
+ ? parseFloat(retryAfter) * 1_000
226
+ : BASE_RETRY_DELAY_MS * Math.pow(2, attempt);
227
+ if (attempt < MAX_RETRIES) {
228
+ await sleep(delayMs);
229
+ continue;
230
+ }
231
+ // Exhausted retries — throw enriched error
232
+ let errorBody;
233
+ try {
234
+ errorBody = await response.json();
235
+ }
236
+ catch {
237
+ errorBody = undefined;
238
+ }
239
+ throw enrichPercyError(429, errorBody, `${method} ${url}`);
240
+ }
241
+ // Non-2xx error
242
+ if (!response.ok) {
243
+ let errorBody;
244
+ try {
245
+ errorBody = await response.json();
246
+ }
247
+ catch {
248
+ errorBody = undefined;
249
+ }
250
+ throw enrichPercyError(response.status, errorBody, `${method} ${url}`);
251
+ }
252
+ // Successful JSON response — deserialize JSON:API
253
+ const json = await response.json();
254
+ // If the response has a JSON:API `data` key, deserialize it
255
+ if (json && typeof json === "object" && "data" in json) {
256
+ const deserialized = deserialize(json);
257
+ // Unwrap: return the data directly (single object or array)
258
+ // Attach meta as a non-enumerable property so it's accessible but doesn't clutter
259
+ const result = deserialized.data;
260
+ if (result && typeof result === "object" && deserialized.meta) {
261
+ Object.defineProperty(result, "__meta", {
262
+ value: deserialized.meta,
263
+ enumerable: false,
264
+ writable: false,
265
+ });
266
+ }
267
+ return result;
268
+ }
269
+ // Non-JSON:API response — return as-is
270
+ return json;
271
+ }
272
+ // Should not reach here, but satisfy TypeScript
273
+ throw lastError ?? new Error("Request failed after retries");
274
+ }
275
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Percy API error enrichment module.
3
+ * Maps Percy API error responses to actionable, user-friendly messages.
4
+ */
5
+ export declare class PercyApiError extends Error {
6
+ statusCode: number;
7
+ errorCode?: string;
8
+ body?: unknown;
9
+ constructor(message: string, statusCode: number, errorCode?: string, body?: unknown);
10
+ }
11
+ /**
12
+ * Maps Percy API error responses to actionable messages.
13
+ * Handles known error codes from Percy's JSON:API responses.
14
+ */
15
+ export declare function enrichPercyError(status: number, body: unknown, context?: string): PercyApiError;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Percy API error enrichment module.
3
+ * Maps Percy API error responses to actionable, user-friendly messages.
4
+ */
5
+ export class PercyApiError extends Error {
6
+ statusCode;
7
+ errorCode;
8
+ body;
9
+ constructor(message, statusCode, errorCode, body) {
10
+ super(message);
11
+ this.name = "PercyApiError";
12
+ this.statusCode = statusCode;
13
+ this.errorCode = errorCode;
14
+ this.body = body;
15
+ }
16
+ }
17
+ /**
18
+ * Maps Percy API error responses to actionable messages.
19
+ * Handles known error codes from Percy's JSON:API responses.
20
+ */
21
+ export function enrichPercyError(status, body, context) {
22
+ const prefix = context ? `${context}: ` : "";
23
+ const errorBody = body;
24
+ const errors = (errorBody?.errors ?? []);
25
+ const firstError = errors[0];
26
+ const errorCode = (firstError?.code ?? firstError?.source);
27
+ const detail = (firstError?.detail ?? firstError?.title ?? "");
28
+ switch (status) {
29
+ case 401:
30
+ return new PercyApiError(`${prefix}Percy token is invalid or expired. Check PERCY_TOKEN environment variable.`, 401, errorCode, body);
31
+ case 403: {
32
+ if (errorCode === "project_rbac_access_denied") {
33
+ return new PercyApiError(`${prefix}Insufficient permissions. This operation requires write access to the project.`, 403, errorCode, body);
34
+ }
35
+ if (errorCode === "build_deleted") {
36
+ return new PercyApiError(`${prefix}This build has been deleted.`, 403, errorCode, body);
37
+ }
38
+ if (errorCode === "plan_history_exceeded") {
39
+ return new PercyApiError(`${prefix}This build is outside your plan's history limit.`, 403, errorCode, body);
40
+ }
41
+ return new PercyApiError(`${prefix}Forbidden: ${detail || "Access denied."}`, 403, errorCode, body);
42
+ }
43
+ case 404:
44
+ return new PercyApiError(`${prefix}Resource not found. Check the ID and try again.`, 404, errorCode, body);
45
+ case 422:
46
+ return new PercyApiError(`${prefix}Invalid request: ${detail || "Unprocessable entity."}`, 422, errorCode, body);
47
+ case 429:
48
+ return new PercyApiError(`${prefix}Rate limit exceeded. Try again shortly.`, 429, errorCode, body);
49
+ default:
50
+ return new PercyApiError(`${prefix}Percy API error (${status}): ${detail || "Unknown error"}`, status, errorCode, body);
51
+ }
52
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Markdown formatting utilities for Percy API responses.
3
+ *
4
+ * Each function transforms typed Percy API data into concise,
5
+ * agent-readable markdown. All functions handle null/undefined
6
+ * fields gracefully — showing "N/A" or omitting the section.
7
+ */
8
+ export declare function formatBuild(build: any): string;
9
+ export declare function formatSnapshot(snapshot: any, comparisons?: any[]): string;
10
+ export declare function formatComparison(comparison: any, options?: {
11
+ includeRegions?: boolean;
12
+ }): string;
13
+ export declare function formatSuggestions(suggestions: any[]): string;
14
+ export declare function formatNetworkLogs(logs: any[]): string;
15
+ export declare function formatBuildStatus(build: any): string;
16
+ export declare function formatAiWarning(comparisons: any[]): string;