@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,88 @@
1
+ import { mkdir } from "node:fs/promises";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { FileService } from "./file-service.js";
5
+ const DOWNLOAD_DIR = join(tmpdir(), "el-linear-downloads");
6
+ const UPLOAD_URL_REGEX = /https:\/\/uploads\.linear\.app\/[^\s)>\]"]+/g;
7
+ function collectUploadUrls(text) {
8
+ const matches = text.match(UPLOAD_URL_REGEX);
9
+ return matches ? [...new Set(matches)] : [];
10
+ }
11
+ function replaceUrls(text, urlMap) {
12
+ let result = text;
13
+ for (const [remote, local] of urlMap) {
14
+ result = result.replaceAll(remote, local);
15
+ }
16
+ return result;
17
+ }
18
+ /**
19
+ * Downloads all uploads.linear.app URLs found in an issue's description
20
+ * and comments, replacing them with local file paths so Claude Code
21
+ * can read the images directly.
22
+ */
23
+ export async function downloadLinearUploads(issue, apiToken) {
24
+ const allUrls = new Set();
25
+ if (issue.description) {
26
+ for (const url of collectUploadUrls(issue.description)) {
27
+ allUrls.add(url);
28
+ }
29
+ }
30
+ if (issue.comments) {
31
+ for (const comment of issue.comments) {
32
+ for (const url of collectUploadUrls(comment.body)) {
33
+ allUrls.add(url);
34
+ }
35
+ }
36
+ }
37
+ if (allUrls.size === 0) {
38
+ return issue;
39
+ }
40
+ await mkdir(DOWNLOAD_DIR, { recursive: true });
41
+ const fileService = new FileService(apiToken);
42
+ const urlMap = new Map();
43
+ await Promise.all([...allUrls].map(async (url) => {
44
+ const filename = extractUniqueFilename(url);
45
+ const localPath = join(DOWNLOAD_DIR, filename);
46
+ const result = await fileService.downloadFile(url, {
47
+ output: localPath,
48
+ overwrite: true,
49
+ });
50
+ if (result.success) {
51
+ urlMap.set(url, result.filePath);
52
+ }
53
+ }));
54
+ if (urlMap.size === 0) {
55
+ return issue;
56
+ }
57
+ const updated = { ...issue };
58
+ if (updated.description) {
59
+ updated.description = replaceUrls(updated.description, urlMap);
60
+ }
61
+ if (updated.comments) {
62
+ updated.comments = updated.comments.map((comment) => ({
63
+ ...comment,
64
+ body: replaceUrls(comment.body, urlMap),
65
+ }));
66
+ }
67
+ if (updated.embeds) {
68
+ updated.embeds = updated.embeds.map((embed) => {
69
+ const local = urlMap.get(embed.url);
70
+ return local ? { ...embed, url: local } : embed;
71
+ });
72
+ }
73
+ return updated;
74
+ }
75
+ function extractUniqueFilename(url) {
76
+ try {
77
+ const urlObj = new URL(url);
78
+ // Path like /workspace-id/uuid1/uuid2/filename.png — take last two parts for uniqueness
79
+ const parts = urlObj.pathname.split("/").filter(Boolean);
80
+ if (parts.length >= 2) {
81
+ return `${parts.at(-2).slice(0, 8)}-${parts.at(-1)}`;
82
+ }
83
+ return parts.at(-1) || "download";
84
+ }
85
+ catch {
86
+ return `download-${Date.now()}`;
87
+ }
88
+ }
@@ -0,0 +1,8 @@
1
+ export interface Embed {
2
+ expiresAt: string;
3
+ label: string;
4
+ url: string;
5
+ }
6
+ export declare function extractEmbeds(content: string | null | undefined): Embed[];
7
+ export declare function isLinearUploadUrl(url: string): boolean;
8
+ export declare function extractFilenameFromUrl(url: string): string;
@@ -0,0 +1,54 @@
1
+ function stripCodeContexts(content) {
2
+ let cleaned = content.replace(/\\`/g, "");
3
+ cleaned = cleaned.replace(/```[\s\S]*?```/g, "");
4
+ cleaned = cleaned.replace(/`[^`]+`/g, "");
5
+ return cleaned;
6
+ }
7
+ export function extractEmbeds(content) {
8
+ if (!content) {
9
+ return [];
10
+ }
11
+ const cleanedContent = stripCodeContexts(content);
12
+ const embeds = [];
13
+ const imageRegex = /!\[([^\]]*)\]\(([^)]+)\)/g;
14
+ const linkRegex = /(?<!!)\[([^\]]+)\]\(([^)]+)\)/g;
15
+ const expiresAt = new Date(Date.now() + 3600 * 1000).toISOString();
16
+ for (const match of cleanedContent.matchAll(imageRegex)) {
17
+ const label = match[1] || "file";
18
+ const url = match[2];
19
+ if (isLinearUploadUrl(url)) {
20
+ embeds.push({ label, url, expiresAt });
21
+ }
22
+ }
23
+ for (const match of cleanedContent.matchAll(linkRegex)) {
24
+ const label = match[1] || "file";
25
+ const url = match[2];
26
+ if (isLinearUploadUrl(url)) {
27
+ embeds.push({ label, url, expiresAt });
28
+ }
29
+ }
30
+ return embeds;
31
+ }
32
+ export function isLinearUploadUrl(url) {
33
+ if (!url) {
34
+ return false;
35
+ }
36
+ try {
37
+ const urlObj = new URL(url);
38
+ return urlObj.hostname === "uploads.linear.app";
39
+ }
40
+ catch {
41
+ return false;
42
+ }
43
+ }
44
+ export function extractFilenameFromUrl(url) {
45
+ try {
46
+ const urlObj = new URL(url);
47
+ const pathname = urlObj.pathname;
48
+ const parts = pathname.split("/");
49
+ return parts.at(-1) || "download";
50
+ }
51
+ catch {
52
+ return "download";
53
+ }
54
+ }
@@ -0,0 +1,4 @@
1
+ export declare function notFoundError(entityType: string, identifier: string, context?: string, hint?: string): Error;
2
+ export declare function multipleMatchesError(entityType: string, identifier: string, matches: string[], disambiguation: string): Error;
3
+ export declare function invalidParameterError(parameter: string, reason: string): Error;
4
+ export declare function requiresParameterError(flag: string, requiredFlag: string): Error;
@@ -0,0 +1,17 @@
1
+ export function notFoundError(entityType, identifier, context, hint) {
2
+ const contextStr = context ? ` ${context}` : "";
3
+ const hintStr = hint ? ` ${hint}` : "";
4
+ return new Error(`${entityType} "${identifier}"${contextStr} not found.${hintStr}`);
5
+ }
6
+ export function multipleMatchesError(entityType, identifier, matches, disambiguation) {
7
+ const matchList = matches.join(", ");
8
+ return new Error(`Multiple ${entityType}s found matching "${identifier}". ` +
9
+ `Candidates: ${matchList}. ` +
10
+ `Please ${disambiguation}.`);
11
+ }
12
+ export function invalidParameterError(parameter, reason) {
13
+ return new Error(`Invalid ${parameter}: ${reason}`);
14
+ }
15
+ export function requiresParameterError(flag, requiredFlag) {
16
+ return new Error(`${flag} requires ${requiredFlag} to be specified`);
17
+ }
@@ -0,0 +1,13 @@
1
+ import type { FileDownloadResult, FileUploadResult } from "../types/linear.js";
2
+ export declare class FileService {
3
+ private readonly apiToken;
4
+ constructor(apiToken: string);
5
+ downloadFile(url: string, options?: {
6
+ output?: string;
7
+ overwrite?: boolean;
8
+ }): Promise<FileDownloadResult>;
9
+ uploadFile(filePath: string): Promise<FileUploadResult>;
10
+ private validateFileForUpload;
11
+ private requestUploadUrl;
12
+ private putFileToUrl;
13
+ }
@@ -0,0 +1,239 @@
1
+ import { access, mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
+ import { basename, dirname, extname } from "node:path";
3
+ import { extractFilenameFromUrl, isLinearUploadUrl } from "./embed-parser.js";
4
+ const MAX_FILE_SIZE = 20 * 1024 * 1024;
5
+ const MIME_TYPES = {
6
+ ".png": "image/png",
7
+ ".jpg": "image/jpeg",
8
+ ".jpeg": "image/jpeg",
9
+ ".gif": "image/gif",
10
+ ".webp": "image/webp",
11
+ ".svg": "image/svg+xml",
12
+ ".ico": "image/x-icon",
13
+ ".pdf": "application/pdf",
14
+ ".doc": "application/msword",
15
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
16
+ ".xls": "application/vnd.ms-excel",
17
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
18
+ ".ppt": "application/vnd.ms-powerpoint",
19
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
20
+ ".txt": "text/plain",
21
+ ".csv": "text/csv",
22
+ ".json": "application/json",
23
+ ".xml": "application/xml",
24
+ ".html": "text/html",
25
+ ".css": "text/css",
26
+ ".js": "application/javascript",
27
+ ".ts": "application/typescript",
28
+ ".md": "text/markdown",
29
+ ".zip": "application/zip",
30
+ ".tar": "application/x-tar",
31
+ ".gz": "application/gzip",
32
+ ".mp4": "video/mp4",
33
+ ".mp3": "audio/mpeg",
34
+ ".wav": "audio/wav",
35
+ };
36
+ function getMimeType(filePath) {
37
+ const ext = extname(filePath).toLowerCase();
38
+ return MIME_TYPES[ext] || "application/octet-stream";
39
+ }
40
+ export class FileService {
41
+ apiToken;
42
+ constructor(apiToken) {
43
+ this.apiToken = apiToken;
44
+ }
45
+ async downloadFile(url, options = {}) {
46
+ if (!isLinearUploadUrl(url)) {
47
+ return {
48
+ success: false,
49
+ error: "URL must be from uploads.linear.app domain",
50
+ };
51
+ }
52
+ const outputPath = options.output || extractFilenameFromUrl(url);
53
+ if (!options.overwrite) {
54
+ try {
55
+ await access(outputPath);
56
+ return {
57
+ success: false,
58
+ error: `File already exists: ${outputPath}. Use --overwrite to replace.`,
59
+ };
60
+ }
61
+ catch {
62
+ // File doesn't exist, good
63
+ }
64
+ }
65
+ try {
66
+ const urlObj = new URL(url);
67
+ const isSignedUrl = urlObj.searchParams.has("signature");
68
+ const headers = {};
69
+ if (!isSignedUrl) {
70
+ headers.Authorization = this.apiToken;
71
+ }
72
+ const response = await fetch(url, { method: "GET", headers });
73
+ if (!response.ok) {
74
+ return {
75
+ success: false,
76
+ error: `HTTP ${response.status}: ${response.statusText}`,
77
+ statusCode: response.status,
78
+ };
79
+ }
80
+ const arrayBuffer = await response.arrayBuffer();
81
+ const buffer = Buffer.from(arrayBuffer);
82
+ const outputDir = dirname(outputPath);
83
+ if (outputDir !== ".") {
84
+ await mkdir(outputDir, { recursive: true });
85
+ }
86
+ await writeFile(outputPath, buffer);
87
+ return { success: true, filePath: outputPath };
88
+ }
89
+ catch (error) {
90
+ return {
91
+ success: false,
92
+ error: error instanceof Error ? error.message : String(error),
93
+ };
94
+ }
95
+ }
96
+ async uploadFile(filePath) {
97
+ const filename = basename(filePath);
98
+ const validation = await this.validateFileForUpload(filePath);
99
+ if (!validation.ok) {
100
+ return validation.error;
101
+ }
102
+ const contentType = getMimeType(filePath);
103
+ try {
104
+ const uploadMeta = await this.requestUploadUrl(contentType, filename, validation.fileSize);
105
+ if (!uploadMeta.ok) {
106
+ return uploadMeta.error;
107
+ }
108
+ return await this.putFileToUrl(filePath, contentType, filename, uploadMeta);
109
+ }
110
+ catch (error) {
111
+ return {
112
+ success: false,
113
+ error: error instanceof Error ? error.message : String(error),
114
+ };
115
+ }
116
+ }
117
+ async validateFileForUpload(filePath) {
118
+ try {
119
+ await access(filePath);
120
+ }
121
+ catch {
122
+ return {
123
+ ok: false,
124
+ error: { success: false, error: `File not found: ${filePath}` },
125
+ };
126
+ }
127
+ try {
128
+ const fileStat = await stat(filePath);
129
+ if (fileStat.size > MAX_FILE_SIZE) {
130
+ const maxMB = MAX_FILE_SIZE / (1024 * 1024);
131
+ const actualMB = fileStat.size / (1024 * 1024);
132
+ return {
133
+ ok: false,
134
+ error: {
135
+ success: false,
136
+ error: `File too large: ${actualMB.toFixed(1)}MB exceeds limit of ${maxMB}MB`,
137
+ },
138
+ };
139
+ }
140
+ return { ok: true, fileSize: fileStat.size };
141
+ }
142
+ catch (error) {
143
+ return {
144
+ ok: false,
145
+ error: {
146
+ success: false,
147
+ error: `Cannot read file: ${error instanceof Error ? error.message : String(error)}`,
148
+ },
149
+ };
150
+ }
151
+ }
152
+ async requestUploadUrl(contentType, filename, fileSize) {
153
+ const query = `
154
+ mutation FileUpload($contentType: String!, $filename: String!, $size: Int!) {
155
+ fileUpload(contentType: $contentType, filename: $filename, size: $size) {
156
+ success
157
+ uploadFile { uploadUrl assetUrl headers { key value } }
158
+ }
159
+ }
160
+ `;
161
+ const response = await fetch("https://api.linear.app/graphql", {
162
+ method: "POST",
163
+ headers: {
164
+ "Content-Type": "application/json",
165
+ Authorization: this.apiToken,
166
+ },
167
+ body: JSON.stringify({
168
+ query,
169
+ variables: { contentType, filename, size: fileSize },
170
+ }),
171
+ });
172
+ if (!response.ok) {
173
+ return {
174
+ ok: false,
175
+ error: {
176
+ success: false,
177
+ error: `GraphQL request failed: HTTP ${response.status}`,
178
+ statusCode: response.status,
179
+ },
180
+ };
181
+ }
182
+ const data = (await response.json());
183
+ if (data.errors) {
184
+ return {
185
+ ok: false,
186
+ error: {
187
+ success: false,
188
+ error: `Failed to request upload URL: ${data.errors[0]?.message || "GraphQL error"}`,
189
+ },
190
+ };
191
+ }
192
+ const fileUpload = data.data?.fileUpload;
193
+ if (!fileUpload?.success) {
194
+ return {
195
+ ok: false,
196
+ error: {
197
+ success: false,
198
+ error: "Failed to request upload URL: success=false",
199
+ },
200
+ };
201
+ }
202
+ const uploadFile = fileUpload.uploadFile;
203
+ if (!(uploadFile?.uploadUrl && uploadFile?.assetUrl)) {
204
+ return {
205
+ ok: false,
206
+ error: {
207
+ success: false,
208
+ error: "Missing uploadUrl or assetUrl in response",
209
+ },
210
+ };
211
+ }
212
+ return {
213
+ ok: true,
214
+ uploadUrl: uploadFile.uploadUrl,
215
+ assetUrl: uploadFile.assetUrl,
216
+ headers: uploadFile.headers || [],
217
+ };
218
+ }
219
+ async putFileToUrl(filePath, contentType, filename, meta) {
220
+ const fileBuffer = await readFile(filePath);
221
+ const putHeaders = { "Content-Type": contentType };
222
+ for (const header of meta.headers) {
223
+ putHeaders[header.key] = header.value;
224
+ }
225
+ const putResponse = await fetch(meta.uploadUrl, {
226
+ method: "PUT",
227
+ headers: putHeaders,
228
+ body: new Uint8Array(fileBuffer),
229
+ });
230
+ if (!putResponse.ok) {
231
+ return {
232
+ success: false,
233
+ error: `File upload failed: HTTP ${putResponse.status}`,
234
+ statusCode: putResponse.status,
235
+ };
236
+ }
237
+ return { success: true, assetUrl: meta.assetUrl, filename };
238
+ }
239
+ }
@@ -0,0 +1,45 @@
1
+ interface GDocElement {
2
+ inlineObjectElement?: {
3
+ inlineObjectId: string;
4
+ };
5
+ textRun?: {
6
+ content: string;
7
+ textStyle?: {
8
+ bold?: boolean;
9
+ italic?: boolean;
10
+ link?: {
11
+ url?: string;
12
+ };
13
+ };
14
+ };
15
+ }
16
+ interface GDocParagraph {
17
+ bullet?: {
18
+ nestingLevel?: number;
19
+ listId?: string;
20
+ };
21
+ elements: GDocElement[];
22
+ paragraphStyle?: {
23
+ namedStyleType?: string;
24
+ };
25
+ }
26
+ interface GDocContent {
27
+ paragraph?: GDocParagraph;
28
+ table?: {
29
+ tableRows: Array<{
30
+ tableCells: Array<{
31
+ content: GDocContent[];
32
+ }>;
33
+ }>;
34
+ };
35
+ }
36
+ interface GDocBody {
37
+ content: GDocContent[];
38
+ }
39
+ interface GDocResponse {
40
+ body: GDocBody;
41
+ title?: string;
42
+ }
43
+ export declare function parseGoogleDoc(doc: GDocResponse): string;
44
+ export declare function extractDocId(input: string): string;
45
+ export {};
@@ -0,0 +1,107 @@
1
+ const TRAILING_NEWLINE_RE = /\n$/;
2
+ const GDOC_URL_RE = /\/document\/d\/([a-zA-Z0-9_-]+)/;
3
+ function formatTextRun(element) {
4
+ const run = element.textRun;
5
+ if (!run?.content) {
6
+ return "";
7
+ }
8
+ let text = run.content;
9
+ const style = run.textStyle;
10
+ if (!style) {
11
+ return text;
12
+ }
13
+ // Don't wrap newlines in formatting
14
+ if (text.trim() === "") {
15
+ return text;
16
+ }
17
+ const trimmedEnd = text.endsWith("\n");
18
+ if (trimmedEnd) {
19
+ text = text.slice(0, -1);
20
+ }
21
+ if (style.link?.url) {
22
+ text = `[${text}](${style.link.url})`;
23
+ }
24
+ else if (style.bold && style.italic) {
25
+ text = `***${text}***`;
26
+ }
27
+ else if (style.bold) {
28
+ text = `**${text}**`;
29
+ }
30
+ else if (style.italic) {
31
+ text = `*${text}*`;
32
+ }
33
+ if (trimmedEnd) {
34
+ text += "\n";
35
+ }
36
+ return text;
37
+ }
38
+ function paragraphToMarkdown(para) {
39
+ const style = para.paragraphStyle?.namedStyleType || "NORMAL_TEXT";
40
+ let line = "";
41
+ for (const element of para.elements) {
42
+ line += formatTextRun(element);
43
+ }
44
+ // Handle bullet lists
45
+ if (para.bullet) {
46
+ const indent = " ".repeat(para.bullet.nestingLevel || 0);
47
+ const content = line.replace(TRAILING_NEWLINE_RE, "");
48
+ return `${indent}- ${content}\n`;
49
+ }
50
+ // Handle headings
51
+ switch (style) {
52
+ case "HEADING_1":
53
+ return `# ${line}`;
54
+ case "HEADING_2":
55
+ return `## ${line}`;
56
+ case "HEADING_3":
57
+ return `### ${line}`;
58
+ case "HEADING_4":
59
+ return `#### ${line}`;
60
+ case "HEADING_5":
61
+ return `##### ${line}`;
62
+ case "HEADING_6":
63
+ return `###### ${line}`;
64
+ default:
65
+ return line;
66
+ }
67
+ }
68
+ export function parseGoogleDoc(doc) {
69
+ const body = doc.body?.content;
70
+ if (!body) {
71
+ return "";
72
+ }
73
+ const parts = [];
74
+ for (const block of body) {
75
+ if (block.paragraph) {
76
+ parts.push(paragraphToMarkdown(block.paragraph));
77
+ }
78
+ if (block.table) {
79
+ for (const row of block.table.tableRows) {
80
+ const cells = row.tableCells.map((cell) => {
81
+ const cellText = cell.content
82
+ .filter((c) => c.paragraph)
83
+ .map((c) => {
84
+ let text = "";
85
+ for (const el of c.paragraph.elements) {
86
+ text += formatTextRun(el);
87
+ }
88
+ return text.replace(TRAILING_NEWLINE_RE, "");
89
+ })
90
+ .join(" ");
91
+ return cellText;
92
+ });
93
+ parts.push(`| ${cells.join(" | ")} |\n`);
94
+ }
95
+ }
96
+ }
97
+ return parts.join("");
98
+ }
99
+ export function extractDocId(input) {
100
+ // Full URL: https://docs.google.com/document/d/DOC_ID/...
101
+ const urlMatch = input.match(GDOC_URL_RE);
102
+ if (urlMatch) {
103
+ return urlMatch[1];
104
+ }
105
+ // Bare ID
106
+ return input;
107
+ }
@@ -0,0 +1,12 @@
1
+ import type { LinearAttachment } from "../types/linear.js";
2
+ import type { AuthOptions } from "./auth.js";
3
+ import { type GraphQLService } from "./graphql-service.js";
4
+ declare class GraphQLAttachmentsService {
5
+ private readonly graphqlService;
6
+ constructor(graphqlService: GraphQLService);
7
+ createAttachment(input: Record<string, unknown>): Promise<LinearAttachment>;
8
+ deleteAttachment(id: string): Promise<boolean>;
9
+ listAttachments(issueId: string): Promise<LinearAttachment[]>;
10
+ }
11
+ export declare function createGraphQLAttachmentsService(options: AuthOptions): GraphQLAttachmentsService;
12
+ export {};
@@ -0,0 +1,46 @@
1
+ import { CREATE_ATTACHMENT_MUTATION, DELETE_ATTACHMENT_MUTATION, LIST_ATTACHMENTS_QUERY, } from "../queries/attachments.js";
2
+ import { createGraphQLService, } from "./graphql-service.js";
3
+ function transformAttachment(att) {
4
+ return {
5
+ id: att.id,
6
+ url: att.url,
7
+ title: att.title || undefined,
8
+ createdAt: att.createdAt || undefined,
9
+ updatedAt: att.updatedAt || undefined,
10
+ };
11
+ }
12
+ class GraphQLAttachmentsService {
13
+ graphqlService;
14
+ constructor(graphqlService) {
15
+ this.graphqlService = graphqlService;
16
+ }
17
+ async createAttachment(input) {
18
+ const result = await this.graphqlService.rawRequest(CREATE_ATTACHMENT_MUTATION, { input });
19
+ const attachmentCreate = result.attachmentCreate;
20
+ if (!attachmentCreate.success) {
21
+ throw new Error(`Failed to create attachment on issue ${input.issueId} for URL "${input.url}"`);
22
+ }
23
+ return transformAttachment(attachmentCreate.attachment);
24
+ }
25
+ async deleteAttachment(id) {
26
+ const result = await this.graphqlService.rawRequest(DELETE_ATTACHMENT_MUTATION, { id });
27
+ const attachmentDelete = result.attachmentDelete;
28
+ if (!attachmentDelete.success) {
29
+ throw new Error(`Failed to delete attachment: ${id}`);
30
+ }
31
+ return true;
32
+ }
33
+ async listAttachments(issueId) {
34
+ const result = await this.graphqlService.rawRequest(LIST_ATTACHMENTS_QUERY, { issueId });
35
+ if (!result.issue) {
36
+ throw new Error(`Issue not found: ${issueId}`);
37
+ }
38
+ const issue = result.issue;
39
+ const attachments = issue.attachments;
40
+ return attachments.nodes.map(transformAttachment);
41
+ }
42
+ }
43
+ export function createGraphQLAttachmentsService(options) {
44
+ const graphqlService = createGraphQLService(options);
45
+ return new GraphQLAttachmentsService(graphqlService);
46
+ }
@@ -0,0 +1,18 @@
1
+ import type { LinearDocument } from "../types/linear.js";
2
+ import type { AuthOptions } from "./auth.js";
3
+ import { type GraphQLService } from "./graphql-service.js";
4
+ declare class GraphQLDocumentsService {
5
+ private readonly graphqlService;
6
+ constructor(graphqlService: GraphQLService);
7
+ createDocument(input: Record<string, unknown>): Promise<LinearDocument>;
8
+ updateDocument(id: string, input: Record<string, unknown>): Promise<LinearDocument>;
9
+ getDocument(id: string): Promise<LinearDocument>;
10
+ listDocuments(options?: {
11
+ projectId?: string;
12
+ first?: number;
13
+ }): Promise<LinearDocument[]>;
14
+ deleteDocument(id: string): Promise<boolean>;
15
+ listDocumentsBySlugIds(slugIds: string[], limit?: number): Promise<LinearDocument[]>;
16
+ }
17
+ export declare function createGraphQLDocumentsService(options: AuthOptions): GraphQLDocumentsService;
18
+ export {};