@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,325 @@
1
+ /**
2
+ * Lightweight markdown → ProseMirror document converter.
3
+ *
4
+ * Handles the subset of markdown commonly used in Linear comments:
5
+ * headings, bullet/ordered lists, code blocks, blockquotes, horizontal rules,
6
+ * and inline marks (bold, italic, code, links).
7
+ */
8
+ // ── Block parsing ────────────────────────────────────────────────────
9
+ const HEADING_RE = /^(#{1,6})\s+(.+)$/;
10
+ const BULLET_RE = /^[\t ]*[-*]\s+(.+)$/;
11
+ const ORDERED_RE = /^[\t ]*\d+[.)]\s+(.+)$/;
12
+ const BLOCKQUOTE_RE = /^>\s?(.*)$/;
13
+ const HR_RE = /^([-*_])\1{2,}\s*$/;
14
+ const TABLE_ROW_RE = /^\|(.+)\|$/;
15
+ const TABLE_SEP_RE = /^\|[\s:]*-{2,}[\s:]*(\|[\s:]*-{2,}[\s:]*)*\|$/;
16
+ const FENCE_OPEN_RE = /^```(\w*)$/;
17
+ const FENCE_CLOSE_RE = /^```\s*$/;
18
+ // Inline patterns (top-level for performance)
19
+ const INLINE_CODE_RE = /`([^`]+)`/;
20
+ const INLINE_LINK_RE = /\[([^\]]+)\]\(([^)]+)\)/;
21
+ const INLINE_BOLD_RE = /\*\*(.+?)\*\*|__(.+?)__/;
22
+ const INLINE_ITALIC_RE = /(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)|(?<!_)_(?!_)(.+?)(?<!_)_(?!_)/;
23
+ export function markdownToProseMirror(text) {
24
+ const state = { lines: text.split("\n"), content: [], i: 0 };
25
+ while (state.i < state.lines.length) {
26
+ const line = state.lines[state.i];
27
+ if (line.trim() === "") {
28
+ state.i++;
29
+ continue;
30
+ }
31
+ if (parseFencedCodeBlock(state, line)) {
32
+ continue;
33
+ }
34
+ if (parseHorizontalRule(state, line)) {
35
+ continue;
36
+ }
37
+ if (parseHeading(state, line)) {
38
+ continue;
39
+ }
40
+ if (parseBulletList(state, line)) {
41
+ continue;
42
+ }
43
+ if (parseOrderedList(state, line)) {
44
+ continue;
45
+ }
46
+ if (parseBlockquote(state)) {
47
+ continue;
48
+ }
49
+ if (parseTable(state, line)) {
50
+ continue;
51
+ }
52
+ parseParagraph(state);
53
+ }
54
+ return { type: "doc", content: state.content };
55
+ }
56
+ function parseFencedCodeBlock(state, line) {
57
+ const fenceMatch = line.match(FENCE_OPEN_RE);
58
+ if (!fenceMatch) {
59
+ return false;
60
+ }
61
+ const language = fenceMatch[1] || undefined;
62
+ const codeLines = [];
63
+ state.i++;
64
+ while (state.i < state.lines.length &&
65
+ !FENCE_CLOSE_RE.test(state.lines[state.i])) {
66
+ codeLines.push(state.lines[state.i]);
67
+ state.i++;
68
+ }
69
+ state.i++; // skip closing fence
70
+ const attrs = {};
71
+ if (language) {
72
+ attrs.language = language;
73
+ }
74
+ state.content.push({
75
+ type: "codeBlock",
76
+ ...(Object.keys(attrs).length > 0 ? { attrs } : {}),
77
+ content: codeLines.length > 0
78
+ ? [{ type: "text", text: codeLines.join("\n") }]
79
+ : undefined,
80
+ });
81
+ return true;
82
+ }
83
+ function parseHorizontalRule(state, line) {
84
+ if (!HR_RE.test(line)) {
85
+ return false;
86
+ }
87
+ state.content.push({ type: "horizontalRule" });
88
+ state.i++;
89
+ return true;
90
+ }
91
+ function parseHeading(state, line) {
92
+ const headingMatch = line.match(HEADING_RE);
93
+ if (!headingMatch) {
94
+ return false;
95
+ }
96
+ state.content.push({
97
+ type: "heading",
98
+ attrs: { level: headingMatch[1].length },
99
+ content: parseInline(headingMatch[2]),
100
+ });
101
+ state.i++;
102
+ return true;
103
+ }
104
+ function parseBulletList(state, line) {
105
+ if (!BULLET_RE.test(line)) {
106
+ return false;
107
+ }
108
+ const items = [];
109
+ while (state.i < state.lines.length && BULLET_RE.test(state.lines[state.i])) {
110
+ const m = state.lines[state.i].match(BULLET_RE);
111
+ items.push({
112
+ type: "listItem",
113
+ content: [{ type: "paragraph", content: parseInline(m[1]) }],
114
+ });
115
+ state.i++;
116
+ }
117
+ state.content.push({ type: "bulletList", content: items });
118
+ return true;
119
+ }
120
+ function parseOrderedList(state, line) {
121
+ if (!ORDERED_RE.test(line)) {
122
+ return false;
123
+ }
124
+ const items = [];
125
+ while (state.i < state.lines.length &&
126
+ ORDERED_RE.test(state.lines[state.i])) {
127
+ const m = state.lines[state.i].match(ORDERED_RE);
128
+ items.push({
129
+ type: "listItem",
130
+ content: [{ type: "paragraph", content: parseInline(m[1]) }],
131
+ });
132
+ state.i++;
133
+ }
134
+ state.content.push({ type: "orderedList", content: items });
135
+ return true;
136
+ }
137
+ function parseBlockquote(state) {
138
+ if (!BLOCKQUOTE_RE.test(state.lines[state.i])) {
139
+ return false;
140
+ }
141
+ const quoteLines = [];
142
+ while (state.i < state.lines.length &&
143
+ BLOCKQUOTE_RE.test(state.lines[state.i])) {
144
+ const m = state.lines[state.i].match(BLOCKQUOTE_RE);
145
+ quoteLines.push(m[1]);
146
+ state.i++;
147
+ }
148
+ const inner = markdownToProseMirror(quoteLines.join("\n"));
149
+ state.content.push({
150
+ type: "blockquote",
151
+ content: inner.content && inner.content.length > 0
152
+ ? inner.content
153
+ : [{ type: "paragraph", content: [] }],
154
+ });
155
+ return true;
156
+ }
157
+ // Split a table row's inner content on unescaped `|`, preserving `\|` as a literal pipe.
158
+ // Caller strips leading/trailing pipes before calling; each returned cell is trimmed.
159
+ function splitTableRow(inner) {
160
+ const cells = [];
161
+ let current = "";
162
+ for (let i = 0; i < inner.length; i++) {
163
+ if (inner[i] === "\\" && inner[i + 1] === "|") {
164
+ current += "|";
165
+ i++;
166
+ }
167
+ else if (inner[i] === "|") {
168
+ cells.push(current.trim());
169
+ current = "";
170
+ }
171
+ else {
172
+ current += inner[i];
173
+ }
174
+ }
175
+ cells.push(current.trim());
176
+ return cells;
177
+ }
178
+ // Build ProseMirror inline content for a cell, handling empty strings gracefully.
179
+ function cellContent(cell) {
180
+ const inline = parseInline(cell);
181
+ return inline.length > 0 ? inline : [];
182
+ }
183
+ function parseTable(state, line) {
184
+ const rowMatch = line.match(TABLE_ROW_RE);
185
+ if (!rowMatch) {
186
+ return false;
187
+ }
188
+ // Peek ahead: a table needs at least a header row + separator row
189
+ if (state.i + 1 >= state.lines.length ||
190
+ !TABLE_SEP_RE.test(state.lines[state.i + 1].trim())) {
191
+ return false;
192
+ }
193
+ const rows = [];
194
+ const headerCells = splitTableRow(rowMatch[1]);
195
+ const cols = headerCells.length;
196
+ rows.push({
197
+ type: "tableRow",
198
+ content: headerCells.map((cell) => ({
199
+ type: "tableHeader",
200
+ content: [{ type: "paragraph", content: cellContent(cell) }],
201
+ })),
202
+ });
203
+ state.i++; // header row
204
+ state.i++; // separator row
205
+ // Parse body rows — pad/truncate to match header column count so Linear gets a
206
+ // rectangular table (ProseMirror tolerates uneven rows but Linear's validator may not).
207
+ while (state.i < state.lines.length) {
208
+ const bodyMatch = state.lines[state.i].trim().match(TABLE_ROW_RE);
209
+ if (!bodyMatch) {
210
+ break;
211
+ }
212
+ const cells = splitTableRow(bodyMatch[1]);
213
+ while (cells.length < cols) {
214
+ cells.push("");
215
+ }
216
+ cells.length = cols;
217
+ rows.push({
218
+ type: "tableRow",
219
+ content: cells.map((cell) => ({
220
+ type: "tableCell",
221
+ content: [{ type: "paragraph", content: cellContent(cell) }],
222
+ })),
223
+ });
224
+ state.i++;
225
+ }
226
+ state.content.push({ type: "table", content: rows });
227
+ return true;
228
+ }
229
+ function isBlockStart(line) {
230
+ return (HEADING_RE.test(line) ||
231
+ FENCE_OPEN_RE.test(line) ||
232
+ HR_RE.test(line) ||
233
+ BULLET_RE.test(line) ||
234
+ ORDERED_RE.test(line) ||
235
+ BLOCKQUOTE_RE.test(line) ||
236
+ TABLE_ROW_RE.test(line));
237
+ }
238
+ function parseParagraph(state) {
239
+ // Always consume the current line — the main loop has already determined no
240
+ // block parser handled it. Skipping the first-line block-start check avoids
241
+ // an infinite loop when a line looks like a block (e.g. table row) but fails
242
+ // the stricter block parser (e.g. table row with no separator line).
243
+ const paraLines = [state.lines[state.i]];
244
+ state.i++;
245
+ while (state.i < state.lines.length &&
246
+ state.lines[state.i].trim() !== "" &&
247
+ !isBlockStart(state.lines[state.i])) {
248
+ paraLines.push(state.lines[state.i]);
249
+ state.i++;
250
+ }
251
+ const inlineNodes = parseInline(paraLines.join("\n"));
252
+ if (inlineNodes.length > 0) {
253
+ state.content.push({ type: "paragraph", content: inlineNodes });
254
+ }
255
+ }
256
+ // ── Inline parsing ───────────────────────────────────────────────────
257
+ /**
258
+ * Parse inline markdown (bold, italic, code, links) into ProseMirror nodes with marks.
259
+ */
260
+ export function parseInline(text) {
261
+ const nodes = [];
262
+ let remaining = text;
263
+ while (remaining.length > 0) {
264
+ const candidate = findEarliestInlineMatch(remaining);
265
+ if (!candidate) {
266
+ nodes.push({ type: "text", text: remaining });
267
+ break;
268
+ }
269
+ if (candidate.index > 0) {
270
+ nodes.push({ type: "text", text: remaining.slice(0, candidate.index) });
271
+ }
272
+ nodes.push({
273
+ type: "text",
274
+ text: candidate.innerText,
275
+ marks: candidate.marks,
276
+ });
277
+ remaining = remaining.slice(candidate.index + candidate.length);
278
+ }
279
+ return nodes;
280
+ }
281
+ function findEarliestInlineMatch(text) {
282
+ const candidates = [];
283
+ const codeMatch = text.match(INLINE_CODE_RE);
284
+ if (codeMatch) {
285
+ candidates.push({
286
+ index: codeMatch.index,
287
+ length: codeMatch[0].length,
288
+ innerText: codeMatch[1],
289
+ marks: [{ type: "code" }],
290
+ });
291
+ }
292
+ const linkMatch = text.match(INLINE_LINK_RE);
293
+ if (linkMatch) {
294
+ candidates.push({
295
+ index: linkMatch.index,
296
+ length: linkMatch[0].length,
297
+ innerText: linkMatch[1],
298
+ marks: [{ type: "link", attrs: { href: linkMatch[2] } }],
299
+ });
300
+ }
301
+ const boldMatch = text.match(INLINE_BOLD_RE);
302
+ if (boldMatch) {
303
+ candidates.push({
304
+ index: boldMatch.index,
305
+ length: boldMatch[0].length,
306
+ innerText: boldMatch[1] ?? boldMatch[2],
307
+ marks: [{ type: "bold" }],
308
+ });
309
+ }
310
+ const italicMatch = text.match(INLINE_ITALIC_RE);
311
+ if (italicMatch) {
312
+ candidates.push({
313
+ index: italicMatch.index,
314
+ length: italicMatch[0].length,
315
+ innerText: italicMatch[1] ?? italicMatch[2],
316
+ marks: [{ type: "italic" }],
317
+ });
318
+ }
319
+ if (candidates.length === 0) {
320
+ return null;
321
+ }
322
+ // Return the earliest match; on tie, prefer the longer match (more specific)
323
+ candidates.sort((a, b) => a.index - b.index || b.length - a.length);
324
+ return candidates[0];
325
+ }
@@ -0,0 +1,31 @@
1
+ import type { LinearService } from "./linear-service.js";
2
+ import type { ProseMirrorNode } from "./markdown-prosemirror.js";
3
+ export interface ResolveMentionsOptions {
4
+ /**
5
+ * When true, bare capitalized-word references matching known config members
6
+ * are auto-converted to mentions. Default: true.
7
+ */
8
+ autoMention?: boolean;
9
+ /**
10
+ * UUID of the commenting user. Bare references that resolve to this UUID
11
+ * are skipped (you shouldn't @-mention yourself).
12
+ */
13
+ selfUserId?: string;
14
+ }
15
+ /**
16
+ * Resolve mentions in markdown body text to Linear user IDs.
17
+ *
18
+ * Explicit `@name` tokens are always resolved via config aliases, full names,
19
+ * and platform handles — falling back to the Linear API.
20
+ *
21
+ * When `autoMention` is true (default), bare capitalized-word references that
22
+ * match a known config member (alias or first-name/last-name of a fullName)
23
+ * are also converted to mentions — without requiring the `@` prefix. This
24
+ * catches cases like "Bob owns the design" → "@bob owns the design".
25
+ *
26
+ * Returns a ProseMirror `bodyData` doc with `suggestion_userMentions` nodes,
27
+ * or `null` if nothing was resolved.
28
+ */
29
+ export declare function resolveMentions(body: string, linearService: LinearService, options?: ResolveMentionsOptions): Promise<{
30
+ bodyData: ProseMirrorNode;
31
+ } | null>;
@@ -0,0 +1,234 @@
1
+ import { loadConfig } from "../config/config.js";
2
+ import { resolveMember } from "../config/resolver.js";
3
+ import { markdownToProseMirror } from "./markdown-prosemirror.js";
4
+ import { isUuid } from "./uuid.js";
5
+ const EXPLICIT_MENTION_REGEX = /@(\w+)/g;
6
+ const WHITESPACE_SPLIT_REGEX = /\s+/;
7
+ const FENCED_CODE_REGEX = /```[\s\S]*?```/g;
8
+ const INLINE_CODE_REGEX = /`[^`]+`/g;
9
+ const MARKDOWN_LINK_REGEX = /\[([^\]]+)\]\(([^)]+)\)/g;
10
+ /**
11
+ * Resolve mentions in markdown body text to Linear user IDs.
12
+ *
13
+ * Explicit `@name` tokens are always resolved via config aliases, full names,
14
+ * and platform handles — falling back to the Linear API.
15
+ *
16
+ * When `autoMention` is true (default), bare capitalized-word references that
17
+ * match a known config member (alias or first-name/last-name of a fullName)
18
+ * are also converted to mentions — without requiring the `@` prefix. This
19
+ * catches cases like "Bob owns the design" → "@bob owns the design".
20
+ *
21
+ * Returns a ProseMirror `bodyData` doc with `suggestion_userMentions` nodes,
22
+ * or `null` if nothing was resolved.
23
+ */
24
+ export async function resolveMentions(body, linearService, options = {}) {
25
+ const autoMention = options.autoMention !== false;
26
+ const selfUserId = options.selfUserId;
27
+ // 1. Resolve explicit @name mentions (existing behavior).
28
+ const explicit = new Map();
29
+ const explicitNames = [...body.matchAll(EXPLICIT_MENTION_REGEX)].map((m) => m[1]);
30
+ for (const name of new Set(explicitNames)) {
31
+ const userId = await resolveUserByName(name, linearService);
32
+ if (userId) {
33
+ explicit.set(name, { userId, label: name });
34
+ }
35
+ }
36
+ // 2. Bare-name mentions: scan for candidate names that actually appear as
37
+ // standalone words in the body (outside code / link contexts).
38
+ // Config-only — no API fallback — so false positives stay bounded.
39
+ const bare = new Map();
40
+ if (autoMention) {
41
+ const stripped = stripCodeAndLinks(body);
42
+ for (const candidate of getBareMentionCandidates()) {
43
+ const pattern = new RegExp(`\\b${escapeRegex(candidate)}\\b`);
44
+ if (!pattern.test(stripped)) {
45
+ continue;
46
+ }
47
+ const userId = resolveMemberLocal(candidate);
48
+ if (!userId) {
49
+ continue;
50
+ }
51
+ if (selfUserId && userId === selfUserId) {
52
+ continue;
53
+ }
54
+ bare.set(candidate, { userId, label: candidate });
55
+ }
56
+ }
57
+ if (explicit.size === 0 && bare.size === 0) {
58
+ return null;
59
+ }
60
+ const doc = markdownToProseMirror(body);
61
+ const bodyData = injectMentions(doc, explicit, bare);
62
+ return { bodyData };
63
+ }
64
+ async function resolveUserByName(name, linearService) {
65
+ const configResult = resolveMember(name);
66
+ if (isUuid(configResult)) {
67
+ return configResult;
68
+ }
69
+ try {
70
+ return await linearService.resolveUserId(name);
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ /**
77
+ * Config-only resolution (no API fallback). Used for bare-mention detection,
78
+ * where matching a random word against the API would risk false positives.
79
+ */
80
+ function resolveMemberLocal(name) {
81
+ const result = resolveMember(name);
82
+ return isUuid(result) ? result : null;
83
+ }
84
+ /**
85
+ * Build the list of candidate names for bare-mention detection from config:
86
+ * - alias keys ("bob", "erin", "carol")
87
+ * - uuids keys (short display names like "Bob", "David")
88
+ * - every space-separated token of every fullName (so "Surkov" resolves too)
89
+ *
90
+ * Each candidate is returned capitalized (first letter upper, rest lower) —
91
+ * text matches use word boundaries and the capitalization is a proper-noun
92
+ * signal that filters out code/keyword matches.
93
+ */
94
+ function getBareMentionCandidates() {
95
+ const config = loadConfig();
96
+ const candidates = new Set();
97
+ for (const alias of Object.keys(config.members.aliases)) {
98
+ candidates.add(capitalize(alias));
99
+ }
100
+ for (const name of Object.keys(config.members.uuids)) {
101
+ candidates.add(capitalize(name));
102
+ }
103
+ for (const fullName of Object.values(config.members.fullNames)) {
104
+ for (const part of fullName.split(WHITESPACE_SPLIT_REGEX)) {
105
+ if (part.length >= 3) {
106
+ candidates.add(capitalize(part));
107
+ }
108
+ }
109
+ }
110
+ return [...candidates];
111
+ }
112
+ function capitalize(s) {
113
+ if (s.length === 0) {
114
+ return s;
115
+ }
116
+ return s.charAt(0).toUpperCase() + s.slice(1).toLowerCase();
117
+ }
118
+ /**
119
+ * Remove fenced code blocks, inline code, and link text/URLs from the body
120
+ * so the bare-name scan doesn't match inside them. This is a conservative
121
+ * pre-filter — the ProseMirror-walk injection still skips these contexts at
122
+ * emit time, but pre-filtering keeps the bare map from recording candidates
123
+ * that wouldn't actually be converted.
124
+ */
125
+ function stripCodeAndLinks(body) {
126
+ return body
127
+ .replace(FENCED_CODE_REGEX, "")
128
+ .replace(INLINE_CODE_REGEX, "")
129
+ .replace(MARKDOWN_LINK_REGEX, "");
130
+ }
131
+ /**
132
+ * Walk a ProseMirror document tree and replace @name + bare name references
133
+ * with mention nodes.
134
+ */
135
+ function injectMentions(doc, explicit, bare) {
136
+ if (!doc.content) {
137
+ return doc;
138
+ }
139
+ return {
140
+ ...doc,
141
+ content: doc.content.map((node) => {
142
+ if (node.content && node.type !== "codeBlock") {
143
+ const processed = injectMentions(node, explicit, bare);
144
+ if (node.type === "paragraph" || node.type === "heading") {
145
+ return {
146
+ ...processed,
147
+ content: processed.content?.flatMap((child) => splitMentions(child, explicit, bare)),
148
+ };
149
+ }
150
+ return processed;
151
+ }
152
+ return node;
153
+ }),
154
+ };
155
+ }
156
+ /**
157
+ * Split a text node on mention matches. Non-text nodes and code/link-marked
158
+ * nodes pass through unchanged — link text like "[Dima's doc](url)" is an
159
+ * explicit label, not a team reference.
160
+ */
161
+ function splitMentions(node, explicit, bare) {
162
+ if (node.type !== "text" || !node.text) {
163
+ return [node];
164
+ }
165
+ if (node.marks?.some((m) => m.type === "code" || m.type === "link")) {
166
+ return [node];
167
+ }
168
+ const combinedRegex = buildCombinedRegex(explicit, bare);
169
+ if (!combinedRegex) {
170
+ return [node];
171
+ }
172
+ const result = [];
173
+ let lastIndex = 0;
174
+ combinedRegex.lastIndex = 0;
175
+ let match = combinedRegex.exec(node.text);
176
+ while (match !== null) {
177
+ const raw = match[0];
178
+ const resolved = raw.startsWith("@")
179
+ ? explicit.get(raw.slice(1))
180
+ : bare.get(raw);
181
+ if (!resolved) {
182
+ match = combinedRegex.exec(node.text);
183
+ continue;
184
+ }
185
+ if (match.index > lastIndex) {
186
+ result.push({
187
+ type: "text",
188
+ text: node.text.slice(lastIndex, match.index),
189
+ ...(node.marks ? { marks: node.marks } : {}),
190
+ });
191
+ }
192
+ result.push({
193
+ type: "suggestion_userMentions",
194
+ attrs: { id: resolved.userId, label: resolved.label },
195
+ });
196
+ lastIndex = match.index + raw.length;
197
+ match = combinedRegex.exec(node.text);
198
+ }
199
+ if (lastIndex < node.text.length) {
200
+ result.push({
201
+ type: "text",
202
+ text: node.text.slice(lastIndex),
203
+ ...(node.marks ? { marks: node.marks } : {}),
204
+ });
205
+ }
206
+ return result.length > 0 ? result : [node];
207
+ }
208
+ /**
209
+ * Build a single regex that matches either an explicit `@name` or any bare
210
+ * candidate name as a whole word. Returns null when both maps are empty.
211
+ *
212
+ * Bare names are anchored with `\b` so "Dima" inside "Dimadex" doesn't match.
213
+ * Longest candidates come first so multi-word names win over prefixes.
214
+ */
215
+ function buildCombinedRegex(explicit, bare) {
216
+ const parts = [];
217
+ if (explicit.size > 0) {
218
+ parts.push("@\\w+");
219
+ }
220
+ if (bare.size > 0) {
221
+ const alternation = [...bare.keys()]
222
+ .sort((a, b) => b.length - a.length)
223
+ .map(escapeRegex)
224
+ .join("|");
225
+ parts.push(`\\b(?:${alternation})\\b`);
226
+ }
227
+ if (parts.length === 0) {
228
+ return null;
229
+ }
230
+ return new RegExp(parts.join("|"), "g");
231
+ }
232
+ function escapeRegex(s) {
233
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
234
+ }
@@ -0,0 +1,7 @@
1
+ export declare function setRawMode(enabled: boolean): void;
2
+ export declare function setJqFilter(filter: string | null): void;
3
+ export declare function setFieldsFilter(fields: string[] | null): void;
4
+ export declare function outputSuccess(data: unknown): void;
5
+ export declare function outputWarning(message: string | string[], _type?: string): void;
6
+ export declare function resetWarnings(): void;
7
+ export declare function handleAsyncCommand<TArgs extends unknown[]>(asyncFn: (...args: TArgs) => Promise<void>): (...args: TArgs) => Promise<void>;