@enrichlayer/el-linear 1.9.0 → 1.15.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.
- package/README.md +139 -10
- package/claude-skills/linear-operations/SKILL.md +41 -1
- package/dist/auth/linear-credential.d.ts +27 -0
- package/dist/auth/linear-credential.js +1 -0
- package/dist/auth/oauth-app-config.d.ts +4 -3
- package/dist/auth/oauth-app-config.js +13 -2
- package/dist/auth/oauth-callback.d.ts +2 -3
- package/dist/auth/oauth-callback.js +2 -2
- package/dist/auth/oauth-client.d.ts +8 -2
- package/dist/auth/oauth-client.js +26 -0
- package/dist/auth/oauth-fs.d.ts +2 -1
- package/dist/auth/oauth-headless.d.ts +2 -1
- package/dist/auth/oauth-storage.d.ts +5 -1
- package/dist/auth/oauth-storage.js +1 -1
- package/dist/auth/oauth-token.d.ts +4 -3
- package/dist/auth/oauth-token.js +16 -4
- package/dist/auth/token-resolver.d.ts +14 -5
- package/dist/auth/token-resolver.js +6 -1
- package/dist/commands/attachments.js +2 -1
- package/dist/commands/batch.js +18 -21
- package/dist/commands/comments.js +22 -33
- package/dist/commands/config.js +178 -5
- package/dist/commands/cycles.js +2 -1
- package/dist/commands/documents.js +2 -1
- package/dist/commands/graphql.js +4 -6
- package/dist/commands/init/aliases.js +1 -1
- package/dist/commands/init/defaults.d.ts +2 -1
- package/dist/commands/init/index.js +45 -35
- package/dist/commands/init/oauth.d.ts +4 -1
- package/dist/commands/init/oauth.js +22 -4
- package/dist/commands/init/shared.d.ts +24 -2
- package/dist/commands/init/shared.js +35 -4
- package/dist/commands/init/token.d.ts +3 -3
- package/dist/commands/init/token.js +5 -24
- package/dist/commands/init/workspace.d.ts +2 -1
- package/dist/commands/init/workspace.js +1 -1
- package/dist/commands/introspect.d.ts +27 -0
- package/dist/commands/introspect.js +178 -0
- package/dist/commands/issue-id.js +1 -3
- package/dist/commands/issues/branch.js +9 -1
- package/dist/commands/issues/description.js +2 -6
- package/dist/commands/issues/link-references.d.ts +21 -0
- package/dist/commands/issues/link-references.js +171 -0
- package/dist/commands/issues/relations.d.ts +44 -0
- package/dist/commands/issues/relations.js +132 -0
- package/dist/commands/issues.js +269 -309
- package/dist/commands/labels.js +15 -24
- package/dist/commands/profile.js +1 -0
- package/dist/commands/project-milestones.js +13 -20
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +157 -44
- package/dist/commands/read-shortcut.d.ts +1 -1
- package/dist/commands/read-shortcut.js +28 -8
- package/dist/commands/refs.js +75 -8
- package/dist/commands/releases.js +26 -30
- package/dist/commands/search.js +49 -33
- package/dist/commands/teams.js +2 -1
- package/dist/commands/templates.js +9 -14
- package/dist/commands/users.js +5 -2
- package/dist/config/config.d.ts +99 -1
- package/dist/config/config.js +264 -52
- package/dist/config/error-enrichment.d.ts +62 -0
- package/dist/config/error-enrichment.js +417 -0
- package/dist/config/issue-validation.d.ts +37 -0
- package/dist/config/issue-validation.js +63 -1
- package/dist/config/paths.d.ts +2 -8
- package/dist/config/paths.js +4 -2
- package/dist/config/resolver.d.ts +8 -1
- package/dist/config/resolver.js +11 -5
- package/dist/main.js +13 -1
- package/dist/queries/attachments-types.d.ts +30 -0
- package/dist/queries/attachments-types.js +5 -0
- package/dist/queries/comments-types.d.ts +55 -0
- package/dist/queries/comments-types.js +5 -0
- package/dist/queries/common.d.ts +2 -2
- package/dist/queries/common.js +8 -0
- package/dist/queries/documents-types.d.ts +62 -0
- package/dist/queries/documents-types.js +9 -0
- package/dist/queries/introspect-types.d.ts +58 -0
- package/dist/queries/introspect-types.js +10 -0
- package/dist/queries/issues-types.d.ts +481 -0
- package/dist/queries/issues-types.js +23 -0
- package/dist/queries/issues.d.ts +51 -10
- package/dist/queries/issues.js +147 -5
- package/dist/queries/labels-types.d.ts +65 -0
- package/dist/queries/labels-types.js +5 -0
- package/dist/queries/project-milestones-types.d.ts +92 -0
- package/dist/queries/project-milestones-types.js +10 -0
- package/dist/queries/project-milestones.d.ts +1 -1
- package/dist/queries/projects-types.d.ts +76 -0
- package/dist/queries/projects-types.js +5 -0
- package/dist/queries/projects.d.ts +2 -0
- package/dist/queries/projects.js +22 -0
- package/dist/queries/releases-types.d.ts +85 -0
- package/dist/queries/releases-types.js +5 -0
- package/dist/queries/search-types.d.ts +102 -0
- package/dist/queries/search-types.js +6 -0
- package/dist/queries/templates-types.d.ts +62 -0
- package/dist/queries/templates-types.js +9 -0
- package/dist/types/linear.d.ts +21 -3
- package/dist/utils/auto-link-references.d.ts +3 -3
- package/dist/utils/auto-link-references.js +30 -34
- package/dist/utils/extract-field.d.ts +19 -0
- package/dist/utils/extract-field.js +99 -0
- package/dist/utils/file-service.d.ts +6 -13
- package/dist/utils/file-service.js +0 -2
- package/dist/utils/formatters/summary.js +6 -1
- package/dist/utils/graphql-attachments-service.js +6 -9
- package/dist/utils/graphql-documents-service.js +19 -25
- package/dist/utils/graphql-issues-service.d.ts +112 -46
- package/dist/utils/graphql-issues-service.js +398 -206
- package/dist/utils/graphql-service.d.ts +10 -12
- package/dist/utils/graphql-service.js +0 -3
- package/dist/utils/issue-reference-extractor.d.ts +7 -0
- package/dist/utils/issue-reference-extractor.js +5 -3
- package/dist/utils/issues-service-bootstrap.d.ts +28 -0
- package/dist/utils/issues-service-bootstrap.js +27 -0
- package/dist/utils/linear-service.d.ts +21 -14
- package/dist/utils/linear-service.js +73 -11
- package/dist/utils/markdown-prosemirror.js +12 -12
- package/dist/utils/mention-resolver.js +1 -1
- package/dist/utils/output.d.ts +82 -2
- package/dist/utils/output.js +76 -11
- package/dist/utils/project-slug.d.ts +21 -0
- package/dist/utils/project-slug.js +45 -0
- package/dist/utils/protected-ranges.d.ts +14 -0
- package/dist/utils/protected-ranges.js +88 -2
- package/dist/utils/sanitize-for-log.d.ts +24 -0
- package/dist/utils/sanitize-for-log.js +38 -0
- package/dist/utils/table-formatter.js +24 -0
- package/dist/utils/validators.d.ts +7 -2
- package/dist/utils/validators.js +6 -0
- package/dist/utils/workspace-url.d.ts +5 -1
- package/dist/utils/workspace-url.js +53 -7
- package/package.json +2 -2
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for `SEMANTIC_SEARCH_QUERY` and the inline
|
|
3
|
+
* templates listing in `commands/search.ts`. See `./issues-types.ts`
|
|
4
|
+
* for the rationale (ALL-937).
|
|
5
|
+
*/
|
|
6
|
+
import type { LinearPriority } from "../types/linear.js";
|
|
7
|
+
interface SearchIssueRef {
|
|
8
|
+
id: string;
|
|
9
|
+
identifier: string;
|
|
10
|
+
title: string;
|
|
11
|
+
priority: LinearPriority | null;
|
|
12
|
+
state: {
|
|
13
|
+
id: string;
|
|
14
|
+
name: string;
|
|
15
|
+
} | null;
|
|
16
|
+
team: {
|
|
17
|
+
id: string;
|
|
18
|
+
key: string;
|
|
19
|
+
name: string;
|
|
20
|
+
} | null;
|
|
21
|
+
assignee: {
|
|
22
|
+
id: string;
|
|
23
|
+
name: string;
|
|
24
|
+
url: string | null;
|
|
25
|
+
} | null;
|
|
26
|
+
project: {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
} | null;
|
|
30
|
+
}
|
|
31
|
+
interface SearchProjectRef {
|
|
32
|
+
id: string;
|
|
33
|
+
name: string;
|
|
34
|
+
state: string;
|
|
35
|
+
}
|
|
36
|
+
interface SearchInitiativeRef {
|
|
37
|
+
id: string;
|
|
38
|
+
name: string;
|
|
39
|
+
status: string | null;
|
|
40
|
+
}
|
|
41
|
+
interface SearchDocumentRef {
|
|
42
|
+
id: string;
|
|
43
|
+
title: string;
|
|
44
|
+
slugId: string | null;
|
|
45
|
+
project: {
|
|
46
|
+
id: string;
|
|
47
|
+
name: string;
|
|
48
|
+
} | null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* A single semanticSearch result. Discriminated union on `type` — given
|
|
52
|
+
* a row, only the field named by `type` is meaningful (the other three
|
|
53
|
+
* are absent / null on the wire). Consumers `switch (r.type)` and access
|
|
54
|
+
* the corresponding payload directly; the compiler statically rejects
|
|
55
|
+
* cross-arm access like `r.project` in a `case "issue"` branch.
|
|
56
|
+
*
|
|
57
|
+
* The payload itself is `| null` because Linear can return null when
|
|
58
|
+
* the underlying entity was deleted between indexing and query time —
|
|
59
|
+
* that's an orthogonal concern from the discriminant.
|
|
60
|
+
*
|
|
61
|
+
* The four `type` literals match Linear's `semanticSearch.results.type`
|
|
62
|
+
* enum verbatim — keep in lock-step with the GraphQL schema's enum.
|
|
63
|
+
*/
|
|
64
|
+
export type SemanticSearchResult = {
|
|
65
|
+
type: "issue";
|
|
66
|
+
issue: SearchIssueRef | null;
|
|
67
|
+
} | {
|
|
68
|
+
type: "project";
|
|
69
|
+
project: SearchProjectRef | null;
|
|
70
|
+
} | {
|
|
71
|
+
type: "initiative";
|
|
72
|
+
initiative: SearchInitiativeRef | null;
|
|
73
|
+
} | {
|
|
74
|
+
type: "document";
|
|
75
|
+
document: SearchDocumentRef | null;
|
|
76
|
+
};
|
|
77
|
+
export interface SemanticSearchResponse {
|
|
78
|
+
semanticSearch: {
|
|
79
|
+
results: SemanticSearchResult[];
|
|
80
|
+
} | null;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Mirrors the inline `TEMPLATES_QUERY` in `commands/search.ts` —
|
|
84
|
+
* a smaller selection set than the full templates query, dropping
|
|
85
|
+
* fields the search command doesn't display.
|
|
86
|
+
*/
|
|
87
|
+
export interface SearchTemplateNode {
|
|
88
|
+
id: string;
|
|
89
|
+
name: string;
|
|
90
|
+
type: string;
|
|
91
|
+
description: string | null;
|
|
92
|
+
team: {
|
|
93
|
+
key: string;
|
|
94
|
+
} | null;
|
|
95
|
+
creator: {
|
|
96
|
+
name: string;
|
|
97
|
+
} | null;
|
|
98
|
+
}
|
|
99
|
+
export interface SearchTemplatesResponse {
|
|
100
|
+
templates: SearchTemplateNode[];
|
|
101
|
+
}
|
|
102
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./templates.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*
|
|
5
|
+
* Note: `templates` is unusual — it returns the array directly rather
|
|
6
|
+
* than wrapped in `{ nodes: ... }`. The response type below mirrors
|
|
7
|
+
* what the API actually returns, not the connection convention.
|
|
8
|
+
*/
|
|
9
|
+
interface TemplateTeamRef {
|
|
10
|
+
id: string;
|
|
11
|
+
key: string;
|
|
12
|
+
name: string;
|
|
13
|
+
}
|
|
14
|
+
interface TemplateCreatorRef {
|
|
15
|
+
id: string;
|
|
16
|
+
name: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Mirrors the template selection set used by `TEMPLATES_LIST_QUERY` /
|
|
20
|
+
* `TEMPLATE_BY_ID_QUERY` / `TEMPLATE_CREATE_MUTATION` /
|
|
21
|
+
* `TEMPLATE_UPDATE_MUTATION`. (Some of those omit `creator` and
|
|
22
|
+
* `createdAt`; we model the union — consumers only read what their
|
|
23
|
+
* query selected.)
|
|
24
|
+
*/
|
|
25
|
+
export interface TemplateNode {
|
|
26
|
+
id: string;
|
|
27
|
+
name: string;
|
|
28
|
+
type: string;
|
|
29
|
+
description: string | null;
|
|
30
|
+
templateData: unknown;
|
|
31
|
+
createdAt: string;
|
|
32
|
+
updatedAt: string;
|
|
33
|
+
team: TemplateTeamRef | null;
|
|
34
|
+
creator: TemplateCreatorRef | null;
|
|
35
|
+
}
|
|
36
|
+
export interface TemplatesListResponse {
|
|
37
|
+
templates: TemplateNode[];
|
|
38
|
+
}
|
|
39
|
+
export interface GetTemplateResponse {
|
|
40
|
+
template: TemplateNode | null;
|
|
41
|
+
}
|
|
42
|
+
export interface CreateTemplateResponse {
|
|
43
|
+
templateCreate: {
|
|
44
|
+
success: boolean;
|
|
45
|
+
lastSyncId?: number;
|
|
46
|
+
template: TemplateNode | null;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
export interface UpdateTemplateResponse {
|
|
50
|
+
templateUpdate: {
|
|
51
|
+
success: boolean;
|
|
52
|
+
lastSyncId?: number;
|
|
53
|
+
template: TemplateNode | null;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export interface DeleteTemplateResponse {
|
|
57
|
+
templateDelete: {
|
|
58
|
+
success: boolean;
|
|
59
|
+
lastSyncId?: number;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed response shapes for the queries in `./templates.ts`.
|
|
3
|
+
* See `./issues-types.ts` for the rationale (ALL-937).
|
|
4
|
+
*
|
|
5
|
+
* Note: `templates` is unusual — it returns the array directly rather
|
|
6
|
+
* than wrapped in `{ nodes: ... }`. The response type below mirrors
|
|
7
|
+
* what the API actually returns, not the connection convention.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
package/dist/types/linear.d.ts
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
* These represent the public API surface — GraphQL responses are
|
|
4
4
|
* transformed into these shapes at the service boundary.
|
|
5
5
|
*/
|
|
6
|
+
/**
|
|
7
|
+
* Linear priority levels — `0` = no priority, `1` = urgent, `2` = high,
|
|
8
|
+
* `3` = medium (alias: normal), `4` = low. Sourced from Linear's
|
|
9
|
+
* `Issue.priority` enum; the GraphQL API returns a number in this range.
|
|
10
|
+
* Use this instead of `number` at every API boundary so an out-of-range
|
|
11
|
+
* value (`9`) fails to compile.
|
|
12
|
+
*
|
|
13
|
+
* The runtime parser is `validatePriority` in `src/utils/validators.ts`
|
|
14
|
+
* — it narrows arbitrary `string` input to this union.
|
|
15
|
+
*/
|
|
16
|
+
export type LinearPriority = 0 | 1 | 2 | 3 | 4;
|
|
6
17
|
interface TeamRef {
|
|
7
18
|
id: string;
|
|
8
19
|
key: string;
|
|
@@ -87,6 +98,7 @@ export interface LinearIssue {
|
|
|
87
98
|
comments?: LinearComment[];
|
|
88
99
|
createdAt: string;
|
|
89
100
|
cycle?: CycleRef;
|
|
101
|
+
delegate?: UserRef;
|
|
90
102
|
description?: string;
|
|
91
103
|
dueDate?: string;
|
|
92
104
|
embeds?: import("../utils/embed-parser.js").Embed[];
|
|
@@ -95,7 +107,7 @@ export interface LinearIssue {
|
|
|
95
107
|
identifier: string;
|
|
96
108
|
labels: LabelRef[];
|
|
97
109
|
parentIssue?: IssueRef;
|
|
98
|
-
priority:
|
|
110
|
+
priority: LinearPriority;
|
|
99
111
|
project?: ProjectRef;
|
|
100
112
|
projectMilestone?: MilestoneRef;
|
|
101
113
|
state?: StateRef;
|
|
@@ -208,8 +220,14 @@ type GraphQLPrimitive = string | number | boolean | null | undefined;
|
|
|
208
220
|
type GraphQLValue = GraphQLPrimitive | GraphQLResponseData | GraphQLResponseData[];
|
|
209
221
|
/**
|
|
210
222
|
* Recursive index type for raw GraphQL responses.
|
|
211
|
-
*
|
|
212
|
-
*
|
|
223
|
+
*
|
|
224
|
+
* **Prefer per-query response types** (see `src/queries/*-types.ts`)
|
|
225
|
+
* over `GraphQLResponseData` for any new consumer. ALL-937 swept the
|
|
226
|
+
* old service/command consumers; this type is kept as the fallback
|
|
227
|
+
* for the few generic transports (e.g. `graphql-service.rawRequest`'s
|
|
228
|
+
* default generic). Don't reach for `as GraphQLResponseData` to skip
|
|
229
|
+
* a typed shape — the per-query types catch query/consumer drift at
|
|
230
|
+
* compile time. This type cannot.
|
|
213
231
|
*/
|
|
214
232
|
export interface GraphQLResponseData {
|
|
215
233
|
[key: string]: GraphQLValue;
|
|
@@ -1,20 +1,20 @@
|
|
|
1
1
|
import type { GraphQLService } from "./graphql-service.js";
|
|
2
2
|
import { type IssueRelationType } from "./issue-reference-extractor.js";
|
|
3
3
|
import type { LinearService } from "./linear-service.js";
|
|
4
|
-
|
|
4
|
+
interface AutoLinkLinked {
|
|
5
5
|
identifier: string;
|
|
6
6
|
/** True when source/target were swapped on the create (e.g. "blocked by X" → X→source) */
|
|
7
7
|
reverse: boolean;
|
|
8
8
|
title: string;
|
|
9
9
|
type: IssueRelationType;
|
|
10
10
|
}
|
|
11
|
-
|
|
11
|
+
interface AutoLinkSkipped {
|
|
12
12
|
existingType: string;
|
|
13
13
|
identifier: string;
|
|
14
14
|
/** Type that would have been created had the reference been new */
|
|
15
15
|
inferredType: IssueRelationType;
|
|
16
16
|
}
|
|
17
|
-
|
|
17
|
+
interface AutoLinkFailed {
|
|
18
18
|
identifier: string;
|
|
19
19
|
reason: string;
|
|
20
20
|
}
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import { GET_ISSUE_RELATIONS_QUERY, ISSUE_RELATION_CREATE_MUTATION, } from "../queries/issues.js";
|
|
2
|
-
import { extractIssueReferences, } from "./issue-reference-extractor.js";
|
|
3
|
-
function specificity(ref) {
|
|
4
|
-
if (ref.type === "duplicate") {
|
|
5
|
-
return 3;
|
|
6
|
-
}
|
|
7
|
-
if (ref.type === "blocks") {
|
|
8
|
-
return 2;
|
|
9
|
-
}
|
|
10
|
-
return 1;
|
|
11
|
-
}
|
|
2
|
+
import { extractIssueReferences, specificity, } from "./issue-reference-extractor.js";
|
|
12
3
|
/**
|
|
13
4
|
* Merge candidates from multiple sources (description, comments) into one list,
|
|
14
5
|
* keeping the strongest (most-specific) inference per identifier. The first source
|
|
@@ -26,37 +17,44 @@ function mergeCandidates(...sources) {
|
|
|
26
17
|
}
|
|
27
18
|
return [...byId.values()];
|
|
28
19
|
}
|
|
29
|
-
function
|
|
30
|
-
for (const rel of nodes
|
|
31
|
-
const peer = rel
|
|
20
|
+
function mergeOutgoingRelations(nodes, out) {
|
|
21
|
+
for (const rel of nodes) {
|
|
22
|
+
const peer = rel.relatedIssue;
|
|
32
23
|
if (!peer) {
|
|
33
24
|
continue;
|
|
34
25
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const type = normalizeType(rel.type);
|
|
38
|
-
if (id && !out.byIdentifier.has(id)) {
|
|
39
|
-
out.byIdentifier.set(id, type);
|
|
26
|
+
if (!out.byIdentifier.has(peer.identifier)) {
|
|
27
|
+
out.byIdentifier.set(peer.identifier, rel.type);
|
|
40
28
|
}
|
|
41
|
-
if (
|
|
42
|
-
out.byUuid.set(
|
|
29
|
+
if (!out.byUuid.has(peer.id)) {
|
|
30
|
+
out.byUuid.set(peer.id, rel.type);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function mergeIncomingRelations(nodes, out) {
|
|
35
|
+
for (const rel of nodes) {
|
|
36
|
+
const peer = rel.issue;
|
|
37
|
+
if (!peer) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
// Normalize incoming "blocks" → "blockedBy" so the reported type reads naturally
|
|
41
|
+
const type = rel.type === "blocks" ? "blockedBy" : rel.type;
|
|
42
|
+
if (!out.byIdentifier.has(peer.identifier)) {
|
|
43
|
+
out.byIdentifier.set(peer.identifier, type);
|
|
44
|
+
}
|
|
45
|
+
if (!out.byUuid.has(peer.id)) {
|
|
46
|
+
out.byUuid.set(peer.id, type);
|
|
43
47
|
}
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
50
|
async function fetchExistingRelations(issueId, graphQLService) {
|
|
47
|
-
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
|
|
48
|
-
id: issueId,
|
|
49
|
-
});
|
|
50
|
-
const issue = result.issue;
|
|
51
|
+
const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: issueId });
|
|
51
52
|
const out = { byIdentifier: new Map(), byUuid: new Map() };
|
|
52
|
-
if (!issue) {
|
|
53
|
+
if (!result.issue) {
|
|
53
54
|
return out;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const incoming = issue.inverseRelations;
|
|
58
|
-
// Normalize incoming "blocks" → "blockedBy" so the reported type reads naturally
|
|
59
|
-
mergeRelationNodes(incoming?.nodes, "issue", (raw) => (raw === "blocks" ? "blockedBy" : raw), out);
|
|
56
|
+
mergeOutgoingRelations(result.issue.relations.nodes, out);
|
|
57
|
+
mergeIncomingRelations(result.issue.inverseRelations.nodes, out);
|
|
60
58
|
return out;
|
|
61
59
|
}
|
|
62
60
|
function describeError(err) {
|
|
@@ -70,11 +68,9 @@ async function createTypedRelation(ref, sourceId, resolvedId, graphQLService) {
|
|
|
70
68
|
type: ref.type,
|
|
71
69
|
},
|
|
72
70
|
});
|
|
73
|
-
const
|
|
74
|
-
const issueRelation = create?.issueRelation;
|
|
71
|
+
const issueRelation = result.issueRelationCreate.issueRelation;
|
|
75
72
|
// The peer (the issue NOT identified by `sourceId`) is whichever side wasn't the source
|
|
76
|
-
const
|
|
77
|
-
const peer = issueRelation?.[peerKey];
|
|
73
|
+
const peer = ref.reverse ? issueRelation?.issue : issueRelation?.relatedIssue;
|
|
78
74
|
return {
|
|
79
75
|
identifier: peer?.identifier ?? ref.identifier,
|
|
80
76
|
title: peer?.title ?? "",
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a named section from a Linear issue's markdown description.
|
|
3
|
+
*
|
|
4
|
+
* Linear issue bodies follow a small set of repeated structures —
|
|
5
|
+
* "Why we need this", "Done when", "Scope", "Out of scope", etc. —
|
|
6
|
+
* usually as `##` or `###` headers, sometimes as bold pseudo-headers
|
|
7
|
+
* (`**Done when**`). Agents end up repeatedly piping
|
|
8
|
+
* `el-linear issues read X | python -c '...desc.find("Done when")...'`
|
|
9
|
+
* to pull one section out. This helper centralizes that.
|
|
10
|
+
*
|
|
11
|
+
* Matching is case-insensitive on the header text. The first matching
|
|
12
|
+
* header wins. Content is everything between that header and the next
|
|
13
|
+
* header line (any `#{1,6}` or bold pseudo-header at line start) or EOF,
|
|
14
|
+
* trimmed of surrounding whitespace.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` when the field isn't found, so callers can distinguish
|
|
17
|
+
* "missing" from "empty body".
|
|
18
|
+
*/
|
|
19
|
+
export declare function extractField(body: string, fieldName: string): string | null;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extract a named section from a Linear issue's markdown description.
|
|
3
|
+
*
|
|
4
|
+
* Linear issue bodies follow a small set of repeated structures —
|
|
5
|
+
* "Why we need this", "Done when", "Scope", "Out of scope", etc. —
|
|
6
|
+
* usually as `##` or `###` headers, sometimes as bold pseudo-headers
|
|
7
|
+
* (`**Done when**`). Agents end up repeatedly piping
|
|
8
|
+
* `el-linear issues read X | python -c '...desc.find("Done when")...'`
|
|
9
|
+
* to pull one section out. This helper centralizes that.
|
|
10
|
+
*
|
|
11
|
+
* Matching is case-insensitive on the header text. The first matching
|
|
12
|
+
* header wins. Content is everything between that header and the next
|
|
13
|
+
* header line (any `#{1,6}` or bold pseudo-header at line start) or EOF,
|
|
14
|
+
* trimmed of surrounding whitespace.
|
|
15
|
+
*
|
|
16
|
+
* Returns `null` when the field isn't found, so callers can distinguish
|
|
17
|
+
* "missing" from "empty body".
|
|
18
|
+
*/
|
|
19
|
+
// Header forms accepted:
|
|
20
|
+
// `# ... `, `## ...`, `### ...`, ... up to `######`
|
|
21
|
+
// `**Done when**`, `**Done when:**` (bold pseudo-headers)
|
|
22
|
+
// Trailing colons after the header text are stripped.
|
|
23
|
+
const ATX_HEADER_RE = /^(?:#{1,6})\s+([^\n]+?)(?::\s*)?\s*$/;
|
|
24
|
+
const BOLD_PSEUDO_HEADER_RE = /^\*\*([^*\n]+?)(?::\s*)?\*\*\s*$/;
|
|
25
|
+
// Fenced code block delimiters per CommonMark: at least three backticks or
|
|
26
|
+
// tildes at the start of a line, with up to 3 leading SPACES of indent (4+
|
|
27
|
+
// is an indented code block, which is a different construct; tabs are not
|
|
28
|
+
// permitted as fence indent per the CommonMark spec). We toggle an inFence
|
|
29
|
+
// flag while scanning so section headers inside code samples don't
|
|
30
|
+
// terminate the real section.
|
|
31
|
+
const FENCE_RE = /^ {0,3}(`{3,}|~{3,})/;
|
|
32
|
+
function normalize(s) {
|
|
33
|
+
return s.toLowerCase().trim().replace(/\s+/g, " ");
|
|
34
|
+
}
|
|
35
|
+
// Bold pseudo-header heuristic: any `**...**` line could be a header, but
|
|
36
|
+
// inline emphasis paragraphs (e.g. a single bolded sentence in prose) are
|
|
37
|
+
// false positives. Require either a trailing colon OR ≤6 words so that
|
|
38
|
+
// "**Done when:**" and "**Why we need this**" qualify but "**Note that all
|
|
39
|
+
// downstream consumers ...**" does not.
|
|
40
|
+
function isLikelyBoldHeader(text, hadTrailingColon) {
|
|
41
|
+
if (hadTrailingColon)
|
|
42
|
+
return true;
|
|
43
|
+
const wordCount = text.trim().split(/\s+/).filter(Boolean).length;
|
|
44
|
+
return wordCount > 0 && wordCount <= 6;
|
|
45
|
+
}
|
|
46
|
+
function matchHeader(line) {
|
|
47
|
+
const atx = line.match(ATX_HEADER_RE);
|
|
48
|
+
if (atx)
|
|
49
|
+
return { text: atx[1] };
|
|
50
|
+
const bold = line.match(BOLD_PSEUDO_HEADER_RE);
|
|
51
|
+
if (bold) {
|
|
52
|
+
const text = bold[1];
|
|
53
|
+
// match[1] excludes the bold delimiters; the regex consumed a trailing
|
|
54
|
+
// colon if present, so detect by re-checking the raw line.
|
|
55
|
+
const hadColon = /:\*\*\s*$/.test(line);
|
|
56
|
+
if (!isLikelyBoldHeader(text, hadColon))
|
|
57
|
+
return null;
|
|
58
|
+
return { text };
|
|
59
|
+
}
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
export function extractField(body, fieldName) {
|
|
63
|
+
if (!body)
|
|
64
|
+
return null;
|
|
65
|
+
const target = normalize(fieldName);
|
|
66
|
+
const lines = body.split("\n");
|
|
67
|
+
let inFence = false;
|
|
68
|
+
let startIdx = -1;
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
if (FENCE_RE.test(lines[i])) {
|
|
71
|
+
inFence = !inFence;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (inFence)
|
|
75
|
+
continue;
|
|
76
|
+
const match = matchHeader(lines[i]);
|
|
77
|
+
if (!match)
|
|
78
|
+
continue;
|
|
79
|
+
if (normalize(match.text) === target) {
|
|
80
|
+
startIdx = i + 1;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (startIdx === -1)
|
|
85
|
+
return null;
|
|
86
|
+
const out = [];
|
|
87
|
+
let sectionInFence = false;
|
|
88
|
+
for (let i = startIdx; i < lines.length; i++) {
|
|
89
|
+
if (FENCE_RE.test(lines[i])) {
|
|
90
|
+
sectionInFence = !sectionInFence;
|
|
91
|
+
out.push(lines[i]);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (!sectionInFence && matchHeader(lines[i]) !== null)
|
|
95
|
+
break;
|
|
96
|
+
out.push(lines[i]);
|
|
97
|
+
}
|
|
98
|
+
return out.join("\n").trim();
|
|
99
|
+
}
|
|
@@ -1,19 +1,12 @@
|
|
|
1
|
+
import type { LinearCredential } from "../auth/linear-credential.js";
|
|
1
2
|
import type { FileDownloadResult, FileUploadResult } from "../types/linear.js";
|
|
2
3
|
/**
|
|
3
|
-
* Constructor arg
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `Authorization: Bearer <token>`).
|
|
8
|
-
*
|
|
9
|
-
* The string variant exists because dozens of tests construct `FileService`
|
|
10
|
-
* with a plain string. We continue to support it indefinitely.
|
|
4
|
+
* Constructor arg for `FileService`. Re-exported alias of the shared
|
|
5
|
+
* `LinearCredential` union (`{ apiKey } | { oauthToken }`). See
|
|
6
|
+
* `src/auth/linear-credential.ts` for the contract. The bare-string
|
|
7
|
+
* legacy arm was dropped in DEV-4068 T7.
|
|
11
8
|
*/
|
|
12
|
-
export type FileServiceAuth =
|
|
13
|
-
apiKey: string;
|
|
14
|
-
} | {
|
|
15
|
-
oauthToken: string;
|
|
16
|
-
};
|
|
9
|
+
export type FileServiceAuth = LinearCredential;
|
|
17
10
|
export declare class FileService {
|
|
18
11
|
private readonly authHeader;
|
|
19
12
|
constructor(auth: FileServiceAuth);
|
|
@@ -38,8 +38,6 @@ function getMimeType(filePath) {
|
|
|
38
38
|
return MIME_TYPES[ext] || "application/octet-stream";
|
|
39
39
|
}
|
|
40
40
|
function buildAuthHeader(auth) {
|
|
41
|
-
if (typeof auth === "string")
|
|
42
|
-
return auth;
|
|
43
41
|
if ("oauthToken" in auth)
|
|
44
42
|
return `Bearer ${auth.oauthToken}`;
|
|
45
43
|
return auth.apiKey;
|
|
@@ -591,7 +591,12 @@ function formatGenericInline(v) {
|
|
|
591
591
|
if (o)
|
|
592
592
|
return s(o.name ?? o.displayName ?? o.identifier ?? o.id ?? "{…}");
|
|
593
593
|
if (typeof v === "string" && v.length > 80) {
|
|
594
|
-
|
|
594
|
+
// Route through sanitizeForTerminal (via `s()`) before slicing so that
|
|
595
|
+
// long strings from the generic-fallback path can't smuggle ANSI
|
|
596
|
+
// escapes (cursor moves, clear-screen, OSC 8 hyperlinks) into the
|
|
597
|
+
// user's terminal. Every other branch in this function already goes
|
|
598
|
+
// through `s()`; this one previously didn't.
|
|
599
|
+
return `${s(v).slice(0, 80)}…`;
|
|
595
600
|
}
|
|
596
601
|
return s(v);
|
|
597
602
|
}
|
|
@@ -4,9 +4,9 @@ function transformAttachment(att) {
|
|
|
4
4
|
return {
|
|
5
5
|
id: att.id,
|
|
6
6
|
url: att.url,
|
|
7
|
-
title: att.title
|
|
8
|
-
createdAt: att.createdAt
|
|
9
|
-
updatedAt: att.updatedAt
|
|
7
|
+
title: att.title ?? undefined,
|
|
8
|
+
createdAt: att.createdAt ?? undefined,
|
|
9
|
+
updatedAt: att.updatedAt ?? undefined,
|
|
10
10
|
};
|
|
11
11
|
}
|
|
12
12
|
class GraphQLAttachmentsService {
|
|
@@ -17,15 +17,14 @@ class GraphQLAttachmentsService {
|
|
|
17
17
|
async createAttachment(input) {
|
|
18
18
|
const result = await this.graphqlService.rawRequest(CREATE_ATTACHMENT_MUTATION, { input });
|
|
19
19
|
const attachmentCreate = result.attachmentCreate;
|
|
20
|
-
if (!attachmentCreate.success) {
|
|
20
|
+
if (!attachmentCreate.success || !attachmentCreate.attachment) {
|
|
21
21
|
throw new Error(`Failed to create attachment on issue ${input.issueId} for URL "${input.url}"`);
|
|
22
22
|
}
|
|
23
23
|
return transformAttachment(attachmentCreate.attachment);
|
|
24
24
|
}
|
|
25
25
|
async deleteAttachment(id) {
|
|
26
26
|
const result = await this.graphqlService.rawRequest(DELETE_ATTACHMENT_MUTATION, { id });
|
|
27
|
-
|
|
28
|
-
if (!attachmentDelete.success) {
|
|
27
|
+
if (!result.attachmentDelete.success) {
|
|
29
28
|
throw new Error(`Failed to delete attachment: ${id}`);
|
|
30
29
|
}
|
|
31
30
|
return true;
|
|
@@ -35,9 +34,7 @@ class GraphQLAttachmentsService {
|
|
|
35
34
|
if (!result.issue) {
|
|
36
35
|
throw new Error(`Issue not found: ${issueId}`);
|
|
37
36
|
}
|
|
38
|
-
|
|
39
|
-
const attachments = issue.attachments;
|
|
40
|
-
return attachments.nodes.map(transformAttachment);
|
|
37
|
+
return result.issue.attachments.nodes.map(transformAttachment);
|
|
41
38
|
}
|
|
42
39
|
}
|
|
43
40
|
export async function createGraphQLAttachmentsService(options) {
|
|
@@ -1,32 +1,29 @@
|
|
|
1
1
|
import { CREATE_DOCUMENT_MUTATION, DELETE_DOCUMENT_MUTATION, GET_DOCUMENT_QUERY, LIST_DOCUMENTS_QUERY, UPDATE_DOCUMENT_MUTATION, } from "../queries/documents.js";
|
|
2
2
|
import { createGraphQLService, } from "./graphql-service.js";
|
|
3
3
|
function transformDocument(doc) {
|
|
4
|
-
const creator = doc.creator;
|
|
5
|
-
const project = doc.project;
|
|
6
|
-
const issue = doc.issue;
|
|
7
4
|
return {
|
|
8
5
|
id: doc.id,
|
|
9
6
|
title: doc.title,
|
|
10
|
-
content: doc.content
|
|
11
|
-
color: doc.color
|
|
12
|
-
icon: doc.icon
|
|
13
|
-
slugId: doc.slugId
|
|
14
|
-
url: doc.url
|
|
15
|
-
creator: creator
|
|
16
|
-
? { id: creator.id, name: creator.name }
|
|
7
|
+
content: doc.content ?? undefined,
|
|
8
|
+
color: doc.color ?? undefined,
|
|
9
|
+
icon: doc.icon ?? undefined,
|
|
10
|
+
slugId: doc.slugId ?? undefined,
|
|
11
|
+
url: doc.url ?? undefined,
|
|
12
|
+
creator: doc.creator
|
|
13
|
+
? { id: doc.creator.id, name: doc.creator.name }
|
|
17
14
|
: undefined,
|
|
18
|
-
project: project
|
|
19
|
-
? { id: project.id, name: project.name }
|
|
15
|
+
project: doc.project
|
|
16
|
+
? { id: doc.project.id, name: doc.project.name }
|
|
20
17
|
: undefined,
|
|
21
|
-
issue: issue
|
|
18
|
+
issue: doc.issue
|
|
22
19
|
? {
|
|
23
|
-
id: issue.id,
|
|
24
|
-
identifier: issue.identifier,
|
|
25
|
-
title: issue.title,
|
|
20
|
+
id: doc.issue.id,
|
|
21
|
+
identifier: doc.issue.identifier,
|
|
22
|
+
title: doc.issue.title,
|
|
26
23
|
}
|
|
27
24
|
: undefined,
|
|
28
|
-
createdAt: doc.createdAt
|
|
29
|
-
updatedAt: doc.updatedAt
|
|
25
|
+
createdAt: doc.createdAt ?? undefined,
|
|
26
|
+
updatedAt: doc.updatedAt ?? undefined,
|
|
30
27
|
};
|
|
31
28
|
}
|
|
32
29
|
class GraphQLDocumentsService {
|
|
@@ -37,7 +34,7 @@ class GraphQLDocumentsService {
|
|
|
37
34
|
async createDocument(input) {
|
|
38
35
|
const result = await this.graphqlService.rawRequest(CREATE_DOCUMENT_MUTATION, { input });
|
|
39
36
|
const createData = result.documentCreate;
|
|
40
|
-
if (!createData.success) {
|
|
37
|
+
if (!createData.success || !createData.document) {
|
|
41
38
|
throw new Error(`Failed to create document "${input.title}"${input.projectId ? ` in project ${input.projectId}` : ""}${input.teamId ? ` for team ${input.teamId}` : ""}`);
|
|
42
39
|
}
|
|
43
40
|
return transformDocument(createData.document);
|
|
@@ -45,15 +42,13 @@ class GraphQLDocumentsService {
|
|
|
45
42
|
async updateDocument(id, input) {
|
|
46
43
|
const result = await this.graphqlService.rawRequest(UPDATE_DOCUMENT_MUTATION, { id, input });
|
|
47
44
|
const updateData = result.documentUpdate;
|
|
48
|
-
if (!updateData.success) {
|
|
45
|
+
if (!updateData.success || !updateData.document) {
|
|
49
46
|
throw new Error(`Failed to update document: ${id}`);
|
|
50
47
|
}
|
|
51
48
|
return transformDocument(updateData.document);
|
|
52
49
|
}
|
|
53
50
|
async getDocument(id) {
|
|
54
|
-
const result = await this.graphqlService.rawRequest(GET_DOCUMENT_QUERY, {
|
|
55
|
-
id,
|
|
56
|
-
});
|
|
51
|
+
const result = await this.graphqlService.rawRequest(GET_DOCUMENT_QUERY, { id });
|
|
57
52
|
if (!result.document) {
|
|
58
53
|
throw new Error(`Document not found: ${id}`);
|
|
59
54
|
}
|
|
@@ -71,8 +66,7 @@ class GraphQLDocumentsService {
|
|
|
71
66
|
}
|
|
72
67
|
async deleteDocument(id) {
|
|
73
68
|
const result = await this.graphqlService.rawRequest(DELETE_DOCUMENT_MUTATION, { id });
|
|
74
|
-
|
|
75
|
-
if (!deleteData.success) {
|
|
69
|
+
if (!result.documentDelete.success) {
|
|
76
70
|
throw new Error(`Failed to delete document: ${id}`);
|
|
77
71
|
}
|
|
78
72
|
return true;
|