@enrichlayer/el-linear 1.44.1 → 1.45.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.
- package/README.md +159 -0
- package/claude-skills/linear-operations/SKILL.md +32 -0
- package/dist/auth/oauth-storage.d.ts +5 -3
- package/dist/auth/oauth-token.d.ts +7 -0
- package/dist/auth/oauth-token.js +13 -0
- package/dist/auth/token-resolver.d.ts +2 -0
- package/dist/auth/token-resolver.js +25 -10
- package/dist/commands/init/index.js +17 -1
- package/dist/commands/init/oauth.d.ts +9 -1
- package/dist/commands/init/oauth.js +62 -3
- package/dist/queries/catalog-types.d.ts +129 -0
- package/dist/queries/catalog-types.js +1 -0
- package/dist/queries/catalog.d.ts +20 -0
- package/dist/queries/catalog.js +140 -0
- package/dist/utils/disk-cache.js +48 -14
- package/dist/utils/graphql-service.d.ts +42 -1
- package/dist/utils/graphql-service.js +267 -14
- package/dist/utils/linear-graphql-error.d.ts +100 -0
- package/dist/utils/linear-graphql-error.js +182 -0
- package/dist/utils/linear-service.d.ts +19 -2
- package/dist/utils/linear-service.js +194 -214
- package/dist/utils/output.d.ts +39 -0
- package/dist/utils/output.js +152 -1
- package/dist/utils/rate-limit-admission.d.ts +33 -0
- package/dist/utils/rate-limit-admission.js +240 -0
- package/package.json +79 -79
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured classification for a failed Linear GraphQL request (DEV-7987).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Every command routes its failures through `outputError`
|
|
5
|
+
* in `utils/output.ts`, which used to emit `{ error, activeProfile }` and
|
|
6
|
+
* nothing else. That flattens a rate limit, a gateway blip and a permanent
|
|
7
|
+
* schema error into one indistinguishable shape, so a machine caller
|
|
8
|
+
* shelling out to `el-linear` (a job runner deciding whether to retry, say)
|
|
9
|
+
* had to substring-match the message to guess. This module carries the two
|
|
10
|
+
* facts that were being thrown away — the HTTP status and the GraphQL
|
|
11
|
+
* `extensions.code` — plus the retryable verdict derived from them, so the
|
|
12
|
+
* envelope can state them.
|
|
13
|
+
*
|
|
14
|
+
* Kept in its own module, free of `@linear/sdk`, so `utils/output.ts` can
|
|
15
|
+
* read the classification without importing the GraphQL client.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* GraphQL `extensions.code` values Linear returns for conditions that clear
|
|
19
|
+
* on their own. A caller may retry these regardless of the HTTP status —
|
|
20
|
+
* Linear answers a rate limit with `RATELIMITED` under more than one
|
|
21
|
+
* status, so the code is load-bearing on its own.
|
|
22
|
+
*/
|
|
23
|
+
export declare const RETRYABLE_GRAPHQL_ERROR_CODES: readonly string[];
|
|
24
|
+
/**
|
|
25
|
+
* The classification block `el-linear` publishes as `errorDetail` on a
|
|
26
|
+
* failed command's JSON envelope. Status, code and retryability are always
|
|
27
|
+
* present; `null` means no such response signal. A known admission deadline
|
|
28
|
+
* is additive and optional.
|
|
29
|
+
*/
|
|
30
|
+
export interface LinearGraphQLErrorDetail {
|
|
31
|
+
/** HTTP status of the Linear response, or `null` if it never arrived. */
|
|
32
|
+
httpStatus: number | null;
|
|
33
|
+
/** GraphQL `extensions.code` of the first error, or `null`. */
|
|
34
|
+
code: string | null;
|
|
35
|
+
/** Whether retrying the identical request could plausibly succeed. */
|
|
36
|
+
retryable: boolean;
|
|
37
|
+
/** Known quota reset or recovery-probe deadline, when admission refused locally. */
|
|
38
|
+
resetAt?: string;
|
|
39
|
+
}
|
|
40
|
+
/** HTTP status of a rejected request, or `null` if no response arrived. */
|
|
41
|
+
export declare function graphQLErrorHttpStatus(error: unknown): number | null;
|
|
42
|
+
/** First GraphQL error's `extensions.code`, or `null` when absent. */
|
|
43
|
+
export declare function graphQLErrorCode(error: unknown): string | null;
|
|
44
|
+
/** True when an HTTP status alone justifies a retry. */
|
|
45
|
+
export declare function isRetryableHttpStatus(status: number | null): boolean;
|
|
46
|
+
/** True when a GraphQL `extensions.code` alone justifies a retry. */
|
|
47
|
+
export declare function isRetryableGraphQLCode(code: string | null): boolean;
|
|
48
|
+
/**
|
|
49
|
+
* The retryable verdict from the authoritative response status and GraphQL code.
|
|
50
|
+
*
|
|
51
|
+
* "Retryable" here means *the failure is transient* — a caller that waits
|
|
52
|
+
* an appropriate interval could succeed. It is NOT the same question as
|
|
53
|
+
* `isTransientGraphQLError` in `graphql-service.ts`, which gates this CLI's
|
|
54
|
+
* own immediate 150 ms / 400 ms retry: a rate limit is transient (so
|
|
55
|
+
* `retryable: true`) but a terrible candidate for an immediate retry.
|
|
56
|
+
*
|
|
57
|
+
* The last arm is the one that is easy to get wrong: a rejection carrying
|
|
58
|
+
* NEITHER a status NOR a code never got an answer out of Linear at all —
|
|
59
|
+
* DNS, a refused connection, a dropped socket — and those are exactly the
|
|
60
|
+
* failures worth retrying. Every failure Linear itself produced arrives
|
|
61
|
+
* with a status (`LinearError.status` is set from the HTTP response), so
|
|
62
|
+
* this arm cannot swallow a permanent schema or permission error. Message
|
|
63
|
+
* text is deliberately excluded: a permanent response may legitimately
|
|
64
|
+
* mention a transient-looking number such as a validation limit of 500.
|
|
65
|
+
*/
|
|
66
|
+
export declare function classifyGraphQLFailure(httpStatus: number | null, code: string | null): boolean;
|
|
67
|
+
/**
|
|
68
|
+
* A Linear GraphQL failure that carries its own classification.
|
|
69
|
+
*
|
|
70
|
+
* The `message` is unchanged from what the plain `Error` used to carry, so
|
|
71
|
+
* every existing consumer that matches on the message text keeps working;
|
|
72
|
+
* the classification is purely additive.
|
|
73
|
+
*/
|
|
74
|
+
export declare class LinearGraphQLError extends Error {
|
|
75
|
+
readonly httpStatus: number | null;
|
|
76
|
+
readonly code: string | null;
|
|
77
|
+
readonly retryable: boolean;
|
|
78
|
+
readonly resetAt?: string;
|
|
79
|
+
constructor(message: string, detail: LinearGraphQLErrorDetail);
|
|
80
|
+
get detail(): LinearGraphQLErrorDetail;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Wrap a rejection from the Linear GraphQL client in a classified error,
|
|
84
|
+
* keeping `message` exactly as the caller composed it.
|
|
85
|
+
*
|
|
86
|
+
* This is the seam every throw out of `GraphQLService.rawRequest` must pass
|
|
87
|
+
* through. A branch that builds its own message and throws a bare `Error`
|
|
88
|
+
* instead — a rate-limit path with a friendlier wording, say — produces a
|
|
89
|
+
* failure with NO `errorDetail` on the envelope, which is precisely the
|
|
90
|
+
* blindness DEV-7987 exists to remove, and it fails silently.
|
|
91
|
+
*/
|
|
92
|
+
export declare function toLinearGraphQLError(error: unknown, message: string): LinearGraphQLError;
|
|
93
|
+
/**
|
|
94
|
+
* Read the classification off an arbitrary thrown value, or `null` when it
|
|
95
|
+
* carries none. Structural rather than `instanceof` so a
|
|
96
|
+
* `LinearGraphQLError` that crossed a module-instance boundary (a vitest
|
|
97
|
+
* mock, a duplicated dependency copy) is still recognized — the envelope
|
|
98
|
+
* contract must not depend on prototype identity.
|
|
99
|
+
*/
|
|
100
|
+
export declare function readGraphQLErrorDetail(error: unknown): LinearGraphQLErrorDetail | null;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured classification for a failed Linear GraphQL request (DEV-7987).
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS EXISTS. Every command routes its failures through `outputError`
|
|
5
|
+
* in `utils/output.ts`, which used to emit `{ error, activeProfile }` and
|
|
6
|
+
* nothing else. That flattens a rate limit, a gateway blip and a permanent
|
|
7
|
+
* schema error into one indistinguishable shape, so a machine caller
|
|
8
|
+
* shelling out to `el-linear` (a job runner deciding whether to retry, say)
|
|
9
|
+
* had to substring-match the message to guess. This module carries the two
|
|
10
|
+
* facts that were being thrown away — the HTTP status and the GraphQL
|
|
11
|
+
* `extensions.code` — plus the retryable verdict derived from them, so the
|
|
12
|
+
* envelope can state them.
|
|
13
|
+
*
|
|
14
|
+
* Kept in its own module, free of `@linear/sdk`, so `utils/output.ts` can
|
|
15
|
+
* read the classification without importing the GraphQL client.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* GraphQL `extensions.code` values Linear returns for conditions that clear
|
|
19
|
+
* on their own. A caller may retry these regardless of the HTTP status —
|
|
20
|
+
* Linear answers a rate limit with `RATELIMITED` under more than one
|
|
21
|
+
* status, so the code is load-bearing on its own.
|
|
22
|
+
*/
|
|
23
|
+
export const RETRYABLE_GRAPHQL_ERROR_CODES = [
|
|
24
|
+
"RATELIMITED",
|
|
25
|
+
"INTERNAL_SERVER_ERROR",
|
|
26
|
+
"SERVICE_UNAVAILABLE",
|
|
27
|
+
];
|
|
28
|
+
/** HTTP statuses that mean "the request was fine, the moment wasn't". */
|
|
29
|
+
const RETRYABLE_HTTP_STATUSES = [408, 429];
|
|
30
|
+
/** Accept explicit UTC timestamps only; error messages never establish a deadline. */
|
|
31
|
+
function normalizeResetAt(value) {
|
|
32
|
+
if (typeof value !== "string" ||
|
|
33
|
+
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/.test(value)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const millis = Date.parse(value);
|
|
37
|
+
if (!Number.isFinite(millis))
|
|
38
|
+
return undefined;
|
|
39
|
+
const normalized = new Date(millis).toISOString();
|
|
40
|
+
// Date.parse normalizes impossible calendar dates rather than rejecting them.
|
|
41
|
+
return normalized.slice(0, 19) === value.slice(0, 19)
|
|
42
|
+
? normalized
|
|
43
|
+
: undefined;
|
|
44
|
+
}
|
|
45
|
+
function asNumber(value) {
|
|
46
|
+
return typeof value === "number" ? value : null;
|
|
47
|
+
}
|
|
48
|
+
function asString(value) {
|
|
49
|
+
return typeof value === "string" ? value : null;
|
|
50
|
+
}
|
|
51
|
+
/** HTTP status of a rejected request, or `null` if no response arrived. */
|
|
52
|
+
export function graphQLErrorHttpStatus(error) {
|
|
53
|
+
if (typeof error !== "object" || error === null) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
const rejection = error;
|
|
57
|
+
return (asNumber(rejection.status) ??
|
|
58
|
+
asNumber(rejection.response?.status) ??
|
|
59
|
+
asNumber(rejection.response?.statusCode) ??
|
|
60
|
+
asNumber(rejection.raw?.response?.status));
|
|
61
|
+
}
|
|
62
|
+
/** First GraphQL error's `extensions.code`, or `null` when absent. */
|
|
63
|
+
export function graphQLErrorCode(error) {
|
|
64
|
+
if (typeof error !== "object" || error === null) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const rejection = error;
|
|
68
|
+
return (asString(rejection.response?.errors?.[0]?.extensions?.code) ??
|
|
69
|
+
asString(rejection.raw?.response?.errors?.[0]?.extensions?.code));
|
|
70
|
+
}
|
|
71
|
+
/** True when an HTTP status alone justifies a retry. */
|
|
72
|
+
export function isRetryableHttpStatus(status) {
|
|
73
|
+
if (status === null) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
return RETRYABLE_HTTP_STATUSES.includes(status) || status >= 500;
|
|
77
|
+
}
|
|
78
|
+
/** True when a GraphQL `extensions.code` alone justifies a retry. */
|
|
79
|
+
export function isRetryableGraphQLCode(code) {
|
|
80
|
+
return code !== null && RETRYABLE_GRAPHQL_ERROR_CODES.includes(code);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The retryable verdict from the authoritative response status and GraphQL code.
|
|
84
|
+
*
|
|
85
|
+
* "Retryable" here means *the failure is transient* — a caller that waits
|
|
86
|
+
* an appropriate interval could succeed. It is NOT the same question as
|
|
87
|
+
* `isTransientGraphQLError` in `graphql-service.ts`, which gates this CLI's
|
|
88
|
+
* own immediate 150 ms / 400 ms retry: a rate limit is transient (so
|
|
89
|
+
* `retryable: true`) but a terrible candidate for an immediate retry.
|
|
90
|
+
*
|
|
91
|
+
* The last arm is the one that is easy to get wrong: a rejection carrying
|
|
92
|
+
* NEITHER a status NOR a code never got an answer out of Linear at all —
|
|
93
|
+
* DNS, a refused connection, a dropped socket — and those are exactly the
|
|
94
|
+
* failures worth retrying. Every failure Linear itself produced arrives
|
|
95
|
+
* with a status (`LinearError.status` is set from the HTTP response), so
|
|
96
|
+
* this arm cannot swallow a permanent schema or permission error. Message
|
|
97
|
+
* text is deliberately excluded: a permanent response may legitimately
|
|
98
|
+
* mention a transient-looking number such as a validation limit of 500.
|
|
99
|
+
*/
|
|
100
|
+
export function classifyGraphQLFailure(httpStatus, code) {
|
|
101
|
+
if (isRetryableHttpStatus(httpStatus) || isRetryableGraphQLCode(code)) {
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
if (httpStatus === null && code === null) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
// A response status or GraphQL code is authoritative. Message text may
|
|
108
|
+
// contain an unrelated number such as a validation limit of 500; it must
|
|
109
|
+
// never turn a known permanent 400/401/403 into a retryable transport fault.
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* A Linear GraphQL failure that carries its own classification.
|
|
114
|
+
*
|
|
115
|
+
* The `message` is unchanged from what the plain `Error` used to carry, so
|
|
116
|
+
* every existing consumer that matches on the message text keeps working;
|
|
117
|
+
* the classification is purely additive.
|
|
118
|
+
*/
|
|
119
|
+
export class LinearGraphQLError extends Error {
|
|
120
|
+
httpStatus;
|
|
121
|
+
code;
|
|
122
|
+
retryable;
|
|
123
|
+
resetAt;
|
|
124
|
+
constructor(message, detail) {
|
|
125
|
+
super(message);
|
|
126
|
+
this.name = "LinearGraphQLError";
|
|
127
|
+
this.httpStatus = detail.httpStatus;
|
|
128
|
+
this.code = detail.code;
|
|
129
|
+
this.retryable = detail.retryable;
|
|
130
|
+
this.resetAt = normalizeResetAt(detail.resetAt);
|
|
131
|
+
}
|
|
132
|
+
get detail() {
|
|
133
|
+
return {
|
|
134
|
+
httpStatus: this.httpStatus,
|
|
135
|
+
code: this.code,
|
|
136
|
+
retryable: this.retryable,
|
|
137
|
+
...(this.resetAt ? { resetAt: this.resetAt } : {}),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Wrap a rejection from the Linear GraphQL client in a classified error,
|
|
143
|
+
* keeping `message` exactly as the caller composed it.
|
|
144
|
+
*
|
|
145
|
+
* This is the seam every throw out of `GraphQLService.rawRequest` must pass
|
|
146
|
+
* through. A branch that builds its own message and throws a bare `Error`
|
|
147
|
+
* instead — a rate-limit path with a friendlier wording, say — produces a
|
|
148
|
+
* failure with NO `errorDetail` on the envelope, which is precisely the
|
|
149
|
+
* blindness DEV-7987 exists to remove, and it fails silently.
|
|
150
|
+
*/
|
|
151
|
+
export function toLinearGraphQLError(error, message) {
|
|
152
|
+
const httpStatus = graphQLErrorHttpStatus(error);
|
|
153
|
+
const code = graphQLErrorCode(error);
|
|
154
|
+
return new LinearGraphQLError(message, {
|
|
155
|
+
httpStatus,
|
|
156
|
+
code,
|
|
157
|
+
retryable: classifyGraphQLFailure(httpStatus, code),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Read the classification off an arbitrary thrown value, or `null` when it
|
|
162
|
+
* carries none. Structural rather than `instanceof` so a
|
|
163
|
+
* `LinearGraphQLError` that crossed a module-instance boundary (a vitest
|
|
164
|
+
* mock, a duplicated dependency copy) is still recognized — the envelope
|
|
165
|
+
* contract must not depend on prototype identity.
|
|
166
|
+
*/
|
|
167
|
+
export function readGraphQLErrorDetail(error) {
|
|
168
|
+
if (typeof error !== "object" || error === null) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
const candidate = error;
|
|
172
|
+
if (typeof candidate.retryable !== "boolean") {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const resetAt = normalizeResetAt(candidate.resetAt);
|
|
176
|
+
return {
|
|
177
|
+
httpStatus: asNumber(candidate.httpStatus),
|
|
178
|
+
code: asString(candidate.code),
|
|
179
|
+
retryable: candidate.retryable,
|
|
180
|
+
...(resetAt ? { resetAt } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
import { LinearClient } from "@linear/sdk";
|
|
1
2
|
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
2
3
|
import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
|
|
3
4
|
import type { AuthOptions } from "./auth.js";
|
|
5
|
+
import { GraphQLService } from "./graphql-service.js";
|
|
6
|
+
import { type RateLimitAdmission } from "./rate-limit-admission.js";
|
|
4
7
|
/**
|
|
5
8
|
* Constructor arg for `LinearService`. Re-exported alias of the shared
|
|
6
9
|
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). See
|
|
@@ -8,9 +11,24 @@ import type { AuthOptions } from "./auth.js";
|
|
|
8
11
|
* legacy arm was dropped in DEV-4068 T7.
|
|
9
12
|
*/
|
|
10
13
|
export type LinearServiceAuth = LinearCredential;
|
|
14
|
+
/**
|
|
15
|
+
* Classify every generated SDK operation after the SDK has normalized its raw
|
|
16
|
+
* transport rejection. Patching `client.client.request` is too early: the
|
|
17
|
+
* LinearClient constructor wraps that transport with `parseLinearError`, which
|
|
18
|
+
* would immediately strip our additive classification fields again. Keeping
|
|
19
|
+
* the wrapper here (rather than guessing in `outputError`) also leaves local
|
|
20
|
+
* validation and not-found errors unclassified.
|
|
21
|
+
*/
|
|
22
|
+
export declare function instrumentLinearGraphQLErrorClassification(client: LinearClient): LinearClient;
|
|
23
|
+
export declare function instrumentClientCredentialsRenewal(client: LinearClient, renewAccessToken?: () => Promise<string>): LinearClient;
|
|
11
24
|
export declare class LinearService {
|
|
12
25
|
private readonly client;
|
|
13
|
-
|
|
26
|
+
private readonly graphQLService;
|
|
27
|
+
constructor(auth: LinearServiceAuth, options?: {
|
|
28
|
+
admission?: RateLimitAdmission;
|
|
29
|
+
graphQLService?: GraphQLService;
|
|
30
|
+
renewAccessToken?: () => Promise<string>;
|
|
31
|
+
});
|
|
14
32
|
resolveIssueId(issueId: string): Promise<string>;
|
|
15
33
|
getTeams(limit?: number): Promise<LinearTeam[]>;
|
|
16
34
|
resolveUserId(nameOrEmailOrId: string): Promise<string>;
|
|
@@ -32,7 +50,6 @@ export declare class LinearService {
|
|
|
32
50
|
}): Promise<LinearProject[]>;
|
|
33
51
|
resolveTeamId(teamKeyOrNameOrId: string): Promise<string>;
|
|
34
52
|
resolveStatusId(statusName: string, teamId?: string): Promise<string>;
|
|
35
|
-
private buildLabelData;
|
|
36
53
|
getLabels(teamFilter?: string, limit?: number, nameFilter?: string): Promise<{
|
|
37
54
|
labels: LinearLabel[];
|
|
38
55
|
}>;
|