@enrichlayer/el-linear 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/claude-skills/linear-operations/SKILL.md +315 -0
  4. package/claude-skills/linear-operations/evals/evals.json +46 -0
  5. package/dist/commands/attachments.d.ts +2 -0
  6. package/dist/commands/attachments.js +57 -0
  7. package/dist/commands/batch.d.ts +2 -0
  8. package/dist/commands/batch.js +309 -0
  9. package/dist/commands/comments.d.ts +2 -0
  10. package/dist/commands/comments.js +272 -0
  11. package/dist/commands/config.d.ts +2 -0
  12. package/dist/commands/config.js +15 -0
  13. package/dist/commands/cycles.d.ts +2 -0
  14. package/dist/commands/cycles.js +63 -0
  15. package/dist/commands/documents.d.ts +2 -0
  16. package/dist/commands/documents.js +175 -0
  17. package/dist/commands/embeds.d.ts +2 -0
  18. package/dist/commands/embeds.js +65 -0
  19. package/dist/commands/gdoc.d.ts +2 -0
  20. package/dist/commands/gdoc.js +37 -0
  21. package/dist/commands/graphql.d.ts +2 -0
  22. package/dist/commands/graphql.js +70 -0
  23. package/dist/commands/init/aliases.d.ts +109 -0
  24. package/dist/commands/init/aliases.js +569 -0
  25. package/dist/commands/init/defaults.d.ts +25 -0
  26. package/dist/commands/init/defaults.js +112 -0
  27. package/dist/commands/init/index.d.ts +18 -0
  28. package/dist/commands/init/index.js +182 -0
  29. package/dist/commands/init/shared.d.ts +88 -0
  30. package/dist/commands/init/shared.js +164 -0
  31. package/dist/commands/init/token.d.ts +50 -0
  32. package/dist/commands/init/token.js +141 -0
  33. package/dist/commands/init/workspace.d.ts +20 -0
  34. package/dist/commands/init/workspace.js +80 -0
  35. package/dist/commands/issue-id.d.ts +28 -0
  36. package/dist/commands/issue-id.js +81 -0
  37. package/dist/commands/issues.d.ts +2 -0
  38. package/dist/commands/issues.js +1145 -0
  39. package/dist/commands/labels.d.ts +2 -0
  40. package/dist/commands/labels.js +100 -0
  41. package/dist/commands/project-milestones.d.ts +2 -0
  42. package/dist/commands/project-milestones.js +143 -0
  43. package/dist/commands/projects.d.ts +2 -0
  44. package/dist/commands/projects.js +336 -0
  45. package/dist/commands/read-shortcut.d.ts +6 -0
  46. package/dist/commands/read-shortcut.js +69 -0
  47. package/dist/commands/releases.d.ts +2 -0
  48. package/dist/commands/releases.js +142 -0
  49. package/dist/commands/search.d.ts +2 -0
  50. package/dist/commands/search.js +171 -0
  51. package/dist/commands/teams.d.ts +2 -0
  52. package/dist/commands/teams.js +19 -0
  53. package/dist/commands/templates.d.ts +2 -0
  54. package/dist/commands/templates.js +58 -0
  55. package/dist/commands/users.d.ts +2 -0
  56. package/dist/commands/users.js +17 -0
  57. package/dist/config/config.d.ts +43 -0
  58. package/dist/config/config.js +81 -0
  59. package/dist/config/issue-validation.d.ts +39 -0
  60. package/dist/config/issue-validation.js +264 -0
  61. package/dist/config/paths.d.ts +20 -0
  62. package/dist/config/paths.js +22 -0
  63. package/dist/config/resolver.d.ts +25 -0
  64. package/dist/config/resolver.js +183 -0
  65. package/dist/config/status-defaults.d.ts +13 -0
  66. package/dist/config/status-defaults.js +20 -0
  67. package/dist/config/term-enforcer.d.ts +31 -0
  68. package/dist/config/term-enforcer.js +69 -0
  69. package/dist/main.d.ts +2 -0
  70. package/dist/main.js +76 -0
  71. package/dist/queries/attachments.d.ts +3 -0
  72. package/dist/queries/attachments.js +35 -0
  73. package/dist/queries/comments.d.ts +3 -0
  74. package/dist/queries/comments.js +64 -0
  75. package/dist/queries/common.d.ts +2 -0
  76. package/dist/queries/common.js +109 -0
  77. package/dist/queries/cycles.d.ts +2 -0
  78. package/dist/queries/cycles.js +40 -0
  79. package/dist/queries/documents.d.ts +5 -0
  80. package/dist/queries/documents.js +67 -0
  81. package/dist/queries/introspect.d.ts +2 -0
  82. package/dist/queries/introspect.js +24 -0
  83. package/dist/queries/issues.d.ts +23 -0
  84. package/dist/queries/issues.js +380 -0
  85. package/dist/queries/labels.d.ts +4 -0
  86. package/dist/queries/labels.js +53 -0
  87. package/dist/queries/project-milestones.d.ts +6 -0
  88. package/dist/queries/project-milestones.js +125 -0
  89. package/dist/queries/projects.d.ts +7 -0
  90. package/dist/queries/projects.js +104 -0
  91. package/dist/queries/releases.d.ts +4 -0
  92. package/dist/queries/releases.js +85 -0
  93. package/dist/queries/search.d.ts +1 -0
  94. package/dist/queries/search.js +35 -0
  95. package/dist/queries/templates.d.ts +2 -0
  96. package/dist/queries/templates.js +30 -0
  97. package/dist/types/linear.d.ts +217 -0
  98. package/dist/types/linear.js +6 -0
  99. package/dist/utils/auth.d.ts +4 -0
  100. package/dist/utils/auth.js +23 -0
  101. package/dist/utils/auto-link-references.d.ts +47 -0
  102. package/dist/utils/auto-link-references.js +188 -0
  103. package/dist/utils/date-format.d.ts +4 -0
  104. package/dist/utils/date-format.js +8 -0
  105. package/dist/utils/download-uploads.d.ts +7 -0
  106. package/dist/utils/download-uploads.js +88 -0
  107. package/dist/utils/embed-parser.d.ts +8 -0
  108. package/dist/utils/embed-parser.js +54 -0
  109. package/dist/utils/error-messages.d.ts +4 -0
  110. package/dist/utils/error-messages.js +17 -0
  111. package/dist/utils/file-service.d.ts +13 -0
  112. package/dist/utils/file-service.js +239 -0
  113. package/dist/utils/gdoc-parser.d.ts +45 -0
  114. package/dist/utils/gdoc-parser.js +107 -0
  115. package/dist/utils/graphql-attachments-service.d.ts +12 -0
  116. package/dist/utils/graphql-attachments-service.js +46 -0
  117. package/dist/utils/graphql-documents-service.d.ts +18 -0
  118. package/dist/utils/graphql-documents-service.js +97 -0
  119. package/dist/utils/graphql-issues-service.d.ts +49 -0
  120. package/dist/utils/graphql-issues-service.js +925 -0
  121. package/dist/utils/graphql-service.d.ts +8 -0
  122. package/dist/utils/graphql-service.js +35 -0
  123. package/dist/utils/identifier-parser.d.ts +7 -0
  124. package/dist/utils/identifier-parser.js +20 -0
  125. package/dist/utils/issue-reference-extractor.d.ts +18 -0
  126. package/dist/utils/issue-reference-extractor.js +95 -0
  127. package/dist/utils/issue-reference-wrapper.d.ts +12 -0
  128. package/dist/utils/issue-reference-wrapper.js +91 -0
  129. package/dist/utils/linear-service.d.ts +26 -0
  130. package/dist/utils/linear-service.js +442 -0
  131. package/dist/utils/logger.d.ts +4 -0
  132. package/dist/utils/logger.js +8 -0
  133. package/dist/utils/markdown-prosemirror.d.ts +24 -0
  134. package/dist/utils/markdown-prosemirror.js +325 -0
  135. package/dist/utils/mention-resolver.d.ts +31 -0
  136. package/dist/utils/mention-resolver.js +234 -0
  137. package/dist/utils/output.d.ts +7 -0
  138. package/dist/utils/output.js +125 -0
  139. package/dist/utils/table-formatter.d.ts +4 -0
  140. package/dist/utils/table-formatter.js +249 -0
  141. package/dist/utils/usage.d.ts +2 -0
  142. package/dist/utils/usage.js +24 -0
  143. package/dist/utils/uuid.d.ts +2 -0
  144. package/dist/utils/uuid.js +8 -0
  145. package/dist/utils/validate-references.d.ts +10 -0
  146. package/dist/utils/validate-references.js +33 -0
  147. package/dist/utils/validators.d.ts +7 -0
  148. package/dist/utils/validators.js +71 -0
  149. package/dist/utils/workspace-url.d.ts +4 -0
  150. package/dist/utils/workspace-url.js +44 -0
  151. package/package.json +71 -0
