@renseiai/agentfactory-linear 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +91 -0
  3. package/dist/src/agent-client-project-repo.test.d.ts +2 -0
  4. package/dist/src/agent-client-project-repo.test.d.ts.map +1 -0
  5. package/dist/src/agent-client-project-repo.test.js +153 -0
  6. package/dist/src/agent-client.d.ts +261 -0
  7. package/dist/src/agent-client.d.ts.map +1 -0
  8. package/dist/src/agent-client.js +902 -0
  9. package/dist/src/agent-session.d.ts +303 -0
  10. package/dist/src/agent-session.d.ts.map +1 -0
  11. package/dist/src/agent-session.js +969 -0
  12. package/dist/src/checkbox-utils.d.ts +88 -0
  13. package/dist/src/checkbox-utils.d.ts.map +1 -0
  14. package/dist/src/checkbox-utils.js +120 -0
  15. package/dist/src/circuit-breaker.d.ts +76 -0
  16. package/dist/src/circuit-breaker.d.ts.map +1 -0
  17. package/dist/src/circuit-breaker.js +229 -0
  18. package/dist/src/circuit-breaker.test.d.ts +2 -0
  19. package/dist/src/circuit-breaker.test.d.ts.map +1 -0
  20. package/dist/src/circuit-breaker.test.js +292 -0
  21. package/dist/src/constants.d.ts +87 -0
  22. package/dist/src/constants.d.ts.map +1 -0
  23. package/dist/src/constants.js +101 -0
  24. package/dist/src/defaults/auto-trigger.d.ts +35 -0
  25. package/dist/src/defaults/auto-trigger.d.ts.map +1 -0
  26. package/dist/src/defaults/auto-trigger.js +36 -0
  27. package/dist/src/defaults/index.d.ts +12 -0
  28. package/dist/src/defaults/index.d.ts.map +1 -0
  29. package/dist/src/defaults/index.js +11 -0
  30. package/dist/src/defaults/priority.d.ts +20 -0
  31. package/dist/src/defaults/priority.d.ts.map +1 -0
  32. package/dist/src/defaults/priority.js +37 -0
  33. package/dist/src/defaults/prompts.d.ts +42 -0
  34. package/dist/src/defaults/prompts.d.ts.map +1 -0
  35. package/dist/src/defaults/prompts.js +310 -0
  36. package/dist/src/defaults/prompts.test.d.ts +2 -0
  37. package/dist/src/defaults/prompts.test.d.ts.map +1 -0
  38. package/dist/src/defaults/prompts.test.js +263 -0
  39. package/dist/src/defaults/work-type-detection.d.ts +19 -0
  40. package/dist/src/defaults/work-type-detection.d.ts.map +1 -0
  41. package/dist/src/defaults/work-type-detection.js +93 -0
  42. package/dist/src/errors.d.ts +91 -0
  43. package/dist/src/errors.d.ts.map +1 -0
  44. package/dist/src/errors.js +173 -0
  45. package/dist/src/frontend-adapter.d.ts +168 -0
  46. package/dist/src/frontend-adapter.d.ts.map +1 -0
  47. package/dist/src/frontend-adapter.js +314 -0
  48. package/dist/src/frontend-adapter.test.d.ts +2 -0
  49. package/dist/src/frontend-adapter.test.d.ts.map +1 -0
  50. package/dist/src/frontend-adapter.test.js +545 -0
  51. package/dist/src/index.d.ts +28 -0
  52. package/dist/src/index.d.ts.map +1 -0
  53. package/dist/src/index.js +30 -0
  54. package/dist/src/issue-tracker-proxy.d.ts +140 -0
  55. package/dist/src/issue-tracker-proxy.d.ts.map +1 -0
  56. package/dist/src/issue-tracker-proxy.js +10 -0
  57. package/dist/src/platform-adapter.d.ts +132 -0
  58. package/dist/src/platform-adapter.d.ts.map +1 -0
  59. package/dist/src/platform-adapter.js +260 -0
  60. package/dist/src/platform-adapter.test.d.ts +2 -0
  61. package/dist/src/platform-adapter.test.d.ts.map +1 -0
  62. package/dist/src/platform-adapter.test.js +468 -0
  63. package/dist/src/proxy-client.d.ts +103 -0
  64. package/dist/src/proxy-client.d.ts.map +1 -0
  65. package/dist/src/proxy-client.js +191 -0
  66. package/dist/src/rate-limiter.d.ts +64 -0
  67. package/dist/src/rate-limiter.d.ts.map +1 -0
  68. package/dist/src/rate-limiter.js +163 -0
  69. package/dist/src/rate-limiter.test.d.ts +2 -0
  70. package/dist/src/rate-limiter.test.d.ts.map +1 -0
  71. package/dist/src/rate-limiter.test.js +217 -0
  72. package/dist/src/retry.d.ts +59 -0
  73. package/dist/src/retry.d.ts.map +1 -0
  74. package/dist/src/retry.js +82 -0
  75. package/dist/src/types.d.ts +492 -0
  76. package/dist/src/types.d.ts.map +1 -0
  77. package/dist/src/types.js +143 -0
  78. package/dist/src/utils.d.ts +52 -0
  79. package/dist/src/utils.d.ts.map +1 -0
  80. package/dist/src/utils.js +277 -0
  81. package/dist/src/webhook-types.d.ts +308 -0
  82. package/dist/src/webhook-types.d.ts.map +1 -0
  83. package/dist/src/webhook-types.js +46 -0
  84. package/package.json +70 -0
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Checkbox Utilities
3
+ *
4
+ * Parse and update markdown checkboxes in Linear issue descriptions.
5
+ * Checkboxes follow the standard markdown format:
6
+ * - [ ] Unchecked item
7
+ * - [x] Checked item
8
+ */
9
+ /**
10
+ * Represents a checkbox item parsed from markdown
11
+ */
12
+ export interface CheckboxItem {
13
+ /** Zero-based index among all checkboxes in the document */
14
+ index: number;
15
+ /** Line number in the markdown (zero-based) */
16
+ line: number;
17
+ /** Whether the checkbox is checked */
18
+ checked: boolean;
19
+ /** The text content after the checkbox */
20
+ text: string;
21
+ /** Indentation level (number of leading spaces) */
22
+ indentLevel: number;
23
+ /** The raw line text */
24
+ raw: string;
25
+ }
26
+ /**
27
+ * Checkbox update specification
28
+ */
29
+ export interface CheckboxUpdate {
30
+ /** Update checkbox by its index */
31
+ index?: number;
32
+ /** Update checkbox by matching text pattern */
33
+ textPattern?: string | RegExp;
34
+ /** New checked state */
35
+ checked: boolean;
36
+ }
37
+ /**
38
+ * Parse markdown and extract checkbox items
39
+ *
40
+ * @param markdown - The markdown content to parse
41
+ * @returns Array of checkbox items found in the markdown
42
+ */
43
+ export declare function parseCheckboxes(markdown: string): CheckboxItem[];
44
+ /**
45
+ * Update a checkbox by its index
46
+ *
47
+ * @param markdown - The markdown content
48
+ * @param index - The checkbox index to update
49
+ * @param checked - The new checked state
50
+ * @returns The updated markdown, or the original if checkbox not found
51
+ */
52
+ export declare function updateCheckbox(markdown: string, index: number, checked: boolean): string;
53
+ /**
54
+ * Update a checkbox by matching its text content
55
+ *
56
+ * @param markdown - The markdown content
57
+ * @param textPattern - String or regex to match against checkbox text
58
+ * @param checked - The new checked state
59
+ * @returns The updated markdown, or the original if no match found
60
+ */
61
+ export declare function updateCheckboxByText(markdown: string, textPattern: string | RegExp, checked: boolean): string;
62
+ /**
63
+ * Apply multiple checkbox updates at once
64
+ *
65
+ * @param markdown - The markdown content
66
+ * @param updates - Array of updates to apply
67
+ * @returns The updated markdown
68
+ */
69
+ export declare function updateCheckboxes(markdown: string, updates: CheckboxUpdate[]): string;
70
+ /**
71
+ * Check if markdown contains any checkboxes
72
+ *
73
+ * @param markdown - The markdown content to check
74
+ * @returns True if at least one checkbox is found
75
+ */
76
+ export declare function hasCheckboxes(markdown: string): boolean;
77
+ /**
78
+ * Get summary of checkbox states
79
+ *
80
+ * @param markdown - The markdown content
81
+ * @returns Object with counts of checked and unchecked items
82
+ */
83
+ export declare function getCheckboxSummary(markdown: string): {
84
+ total: number;
85
+ checked: number;
86
+ unchecked: number;
87
+ };
88
+ //# sourceMappingURL=checkbox-utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checkbox-utils.d.ts","sourceRoot":"","sources":["../../src/checkbox-utils.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAA;IACb,+CAA+C;IAC/C,IAAI,EAAE,MAAM,CAAA;IACZ,sCAAsC;IACtC,OAAO,EAAE,OAAO,CAAA;IAChB,0CAA0C;IAC1C,IAAI,EAAE,MAAM,CAAA;IACZ,mDAAmD;IACnD,WAAW,EAAE,MAAM,CAAA;IACnB,wBAAwB;IACxB,GAAG,EAAE,MAAM,CAAA;CACZ;AAED;;GAEG;AACH,MAAM,WAAW,cAAc;IAC7B,mCAAmC;IACnC,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,+CAA+C;IAC/C,WAAW,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;IAC7B,wBAAwB;IACxB,OAAO,EAAE,OAAO,CAAA;CACjB;AAMD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAuBhE;AAED;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,OAAO,GACf,MAAM,CAaR;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,GAAG,MAAM,EAC5B,OAAO,EAAE,OAAO,GACf,MAAM,CAeR;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,cAAc,EAAE,GACxB,MAAM,CAYR;AAED;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAGvD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG;IACpD,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,MAAM,CAAA;IACf,SAAS,EAAE,MAAM,CAAA;CAClB,CASA"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Checkbox Utilities
3
+ *
4
+ * Parse and update markdown checkboxes in Linear issue descriptions.
5
+ * Checkboxes follow the standard markdown format:
6
+ * - [ ] Unchecked item
7
+ * - [x] Checked item
8
+ */
9
+ // Regex to match markdown checkbox lines
10
+ // Captures: (leading whitespace)(checkbox mark)(text content)
11
+ const CHECKBOX_REGEX = /^(\s*)- \[([ xX])\] (.*)$/;
12
+ /**
13
+ * Parse markdown and extract checkbox items
14
+ *
15
+ * @param markdown - The markdown content to parse
16
+ * @returns Array of checkbox items found in the markdown
17
+ */
18
+ export function parseCheckboxes(markdown) {
19
+ const lines = markdown.split('\n');
20
+ const checkboxes = [];
21
+ let checkboxIndex = 0;
22
+ for (let lineNum = 0; lineNum < lines.length; lineNum++) {
23
+ const line = lines[lineNum];
24
+ const match = line.match(CHECKBOX_REGEX);
25
+ if (match) {
26
+ const [, indent, mark, text] = match;
27
+ checkboxes.push({
28
+ index: checkboxIndex++,
29
+ line: lineNum,
30
+ indentLevel: indent.length,
31
+ checked: mark.toLowerCase() === 'x',
32
+ text: text.trim(),
33
+ raw: line,
34
+ });
35
+ }
36
+ }
37
+ return checkboxes;
38
+ }
39
+ /**
40
+ * Update a checkbox by its index
41
+ *
42
+ * @param markdown - The markdown content
43
+ * @param index - The checkbox index to update
44
+ * @param checked - The new checked state
45
+ * @returns The updated markdown, or the original if checkbox not found
46
+ */
47
+ export function updateCheckbox(markdown, index, checked) {
48
+ const checkboxes = parseCheckboxes(markdown);
49
+ const checkbox = checkboxes.find((c) => c.index === index);
50
+ if (!checkbox) {
51
+ return markdown;
52
+ }
53
+ const lines = markdown.split('\n');
54
+ const newCheckmark = checked ? 'x' : ' ';
55
+ lines[checkbox.line] = checkbox.raw.replace(/\[([ xX])\]/, `[${newCheckmark}]`);
56
+ return lines.join('\n');
57
+ }
58
+ /**
59
+ * Update a checkbox by matching its text content
60
+ *
61
+ * @param markdown - The markdown content
62
+ * @param textPattern - String or regex to match against checkbox text
63
+ * @param checked - The new checked state
64
+ * @returns The updated markdown, or the original if no match found
65
+ */
66
+ export function updateCheckboxByText(markdown, textPattern, checked) {
67
+ const checkboxes = parseCheckboxes(markdown);
68
+ const pattern = typeof textPattern === 'string'
69
+ ? new RegExp(textPattern, 'i')
70
+ : textPattern;
71
+ const checkbox = checkboxes.find((c) => pattern.test(c.text));
72
+ if (!checkbox) {
73
+ return markdown;
74
+ }
75
+ return updateCheckbox(markdown, checkbox.index, checked);
76
+ }
77
+ /**
78
+ * Apply multiple checkbox updates at once
79
+ *
80
+ * @param markdown - The markdown content
81
+ * @param updates - Array of updates to apply
82
+ * @returns The updated markdown
83
+ */
84
+ export function updateCheckboxes(markdown, updates) {
85
+ let result = markdown;
86
+ for (const update of updates) {
87
+ if (update.index !== undefined) {
88
+ result = updateCheckbox(result, update.index, update.checked);
89
+ }
90
+ else if (update.textPattern) {
91
+ result = updateCheckboxByText(result, update.textPattern, update.checked);
92
+ }
93
+ }
94
+ return result;
95
+ }
96
+ /**
97
+ * Check if markdown contains any checkboxes
98
+ *
99
+ * @param markdown - The markdown content to check
100
+ * @returns True if at least one checkbox is found
101
+ */
102
+ export function hasCheckboxes(markdown) {
103
+ const lines = markdown.split('\n');
104
+ return lines.some((line) => CHECKBOX_REGEX.test(line));
105
+ }
106
+ /**
107
+ * Get summary of checkbox states
108
+ *
109
+ * @param markdown - The markdown content
110
+ * @returns Object with counts of checked and unchecked items
111
+ */
112
+ export function getCheckboxSummary(markdown) {
113
+ const checkboxes = parseCheckboxes(markdown);
114
+ const checked = checkboxes.filter((c) => c.checked).length;
115
+ return {
116
+ total: checkboxes.length,
117
+ checked,
118
+ unchecked: checkboxes.length - checked,
119
+ };
120
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Circuit Breaker for Linear API calls
3
+ *
4
+ * Prevents wasting rate limit quota on requests that are guaranteed to fail
5
+ * (e.g., expired OAuth tokens, revoked access). Implements the standard
6
+ * closed → open → half-open state machine.
7
+ *
8
+ * State machine:
9
+ * closed → all calls proceed; auth failures increment counter;
10
+ * at threshold → open
11
+ * open → all calls throw CircuitOpenError immediately (zero quota);
12
+ * after resetTimeoutMs → half-open
13
+ * half-open → one probe call allowed; success → closed;
14
+ * failure → open (with exponential backoff on reset timeout)
15
+ */
16
+ import { CircuitOpenError } from './errors.js';
17
+ import type { CircuitBreakerConfig, CircuitBreakerStrategy } from './types.js';
18
+ export type CircuitState = 'closed' | 'open' | 'half-open';
19
+ export declare const DEFAULT_CIRCUIT_BREAKER_CONFIG: Required<CircuitBreakerConfig>;
20
+ export declare class CircuitBreaker implements CircuitBreakerStrategy {
21
+ private _state;
22
+ private consecutiveFailures;
23
+ private openedAt;
24
+ private currentResetTimeoutMs;
25
+ private probeInFlight;
26
+ private readonly config;
27
+ constructor(config?: Partial<CircuitBreakerConfig>);
28
+ get state(): CircuitState;
29
+ /**
30
+ * Check if a call is allowed to proceed.
31
+ * In half-open state, only one probe call is allowed at a time.
32
+ */
33
+ canProceed(): boolean;
34
+ /**
35
+ * Record a successful API call. Resets the circuit to closed.
36
+ */
37
+ recordSuccess(): void;
38
+ /**
39
+ * Record an auth failure. May trip the circuit to open.
40
+ * Called after isAuthError() returns true, so the error is already vetted.
41
+ */
42
+ recordAuthFailure(_statusCode?: number): void;
43
+ /**
44
+ * Check if an error is an auth/rate-limit error that should count as a circuit failure.
45
+ *
46
+ * Detects:
47
+ * - HTTP status codes in authErrorCodes (400, 401, 403)
48
+ * - Linear GraphQL RATELIMITED error code in response body
49
+ * - Linear SDK error objects with nested error details
50
+ */
51
+ isAuthError(error: unknown): boolean;
52
+ /**
53
+ * Extract the status code from an auth error, or 0 if not determinable.
54
+ */
55
+ extractStatusCode(error: unknown): number;
56
+ /**
57
+ * Reset the circuit breaker to closed state.
58
+ */
59
+ reset(): void;
60
+ /**
61
+ * Get diagnostic info for logging/monitoring.
62
+ */
63
+ getStatus(): {
64
+ state: CircuitState;
65
+ consecutiveFailures: number;
66
+ currentResetTimeoutMs: number;
67
+ msSinceOpened: number | null;
68
+ };
69
+ /**
70
+ * Create a CircuitOpenError with current diagnostic info.
71
+ */
72
+ createOpenError(): CircuitOpenError;
73
+ private trip;
74
+ private shouldTransitionToHalfOpen;
75
+ }
76
+ //# sourceMappingURL=circuit-breaker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAC9C,OAAO,KAAK,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,MAAM,YAAY,CAAA;AAE9E,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,MAAM,GAAG,WAAW,CAAA;AAE1D,eAAO,MAAM,8BAA8B,EAAE,QAAQ,CAAC,oBAAoB,CAMzE,CAAA;AAED,qBAAa,cAAe,YAAW,sBAAsB;IAC3D,OAAO,CAAC,MAAM,CAAyB;IACvC,OAAO,CAAC,mBAAmB,CAAI;IAC/B,OAAO,CAAC,QAAQ,CAAI;IACpB,OAAO,CAAC,qBAAqB,CAAQ;IACrC,OAAO,CAAC,aAAa,CAAQ;IAC7B,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAgC;gBAE3C,MAAM,CAAC,EAAE,OAAO,CAAC,oBAAoB,CAAC;IAKlD,IAAI,KAAK,IAAI,YAAY,CAOxB;IAED;;;OAGG;IACH,UAAU,IAAI,OAAO;IAgBrB;;OAEG;IACH,aAAa,IAAI,IAAI;IASrB;;;OAGG;IACH,iBAAiB,CAAC,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI;IAmB7C;;;;;;;OAOG;IACH,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAyBpC;;OAEG;IACH,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM;IAKzC;;OAEG;IACH,KAAK,IAAI,IAAI;IAOb;;OAEG;IACH,SAAS,IAAI;QACX,KAAK,EAAE,YAAY,CAAA;QACnB,mBAAmB,EAAE,MAAM,CAAA;QAC3B,qBAAqB,EAAE,MAAM,CAAA;QAC7B,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;KAC7B;IASD;;OAEG;IACH,eAAe,IAAI,gBAAgB;IAgBnC,OAAO,CAAC,IAAI;IAKZ,OAAO,CAAC,0BAA0B;CAGnC"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Circuit Breaker for Linear API calls
3
+ *
4
+ * Prevents wasting rate limit quota on requests that are guaranteed to fail
5
+ * (e.g., expired OAuth tokens, revoked access). Implements the standard
6
+ * closed → open → half-open state machine.
7
+ *
8
+ * State machine:
9
+ * closed → all calls proceed; auth failures increment counter;
10
+ * at threshold → open
11
+ * open → all calls throw CircuitOpenError immediately (zero quota);
12
+ * after resetTimeoutMs → half-open
13
+ * half-open → one probe call allowed; success → closed;
14
+ * failure → open (with exponential backoff on reset timeout)
15
+ */
16
+ import { CircuitOpenError } from './errors.js';
17
+ export const DEFAULT_CIRCUIT_BREAKER_CONFIG = {
18
+ failureThreshold: 2,
19
+ resetTimeoutMs: 60_000,
20
+ maxResetTimeoutMs: 300_000,
21
+ backoffMultiplier: 2,
22
+ authErrorCodes: [400, 401, 403],
23
+ };
24
+ export class CircuitBreaker {
25
+ _state = 'closed';
26
+ consecutiveFailures = 0;
27
+ openedAt = 0;
28
+ currentResetTimeoutMs;
29
+ probeInFlight = false;
30
+ config;
31
+ constructor(config) {
32
+ this.config = { ...DEFAULT_CIRCUIT_BREAKER_CONFIG, ...config };
33
+ this.currentResetTimeoutMs = this.config.resetTimeoutMs;
34
+ }
35
+ get state() {
36
+ // Check if open circuit should transition to half-open
37
+ if (this._state === 'open' && this.shouldTransitionToHalfOpen()) {
38
+ this._state = 'half-open';
39
+ this.probeInFlight = false;
40
+ }
41
+ return this._state;
42
+ }
43
+ /**
44
+ * Check if a call is allowed to proceed.
45
+ * In half-open state, only one probe call is allowed at a time.
46
+ */
47
+ canProceed() {
48
+ const currentState = this.state; // triggers open → half-open check
49
+ switch (currentState) {
50
+ case 'closed':
51
+ return true;
52
+ case 'open':
53
+ return false;
54
+ case 'half-open':
55
+ // Allow exactly one probe call
56
+ if (this.probeInFlight)
57
+ return false;
58
+ this.probeInFlight = true;
59
+ return true;
60
+ }
61
+ }
62
+ /**
63
+ * Record a successful API call. Resets the circuit to closed.
64
+ */
65
+ recordSuccess() {
66
+ this.consecutiveFailures = 0;
67
+ this.probeInFlight = false;
68
+ if (this._state !== 'closed') {
69
+ this._state = 'closed';
70
+ this.currentResetTimeoutMs = this.config.resetTimeoutMs;
71
+ }
72
+ }
73
+ /**
74
+ * Record an auth failure. May trip the circuit to open.
75
+ * Called after isAuthError() returns true, so the error is already vetted.
76
+ */
77
+ recordAuthFailure(_statusCode) {
78
+ this.probeInFlight = false;
79
+ this.consecutiveFailures++;
80
+ if (this._state === 'half-open') {
81
+ // Probe failed — reopen with exponential backoff
82
+ this.trip();
83
+ this.currentResetTimeoutMs = Math.min(this.currentResetTimeoutMs * this.config.backoffMultiplier, this.config.maxResetTimeoutMs);
84
+ return;
85
+ }
86
+ if (this.consecutiveFailures >= this.config.failureThreshold) {
87
+ this.trip();
88
+ }
89
+ }
90
+ /**
91
+ * Check if an error is an auth/rate-limit error that should count as a circuit failure.
92
+ *
93
+ * Detects:
94
+ * - HTTP status codes in authErrorCodes (400, 401, 403)
95
+ * - Linear GraphQL RATELIMITED error code in response body
96
+ * - Linear SDK error objects with nested error details
97
+ */
98
+ isAuthError(error) {
99
+ if (typeof error !== 'object' || error === null)
100
+ return false;
101
+ const err = error;
102
+ // Check HTTP status code
103
+ const statusCode = extractStatusCode(err);
104
+ if (statusCode !== null && this.config.authErrorCodes.includes(statusCode)) {
105
+ return true;
106
+ }
107
+ // Check for Linear GraphQL RATELIMITED error
108
+ if (isGraphQLRateLimited(err)) {
109
+ return true;
110
+ }
111
+ // Check error message for known auth failure patterns
112
+ const message = err.message ?? '';
113
+ if (/access denied|unauthorized|forbidden/i.test(message)) {
114
+ return true;
115
+ }
116
+ return false;
117
+ }
118
+ /**
119
+ * Extract the status code from an auth error, or 0 if not determinable.
120
+ */
121
+ extractStatusCode(error) {
122
+ if (typeof error !== 'object' || error === null)
123
+ return 0;
124
+ return extractStatusCode(error) ?? 0;
125
+ }
126
+ /**
127
+ * Reset the circuit breaker to closed state.
128
+ */
129
+ reset() {
130
+ this._state = 'closed';
131
+ this.consecutiveFailures = 0;
132
+ this.probeInFlight = false;
133
+ this.currentResetTimeoutMs = this.config.resetTimeoutMs;
134
+ }
135
+ /**
136
+ * Get diagnostic info for logging/monitoring.
137
+ */
138
+ getStatus() {
139
+ return {
140
+ state: this.state,
141
+ consecutiveFailures: this.consecutiveFailures,
142
+ currentResetTimeoutMs: this.currentResetTimeoutMs,
143
+ msSinceOpened: this.openedAt > 0 ? Date.now() - this.openedAt : null,
144
+ };
145
+ }
146
+ /**
147
+ * Create a CircuitOpenError with current diagnostic info.
148
+ */
149
+ createOpenError() {
150
+ const timeRemaining = Math.max(0, this.currentResetTimeoutMs - (Date.now() - this.openedAt));
151
+ return new CircuitOpenError(`Circuit breaker is open — Linear API calls blocked for ${Math.ceil(timeRemaining / 1000)}s. ` +
152
+ `${this.consecutiveFailures} consecutive auth failures detected.`, timeRemaining);
153
+ }
154
+ // ---------------------------------------------------------------------------
155
+ // Private
156
+ // ---------------------------------------------------------------------------
157
+ trip() {
158
+ this._state = 'open';
159
+ this.openedAt = Date.now();
160
+ }
161
+ shouldTransitionToHalfOpen() {
162
+ return Date.now() - this.openedAt >= this.currentResetTimeoutMs;
163
+ }
164
+ }
165
+ // ---------------------------------------------------------------------------
166
+ // Helpers
167
+ // ---------------------------------------------------------------------------
168
+ /**
169
+ * Extract HTTP status code from various error shapes
170
+ */
171
+ function extractStatusCode(err) {
172
+ // Direct status/statusCode property
173
+ if (typeof err.status === 'number')
174
+ return err.status;
175
+ if (typeof err.statusCode === 'number')
176
+ return err.statusCode;
177
+ // Nested in response
178
+ const response = err.response;
179
+ if (response) {
180
+ if (typeof response.status === 'number')
181
+ return response.status;
182
+ if (typeof response.statusCode === 'number')
183
+ return response.statusCode;
184
+ }
185
+ return null;
186
+ }
187
+ /**
188
+ * Check if the error contains a Linear GraphQL RATELIMITED error code.
189
+ *
190
+ * Linear returns HTTP 200 with a GraphQL error body when rate-limited:
191
+ * { errors: [{ extensions: { code: 'RATELIMITED' } }] }
192
+ */
193
+ function isGraphQLRateLimited(err) {
194
+ // Check error.extensions.code directly (Linear SDK error shape)
195
+ const extensions = err.extensions;
196
+ if (extensions?.code === 'RATELIMITED')
197
+ return true;
198
+ // Check nested errors array (raw GraphQL response shape)
199
+ const errors = err.errors;
200
+ if (Array.isArray(errors)) {
201
+ for (const gqlError of errors) {
202
+ const ext = gqlError.extensions;
203
+ if (ext?.code === 'RATELIMITED')
204
+ return true;
205
+ }
206
+ }
207
+ // Check response body for GraphQL errors
208
+ const response = err.response;
209
+ if (response) {
210
+ const body = response.body;
211
+ const data = response.data;
212
+ const target = body ?? data;
213
+ if (target) {
214
+ const bodyErrors = target.errors;
215
+ if (Array.isArray(bodyErrors)) {
216
+ for (const gqlError of bodyErrors) {
217
+ const ext = gqlError.extensions;
218
+ if (ext?.code === 'RATELIMITED')
219
+ return true;
220
+ }
221
+ }
222
+ }
223
+ }
224
+ // Check error message as last resort
225
+ const message = err.message ?? '';
226
+ if (message.includes('RATELIMITED'))
227
+ return true;
228
+ return false;
229
+ }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=circuit-breaker.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"circuit-breaker.test.d.ts","sourceRoot":"","sources":["../../src/circuit-breaker.test.ts"],"names":[],"mappings":""}