@enrichlayer/el-linear 1.2.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/LICENSE +21 -0
- package/README.md +219 -0
- package/claude-skills/linear-operations/SKILL.md +315 -0
- package/claude-skills/linear-operations/evals/evals.json +46 -0
- package/dist/commands/attachments.d.ts +2 -0
- package/dist/commands/attachments.js +57 -0
- package/dist/commands/batch.d.ts +2 -0
- package/dist/commands/batch.js +309 -0
- package/dist/commands/comments.d.ts +2 -0
- package/dist/commands/comments.js +272 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +15 -0
- package/dist/commands/cycles.d.ts +2 -0
- package/dist/commands/cycles.js +63 -0
- package/dist/commands/documents.d.ts +2 -0
- package/dist/commands/documents.js +175 -0
- package/dist/commands/embeds.d.ts +2 -0
- package/dist/commands/embeds.js +65 -0
- package/dist/commands/gdoc.d.ts +2 -0
- package/dist/commands/gdoc.js +37 -0
- package/dist/commands/graphql.d.ts +2 -0
- package/dist/commands/graphql.js +70 -0
- package/dist/commands/init/aliases.d.ts +109 -0
- package/dist/commands/init/aliases.js +569 -0
- package/dist/commands/init/defaults.d.ts +25 -0
- package/dist/commands/init/defaults.js +112 -0
- package/dist/commands/init/index.d.ts +18 -0
- package/dist/commands/init/index.js +182 -0
- package/dist/commands/init/shared.d.ts +88 -0
- package/dist/commands/init/shared.js +164 -0
- package/dist/commands/init/token.d.ts +50 -0
- package/dist/commands/init/token.js +141 -0
- package/dist/commands/init/workspace.d.ts +20 -0
- package/dist/commands/init/workspace.js +80 -0
- package/dist/commands/issue-id.d.ts +28 -0
- package/dist/commands/issue-id.js +81 -0
- package/dist/commands/issues.d.ts +2 -0
- package/dist/commands/issues.js +1145 -0
- package/dist/commands/labels.d.ts +2 -0
- package/dist/commands/labels.js +100 -0
- package/dist/commands/project-milestones.d.ts +2 -0
- package/dist/commands/project-milestones.js +143 -0
- package/dist/commands/projects.d.ts +2 -0
- package/dist/commands/projects.js +336 -0
- package/dist/commands/read-shortcut.d.ts +6 -0
- package/dist/commands/read-shortcut.js +69 -0
- package/dist/commands/releases.d.ts +2 -0
- package/dist/commands/releases.js +142 -0
- package/dist/commands/search.d.ts +2 -0
- package/dist/commands/search.js +171 -0
- package/dist/commands/teams.d.ts +2 -0
- package/dist/commands/teams.js +19 -0
- package/dist/commands/templates.d.ts +2 -0
- package/dist/commands/templates.js +58 -0
- package/dist/commands/users.d.ts +2 -0
- package/dist/commands/users.js +17 -0
- package/dist/config/config.d.ts +43 -0
- package/dist/config/config.js +81 -0
- package/dist/config/issue-validation.d.ts +39 -0
- package/dist/config/issue-validation.js +264 -0
- package/dist/config/paths.d.ts +20 -0
- package/dist/config/paths.js +22 -0
- package/dist/config/resolver.d.ts +25 -0
- package/dist/config/resolver.js +183 -0
- package/dist/config/status-defaults.d.ts +13 -0
- package/dist/config/status-defaults.js +20 -0
- package/dist/config/term-enforcer.d.ts +31 -0
- package/dist/config/term-enforcer.js +69 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +76 -0
- package/dist/queries/attachments.d.ts +3 -0
- package/dist/queries/attachments.js +35 -0
- package/dist/queries/comments.d.ts +3 -0
- package/dist/queries/comments.js +64 -0
- package/dist/queries/common.d.ts +2 -0
- package/dist/queries/common.js +109 -0
- package/dist/queries/cycles.d.ts +2 -0
- package/dist/queries/cycles.js +40 -0
- package/dist/queries/documents.d.ts +5 -0
- package/dist/queries/documents.js +67 -0
- package/dist/queries/introspect.d.ts +2 -0
- package/dist/queries/introspect.js +24 -0
- package/dist/queries/issues.d.ts +23 -0
- package/dist/queries/issues.js +380 -0
- package/dist/queries/labels.d.ts +4 -0
- package/dist/queries/labels.js +53 -0
- package/dist/queries/project-milestones.d.ts +6 -0
- package/dist/queries/project-milestones.js +125 -0
- package/dist/queries/projects.d.ts +7 -0
- package/dist/queries/projects.js +104 -0
- package/dist/queries/releases.d.ts +4 -0
- package/dist/queries/releases.js +85 -0
- package/dist/queries/search.d.ts +1 -0
- package/dist/queries/search.js +35 -0
- package/dist/queries/templates.d.ts +2 -0
- package/dist/queries/templates.js +30 -0
- package/dist/types/linear.d.ts +217 -0
- package/dist/types/linear.js +6 -0
- package/dist/utils/auth.d.ts +4 -0
- package/dist/utils/auth.js +23 -0
- package/dist/utils/auto-link-references.d.ts +47 -0
- package/dist/utils/auto-link-references.js +188 -0
- package/dist/utils/date-format.d.ts +4 -0
- package/dist/utils/date-format.js +8 -0
- package/dist/utils/download-uploads.d.ts +7 -0
- package/dist/utils/download-uploads.js +88 -0
- package/dist/utils/embed-parser.d.ts +8 -0
- package/dist/utils/embed-parser.js +54 -0
- package/dist/utils/error-messages.d.ts +4 -0
- package/dist/utils/error-messages.js +17 -0
- package/dist/utils/file-service.d.ts +13 -0
- package/dist/utils/file-service.js +239 -0
- package/dist/utils/gdoc-parser.d.ts +45 -0
- package/dist/utils/gdoc-parser.js +107 -0
- package/dist/utils/graphql-attachments-service.d.ts +12 -0
- package/dist/utils/graphql-attachments-service.js +46 -0
- package/dist/utils/graphql-documents-service.d.ts +18 -0
- package/dist/utils/graphql-documents-service.js +97 -0
- package/dist/utils/graphql-issues-service.d.ts +49 -0
- package/dist/utils/graphql-issues-service.js +925 -0
- package/dist/utils/graphql-service.d.ts +8 -0
- package/dist/utils/graphql-service.js +35 -0
- package/dist/utils/identifier-parser.d.ts +7 -0
- package/dist/utils/identifier-parser.js +20 -0
- package/dist/utils/issue-reference-extractor.d.ts +18 -0
- package/dist/utils/issue-reference-extractor.js +95 -0
- package/dist/utils/issue-reference-wrapper.d.ts +12 -0
- package/dist/utils/issue-reference-wrapper.js +91 -0
- package/dist/utils/linear-service.d.ts +26 -0
- package/dist/utils/linear-service.js +442 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.js +8 -0
- package/dist/utils/markdown-prosemirror.d.ts +24 -0
- package/dist/utils/markdown-prosemirror.js +325 -0
- package/dist/utils/mention-resolver.d.ts +31 -0
- package/dist/utils/mention-resolver.js +234 -0
- package/dist/utils/output.d.ts +7 -0
- package/dist/utils/output.js +125 -0
- package/dist/utils/table-formatter.d.ts +4 -0
- package/dist/utils/table-formatter.js +249 -0
- package/dist/utils/usage.d.ts +2 -0
- package/dist/utils/usage.js +24 -0
- package/dist/utils/uuid.d.ts +2 -0
- package/dist/utils/uuid.js +8 -0
- package/dist/utils/validate-references.d.ts +10 -0
- package/dist/utils/validate-references.js +33 -0
- package/dist/utils/validators.d.ts +7 -0
- package/dist/utils/validators.js +71 -0
- package/dist/utils/workspace-url.d.ts +4 -0
- package/dist/utils/workspace-url.js +44 -0
- package/package.json +71 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
export const GET_RELEASES_QUERY = `
|
|
2
|
+
query GetReleases($first: Int!, $filter: ReleaseFilter) {
|
|
3
|
+
releases(first: $first, filter: $filter) {
|
|
4
|
+
nodes {
|
|
5
|
+
id
|
|
6
|
+
name
|
|
7
|
+
description
|
|
8
|
+
version
|
|
9
|
+
url
|
|
10
|
+
startDate
|
|
11
|
+
targetDate
|
|
12
|
+
startedAt
|
|
13
|
+
completedAt
|
|
14
|
+
canceledAt
|
|
15
|
+
stage { id name type color }
|
|
16
|
+
pipeline { id name }
|
|
17
|
+
createdAt
|
|
18
|
+
updatedAt
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
`;
|
|
23
|
+
export const GET_RELEASE_BY_ID_QUERY = `
|
|
24
|
+
query GetReleaseById($id: String!) {
|
|
25
|
+
release(id: $id) {
|
|
26
|
+
id
|
|
27
|
+
name
|
|
28
|
+
description
|
|
29
|
+
version
|
|
30
|
+
url
|
|
31
|
+
startDate
|
|
32
|
+
targetDate
|
|
33
|
+
startedAt
|
|
34
|
+
completedAt
|
|
35
|
+
canceledAt
|
|
36
|
+
stage { id name type color }
|
|
37
|
+
pipeline { id name }
|
|
38
|
+
documents {
|
|
39
|
+
nodes {
|
|
40
|
+
id
|
|
41
|
+
title
|
|
42
|
+
slugId
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
createdAt
|
|
46
|
+
updatedAt
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
export const CREATE_RELEASE_MUTATION = `
|
|
51
|
+
mutation CreateRelease($input: ReleaseCreateInput!) {
|
|
52
|
+
releaseCreate(input: $input) {
|
|
53
|
+
success
|
|
54
|
+
release {
|
|
55
|
+
id
|
|
56
|
+
name
|
|
57
|
+
description
|
|
58
|
+
version
|
|
59
|
+
url
|
|
60
|
+
stage { id name type color }
|
|
61
|
+
pipeline { id name }
|
|
62
|
+
createdAt
|
|
63
|
+
updatedAt
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
`;
|
|
68
|
+
export const GET_RELEASE_PIPELINES_QUERY = `
|
|
69
|
+
query GetReleasePipelines($first: Int!) {
|
|
70
|
+
releasePipelines(first: $first) {
|
|
71
|
+
nodes {
|
|
72
|
+
id
|
|
73
|
+
name
|
|
74
|
+
stages {
|
|
75
|
+
nodes {
|
|
76
|
+
id
|
|
77
|
+
name
|
|
78
|
+
type
|
|
79
|
+
color
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const SEMANTIC_SEARCH_QUERY = "\n query SemanticSearch($query: String!, $maxResults: Int, $filters: SemanticSearchFilters) {\n semanticSearch(query: $query, maxResults: $maxResults, filters: $filters) {\n results {\n type\n issue {\n id\n identifier\n title\n priority\n state { id name }\n team { id key name }\n assignee { id name url }\n project { id name }\n }\n project {\n id\n name\n state\n }\n initiative {\n id\n name\n status\n }\n document {\n id\n title\n slugId\n project { id name }\n }\n }\n }\n }\n";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const SEMANTIC_SEARCH_QUERY = `
|
|
2
|
+
query SemanticSearch($query: String!, $maxResults: Int, $filters: SemanticSearchFilters) {
|
|
3
|
+
semanticSearch(query: $query, maxResults: $maxResults, filters: $filters) {
|
|
4
|
+
results {
|
|
5
|
+
type
|
|
6
|
+
issue {
|
|
7
|
+
id
|
|
8
|
+
identifier
|
|
9
|
+
title
|
|
10
|
+
priority
|
|
11
|
+
state { id name }
|
|
12
|
+
team { id key name }
|
|
13
|
+
assignee { id name url }
|
|
14
|
+
project { id name }
|
|
15
|
+
}
|
|
16
|
+
project {
|
|
17
|
+
id
|
|
18
|
+
name
|
|
19
|
+
state
|
|
20
|
+
}
|
|
21
|
+
initiative {
|
|
22
|
+
id
|
|
23
|
+
name
|
|
24
|
+
status
|
|
25
|
+
}
|
|
26
|
+
document {
|
|
27
|
+
id
|
|
28
|
+
title
|
|
29
|
+
slugId
|
|
30
|
+
project { id name }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
`;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const TEMPLATES_LIST_QUERY = "\n query {\n templates {\n id\n name\n type\n description\n templateData\n createdAt\n updatedAt\n team { id key name }\n creator { id name }\n }\n }\n";
|
|
2
|
+
export declare const TEMPLATE_BY_ID_QUERY = "\n query ($id: String!) {\n template(id: $id) {\n id\n name\n type\n description\n templateData\n createdAt\n updatedAt\n team { id key name }\n creator { id name }\n }\n }\n";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export const TEMPLATES_LIST_QUERY = `
|
|
2
|
+
query {
|
|
3
|
+
templates {
|
|
4
|
+
id
|
|
5
|
+
name
|
|
6
|
+
type
|
|
7
|
+
description
|
|
8
|
+
templateData
|
|
9
|
+
createdAt
|
|
10
|
+
updatedAt
|
|
11
|
+
team { id key name }
|
|
12
|
+
creator { id name }
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
`;
|
|
16
|
+
export const TEMPLATE_BY_ID_QUERY = `
|
|
17
|
+
query ($id: String!) {
|
|
18
|
+
template(id: $id) {
|
|
19
|
+
id
|
|
20
|
+
name
|
|
21
|
+
type
|
|
22
|
+
description
|
|
23
|
+
templateData
|
|
24
|
+
createdAt
|
|
25
|
+
updatedAt
|
|
26
|
+
team { id key name }
|
|
27
|
+
creator { id name }
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
`;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core entity types returned by el-linear service methods.
|
|
3
|
+
* These represent the public API surface — GraphQL responses are
|
|
4
|
+
* transformed into these shapes at the service boundary.
|
|
5
|
+
*/
|
|
6
|
+
interface TeamRef {
|
|
7
|
+
id: string;
|
|
8
|
+
key: string;
|
|
9
|
+
name: string;
|
|
10
|
+
}
|
|
11
|
+
interface UserRef {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
url?: string;
|
|
15
|
+
}
|
|
16
|
+
interface StateRef {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}
|
|
20
|
+
interface ProjectRef {
|
|
21
|
+
id: string;
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
24
|
+
interface LabelRef {
|
|
25
|
+
id: string;
|
|
26
|
+
name: string;
|
|
27
|
+
}
|
|
28
|
+
interface CycleRef {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
number: number;
|
|
32
|
+
}
|
|
33
|
+
interface MilestoneRef {
|
|
34
|
+
id: string;
|
|
35
|
+
name: string;
|
|
36
|
+
targetDate?: string;
|
|
37
|
+
}
|
|
38
|
+
interface IssueRef {
|
|
39
|
+
id: string;
|
|
40
|
+
identifier: string;
|
|
41
|
+
title: string;
|
|
42
|
+
}
|
|
43
|
+
export interface LinearTeam {
|
|
44
|
+
description: string | null;
|
|
45
|
+
id: string;
|
|
46
|
+
key: string;
|
|
47
|
+
name: string;
|
|
48
|
+
}
|
|
49
|
+
export interface LinearUser {
|
|
50
|
+
active: boolean;
|
|
51
|
+
displayName: string;
|
|
52
|
+
email: string;
|
|
53
|
+
id: string;
|
|
54
|
+
name: string;
|
|
55
|
+
}
|
|
56
|
+
export interface LinearLabel {
|
|
57
|
+
color: string;
|
|
58
|
+
group?: LabelRef;
|
|
59
|
+
id: string;
|
|
60
|
+
name: string;
|
|
61
|
+
scope: "team" | "workspace";
|
|
62
|
+
team?: TeamRef;
|
|
63
|
+
}
|
|
64
|
+
export interface LinearProject {
|
|
65
|
+
createdAt: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
id: string;
|
|
68
|
+
lead?: UserRef;
|
|
69
|
+
name: string;
|
|
70
|
+
progress: number;
|
|
71
|
+
state: string;
|
|
72
|
+
targetDate?: string;
|
|
73
|
+
teams: TeamRef[];
|
|
74
|
+
updatedAt: string;
|
|
75
|
+
}
|
|
76
|
+
export interface LinearComment {
|
|
77
|
+
body: string;
|
|
78
|
+
createdAt: string;
|
|
79
|
+
embeds?: import("../utils/embed-parser.js").Embed[];
|
|
80
|
+
id: string;
|
|
81
|
+
updatedAt: string;
|
|
82
|
+
user?: UserRef;
|
|
83
|
+
}
|
|
84
|
+
export interface LinearIssue {
|
|
85
|
+
assignee?: UserRef;
|
|
86
|
+
branchName?: string;
|
|
87
|
+
comments?: LinearComment[];
|
|
88
|
+
createdAt: string;
|
|
89
|
+
cycle?: CycleRef;
|
|
90
|
+
description?: string;
|
|
91
|
+
dueDate?: string;
|
|
92
|
+
embeds?: import("../utils/embed-parser.js").Embed[];
|
|
93
|
+
estimate?: number;
|
|
94
|
+
id: string;
|
|
95
|
+
identifier: string;
|
|
96
|
+
labels: LabelRef[];
|
|
97
|
+
parentIssue?: IssueRef;
|
|
98
|
+
priority: number;
|
|
99
|
+
project?: ProjectRef;
|
|
100
|
+
projectMilestone?: MilestoneRef;
|
|
101
|
+
state?: StateRef;
|
|
102
|
+
subIssues?: IssueRef[];
|
|
103
|
+
summary?: string;
|
|
104
|
+
team?: TeamRef;
|
|
105
|
+
title: string;
|
|
106
|
+
updatedAt: string;
|
|
107
|
+
url: string;
|
|
108
|
+
}
|
|
109
|
+
export interface LinearCycleSummary {
|
|
110
|
+
endsAt?: string;
|
|
111
|
+
id: string;
|
|
112
|
+
isActive: boolean;
|
|
113
|
+
isNext?: boolean;
|
|
114
|
+
isPrevious?: boolean;
|
|
115
|
+
issueCountHistory?: number[];
|
|
116
|
+
name?: string;
|
|
117
|
+
number: number;
|
|
118
|
+
progress: number;
|
|
119
|
+
startsAt?: string;
|
|
120
|
+
team?: TeamRef;
|
|
121
|
+
}
|
|
122
|
+
export interface LinearCycleDetail extends LinearCycleSummary {
|
|
123
|
+
issues: LinearIssue[];
|
|
124
|
+
}
|
|
125
|
+
export interface LinearDocument {
|
|
126
|
+
color?: string;
|
|
127
|
+
content?: string;
|
|
128
|
+
createdAt?: string;
|
|
129
|
+
creator?: UserRef;
|
|
130
|
+
icon?: string;
|
|
131
|
+
id: string;
|
|
132
|
+
issue?: IssueRef;
|
|
133
|
+
project?: ProjectRef;
|
|
134
|
+
slugId?: string;
|
|
135
|
+
title: string;
|
|
136
|
+
updatedAt?: string;
|
|
137
|
+
url?: string;
|
|
138
|
+
}
|
|
139
|
+
export interface LinearAttachment {
|
|
140
|
+
createdAt?: string;
|
|
141
|
+
id: string;
|
|
142
|
+
title?: string;
|
|
143
|
+
updatedAt?: string;
|
|
144
|
+
url: string;
|
|
145
|
+
}
|
|
146
|
+
export interface LinearIssueRelation {
|
|
147
|
+
id: string;
|
|
148
|
+
issue: IssueRef;
|
|
149
|
+
relatedIssue: IssueRef;
|
|
150
|
+
type: string;
|
|
151
|
+
}
|
|
152
|
+
export interface LinearRelease {
|
|
153
|
+
canceledAt?: string;
|
|
154
|
+
completedAt?: string;
|
|
155
|
+
createdAt: string;
|
|
156
|
+
description?: string;
|
|
157
|
+
documents?: {
|
|
158
|
+
id: string;
|
|
159
|
+
title: string;
|
|
160
|
+
slugId: string;
|
|
161
|
+
}[];
|
|
162
|
+
id: string;
|
|
163
|
+
name: string;
|
|
164
|
+
pipeline?: {
|
|
165
|
+
id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
};
|
|
168
|
+
stage?: {
|
|
169
|
+
id: string;
|
|
170
|
+
name: string;
|
|
171
|
+
type: string;
|
|
172
|
+
};
|
|
173
|
+
startDate?: string;
|
|
174
|
+
startedAt?: string;
|
|
175
|
+
targetDate?: string;
|
|
176
|
+
updatedAt: string;
|
|
177
|
+
url?: string;
|
|
178
|
+
version?: string;
|
|
179
|
+
}
|
|
180
|
+
export interface IssueStateSpan {
|
|
181
|
+
endedAt?: string;
|
|
182
|
+
startedAt: string;
|
|
183
|
+
state: StateRef & {
|
|
184
|
+
type: string;
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
export type FileDownloadResult = {
|
|
188
|
+
success: true;
|
|
189
|
+
filePath: string;
|
|
190
|
+
} | {
|
|
191
|
+
success: false;
|
|
192
|
+
error: string;
|
|
193
|
+
statusCode?: number;
|
|
194
|
+
};
|
|
195
|
+
export type FileUploadResult = {
|
|
196
|
+
success: true;
|
|
197
|
+
assetUrl: string;
|
|
198
|
+
filename: string;
|
|
199
|
+
} | {
|
|
200
|
+
success: false;
|
|
201
|
+
error: string;
|
|
202
|
+
statusCode?: number;
|
|
203
|
+
};
|
|
204
|
+
export type GraphQLVariables = Record<string, unknown>;
|
|
205
|
+
/** Primitive values that appear in GraphQL JSON responses. */
|
|
206
|
+
type GraphQLPrimitive = string | number | boolean | null | undefined;
|
|
207
|
+
/** Union of all value types in a raw GraphQL response. */
|
|
208
|
+
type GraphQLValue = GraphQLPrimitive | GraphQLResponseData | GraphQLResponseData[];
|
|
209
|
+
/**
|
|
210
|
+
* Recursive index type for raw GraphQL responses.
|
|
211
|
+
* All nested property access requires narrowing via `as GraphQLResponseData`
|
|
212
|
+
* or `as string` etc. — this prevents `any` from leaking into the type system.
|
|
213
|
+
*/
|
|
214
|
+
export interface GraphQLResponseData {
|
|
215
|
+
[key: string]: GraphQLValue;
|
|
216
|
+
}
|
|
217
|
+
export {};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { LEGACY_LINCTL_TOKEN_PATH, LEGACY_TOKEN_PATH, TOKEN_PATH, } from "../config/paths.js";
|
|
3
|
+
export function getApiToken(options) {
|
|
4
|
+
if (options.apiToken) {
|
|
5
|
+
return options.apiToken;
|
|
6
|
+
}
|
|
7
|
+
if (process.env.LINEAR_API_TOKEN) {
|
|
8
|
+
return process.env.LINEAR_API_TOKEN;
|
|
9
|
+
}
|
|
10
|
+
if (fs.existsSync(TOKEN_PATH)) {
|
|
11
|
+
return fs.readFileSync(TOKEN_PATH, "utf8").trim();
|
|
12
|
+
}
|
|
13
|
+
// Fallback to the linctl-era token (the CLI was briefly published as
|
|
14
|
+
// `@enrichlayer/linctl` then reverted). Kept for one release.
|
|
15
|
+
if (fs.existsSync(LEGACY_LINCTL_TOKEN_PATH)) {
|
|
16
|
+
return fs.readFileSync(LEGACY_LINCTL_TOKEN_PATH, "utf8").trim();
|
|
17
|
+
}
|
|
18
|
+
// Even older fallback from before the `~/.config/...` move.
|
|
19
|
+
if (fs.existsSync(LEGACY_TOKEN_PATH)) {
|
|
20
|
+
return fs.readFileSync(LEGACY_TOKEN_PATH, "utf8").trim();
|
|
21
|
+
}
|
|
22
|
+
throw new Error("No API token found. Use --api-token, LINEAR_API_TOKEN env var, ~/.config/el-linear/token, or ~/.linear_api_token file");
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { GraphQLService } from "./graphql-service.js";
|
|
2
|
+
import { type IssueRelationType } from "./issue-reference-extractor.js";
|
|
3
|
+
import type { LinearService } from "./linear-service.js";
|
|
4
|
+
export interface AutoLinkLinked {
|
|
5
|
+
identifier: string;
|
|
6
|
+
/** True when source/target were swapped on the create (e.g. "blocked by X" → X→source) */
|
|
7
|
+
reverse: boolean;
|
|
8
|
+
title: string;
|
|
9
|
+
type: IssueRelationType;
|
|
10
|
+
}
|
|
11
|
+
export interface AutoLinkSkipped {
|
|
12
|
+
existingType: string;
|
|
13
|
+
identifier: string;
|
|
14
|
+
/** Type that would have been created had the reference been new */
|
|
15
|
+
inferredType: IssueRelationType;
|
|
16
|
+
}
|
|
17
|
+
export interface AutoLinkFailed {
|
|
18
|
+
identifier: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
21
|
+
export interface AutoLinkResult {
|
|
22
|
+
failed: AutoLinkFailed[];
|
|
23
|
+
linked: AutoLinkLinked[];
|
|
24
|
+
skipped: AutoLinkSkipped[];
|
|
25
|
+
}
|
|
26
|
+
interface AutoLinkInput {
|
|
27
|
+
/** Optional comment bodies to also scan (description wins for type inference on conflicts) */
|
|
28
|
+
comments?: string[] | undefined;
|
|
29
|
+
/** Description text to scan for references */
|
|
30
|
+
description: string | null | undefined;
|
|
31
|
+
/** When true, skip the create mutation but still report what would be linked */
|
|
32
|
+
dryRun?: boolean;
|
|
33
|
+
graphQLService: GraphQLService;
|
|
34
|
+
/** Human identifier of the source issue (e.g. "EMW-258") — used to skip self-references */
|
|
35
|
+
identifier: string;
|
|
36
|
+
/** Resolved UUID of the source issue */
|
|
37
|
+
issueId: string;
|
|
38
|
+
linearService: LinearService;
|
|
39
|
+
/**
|
|
40
|
+
* Optional pre-resolved Map<identifier, uuid>. When provided, the linker uses these
|
|
41
|
+
* UUIDs instead of re-calling resolveIssueId. Identifiers absent from the map are
|
|
42
|
+
* still resolved on-demand (so callers can pass partial maps).
|
|
43
|
+
*/
|
|
44
|
+
preResolved?: Map<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export declare function autoLinkReferences(input: AutoLinkInput): Promise<AutoLinkResult>;
|
|
47
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Merge candidates from multiple sources (description, comments) into one list,
|
|
14
|
+
* keeping the strongest (most-specific) inference per identifier. The first source
|
|
15
|
+
* wins on ties — so callers should pass description first.
|
|
16
|
+
*/
|
|
17
|
+
function mergeCandidates(...sources) {
|
|
18
|
+
const byId = new Map();
|
|
19
|
+
for (const refs of sources) {
|
|
20
|
+
for (const ref of refs) {
|
|
21
|
+
const existing = byId.get(ref.identifier);
|
|
22
|
+
if (!existing || specificity(ref) > specificity(existing)) {
|
|
23
|
+
byId.set(ref.identifier, ref);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return [...byId.values()];
|
|
28
|
+
}
|
|
29
|
+
function mergeRelationNodes(nodes, peerKey, normalizeType, out) {
|
|
30
|
+
for (const rel of nodes ?? []) {
|
|
31
|
+
const peer = rel[peerKey];
|
|
32
|
+
if (!peer) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const id = peer.identifier;
|
|
36
|
+
const uuid = peer.id;
|
|
37
|
+
const type = normalizeType(rel.type);
|
|
38
|
+
if (id && !out.byIdentifier.has(id)) {
|
|
39
|
+
out.byIdentifier.set(id, type);
|
|
40
|
+
}
|
|
41
|
+
if (uuid && !out.byUuid.has(uuid)) {
|
|
42
|
+
out.byUuid.set(uuid, type);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
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 out = { byIdentifier: new Map(), byUuid: new Map() };
|
|
52
|
+
if (!issue) {
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
const outgoing = issue.relations;
|
|
56
|
+
mergeRelationNodes(outgoing?.nodes, "relatedIssue", (raw) => raw, out);
|
|
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);
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
function describeError(err) {
|
|
63
|
+
return err instanceof Error ? err.message : String(err);
|
|
64
|
+
}
|
|
65
|
+
async function createTypedRelation(ref, sourceId, resolvedId, graphQLService) {
|
|
66
|
+
const result = await graphQLService.rawRequest(ISSUE_RELATION_CREATE_MUTATION, {
|
|
67
|
+
input: {
|
|
68
|
+
issueId: ref.reverse ? resolvedId : sourceId,
|
|
69
|
+
relatedIssueId: ref.reverse ? sourceId : resolvedId,
|
|
70
|
+
type: ref.type,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
const create = result.issueRelationCreate;
|
|
74
|
+
const issueRelation = create?.issueRelation;
|
|
75
|
+
// The peer (the issue NOT identified by `sourceId`) is whichever side wasn't the source
|
|
76
|
+
const peerKey = ref.reverse ? "issue" : "relatedIssue";
|
|
77
|
+
const peer = issueRelation?.[peerKey];
|
|
78
|
+
return {
|
|
79
|
+
identifier: peer?.identifier ?? ref.identifier,
|
|
80
|
+
title: peer?.title ?? "",
|
|
81
|
+
type: ref.type,
|
|
82
|
+
reverse: ref.reverse,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function processCandidate(ref, ctx) {
|
|
86
|
+
const existingType = ctx.existing.byIdentifier.get(ref.identifier);
|
|
87
|
+
if (existingType) {
|
|
88
|
+
return {
|
|
89
|
+
kind: "skipped",
|
|
90
|
+
entry: {
|
|
91
|
+
identifier: ref.identifier,
|
|
92
|
+
existingType,
|
|
93
|
+
inferredType: ref.type,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
let resolvedId;
|
|
98
|
+
const cached = ctx.preResolved?.get(ref.identifier);
|
|
99
|
+
if (cached) {
|
|
100
|
+
resolvedId = cached;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
try {
|
|
104
|
+
resolvedId = await ctx.linearService.resolveIssueId(ref.identifier);
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
kind: "failed",
|
|
109
|
+
entry: { identifier: ref.identifier, reason: describeError(err) },
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (resolvedId === ctx.sourceId) {
|
|
114
|
+
return { kind: "self" };
|
|
115
|
+
}
|
|
116
|
+
const existingByUuid = ctx.existing.byUuid.get(resolvedId);
|
|
117
|
+
if (existingByUuid) {
|
|
118
|
+
return {
|
|
119
|
+
kind: "skipped",
|
|
120
|
+
entry: {
|
|
121
|
+
identifier: ref.identifier,
|
|
122
|
+
existingType: existingByUuid,
|
|
123
|
+
inferredType: ref.type,
|
|
124
|
+
},
|
|
125
|
+
resolvedId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
if (ctx.dryRun) {
|
|
129
|
+
return {
|
|
130
|
+
kind: "linked",
|
|
131
|
+
entry: {
|
|
132
|
+
identifier: ref.identifier,
|
|
133
|
+
title: "",
|
|
134
|
+
type: ref.type,
|
|
135
|
+
reverse: ref.reverse,
|
|
136
|
+
},
|
|
137
|
+
resolvedId,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
const linked = await createTypedRelation(ref, ctx.sourceId, resolvedId, ctx.graphQLService);
|
|
142
|
+
return { kind: "linked", entry: linked, resolvedId };
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
return {
|
|
146
|
+
kind: "failed",
|
|
147
|
+
entry: { identifier: ref.identifier, reason: describeError(err) },
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
export async function autoLinkReferences(input) {
|
|
152
|
+
const { issueId, identifier, description, comments, graphQLService, linearService, dryRun, preResolved, } = input;
|
|
153
|
+
const linked = [];
|
|
154
|
+
const skipped = [];
|
|
155
|
+
const failed = [];
|
|
156
|
+
const descriptionRefs = extractIssueReferences(description ?? "", identifier);
|
|
157
|
+
const commentRefs = (comments ?? []).flatMap((body) => extractIssueReferences(body, identifier));
|
|
158
|
+
const candidates = mergeCandidates(descriptionRefs, commentRefs);
|
|
159
|
+
if (candidates.length === 0) {
|
|
160
|
+
return { linked, skipped, failed };
|
|
161
|
+
}
|
|
162
|
+
const existing = await fetchExistingRelations(issueId, graphQLService);
|
|
163
|
+
const ctx = {
|
|
164
|
+
sourceId: issueId,
|
|
165
|
+
existing,
|
|
166
|
+
dryRun: Boolean(dryRun),
|
|
167
|
+
graphQLService,
|
|
168
|
+
linearService,
|
|
169
|
+
preResolved,
|
|
170
|
+
};
|
|
171
|
+
for (const ref of candidates) {
|
|
172
|
+
const outcome = await processCandidate(ref, ctx);
|
|
173
|
+
if (outcome.kind === "linked") {
|
|
174
|
+
linked.push(outcome.entry);
|
|
175
|
+
// Track in the existing maps so the same ref repeated in input isn't re-linked
|
|
176
|
+
const recordedType = ref.reverse && ref.type === "blocks" ? "blockedBy" : ref.type;
|
|
177
|
+
existing.byIdentifier.set(ref.identifier, recordedType);
|
|
178
|
+
existing.byUuid.set(outcome.resolvedId, recordedType);
|
|
179
|
+
}
|
|
180
|
+
else if (outcome.kind === "skipped") {
|
|
181
|
+
skipped.push(outcome.entry);
|
|
182
|
+
}
|
|
183
|
+
else if (outcome.kind === "failed") {
|
|
184
|
+
failed.push(outcome.entry);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { linked, skipped, failed };
|
|
188
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Convert a date-like value to an ISO string, falling back to the current time. */
|
|
2
|
+
export declare function toISOStringOrNow(value: string | Date | null | undefined): string;
|
|
3
|
+
/** Convert a date-like value to an ISO string, or return undefined if absent. */
|
|
4
|
+
export declare function toISOStringOrUndefined(value: string | Date | null | undefined): string | undefined;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** Convert a date-like value to an ISO string, falling back to the current time. */
|
|
2
|
+
export function toISOStringOrNow(value) {
|
|
3
|
+
return value ? new Date(value).toISOString() : new Date().toISOString();
|
|
4
|
+
}
|
|
5
|
+
/** Convert a date-like value to an ISO string, or return undefined if absent. */
|
|
6
|
+
export function toISOStringOrUndefined(value) {
|
|
7
|
+
return value ? new Date(value).toISOString() : undefined;
|
|
8
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { LinearIssue } from "../types/linear.js";
|
|
2
|
+
/**
|
|
3
|
+
* Downloads all uploads.linear.app URLs found in an issue's description
|
|
4
|
+
* and comments, replacing them with local file paths so Claude Code
|
|
5
|
+
* can read the images directly.
|
|
6
|
+
*/
|
|
7
|
+
export declare function downloadLinearUploads(issue: LinearIssue, apiToken: string): Promise<LinearIssue>;
|