@@ -0,0 +1,8 @@
1
+ import type { GraphQLResponseData, GraphQLVariables } from "../types/linear.js";
2
+ import { type AuthOptions } from "./auth.js";
3
+ export declare class GraphQLService {
4
+ private readonly graphQLClient;
5
+ constructor(apiToken: string);
6
+ rawRequest<T = GraphQLResponseData>(query: string, variables?: GraphQLVariables): Promise<T>;
7
+ }
8
+ export declare function createGraphQLService(options: AuthOptions): GraphQLService;
@@ -0,0 +1,35 @@
1
+ import { LinearClient } from "@linear/sdk";
2
+ import { getApiToken } from "./auth.js";
3
+ export class GraphQLService {
4
+ graphQLClient;
5
+ constructor(apiToken) {
6
+ const client = new LinearClient({
7
+ apiKey: apiToken,
8
+ headers: {
9
+ "public-file-urls-expire-in": "3600",
10
+ },
11
+ });
12
+ // LinearClient stores a private graphql-request client — access via escape hatch
13
+ this.graphQLClient = client.client;
14
+ }
15
+ // Default type allows property access on raw GraphQL responses.
16
+ // Callers can narrow with explicit type parameter: rawRequest<{ issues: { nodes: T[] } }>(...)
17
+ async rawRequest(query, variables) {
18
+ try {
19
+ const response = await this.graphQLClient.rawRequest(query, variables);
20
+ return response.data;
21
+ }
22
+ catch (error) {
23
+ const err = error;
24
+ if (err.response?.errors) {
25
+ const graphQLError = err.response.errors[0];
26
+ throw new Error(graphQLError.message || "GraphQL query failed");
27
+ }
28
+ throw new Error(`GraphQL request failed: ${err.message}`);
29
+ }
30
+ }
31
+ }
32
+ export function createGraphQLService(options) {
33
+ const apiToken = getApiToken(options);
34
+ return new GraphQLService(apiToken);
35
+ }
@@ -0,0 +1,7 @@
1
+ interface ParsedIdentifier {
2
+ issueNumber: number;
3
+ teamKey: string;
4
+ }
5
+ export declare function parseIssueIdentifier(identifier: string): ParsedIdentifier;
6
+ export declare function tryParseIssueIdentifier(identifier: string): ParsedIdentifier | null;
7
+ export {};
@@ -0,0 +1,20 @@
1
+ export function parseIssueIdentifier(identifier) {
2
+ const parts = identifier.split("-");
3
+ if (parts.length !== 2) {
4
+ throw new Error(`Invalid issue identifier format: "${identifier}". Expected format: TEAM-123`);
5
+ }
6
+ const teamKey = parts[0];
7
+ const issueNumber = Number.parseInt(parts[1], 10);
8
+ if (Number.isNaN(issueNumber)) {
9
+ throw new Error(`Invalid issue number in identifier: "${identifier}"`);
10
+ }
11
+ return { teamKey, issueNumber };
12
+ }
13
+ export function tryParseIssueIdentifier(identifier) {
14
+ try {
15
+ return parseIssueIdentifier(identifier);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
@@ -0,0 +1,18 @@
1
+ export type IssueRelationType = "related" | "blocks" | "duplicate";
2
+ export interface IssueReference {
3
+ identifier: string;
4
+ /** When true, swap source/target on the create mutation (e.g. "blocked by X" means X→source). */
5
+ reverse: boolean;
6
+ type: IssueRelationType;
7
+ }
8
+ /**
9
+ * Extract issue identifiers from text along with the inferred relation type.
10
+ * Strips fenced code blocks before scanning to avoid false positives in pasted logs.
11
+ *
12
+ * The relation type is inferred from prose keywords immediately before the identifier
13
+ * (e.g. "blocked by DEV-100" → blocks/reverse=true). Default is `related`.
14
+ *
15
+ * Each identifier appears at most once in the result. If the same identifier appears
16
+ * with different qualifiers, the strongest non-default inference wins.
17
+ */
18
+ export declare function extractIssueReferences(text: string, selfIdentifier?: string): IssueReference[];
@@ -0,0 +1,95 @@
1
+ const IDENTIFIER_REGEX = /\b([A-Z][A-Z0-9]*-\d+)\b/g;
2
+ const FENCED_CODE_BLOCK_REGEX = /(?:^|\n)([ \t]*)(?:```|~~~)[^\n]*\n[\s\S]*?\n\1?(?:```|~~~)(?=\n|$)/g;
3
+ /**
4
+ * Window of text immediately before an identifier where we look for relation keywords.
5
+ * 30 chars covers phrases like "is a prerequisite of" comfortably.
6
+ */
7
+ const KEYWORD_WINDOW = 30;
8
+ /**
9
+ * Phrase patterns matched against the text immediately preceding an identifier.
10
+ * Each pattern is anchored at the *end* of the window (so the phrase must directly precede
11
+ * the identifier, modulo whitespace). Order matters — most-specific phrases first.
12
+ *
13
+ * `reverse: true` flips the source/target on the relation create:
14
+ * - "blocked by DEV-100" → DEV-100 blocks the source issue
15
+ * - "duplicated by EMW-258" → EMW-258 duplicates the source issue
16
+ */
17
+ const PHRASE_PATTERNS = [
18
+ {
19
+ regex: /\bduplicated by\s*$/i,
20
+ type: "duplicate",
21
+ reverse: true,
22
+ },
23
+ {
24
+ regex: /\b(?:duplicate of|dup(?:e)? of|duplicates)\s*$/i,
25
+ type: "duplicate",
26
+ reverse: false,
27
+ },
28
+ {
29
+ regex: /\b(?:blocked by|blocked on|depends on|waiting on|requires)\s*$/i,
30
+ type: "blocks",
31
+ reverse: true,
32
+ },
33
+ {
34
+ regex: /\b(?:blocks|unblocks|prerequisite (?:for|of)|required by)\s*$/i,
35
+ type: "blocks",
36
+ reverse: false,
37
+ },
38
+ ];
39
+ function stripFencedCodeBlocks(text) {
40
+ return text.replace(FENCED_CODE_BLOCK_REGEX, "");
41
+ }
42
+ function inferRelation(textBefore) {
43
+ const window = textBefore.slice(-KEYWORD_WINDOW);
44
+ for (const { regex, type, reverse } of PHRASE_PATTERNS) {
45
+ if (regex.test(window)) {
46
+ return { type, reverse };
47
+ }
48
+ }
49
+ return { type: "related", reverse: false };
50
+ }
51
+ /**
52
+ * Specificity ranking — used when the same identifier appears more than once in the text
53
+ * with different qualifiers. The strongest non-default inference wins.
54
+ */
55
+ function specificity(ref) {
56
+ if (ref.type === "duplicate") {
57
+ return 3;
58
+ }
59
+ if (ref.type === "blocks") {
60
+ return 2;
61
+ }
62
+ return 1; // related
63
+ }
64
+ /**
65
+ * Extract issue identifiers from text along with the inferred relation type.
66
+ * Strips fenced code blocks before scanning to avoid false positives in pasted logs.
67
+ *
68
+ * The relation type is inferred from prose keywords immediately before the identifier
69
+ * (e.g. "blocked by DEV-100" → blocks/reverse=true). Default is `related`.
70
+ *
71
+ * Each identifier appears at most once in the result. If the same identifier appears
72
+ * with different qualifiers, the strongest non-default inference wins.
73
+ */
74
+ export function extractIssueReferences(text, selfIdentifier) {
75
+ if (!text) {
76
+ return [];
77
+ }
78
+ const stripped = stripFencedCodeBlocks(text);
79
+ const byIdentifier = new Map();
80
+ for (const match of stripped.matchAll(IDENTIFIER_REGEX)) {
81
+ const id = match[1];
82
+ if (selfIdentifier && id === selfIdentifier) {
83
+ continue;
84
+ }
85
+ const matchIndex = match.index ?? 0;
86
+ const textBefore = stripped.slice(0, matchIndex);
87
+ const { type, reverse } = inferRelation(textBefore);
88
+ const candidate = { identifier: id, type, reverse };
89
+ const existing = byIdentifier.get(id);
90
+ if (!existing || specificity(candidate) > specificity(existing)) {
91
+ byIdentifier.set(id, candidate);
92
+ }
93
+ }
94
+ return [...byIdentifier.values()];
95
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Rewrite `text`, wrapping each occurrence of a known-valid Linear issue identifier
3
+ * as a markdown link. Skips any occurrence inside protected ranges (code blocks,
4
+ * inline code, existing markdown links, angle-bracket autolinks).
5
+ *
6
+ * Only IDs in `validIdentifiers` are wrapped — IDs that don't resolve in the
7
+ * workspace are left as plain text (handles false positives like ISO codes).
8
+ *
9
+ * If `text` already contains `[ID](url)` for a given ID, that occurrence is left
10
+ * alone (it's inside a protected range), so this function is idempotent.
11
+ */
12
+ export declare function wrapIssueReferencesAsLinks(text: string, validIdentifiers: Set<string>, workspaceUrlKey: string): string;
@@ -0,0 +1,91 @@
1
+ // Workspace URL key resolution lives in `./workspace-url.ts` — call
2
+ // `getWorkspaceUrlKey(graphQLService)` to obtain it before invoking
3
+ // `wrapIssueReferencesAsLinks`. Linear URLs look like
4
+ // `https://linear.app/<urlKey>/issue/<identifier>/`.
5
+ const IDENTIFIER_REGEX = /\b([A-Z][A-Z0-9]*-\d+)\b/g;
6
+ const FENCED_CODE_BLOCK_REGEX = /(?:^|\n)([ \t]*)(?:```|~~~)[^\n]*\n[\s\S]*?\n\1?(?:```|~~~)(?=\n|$)/g;
7
+ // Inline backtick spans — `EMW-258`. Markdown allows multiple backticks for spans containing
8
+ // backticks; we keep it simple and match single-backtick spans.
9
+ const INLINE_CODE_REGEX = /`[^`\n]+?`/g;
10
+ // Existing markdown links: [text](url). We protect both the text and the url.
11
+ const MARKDOWN_LINK_REGEX = /\[([^\]]*)\]\(([^)]*)\)/g;
12
+ // Angle-bracket autolinks: <https://...>
13
+ const ANGLE_AUTOLINK_REGEX = /<[^>\s]+>/g;
14
+ // Bare URLs in prose. We protect these so identifiers inside paths
15
+ // (e.g. "https://github.com/foo/DEV-100") don't get split by wrapping.
16
+ const BARE_URL_REGEX = /https?:\/\/\S+/g;
17
+ /**
18
+ * Find ranges of `text` that should NOT have identifiers wrapped:
19
+ * - Fenced code blocks (``` or ~~~)
20
+ * - Inline backtick code spans
21
+ * - Existing markdown links — both `[text]` and `(url)`
22
+ * - Angle-bracket autolinks
23
+ *
24
+ * The returned ranges may overlap; callers only need to test "is this position
25
+ * inside any protected range" so overlap is fine.
26
+ */
27
+ function findProtectedRanges(text) {
28
+ const ranges = [];
29
+ for (const re of [
30
+ FENCED_CODE_BLOCK_REGEX,
31
+ INLINE_CODE_REGEX,
32
+ MARKDOWN_LINK_REGEX,
33
+ ANGLE_AUTOLINK_REGEX,
34
+ BARE_URL_REGEX,
35
+ ]) {
36
+ for (const m of text.matchAll(re)) {
37
+ const start = m.index ?? 0;
38
+ ranges.push({ start, end: start + m[0].length });
39
+ }
40
+ }
41
+ return ranges;
42
+ }
43
+ function isProtected(pos, ranges) {
44
+ for (const r of ranges) {
45
+ if (pos >= r.start && pos < r.end) {
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ function buildIssueUrl(identifier, workspaceUrlKey) {
52
+ return `https://linear.app/${workspaceUrlKey}/issue/${identifier}/`;
53
+ }
54
+ /**
55
+ * Rewrite `text`, wrapping each occurrence of a known-valid Linear issue identifier
56
+ * as a markdown link. Skips any occurrence inside protected ranges (code blocks,
57
+ * inline code, existing markdown links, angle-bracket autolinks).
58
+ *
59
+ * Only IDs in `validIdentifiers` are wrapped — IDs that don't resolve in the
60
+ * workspace are left as plain text (handles false positives like ISO codes).
61
+ *
62
+ * If `text` already contains `[ID](url)` for a given ID, that occurrence is left
63
+ * alone (it's inside a protected range), so this function is idempotent.
64
+ */
65
+ export function wrapIssueReferencesAsLinks(text, validIdentifiers, workspaceUrlKey) {
66
+ if (!text || validIdentifiers.size === 0) {
67
+ return text;
68
+ }
69
+ const ranges = findProtectedRanges(text);
70
+ // Walk through matches and build the result string in pieces.
71
+ // We can't use String.replace with a callback because we need positional protection checks.
72
+ let result = "";
73
+ let cursor = 0;
74
+ for (const m of text.matchAll(IDENTIFIER_REGEX)) {
75
+ const id = m[1];
76
+ const start = m.index ?? 0;
77
+ const end = start + m[0].length;
78
+ if (!validIdentifiers.has(id)) {
79
+ continue;
80
+ }
81
+ if (isProtected(start, ranges)) {
82
+ continue;
83
+ }
84
+ // Append everything up to this match unchanged
85
+ result += text.slice(cursor, start);
86
+ result += `[${id}](${buildIssueUrl(id, workspaceUrlKey)})`;
87
+ cursor = end;
88
+ }
89
+ result += text.slice(cursor);
90
+ return result;
91
+ }
@@ -0,0 +1,26 @@
1
+ import type { LinearComment, LinearCycleDetail, LinearCycleSummary, LinearLabel, LinearProject, LinearTeam, LinearUser } from "../types/linear.js";
2
+ import { type AuthOptions } from "./auth.js";
3
+ export declare class LinearService {
4
+ private readonly client;
5
+ constructor(apiToken: string);
6
+ resolveIssueId(issueId: string): Promise<string>;
7
+ getTeams(limit?: number): Promise<LinearTeam[]>;
8
+ resolveUserId(nameOrEmailOrId: string): Promise<string>;
9
+ getUsers(activeOnly?: boolean, limit?: number): Promise<LinearUser[]>;
10
+ getProjects(limit?: number): Promise<LinearProject[]>;
11
+ resolveTeamId(teamKeyOrNameOrId: string): Promise<string>;
12
+ resolveStatusId(statusName: string, teamId?: string): Promise<string>;
13
+ private buildLabelData;
14
+ getLabels(teamFilter?: string, limit?: number): Promise<{
15
+ labels: LinearLabel[];
16
+ }>;
17
+ createComment(args: {
18
+ issueId: string;
19
+ body: string;
20
+ }): Promise<LinearComment>;
21
+ getCycles(teamFilter?: string, activeOnly?: boolean, limit?: number): Promise<LinearCycleSummary[]>;
22
+ getCycleById(cycleId: string, issuesLimit?: number): Promise<LinearCycleDetail>;
23
+ resolveCycleId(cycleNameOrId: string, teamFilter?: string): Promise<string>;
24
+ resolveProjectId(projectNameOrId: string): Promise<string>;
25
+ }
26
+ export declare function createLinearService(options: AuthOptions): LinearService;