@enrichlayer/el-linear 1.7.0 → 1.10.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 (108) hide show
  1. package/README.md +82 -1
  2. package/claude-skills/linear-operations/SKILL.md +55 -0
  3. package/dist/auth/oauth-callback.js +14 -7
  4. package/dist/auth/oauth-fs.d.ts +22 -0
  5. package/dist/auth/oauth-fs.js +77 -6
  6. package/dist/auth/oauth-headless.d.ts +13 -8
  7. package/dist/auth/oauth-headless.js +18 -11
  8. package/dist/auth/oauth-storage.d.ts +11 -3
  9. package/dist/auth/oauth-storage.js +15 -8
  10. package/dist/auth/token-resolver.d.ts +9 -0
  11. package/dist/auth/token-resolver.js +57 -37
  12. package/dist/commands/attachments.js +2 -1
  13. package/dist/commands/comments.js +17 -28
  14. package/dist/commands/cycles.js +2 -1
  15. package/dist/commands/documents.js +2 -1
  16. package/dist/commands/graphql.js +4 -6
  17. package/dist/commands/init/index.js +2 -0
  18. package/dist/commands/init/oauth.d.ts +6 -0
  19. package/dist/commands/init/oauth.js +8 -2
  20. package/dist/commands/init/token.d.ts +0 -8
  21. package/dist/commands/init/token.js +13 -1
  22. package/dist/commands/issue-id.js +1 -3
  23. package/dist/commands/issues/branch.d.ts +17 -0
  24. package/dist/commands/issues/branch.js +40 -0
  25. package/dist/commands/issues/description.d.ts +89 -0
  26. package/dist/commands/issues/description.js +183 -0
  27. package/dist/commands/issues/link-references.d.ts +21 -0
  28. package/dist/commands/issues/link-references.js +171 -0
  29. package/dist/commands/issues/relations.d.ts +55 -0
  30. package/dist/commands/issues/relations.js +132 -0
  31. package/dist/commands/issues.js +97 -497
  32. package/dist/commands/labels.js +20 -28
  33. package/dist/commands/profile/migrate-legacy.js +10 -33
  34. package/dist/commands/profile.js +1 -10
  35. package/dist/commands/project-milestones.js +13 -20
  36. package/dist/commands/projects.d.ts +5 -1
  37. package/dist/commands/projects.js +202 -102
  38. package/dist/commands/read-shortcut.d.ts +6 -0
  39. package/dist/commands/read-shortcut.js +6 -1
  40. package/dist/commands/refs.js +10 -1
  41. package/dist/commands/releases.js +26 -30
  42. package/dist/commands/search.js +21 -30
  43. package/dist/commands/teams.js +2 -1
  44. package/dist/commands/templates.js +128 -7
  45. package/dist/commands/users.js +3 -1
  46. package/dist/config/config.js +31 -9
  47. package/dist/config/issue-validation.js +1 -1
  48. package/dist/config/paths.d.ts +11 -0
  49. package/dist/config/paths.js +44 -6
  50. package/dist/config/resolver.js +2 -3
  51. package/dist/config/term-enforcer.js +1 -1
  52. package/dist/main.js +37 -2
  53. package/dist/queries/attachments-types.d.ts +30 -0
  54. package/dist/queries/attachments-types.js +5 -0
  55. package/dist/queries/comments-types.d.ts +49 -0
  56. package/dist/queries/comments-types.js +5 -0
  57. package/dist/queries/documents-types.d.ts +61 -0
  58. package/dist/queries/documents-types.js +9 -0
  59. package/dist/queries/introspect-types.d.ts +57 -0
  60. package/dist/queries/introspect-types.js +10 -0
  61. package/dist/queries/issues-types.d.ts +416 -0
  62. package/dist/queries/issues-types.js +23 -0
  63. package/dist/queries/issues.d.ts +2 -0
  64. package/dist/queries/issues.js +22 -0
  65. package/dist/queries/labels-types.d.ts +64 -0
  66. package/dist/queries/labels-types.js +5 -0
  67. package/dist/queries/project-milestones-types.d.ts +91 -0
  68. package/dist/queries/project-milestones-types.js +10 -0
  69. package/dist/queries/projects-types.d.ts +75 -0
  70. package/dist/queries/projects-types.js +5 -0
  71. package/dist/queries/projects.d.ts +2 -0
  72. package/dist/queries/projects.js +22 -0
  73. package/dist/queries/releases-types.d.ts +84 -0
  74. package/dist/queries/releases-types.js +5 -0
  75. package/dist/queries/search-types.d.ts +86 -0
  76. package/dist/queries/search-types.js +6 -0
  77. package/dist/queries/templates-types.d.ts +61 -0
  78. package/dist/queries/templates-types.js +9 -0
  79. package/dist/queries/templates.d.ts +3 -0
  80. package/dist/queries/templates.js +43 -0
  81. package/dist/types/linear.d.ts +8 -2
  82. package/dist/utils/auth.js +7 -9
  83. package/dist/utils/auto-link-references.js +44 -25
  84. package/dist/utils/disk-cache.js +2 -1
  85. package/dist/utils/formatters/summary.d.ts +62 -0
  86. package/dist/utils/formatters/summary.js +755 -0
  87. package/dist/utils/graphql-attachments-service.js +6 -9
  88. package/dist/utils/graphql-documents-service.js +19 -25
  89. package/dist/utils/graphql-issues-service.d.ts +118 -5
  90. package/dist/utils/graphql-issues-service.js +202 -209
  91. package/dist/utils/issue-reference-extractor.d.ts +9 -1
  92. package/dist/utils/issue-reference-extractor.js +16 -9
  93. package/dist/utils/issue-reference-wrapper.js +1 -54
  94. package/dist/utils/linear-service.d.ts +7 -3
  95. package/dist/utils/linear-service.js +27 -5
  96. package/dist/utils/markdown-prosemirror.js +17 -1
  97. package/dist/utils/mention-resolver.js +17 -5
  98. package/dist/utils/output.d.ts +7 -1
  99. package/dist/utils/output.js +38 -1
  100. package/dist/utils/protected-ranges.d.ts +33 -0
  101. package/dist/utils/protected-ranges.js +73 -0
  102. package/dist/utils/table-formatter.d.ts +36 -0
  103. package/dist/utils/table-formatter.js +46 -24
  104. package/dist/utils/validators.d.ts +9 -1
  105. package/dist/utils/validators.js +10 -0
  106. package/dist/utils/workspace-url.d.ts +5 -1
  107. package/dist/utils/workspace-url.js +35 -5
  108. package/package.json +1 -1
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-milestones.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * The selection sets vary across queries — some include `description`
6
+ * + `createdAt`, others only the lookup-relevant fields. Modelled as
7
+ * a base `MilestoneBaseRef` with extension shapes per query so each
8
+ * consumer reads only what was actually selected.
9
+ */
10
+ import type { IssueNode } from "./issues-types.js";
11
+ export interface MilestoneProjectRef {
12
+ id: string;
13
+ name: string;
14
+ }
15
+ /**
16
+ * Mirrors the milestone selection set in `LIST_PROJECT_MILESTONES_QUERY`
17
+ * — full milestone fields without the project-back-reference.
18
+ */
19
+ export interface MilestoneListNode {
20
+ id: string;
21
+ name: string;
22
+ description: string | null;
23
+ targetDate: string | null;
24
+ sortOrder: number;
25
+ createdAt: string;
26
+ updatedAt: string;
27
+ }
28
+ /**
29
+ * Mirrors the milestone selection set in `GET_PROJECT_MILESTONE_BY_ID_QUERY`
30
+ * — list shape + project + issues connection.
31
+ */
32
+ export interface MilestoneDetailNode extends MilestoneListNode {
33
+ project: MilestoneProjectRef;
34
+ issues: {
35
+ nodes: IssueNode[];
36
+ };
37
+ }
38
+ /**
39
+ * Mirrors the milestone selection set in `FIND_PROJECT_MILESTONE_BY_NAME_*`
40
+ * — minimal lookup shape with project back-reference.
41
+ */
42
+ export interface MilestoneLookupNode {
43
+ id: string;
44
+ name: string;
45
+ targetDate: string | null;
46
+ sortOrder: number;
47
+ project: MilestoneProjectRef;
48
+ }
49
+ /**
50
+ * Mirrors the milestone selection set on the create / update mutations —
51
+ * list shape + project (no issues connection).
52
+ */
53
+ export interface MutatedMilestoneNode extends MilestoneListNode {
54
+ project: MilestoneProjectRef;
55
+ }
56
+ export interface ListProjectMilestonesResponse {
57
+ project: {
58
+ id: string;
59
+ name: string;
60
+ projectMilestones: {
61
+ nodes: MilestoneListNode[];
62
+ };
63
+ } | null;
64
+ }
65
+ export interface GetProjectMilestoneByIdResponse {
66
+ projectMilestone: MilestoneDetailNode | null;
67
+ }
68
+ export interface FindProjectMilestoneScopedResponse {
69
+ project: {
70
+ projectMilestones: {
71
+ nodes: MilestoneLookupNode[];
72
+ };
73
+ } | null;
74
+ }
75
+ export interface FindProjectMilestoneGlobalResponse {
76
+ projectMilestones: {
77
+ nodes: MilestoneLookupNode[];
78
+ };
79
+ }
80
+ export interface CreateProjectMilestoneResponse {
81
+ projectMilestoneCreate: {
82
+ success: boolean;
83
+ projectMilestone: MutatedMilestoneNode | null;
84
+ };
85
+ }
86
+ export interface UpdateProjectMilestoneResponse {
87
+ projectMilestoneUpdate: {
88
+ success: boolean;
89
+ projectMilestone: MutatedMilestoneNode | null;
90
+ };
91
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./project-milestones.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ *
5
+ * The selection sets vary across queries — some include `description`
6
+ * + `createdAt`, others only the lookup-relevant fields. Modelled as
7
+ * a base `MilestoneBaseRef` with extension shapes per query so each
8
+ * consumer reads only what was actually selected.
9
+ */
10
+ export {};
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./projects.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export interface ProjectTeamRef {
6
+ id: string;
7
+ key: string;
8
+ name: string;
9
+ }
10
+ export interface ProjectBaseNode {
11
+ id: string;
12
+ name: string;
13
+ teams: {
14
+ nodes: ProjectTeamRef[];
15
+ };
16
+ }
17
+ /**
18
+ * Mirrors the project selection set used by `SEARCH_PROJECTS_BY_NAME_QUERY`
19
+ * and `CREATE_PROJECT_MUTATION` — base fields + state.
20
+ */
21
+ export interface ProjectWithStateNode extends ProjectBaseNode {
22
+ state: string;
23
+ }
24
+ export interface ProjectIssueRef {
25
+ id: string;
26
+ identifier: string;
27
+ title: string;
28
+ }
29
+ export interface ProjectWithIssuesNode extends ProjectBaseNode {
30
+ issues: {
31
+ nodes: ProjectIssueRef[];
32
+ };
33
+ }
34
+ export interface ProjectByIdResponse {
35
+ project: ProjectBaseNode | null;
36
+ }
37
+ export interface GetProjectResponse {
38
+ projects: {
39
+ nodes: ProjectBaseNode[];
40
+ };
41
+ }
42
+ export interface GetProjectTeamIssuesResponse {
43
+ project: ProjectWithIssuesNode | null;
44
+ }
45
+ export interface SearchProjectsByNameResponse {
46
+ projects: {
47
+ nodes: ProjectWithStateNode[];
48
+ };
49
+ }
50
+ export interface CreateProjectResponse {
51
+ projectCreate: {
52
+ success: boolean;
53
+ project: ProjectWithStateNode | null;
54
+ };
55
+ }
56
+ export interface UpdateProjectResponse {
57
+ projectUpdate: {
58
+ success: boolean;
59
+ project: ProjectBaseNode | null;
60
+ };
61
+ }
62
+ export interface ProjectArchiveEntity {
63
+ id: string;
64
+ }
65
+ export interface ProjectArchivePayload {
66
+ success: boolean;
67
+ lastSyncId: number;
68
+ entity: ProjectArchiveEntity | null;
69
+ }
70
+ export interface ArchiveProjectResponse {
71
+ projectArchive: ProjectArchivePayload;
72
+ }
73
+ export interface DeleteProjectResponse {
74
+ projectDelete: ProjectArchivePayload;
75
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./projects.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};
@@ -5,3 +5,5 @@ export declare const GET_PROJECT_TEAM_ISSUES_QUERY = "\n query GetProjectTeamIs
5
5
  export declare const SEARCH_PROJECTS_BY_NAME_QUERY = "\n query SearchProjectsByName($name: String!) {\n projects(filter: { name: { containsIgnoreCase: $name } }, first: 10) {\n nodes {\n id\n name\n state\n teams {\n nodes { id key name }\n }\n }\n }\n }\n";
6
6
  export declare const CREATE_PROJECT_MUTATION = "\n mutation CreateProject($input: ProjectCreateInput!) {\n projectCreate(input: $input) {\n success\n project {\n id\n name\n state\n teams {\n nodes { id key name }\n }\n }\n }\n }\n";
7
7
  export declare const UPDATE_PROJECT_MUTATION = "\n mutation UpdateProject($id: String!, $input: ProjectUpdateInput!) {\n projectUpdate(id: $id, input: $input) {\n success\n project {\n id\n name\n teams {\n nodes {\n id\n key\n name\n }\n }\n }\n }\n }\n";
8
+ export declare const ARCHIVE_PROJECT_MUTATION = "\n mutation ArchiveProject($id: String!) {\n projectArchive(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
9
+ export declare const DELETE_PROJECT_MUTATION = "\n mutation DeleteProject($id: String!) {\n projectDelete(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
@@ -102,3 +102,25 @@ export const UPDATE_PROJECT_MUTATION = `
102
102
  }
103
103
  }
104
104
  `;
105
+ export const ARCHIVE_PROJECT_MUTATION = `
106
+ mutation ArchiveProject($id: String!) {
107
+ projectArchive(id: $id) {
108
+ success
109
+ lastSyncId
110
+ entity {
111
+ id
112
+ }
113
+ }
114
+ }
115
+ `;
116
+ export const DELETE_PROJECT_MUTATION = `
117
+ mutation DeleteProject($id: String!) {
118
+ projectDelete(id: $id) {
119
+ success
120
+ lastSyncId
121
+ entity {
122
+ id
123
+ }
124
+ }
125
+ }
126
+ `;
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./releases.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export interface ReleaseStageRef {
6
+ id: string;
7
+ name: string;
8
+ type: string;
9
+ color: string | null;
10
+ }
11
+ export interface ReleasePipelineRef {
12
+ id: string;
13
+ name: string;
14
+ }
15
+ export interface ReleaseDocumentRef {
16
+ id: string;
17
+ title: string;
18
+ slugId: string;
19
+ }
20
+ /** Mirrors the release selection set used by `GET_RELEASES_QUERY`. */
21
+ export interface ReleaseListNode {
22
+ id: string;
23
+ name: string;
24
+ description: string | null;
25
+ version: string | null;
26
+ url: string | null;
27
+ startDate: string | null;
28
+ targetDate: string | null;
29
+ startedAt: string | null;
30
+ completedAt: string | null;
31
+ canceledAt: string | null;
32
+ stage: ReleaseStageRef | null;
33
+ pipeline: ReleasePipelineRef | null;
34
+ createdAt: string;
35
+ updatedAt: string;
36
+ }
37
+ /** Mirrors `GET_RELEASE_BY_ID_QUERY` — adds `documents`. */
38
+ export interface ReleaseDetailNode extends ReleaseListNode {
39
+ documents: {
40
+ nodes: ReleaseDocumentRef[];
41
+ };
42
+ }
43
+ /**
44
+ * Mirrors the release selection set on `CREATE_RELEASE_MUTATION` —
45
+ * a subset of `ReleaseListNode` (no startDate/targetDate/lifecycle
46
+ * timestamps; no documents).
47
+ */
48
+ export interface CreatedReleaseNode {
49
+ id: string;
50
+ name: string;
51
+ description: string | null;
52
+ version: string | null;
53
+ url: string | null;
54
+ stage: ReleaseStageRef | null;
55
+ pipeline: ReleasePipelineRef | null;
56
+ createdAt: string;
57
+ updatedAt: string;
58
+ }
59
+ export interface ReleasePipelineNode {
60
+ id: string;
61
+ name: string;
62
+ stages: {
63
+ nodes: ReleaseStageRef[];
64
+ };
65
+ }
66
+ export interface GetReleasesResponse {
67
+ releases: {
68
+ nodes: ReleaseListNode[];
69
+ };
70
+ }
71
+ export interface GetReleaseByIdResponse {
72
+ release: ReleaseDetailNode | null;
73
+ }
74
+ export interface CreateReleaseResponse {
75
+ releaseCreate: {
76
+ success: boolean;
77
+ release: CreatedReleaseNode | null;
78
+ };
79
+ }
80
+ export interface GetReleasePipelinesResponse {
81
+ releasePipelines: {
82
+ nodes: ReleasePipelineNode[];
83
+ };
84
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Typed response shapes for the queries in `./releases.ts`.
3
+ * See `./issues-types.ts` for the rationale (ALL-937).
4
+ */
5
+ export {};
@@ -0,0 +1,86 @@
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
+ export interface SearchIssueRef {
7
+ id: string;
8
+ identifier: string;
9
+ title: string;
10
+ priority: number | null;
11
+ state: {
12
+ id: string;
13
+ name: string;
14
+ } | null;
15
+ team: {
16
+ id: string;
17
+ key: string;
18
+ name: string;
19
+ } | null;
20
+ assignee: {
21
+ id: string;
22
+ name: string;
23
+ url: string | null;
24
+ } | null;
25
+ project: {
26
+ id: string;
27
+ name: string;
28
+ } | null;
29
+ }
30
+ export interface SearchProjectRef {
31
+ id: string;
32
+ name: string;
33
+ state: string;
34
+ }
35
+ export interface SearchInitiativeRef {
36
+ id: string;
37
+ name: string;
38
+ status: string | null;
39
+ }
40
+ export interface SearchDocumentRef {
41
+ id: string;
42
+ title: string;
43
+ slugId: string | null;
44
+ project: {
45
+ id: string;
46
+ name: string;
47
+ } | null;
48
+ }
49
+ /**
50
+ * Mirrors a single semanticSearch result. The discriminator is `type`,
51
+ * but Linear returns *all* sub-objects nullable per row — only the one
52
+ * matching `type` is populated. Modelled as fully-optional sub-objects
53
+ * to match runtime reality.
54
+ */
55
+ export interface SemanticSearchResult {
56
+ type: string;
57
+ issue: SearchIssueRef | null;
58
+ project: SearchProjectRef | null;
59
+ initiative: SearchInitiativeRef | null;
60
+ document: SearchDocumentRef | null;
61
+ }
62
+ export interface SemanticSearchResponse {
63
+ semanticSearch: {
64
+ results: SemanticSearchResult[];
65
+ } | null;
66
+ }
67
+ /**
68
+ * Mirrors the inline `TEMPLATES_QUERY` in `commands/search.ts` —
69
+ * a smaller selection set than the full templates query, dropping
70
+ * fields the search command doesn't display.
71
+ */
72
+ export interface SearchTemplateNode {
73
+ id: string;
74
+ name: string;
75
+ type: string;
76
+ description: string | null;
77
+ team: {
78
+ key: string;
79
+ } | null;
80
+ creator: {
81
+ name: string;
82
+ } | null;
83
+ }
84
+ export interface SearchTemplatesResponse {
85
+ templates: SearchTemplateNode[];
86
+ }
@@ -0,0 +1,6 @@
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
+ export {};
@@ -0,0 +1,61 @@
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 interface TemplateTeamRef {
10
+ id: string;
11
+ key: string;
12
+ name: string;
13
+ }
14
+ export 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
+ }
@@ -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 {};
@@ -1,2 +1,5 @@
1
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
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";
3
+ export declare const TEMPLATE_CREATE_MUTATION = "\n mutation ($input: TemplateCreateInput!) {\n templateCreate(input: $input) {\n success\n lastSyncId\n template {\n id\n name\n type\n description\n templateData\n team { id key name }\n createdAt\n updatedAt\n }\n }\n }\n";
4
+ export declare const TEMPLATE_UPDATE_MUTATION = "\n mutation ($id: String!, $input: TemplateUpdateInput!) {\n templateUpdate(id: $id, input: $input) {\n success\n lastSyncId\n template {\n id\n name\n type\n description\n templateData\n team { id key name }\n updatedAt\n }\n }\n }\n";
5
+ export declare const TEMPLATE_DELETE_MUTATION = "\n mutation ($id: String!) {\n templateDelete(id: $id) {\n success\n lastSyncId\n }\n }\n";
@@ -28,3 +28,46 @@ export const TEMPLATE_BY_ID_QUERY = `
28
28
  }
29
29
  }
30
30
  `;
31
+ export const TEMPLATE_CREATE_MUTATION = `
32
+ mutation ($input: TemplateCreateInput!) {
33
+ templateCreate(input: $input) {
34
+ success
35
+ lastSyncId
36
+ template {
37
+ id
38
+ name
39
+ type
40
+ description
41
+ templateData
42
+ team { id key name }
43
+ createdAt
44
+ updatedAt
45
+ }
46
+ }
47
+ }
48
+ `;
49
+ export const TEMPLATE_UPDATE_MUTATION = `
50
+ mutation ($id: String!, $input: TemplateUpdateInput!) {
51
+ templateUpdate(id: $id, input: $input) {
52
+ success
53
+ lastSyncId
54
+ template {
55
+ id
56
+ name
57
+ type
58
+ description
59
+ templateData
60
+ team { id key name }
61
+ updatedAt
62
+ }
63
+ }
64
+ }
65
+ `;
66
+ export const TEMPLATE_DELETE_MUTATION = `
67
+ mutation ($id: String!) {
68
+ templateDelete(id: $id) {
69
+ success
70
+ lastSyncId
71
+ }
72
+ }
73
+ `;
@@ -208,8 +208,14 @@ type GraphQLPrimitive = string | number | boolean | null | undefined;
208
208
  type GraphQLValue = GraphQLPrimitive | GraphQLResponseData | GraphQLResponseData[];
209
209
  /**
210
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.
211
+ *
212
+ * **Prefer per-query response types** (see `src/queries/*-types.ts`)
213
+ * over `GraphQLResponseData` for any new consumer. ALL-937 swept the
214
+ * old service/command consumers; this type is kept as the fallback
215
+ * for the few generic transports (e.g. `graphql-service.rawRequest`'s
216
+ * default generic). Don't reach for `as GraphQLResponseData` to skip
217
+ * a typed shape — the per-query types catch query/consumer drift at
218
+ * compile time. This type cannot.
213
219
  */
214
220
  export interface GraphQLResponseData {
215
221
  [key: string]: GraphQLValue;
@@ -1,5 +1,5 @@
1
1
  import fs from "node:fs";
2
- import { LEGACY_LINCTL_TOKEN_PATH, LEGACY_TOKEN_PATH, resolveActiveProfile, TOKEN_PATH, } from "../config/paths.js";
2
+ import { LEGACY_LINCTL_TOKEN_PATH, LEGACY_TOKEN_PATH, resolveActiveProfile, } from "../config/paths.js";
3
3
  import { maybeEmitMigrationHint } from "./migration-hint.js";
4
4
  export function getApiToken(options) {
5
5
  if (options.apiToken) {
@@ -9,18 +9,16 @@ export function getApiToken(options) {
9
9
  return process.env.LINEAR_API_TOKEN;
10
10
  }
11
11
  // Profile-aware: read from <CONFIG_DIR>/profiles/<name>/token when a
12
- // profile is active, falling back to the legacy single-file path
13
- // (TOKEN_PATH) for the no-profile case.
12
+ // profile is active. When the user has explicitly selected a profile
13
+ // (--profile / EL_LINEAR_PROFILE / active-profile marker), do NOT
14
+ // fall back to the legacy single-file token — that path silently
15
+ // posts writes to the wrong workspace when a profile token is missing.
14
16
  const active = resolveActiveProfile();
15
17
  if (fs.existsSync(active.tokenPath)) {
16
18
  return fs.readFileSync(active.tokenPath, "utf8").trim();
17
19
  }
18
- // Even when a profile is selected, fall through to the legacy token
19
- // path so an operator who only has the single-file layout doesn't
20
- // suddenly fail. The active-profile name is informational only when
21
- // no profile-scoped token exists yet.
22
- if (active.tokenPath !== TOKEN_PATH && fs.existsSync(TOKEN_PATH)) {
23
- return fs.readFileSync(TOKEN_PATH, "utf8").trim();
20
+ if (active.name !== null) {
21
+ throw new Error(`No token for active profile \`${active.name}\` (expected at ${active.tokenPath}). Run \`el-linear init token --profile ${active.name}\` (or unset --profile / EL_LINEAR_PROFILE / \`el-linear profile use <name>\`) before retrying. Refusing to fall back to the legacy single-file token to avoid posting writes to the wrong workspace.`);
24
22
  }
25
23
  // Fallback to the linctl-era token (the CLI was briefly published as
26
24
  // `@enrichlayer/linctl` then reverted). Kept for one release.
@@ -26,37 +26,44 @@ function mergeCandidates(...sources) {
26
26
  }
27
27
  return [...byId.values()];
28
28
  }
29
- function mergeRelationNodes(nodes, peerKey, normalizeType, out) {
30
- for (const rel of nodes ?? []) {
31
- const peer = rel[peerKey];
29
+ function mergeOutgoingRelations(nodes, out) {
30
+ for (const rel of nodes) {
31
+ const peer = rel.relatedIssue;
32
32
  if (!peer) {
33
33
  continue;
34
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);
35
+ if (!out.byIdentifier.has(peer.identifier)) {
36
+ out.byIdentifier.set(peer.identifier, rel.type);
40
37
  }
41
- if (uuid && !out.byUuid.has(uuid)) {
42
- out.byUuid.set(uuid, type);
38
+ if (!out.byUuid.has(peer.id)) {
39
+ out.byUuid.set(peer.id, rel.type);
40
+ }
41
+ }
42
+ }
43
+ function mergeIncomingRelations(nodes, out) {
44
+ for (const rel of nodes) {
45
+ const peer = rel.issue;
46
+ if (!peer) {
47
+ continue;
48
+ }
49
+ // Normalize incoming "blocks" → "blockedBy" so the reported type reads naturally
50
+ const type = rel.type === "blocks" ? "blockedBy" : rel.type;
51
+ if (!out.byIdentifier.has(peer.identifier)) {
52
+ out.byIdentifier.set(peer.identifier, type);
53
+ }
54
+ if (!out.byUuid.has(peer.id)) {
55
+ out.byUuid.set(peer.id, type);
43
56
  }
44
57
  }
45
58
  }
46
59
  async function fetchExistingRelations(issueId, graphQLService) {
47
- const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, {
48
- id: issueId,
49
- });
50
- const issue = result.issue;
60
+ const result = await graphQLService.rawRequest(GET_ISSUE_RELATIONS_QUERY, { id: issueId });
51
61
  const out = { byIdentifier: new Map(), byUuid: new Map() };
52
- if (!issue) {
62
+ if (!result.issue) {
53
63
  return out;
54
64
  }
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);
65
+ mergeOutgoingRelations(result.issue.relations.nodes, out);
66
+ mergeIncomingRelations(result.issue.inverseRelations.nodes, out);
60
67
  return out;
61
68
  }
62
69
  function describeError(err) {
@@ -70,11 +77,9 @@ async function createTypedRelation(ref, sourceId, resolvedId, graphQLService) {
70
77
  type: ref.type,
71
78
  },
72
79
  });
73
- const create = result.issueRelationCreate;
74
- const issueRelation = create?.issueRelation;
80
+ const issueRelation = result.issueRelationCreate.issueRelation;
75
81
  // 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];
82
+ const peer = ref.reverse ? issueRelation?.issue : issueRelation?.relatedIssue;
78
83
  return {
79
84
  identifier: peer?.identifier ?? ref.identifier,
80
85
  title: peer?.title ?? "",
@@ -155,7 +160,21 @@ export async function autoLinkReferences(input) {
155
160
  const failed = [];
156
161
  const descriptionRefs = extractIssueReferences(description ?? "", identifier);
157
162
  const commentRefs = (comments ?? []).flatMap((body) => extractIssueReferences(body, identifier));
158
- const candidates = mergeCandidates(descriptionRefs, commentRefs);
163
+ const merged = mergeCandidates(descriptionRefs, commentRefs);
164
+ // Cap the number of candidates we'll resolve per call. A malicious
165
+ // (or merely careless) issue body containing hundreds of fake
166
+ // identifiers — `AAA-1, AAA-2, …, AAA-999` — would otherwise trigger
167
+ // 999 GraphQL roundtrips before deciding none resolve. Realistic
168
+ // human-authored bodies stay well under this cap.
169
+ const MAX_CANDIDATES_PER_CALL = 50;
170
+ const candidates = merged.slice(0, MAX_CANDIDATES_PER_CALL);
171
+ if (merged.length > MAX_CANDIDATES_PER_CALL) {
172
+ const overflow = merged.length - MAX_CANDIDATES_PER_CALL;
173
+ failed.push({
174
+ identifier: `+${overflow} more`,
175
+ reason: `Too many candidate references (${merged.length}); only the first ${MAX_CANDIDATES_PER_CALL} are processed.`,
176
+ });
177
+ }
159
178
  if (candidates.length === 0) {
160
179
  return { linked, skipped, failed };
161
180
  }