@enrichlayer/el-linear 1.9.0 → 1.15.0

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