@enrichlayer/el-linear 1.18.1 → 1.20.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.
@@ -1,6 +1,6 @@
1
- export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
2
- export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
3
- export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
1
+ export declare const GET_ISSUES_QUERY = "\n query GetIssues($first: Int!, $orderBy: PaginationOrderBy) {\n issues(\n first: $first\n orderBy: $orderBy\n filter: {\n state: { type: { neq: \"completed\" } }\n }\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
2
+ export declare const SEARCH_ISSUES_QUERY = "\n query SearchIssues($term: String!, $first: Int!) {\n searchIssues(term: $term, first: $first, includeArchived: false) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
3
+ export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIssues(\n $first: Int!\n $filter: IssueFilter\n $orderBy: PaginationOrderBy\n ) {\n issues(\n first: $first\n filter: $filter\n orderBy: $orderBy\n includeArchived: false\n ) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
4
4
  /**
5
5
  * Batch-resolves a search's team/project/assignee/delegate filter inputs.
6
6
  *
@@ -11,8 +11,30 @@ export declare const FILTERED_SEARCH_ISSUES_QUERY = "\n query FilteredSearchIss
11
11
  * name-resolution arm is needed here.)
12
12
  */
13
13
  export declare const BATCH_RESOLVE_FOR_SEARCH_QUERY = "\n query BatchResolveForSearch(\n $teamKey: String\n $teamName: String\n $projectName: String\n $hasProjectName: Boolean = false\n $assigneeEmail: String\n $delegateEmail: String\n ) {\n teams(\n filter: {\n or: [\n { key: { eq: $teamKey } }\n { name: { eqIgnoreCase: $teamName } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n key\n name\n }\n }\n\n projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 1\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n }\n }\n\n users(filter: { email: { eq: $assigneeEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n\n delegates: users(filter: { email: { eq: $delegateEmail } }, first: 1) {\n nodes {\n id\n name\n email\n }\n }\n }\n";
14
- export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
15
- export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
14
+ export declare const GET_ISSUE_BY_ID_QUERY = "\n query GetIssue($id: String!) {\n issue(id: $id) {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n";
15
+ export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdentifier($teamKey: String!, $number: Float!) {\n issues(\n filter: {\n team: { key: { eq: $teamKey } }\n number: { eq: $number }\n }\n first: 1\n ) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
16
+ /**
17
+ * Batch issue fetch. Used by `issues read <id...>` (DEV-4477) to collapse
18
+ * N parallel single-issue queries into one round-trip. The `$filter` is
19
+ * a top-level OR built client-side; each OR clause is either:
20
+ * { id: { in: [...uuids] } } ← all UUIDs in one IN
21
+ * { team: { key: { eq: <key> } }, number: { in: [...nums] } } ← identifiers
22
+ * grouped by team
23
+ * Linear returns nodes in DB order, NOT input order — the caller is
24
+ * responsible for re-sorting to match the input list before surfacing.
25
+ *
26
+ * `$first` is `refCount * 2`, clamped to [100, 250] — the floor absorbs the
27
+ * rare case where Linear returns more rows than requested via OR-clause
28
+ * collisions; the ceiling is Linear's connection-page cap, beyond which the
29
+ * server silently truncates.
30
+ *
31
+ * Raw GraphQL (not @linear/sdk) is required because the SDK's
32
+ * `client.issues({ filter: ... })` strips through the same query under
33
+ * the hood but forces one round-trip per call site; the doctrine in
34
+ * CLAUDE.md ("Performance-critical loops where the SDK's per-edge
35
+ * resolver promises produce N+1 round-trips") explicitly authorizes this.
36
+ */
37
+ export declare const BATCH_GET_ISSUES_QUERY = "\n query BatchGetIssues($filter: IssueFilter!, $first: Int!) {\n issues(filter: $filter, first: $first) {\n nodes {\n \n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n \n comments {\n nodes {\n id\n body\n createdAt\n updatedAt\n user {\n id\n name\n url\n }\n }\n }\n\n\n }\n }\n }\n";
16
38
  /**
17
39
  * Batch-resolves an update's project/milestone/issue inputs.
18
40
  *
@@ -23,8 +45,8 @@ export declare const GET_ISSUE_BY_IDENTIFIER_QUERY = "\n query GetIssueByIdenti
23
45
  * `--project-milestone` name resolution; a name uses `projectsByName`.
24
46
  */
25
47
  export declare const BATCH_RESOLVE_FOR_UPDATE_QUERY = "\n query BatchResolveForUpdate(\n $projectName: String\n $projectId: ID\n $hasProjectName: Boolean = false\n $hasProjectId: Boolean = false\n $teamKey: String\n $issueNumber: Float\n $milestoneName: String\n $hasMilestoneName: Boolean = false\n ) {\n projectsByName: projects(\n filter: { name: { eqIgnoreCase: $projectName } }\n first: 5\n ) @include(if: $hasProjectName) {\n nodes {\n id\n name\n teams {\n nodes { id key }\n }\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n projectsById: projects(\n filter: { id: { eq: $projectId } }\n first: 1\n ) @include(if: $hasProjectId) {\n nodes {\n id\n name\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n\n milestones: projectMilestones(\n filter: { name: { eq: $milestoneName } }\n first: 1\n ) @include(if: $hasMilestoneName) {\n nodes {\n id\n name\n }\n }\n\n issues(\n filter: {\n and: [\n { team: { key: { eq: $teamKey } } }\n { number: { eq: $issueNumber } }\n ]\n }\n first: 1\n ) {\n nodes {\n id\n identifier\n team {\n id\n key\n }\n labels {\n nodes {\n id\n name\n }\n }\n project {\n id\n projectMilestones {\n nodes {\n id\n name\n }\n }\n }\n }\n }\n }\n";
26
- export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
27
- export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
48
+ export declare const CREATE_ISSUE_MUTATION = "\n mutation CreateIssue($input: IssueCreateInput!) {\n issueCreate(input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
49
+ export declare const UPDATE_ISSUE_MUTATION = "\n mutation UpdateIssue($id: String!, $input: IssueUpdateInput!) {\n issueUpdate(id: $id, input: $input) {\n success\n issue {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n";
28
50
  export declare const ARCHIVE_ISSUE_MUTATION = "\n mutation ArchiveIssue($id: String!) {\n issueArchive(id: $id) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
29
51
  export declare const DELETE_ISSUE_MUTATION = "\n mutation DeleteIssue($id: String!, $permanentlyDelete: Boolean) {\n issueDelete(id: $id, permanentlyDelete: $permanentlyDelete) {\n success\n lastSyncId\n entity {\n id\n }\n }\n }\n";
30
52
  /**
@@ -124,6 +124,36 @@ export const GET_ISSUE_BY_IDENTIFIER_QUERY = `
124
124
  }
125
125
  }
126
126
  `;
127
+ /**
128
+ * Batch issue fetch. Used by `issues read <id...>` (DEV-4477) to collapse
129
+ * N parallel single-issue queries into one round-trip. The `$filter` is
130
+ * a top-level OR built client-side; each OR clause is either:
131
+ * { id: { in: [...uuids] } } ← all UUIDs in one IN
132
+ * { team: { key: { eq: <key> } }, number: { in: [...nums] } } ← identifiers
133
+ * grouped by team
134
+ * Linear returns nodes in DB order, NOT input order — the caller is
135
+ * responsible for re-sorting to match the input list before surfacing.
136
+ *
137
+ * `$first` is `refCount * 2`, clamped to [100, 250] — the floor absorbs the
138
+ * rare case where Linear returns more rows than requested via OR-clause
139
+ * collisions; the ceiling is Linear's connection-page cap, beyond which the
140
+ * server silently truncates.
141
+ *
142
+ * Raw GraphQL (not @linear/sdk) is required because the SDK's
143
+ * `client.issues({ filter: ... })` strips through the same query under
144
+ * the hood but forces one round-trip per call site; the doctrine in
145
+ * CLAUDE.md ("Performance-critical loops where the SDK's per-edge
146
+ * resolver promises produce N+1 round-trips") explicitly authorizes this.
147
+ */
148
+ export const BATCH_GET_ISSUES_QUERY = `
149
+ query BatchGetIssues($filter: IssueFilter!, $first: Int!) {
150
+ issues(filter: $filter, first: $first) {
151
+ nodes {
152
+ ${COMPLETE_ISSUE_WITH_COMMENTS_FRAGMENT}
153
+ }
154
+ }
155
+ }
156
+ `;
127
157
  /**
128
158
  * Batch-resolves an update's project/milestone/issue inputs.
129
159
  *
@@ -1,5 +1,5 @@
1
1
  export declare const LIST_PROJECT_MILESTONES_QUERY = "\n query ListProjectMilestones($projectId: String!, $first: Int!) {\n project(id: $projectId) {\n id\n name\n projectMilestones(first: $first) {\n nodes {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n }\n }\n }\n }\n";
2
- export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
2
+ export declare const GET_PROJECT_MILESTONE_BY_ID_QUERY = "\n query GetProjectMilestone($id: String!, $issuesFirst: Int) {\n projectMilestone(id: $id) {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n issues(first: $issuesFirst) {\n nodes {\n \n \n id\n identifier\n title\n description\n summary { content generationStatus }\n branchName\n priority\n estimate\n dueDate\n url\n createdAt\n updatedAt\n\n \n state {\n id\n name\n type\n }\n\n \n assignee {\n id\n name\n url\n }\n\n \n delegate {\n id\n name\n url\n }\n\n \n team {\n id\n key\n name\n }\n\n \n project {\n id\n name\n }\n\n \n labels {\n nodes {\n id\n name\n }\n }\n\n \n cycle {\n id\n name\n number\n }\n\n \n projectMilestone {\n id\n name\n targetDate\n }\n\n \n parent {\n id\n identifier\n title\n }\n\n \n children {\n nodes {\n id\n identifier\n title\n }\n }\n\n\n }\n }\n }\n }\n";
3
3
  export declare const FIND_PROJECT_MILESTONE_BY_NAME_SCOPED = "\n query FindProjectMilestoneScoped($name: String!, $projectId: String!) {\n project(id: $projectId) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n }\n";
4
4
  export declare const FIND_PROJECT_MILESTONE_BY_NAME_GLOBAL = "\n query FindProjectMilestoneGlobal($name: String!) {\n projectMilestones(filter: { name: { eq: $name } }, first: 10) {\n nodes {\n id\n name\n targetDate\n sortOrder\n project {\n id\n name\n }\n }\n }\n }\n";
5
5
  export declare const CREATE_PROJECT_MILESTONE_MUTATION = "\n mutation CreateProjectMilestone($projectId: String!, $name: String!, $description: String, $targetDate: TimelessDate) {\n projectMilestoneCreate(input: {\n projectId: $projectId\n name: $name\n description: $description\n targetDate: $targetDate\n }) {\n success\n projectMilestone {\n id\n name\n description\n targetDate\n sortOrder\n createdAt\n updatedAt\n project {\n id\n name\n }\n }\n }\n }\n";
@@ -27,6 +27,13 @@ interface UserRef {
27
27
  interface StateRef {
28
28
  id: string;
29
29
  name: string;
30
+ /**
31
+ * Linear workflow-state type: `triage` / `backlog` / `unstarted` /
32
+ * `started` / `completed` / `canceled`. Optional because pre-DEV-4478
33
+ * callers and some legacy GraphQL selections didn't request it; new
34
+ * code (default state-filtering, e.g.) should set it.
35
+ */
36
+ type?: string;
30
37
  }
31
38
  interface ProjectRef {
32
39
  id: string;
@@ -16,4 +16,20 @@
16
16
  * Returns `null` when the field isn't found, so callers can distinguish
17
17
  * "missing" from "empty body".
18
18
  */
19
+ /**
20
+ * Multi-section variant of `extractField`. Extracts each requested section
21
+ * by name in one call and returns a `{section -> text|null}` map preserving
22
+ * the caller's order. Missing sections map to `null` so the caller can
23
+ * distinguish "absent" from "empty" the same way `extractField` does.
24
+ *
25
+ * Each name is matched independently via `extractField` — semantics are
26
+ * identical (first-match wins, case-insensitive, fenced-code-block aware).
27
+ *
28
+ * DEV-4479: this is the primitive behind `el-linear issues read --sections
29
+ * "Done when,Out of scope"` so agents can pull several sections in one
30
+ * call instead of N spawns. (The CLI flag landed as `--sections` rather
31
+ * than `--fields` because `--fields` is already the program-level
32
+ * output-key filter — see the option's `addHelpText` for context.)
33
+ */
34
+ export declare function extractFields(body: string, fieldNames: readonly string[]): Map<string, string | null>;
19
35
  export declare function extractField(body: string, fieldName: string): string | null;
@@ -59,6 +59,28 @@ function matchHeader(line) {
59
59
  }
60
60
  return null;
61
61
  }
62
+ /**
63
+ * Multi-section variant of `extractField`. Extracts each requested section
64
+ * by name in one call and returns a `{section -> text|null}` map preserving
65
+ * the caller's order. Missing sections map to `null` so the caller can
66
+ * distinguish "absent" from "empty" the same way `extractField` does.
67
+ *
68
+ * Each name is matched independently via `extractField` — semantics are
69
+ * identical (first-match wins, case-insensitive, fenced-code-block aware).
70
+ *
71
+ * DEV-4479: this is the primitive behind `el-linear issues read --sections
72
+ * "Done when,Out of scope"` so agents can pull several sections in one
73
+ * call instead of N spawns. (The CLI flag landed as `--sections` rather
74
+ * than `--fields` because `--fields` is already the program-level
75
+ * output-key filter — see the option's `addHelpText` for context.)
76
+ */
77
+ export function extractFields(body, fieldNames) {
78
+ const out = new Map();
79
+ for (const name of fieldNames) {
80
+ out.set(name, extractField(body, name));
81
+ }
82
+ return out;
83
+ }
62
84
  export function extractField(body, fieldName) {
63
85
  if (!body)
64
86
  return null;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * ASCII tree formatter for `issues tree <ID>` (DEV-4480).
3
+ *
4
+ * Renders a parent → children tree using the same box-drawing prefixes as
5
+ * the standard `tree` Unix utility (`├── `, `└── `, `│ `, ` `). Each
6
+ * line is `<prefix> <identifier> <title> <state-suffix>`, where the
7
+ * state-suffix is `[Done]` / `[Canceled]` for terminal-typed states and
8
+ * omitted otherwise.
9
+ *
10
+ * Pure rendering — does not fetch or filter. The depth bound and the
11
+ * terminal-state exclusion are enforced by the caller before this runs.
12
+ */
13
+ import type { IssueTreeNode } from "../queries/issue-tree.js";
14
+ export declare function formatTree(root: IssueTreeNode): string;
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ASCII tree formatter for `issues tree <ID>` (DEV-4480).
3
+ *
4
+ * Renders a parent → children tree using the same box-drawing prefixes as
5
+ * the standard `tree` Unix utility (`├── `, `└── `, `│ `, ` `). Each
6
+ * line is `<prefix> <identifier> <title> <state-suffix>`, where the
7
+ * state-suffix is `[Done]` / `[Canceled]` for terminal-typed states and
8
+ * omitted otherwise.
9
+ *
10
+ * Pure rendering — does not fetch or filter. The depth bound and the
11
+ * terminal-state exclusion are enforced by the caller before this runs.
12
+ */
13
+ const BRANCH_TEE = "├── ";
14
+ const BRANCH_END = "└── ";
15
+ const VERTICAL = "│ ";
16
+ const SPACE = " ";
17
+ export function formatTree(root) {
18
+ const lines = [formatNode(root)];
19
+ const kids = root.children?.nodes ?? [];
20
+ for (let i = 0; i < kids.length; i++) {
21
+ appendSubtree(lines, kids[i], "", i === kids.length - 1);
22
+ }
23
+ return lines.join("\n");
24
+ }
25
+ function appendSubtree(lines, node, prefix, isLast) {
26
+ const branch = isLast ? BRANCH_END : BRANCH_TEE;
27
+ lines.push(`${prefix}${branch}${formatNode(node)}`);
28
+ const childPrefix = prefix + (isLast ? SPACE : VERTICAL);
29
+ const kids = node.children?.nodes ?? [];
30
+ for (let i = 0; i < kids.length; i++) {
31
+ appendSubtree(lines, kids[i], childPrefix, i === kids.length - 1);
32
+ }
33
+ }
34
+ function formatNode(node) {
35
+ const stateSuffix = node.state ? formatStateSuffix(node.state) : "";
36
+ const assigneeSuffix = node.assignee ? ` (@${node.assignee.name})` : "";
37
+ return `${node.identifier} ${node.title}${stateSuffix}${assigneeSuffix}`;
38
+ }
39
+ function formatStateSuffix(state) {
40
+ // Only annotate terminal-typed states; the renderer is otherwise
41
+ // state-name-agnostic so workspace-custom workflow states (e.g. "In
42
+ // Review") don't get a noisy suffix.
43
+ if (state.type === "completed" || state.type === "canceled") {
44
+ return ` [${state.name}]`;
45
+ }
46
+ return "";
47
+ }
@@ -41,6 +41,14 @@ export interface SearchIssueArgs {
41
41
  delegateId?: string;
42
42
  /** Issue states to include (e.g. `["Todo", "In Progress"]`). */
43
43
  status?: string[];
44
+ /**
45
+ * Exclude terminal states (workflow type `completed` or `canceled`).
46
+ * Default `false` for back-compat at the service layer; the CLI's
47
+ * `issues list` / `issues search` flip this to `true` by default
48
+ * (DEV-4478) and accept `--include-closed` to opt back in. When
49
+ * `status` is also set, this is ignored — explicit user choice wins.
50
+ */
51
+ excludeTerminalStates?: boolean;
44
52
  /** Label names to require (intersection). */
45
53
  labelNames?: string[];
46
54
  /** Priority values (0-4) to include. */
@@ -149,6 +157,24 @@ export declare class GraphQLIssuesService {
149
157
  constructor(graphQLService: GraphQLService, linearService: LinearService);
150
158
  getIssues(limit?: number): Promise<LinearIssue[]>;
151
159
  getIssueById(issueId: string): Promise<LinearIssue>;
160
+ /**
161
+ * Batch-fetches N issues in a single GraphQL round-trip (DEV-4477).
162
+ *
163
+ * Replaces `Promise.all(refs.map(getIssueById))` — that pattern issues N
164
+ * concurrent queries; this one issues exactly one. Useful for
165
+ * `el-linear issues read <id...>`, the only caller today.
166
+ *
167
+ * Identifier shapes accepted (mirrors `getIssueById`):
168
+ * - UUIDs (`isUuid(ref)` matches) — batched into a single `id.in` clause.
169
+ * - `TEAM-N` identifiers (`parseIssueIdentifier(ref)` matches) — grouped
170
+ * by team key into one `{ team.key.eq, number.in }` clause per team.
171
+ *
172
+ * Linear returns nodes in DB order; this method re-sorts to match the
173
+ * input list and throws `notFoundError` if any ref did not resolve, with
174
+ * the first missing ref named in the error. Order-preservation matters
175
+ * for the read handler — the JSON envelope is a positional array.
176
+ */
177
+ getIssuesByRefs(refs: string[]): Promise<LinearIssue[]>;
152
178
  startIssue(issueId: string): Promise<StartIssueResult>;
153
179
  updateIssue(args: UpdateIssueArgs, labelMode?: string): Promise<LinearIssue>;
154
180
  archiveIssue(issueId: string): Promise<IssueArchiveOperationResult>;
@@ -1,5 +1,5 @@
1
1
  import { resolveUserDisplayName } from "../config/resolver.js";
2
- import { ARCHIVE_ISSUE_MUTATION, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
2
+ import { ARCHIVE_ISSUE_MUTATION, BATCH_GET_ISSUES_QUERY, BATCH_RESOLVE_FOR_CREATE_QUERY, BATCH_RESOLVE_FOR_SEARCH_QUERY, BATCH_RESOLVE_FOR_UPDATE_QUERY, buildResolveLabelsByNameQuery, CREATE_ISSUE_MUTATION, DELETE_ISSUE_MUTATION, FILTERED_SEARCH_ISSUES_QUERY, GET_ISSUE_BY_ID_QUERY, GET_ISSUE_BY_IDENTIFIER_QUERY, GET_ISSUE_START_CONTEXT_QUERY, GET_ISSUE_TEAM_QUERY, GET_ISSUES_QUERY, SEARCH_ISSUES_QUERY, TEAM_STARTED_STATUSES_QUERY, UPDATE_ISSUE_MUTATION, } from "../queries/issues.js";
3
3
  import { CREATE_LABEL_MUTATION } from "../queries/labels.js";
4
4
  import { toISOStringOrNow } from "./date-format.js";
5
5
  import { extractEmbeds } from "./embed-parser.js";
@@ -61,6 +61,95 @@ export class GraphQLIssuesService {
61
61
  }
62
62
  return this.transformIssueData(issueData);
63
63
  }
64
+ /**
65
+ * Batch-fetches N issues in a single GraphQL round-trip (DEV-4477).
66
+ *
67
+ * Replaces `Promise.all(refs.map(getIssueById))` — that pattern issues N
68
+ * concurrent queries; this one issues exactly one. Useful for
69
+ * `el-linear issues read <id...>`, the only caller today.
70
+ *
71
+ * Identifier shapes accepted (mirrors `getIssueById`):
72
+ * - UUIDs (`isUuid(ref)` matches) — batched into a single `id.in` clause.
73
+ * - `TEAM-N` identifiers (`parseIssueIdentifier(ref)` matches) — grouped
74
+ * by team key into one `{ team.key.eq, number.in }` clause per team.
75
+ *
76
+ * Linear returns nodes in DB order; this method re-sorts to match the
77
+ * input list and throws `notFoundError` if any ref did not resolve, with
78
+ * the first missing ref named in the error. Order-preservation matters
79
+ * for the read handler — the JSON envelope is a positional array.
80
+ */
81
+ async getIssuesByRefs(refs) {
82
+ if (refs.length === 0) {
83
+ return [];
84
+ }
85
+ if (refs.length === 1) {
86
+ // Single ref → fall through to the single-issue path, which has
87
+ // the right error semantics (notFoundError on miss) and avoids
88
+ // building an OR clause with one branch.
89
+ return [await this.getIssueById(refs[0])];
90
+ }
91
+ const uuids = [];
92
+ const byTeam = new Map();
93
+ for (const ref of refs) {
94
+ if (isUuid(ref)) {
95
+ uuids.push(ref);
96
+ continue;
97
+ }
98
+ const { teamKey, issueNumber } = parseIssueIdentifier(ref);
99
+ const existing = byTeam.get(teamKey);
100
+ if (existing) {
101
+ existing.push(issueNumber);
102
+ }
103
+ else {
104
+ byTeam.set(teamKey, [issueNumber]);
105
+ }
106
+ }
107
+ const orClauses = [];
108
+ if (uuids.length > 0) {
109
+ orClauses.push({ id: { in: uuids } });
110
+ }
111
+ for (const [teamKey, numbers] of byTeam) {
112
+ orClauses.push({
113
+ team: { key: { eq: teamKey } },
114
+ number: { in: numbers },
115
+ });
116
+ }
117
+ // `first` is sized at 2x refs.length to absorb the rare case where
118
+ // Linear returns extra rows from the OR clauses (e.g. a typo-collision
119
+ // across teams). Floor of 100 covers small batches comfortably; ceiling
120
+ // of 250 is Linear's connection cap — exceeding it would silently
121
+ // truncate and we'd `notFoundError` on the truncated refs. With the
122
+ // ceiling clamped, a >125-ref batch would risk under-fetching, but
123
+ // `read <id...>` is typed-by-hand and never approaches that scale.
124
+ // (Cycle-1 nit.)
125
+ const first = Math.min(Math.max(refs.length * 2, 100), 250);
126
+ const result = await this.graphQLService.rawRequest(BATCH_GET_ISSUES_QUERY, { filter: { or: orClauses }, first });
127
+ const nodes = result.issues?.nodes ?? [];
128
+ // Build an index for O(1) per-ref lookup, then re-emit in input order.
129
+ // A node is reachable by both its UUID and its identifier; both index
130
+ // keys point at the same node object. This is intentional: if a caller
131
+ // passes both forms for the same issue (`["xxxxxx-...", "DEV-1"]`),
132
+ // or the same ref twice (`["DEV-1", "DEV-1"]`), each input ref gets
133
+ // its own output entry — the contract is "output shape mirrors input
134
+ // shape", not "dedupe by underlying issue". (Cycle-1 nit.)
135
+ const byUuid = new Map();
136
+ const byIdentifier = new Map();
137
+ for (const node of nodes) {
138
+ byUuid.set(node.id, node);
139
+ if (node.identifier) {
140
+ byIdentifier.set(node.identifier, node);
141
+ }
142
+ }
143
+ const ordered = [];
144
+ for (const ref of refs) {
145
+ const node = isUuid(ref) ? byUuid.get(ref) : byIdentifier.get(ref);
146
+ if (!node) {
147
+ throw notFoundError("Issue", ref);
148
+ }
149
+ ordered.push(node);
150
+ }
151
+ return ordered.map((issue) => this.transformIssueData(issue));
152
+ }
64
153
  async startIssue(issueId) {
65
154
  const resolvedIssueId = await this.linearService.resolveIssueId(issueId);
66
155
  const context = await this.graphQLService.rawRequest(GET_ISSUE_START_CONTEXT_QUERY, { id: resolvedIssueId });
@@ -391,6 +480,7 @@ export class GraphQLIssuesService {
391
480
  delegateId: finalDelegateId,
392
481
  project: projectFilter,
393
482
  status: args.status,
483
+ excludeTerminalStates: args.excludeTerminalStates,
394
484
  labelNames: args.labelNames,
395
485
  priority: args.priority,
396
486
  });
@@ -401,6 +491,7 @@ export class GraphQLIssuesService {
401
491
  delegateId: finalDelegateId,
402
492
  project: projectFilter,
403
493
  status: args.status,
494
+ excludeTerminalStates: args.excludeTerminalStates,
404
495
  labelNames: args.labelNames,
405
496
  priority: args.priority,
406
497
  });
@@ -820,6 +911,11 @@ export class GraphQLIssuesService {
820
911
  if (filters.status && filters.status.length > 0) {
821
912
  filtered = filtered.filter((issue) => filters.status.includes(issue.state?.name ?? ""));
822
913
  }
914
+ else if (filters.excludeTerminalStates) {
915
+ // Implicit terminal-state exclusion (DEV-4478). Explicit `status`
916
+ // always wins — the user already named the states they want.
917
+ filtered = filtered.filter((issue) => issue.state?.type !== "completed" && issue.state?.type !== "canceled");
918
+ }
823
919
  if (filters.labelNames && filters.labelNames.length > 0) {
824
920
  const lowerNames = filters.labelNames.map((n) => n.toLowerCase());
825
921
  filtered = filtered.filter((issue) => {
@@ -858,6 +954,11 @@ export class GraphQLIssuesService {
858
954
  if (filters.status && filters.status.length > 0) {
859
955
  filter.state = { name: { in: filters.status } };
860
956
  }
957
+ else if (filters.excludeTerminalStates) {
958
+ // Implicit terminal-state exclusion (DEV-4478). Explicit `status`
959
+ // always wins — the user already named the states they want.
960
+ filter.state = { type: { nin: ["completed", "canceled"] } };
961
+ }
861
962
  if (filters.labelNames && filters.labelNames.length > 0) {
862
963
  if (filters.labelNames.length === 1) {
863
964
  filter.labels = {
@@ -1097,7 +1198,13 @@ export class GraphQLIssuesService {
1097
1198
  transformIssueRelations(issue) {
1098
1199
  return {
1099
1200
  state: issue.state
1100
- ? { id: issue.state.id, name: issue.state.name }
1201
+ ? {
1202
+ id: issue.state.id,
1203
+ name: issue.state.name,
1204
+ // DEV-4478: surface workflow-state `type` so callers can filter
1205
+ // terminal states (`completed`, `canceled`) without re-querying.
1206
+ type: issue.state.type,
1207
+ }
1101
1208
  : undefined,
1102
1209
  assignee: issue.assignee
1103
1210
  ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@enrichlayer/el-linear",
3
- "version": "1.18.1",
3
+ "version": "1.20.0",
4
4
  "description": "A pragmatic CLI for Linear.app — deterministic team/label/member resolution, structured issue validation, configurable term enforcement, and a GraphQL escape hatch.",
5
5
  "main": "dist/main.js",
6
6
  "types": "dist/main.d.ts",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@inquirer/prompts": "^8.4.2",
55
- "@linear/sdk": "^84.0.0",
55
+ "@linear/sdk": "^86.0.0",
56
56
  "commander": "^14.0.0",
57
57
  "picocolors": "^1.1.1"
58
58
  },