@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,141 @@
1
+ /**
2
+ * Step 1 of the wizard: Linear API token.
3
+ *
4
+ * The only required step. Validates the token by calling `viewer { ... }`
5
+ * before saving. Token is stored at ~/.config/el-linear/token (mode 0600),
6
+ * never embedded in config.json.
7
+ */
8
+ import { confirm, password } from "@inquirer/prompts";
9
+ import { GraphQLService } from "../../utils/graphql-service.js";
10
+ import { readToken, writeToken } from "./shared.js";
11
+ const TOKEN_GENERATION_URL = "https://linear.app/settings/account/security";
12
+ const VIEWER_QUERY = /* GraphQL */ `
13
+ query {
14
+ viewer {
15
+ id
16
+ name
17
+ email
18
+ displayName
19
+ organization {
20
+ urlKey
21
+ name
22
+ }
23
+ }
24
+ }
25
+ `;
26
+ /**
27
+ * Strip anything that looks like a Linear API token from a string. Defense in
28
+ * depth: today the @linear/sdk error message embeds {query, variables} but not
29
+ * the Authorization header. A future SDK upgrade that includes headers (which
30
+ * upstream graphql-request has done historically) would otherwise silently
31
+ * write `Bearer lin_api_…` into stdout / shell history / CI logs. The regex
32
+ * also catches token shapes that may show up in custom error wrappers.
33
+ */
34
+ export function sanitizeForLog(text) {
35
+ return text.replace(/lin_api_[A-Za-z0-9_-]{16,}/g, "lin_api_***REDACTED***");
36
+ }
37
+ /**
38
+ * Strict shape check on the viewer response. Treats whitespace-only fields as
39
+ * "validated to nothing" — easy to forge with a malformed but truthy stub
40
+ * response, so we require a UUID-shaped id and a basic urlKey.
41
+ */
42
+ function viewerIsValid(viewer) {
43
+ if (!viewer || typeof viewer !== "object")
44
+ return false;
45
+ const v = viewer;
46
+ const id = typeof v.id === "string" ? v.id.trim() : "";
47
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(id))
48
+ return false;
49
+ const org = v.organization;
50
+ if (!org || typeof org !== "object")
51
+ return false;
52
+ const urlKey = typeof org.urlKey === "string" ? org.urlKey.trim() : "";
53
+ if (!/^[a-z0-9-]+$/i.test(urlKey))
54
+ return false;
55
+ return true;
56
+ }
57
+ /**
58
+ * Validate a Linear API token by fetching the viewer. Throws with a
59
+ * sanitized user-readable message on auth failure — the error string is
60
+ * always run through sanitizeForLog so a leaked token in an upstream error
61
+ * is redacted before it hits stdout.
62
+ */
63
+ export async function validateToken(token) {
64
+ const service = new GraphQLService(token);
65
+ let data;
66
+ try {
67
+ data = await service.rawRequest(VIEWER_QUERY);
68
+ }
69
+ catch (err) {
70
+ const raw = err instanceof Error ? err.message : String(err);
71
+ const message = sanitizeForLog(raw);
72
+ if (/AuthenticationFailed|Unauthorized|invalid|expired/i.test(message)) {
73
+ throw new Error(`Token rejected by Linear: ${message}`);
74
+ }
75
+ throw new Error(`Could not validate token: ${message}`);
76
+ }
77
+ if (!viewerIsValid(data?.viewer)) {
78
+ throw new Error("Token validated but the response is missing a viewer with a valid id and organization. " +
79
+ "Try a different token.");
80
+ }
81
+ return data.viewer;
82
+ }
83
+ /**
84
+ * Run the interactive token step. Returns the validated token + viewer info.
85
+ *
86
+ * On re-run with an existing valid token, the prompt defaults to "keep" — so
87
+ * pressing enter is a no-op.
88
+ */
89
+ export async function runTokenStep(options = {}) {
90
+ const existing = await readToken();
91
+ if (existing && !options.force) {
92
+ try {
93
+ const viewer = await validateToken(existing);
94
+ // biome-ignore lint/suspicious/noConsole: wizard
95
+ console.log(` Existing token works — authenticated as ${viewer.displayName} <${viewer.email}>.`);
96
+ const replace = await confirm({
97
+ message: "Replace this token?",
98
+ default: false,
99
+ });
100
+ if (!replace) {
101
+ return { token: existing, viewer };
102
+ }
103
+ }
104
+ catch (err) {
105
+ const raw = err instanceof Error ? err.message : String(err);
106
+ const message = sanitizeForLog(raw);
107
+ // biome-ignore lint/suspicious/noConsole: wizard
108
+ console.log(` Existing token failed validation (${message}). You'll need to provide a new one.`);
109
+ }
110
+ }
111
+ // biome-ignore lint/suspicious/noConsole: wizard
112
+ console.log(` Generate a personal API token: ${TOKEN_GENERATION_URL}`);
113
+ // biome-ignore lint/suspicious/noConsole: wizard
114
+ console.log(" The token stays on your machine. el-linear only sends it to Linear's API. " +
115
+ "It's stored at ~/.config/el-linear/token (mode 0600).");
116
+ // Up to three attempts before giving up.
117
+ for (let attempt = 0; attempt < 3; attempt++) {
118
+ const token = (await password({
119
+ message: attempt === 0
120
+ ? "Linear API token (input hidden):"
121
+ : "Try again (input hidden):",
122
+ mask: "*",
123
+ validate: (input) => input.trim().length > 0 || "Token cannot be empty",
124
+ })).trim();
125
+ try {
126
+ // biome-ignore lint/suspicious/noConsole: wizard
127
+ console.log(" Validating…");
128
+ const viewer = await validateToken(token);
129
+ await writeToken(token);
130
+ // biome-ignore lint/suspicious/noConsole: wizard
131
+ console.log(` ✓ Authenticated as ${viewer.displayName} <${viewer.email}> (${viewer.organization.name}).`);
132
+ return { token, viewer };
133
+ }
134
+ catch (err) {
135
+ const raw = err instanceof Error ? err.message : String(err);
136
+ // biome-ignore lint/suspicious/noConsole: wizard
137
+ console.log(` ✗ ${sanitizeForLog(raw)}`);
138
+ }
139
+ }
140
+ throw new Error("Could not validate a Linear API token after 3 attempts. Aborting.");
141
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Step 2 of the wizard: workspace defaults.
3
+ *
4
+ * Auto-fetches the Linear workspace urlKey (no prompt) since it's deterministic.
5
+ * Lets the user pick a default team, optional. Skip-by-default.
6
+ */
7
+ import type { WizardConfig } from "./shared.js";
8
+ export interface WorkspaceStepResult {
9
+ workspaceUrlKey: string;
10
+ defaultTeam: string | undefined;
11
+ teams: Record<string, string>;
12
+ }
13
+ /**
14
+ * Run the workspace step. Reads existing config to default the team picker
15
+ * to the current default. Returns values to merge into the on-disk config.
16
+ *
17
+ * No-op safe: if the user keeps everything as-is, returns values that match
18
+ * the existing config exactly.
19
+ */
20
+ export declare function runWorkspaceStep(token: string, workspaceUrlKey: string, existing: WizardConfig): Promise<WorkspaceStepResult>;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Step 2 of the wizard: workspace defaults.
3
+ *
4
+ * Auto-fetches the Linear workspace urlKey (no prompt) since it's deterministic.
5
+ * Lets the user pick a default team, optional. Skip-by-default.
6
+ */
7
+ import { confirm, select } from "@inquirer/prompts";
8
+ import { GraphQLService } from "../../utils/graphql-service.js";
9
+ const TEAMS_QUERY = /* GraphQL */ `
10
+ query InitTeams {
11
+ teams(first: 100) {
12
+ nodes {
13
+ id
14
+ key
15
+ name
16
+ }
17
+ }
18
+ }
19
+ `;
20
+ /**
21
+ * Run the workspace step. Reads existing config to default the team picker
22
+ * to the current default. Returns values to merge into the on-disk config.
23
+ *
24
+ * No-op safe: if the user keeps everything as-is, returns values that match
25
+ * the existing config exactly.
26
+ */
27
+ export async function runWorkspaceStep(token, workspaceUrlKey, existing) {
28
+ const service = new GraphQLService(token);
29
+ // Fetch teams once so we can populate both the picker and the cached id map.
30
+ const data = await service.rawRequest(TEAMS_QUERY);
31
+ const teams = data?.teams?.nodes ?? [];
32
+ const teamMap = {};
33
+ for (const t of teams) {
34
+ teamMap[t.key] = t.id;
35
+ }
36
+ // biome-ignore lint/suspicious/noConsole: wizard
37
+ console.log(` Workspace: ${workspaceUrlKey} (${teams.length} teams)`);
38
+ const currentDefault = existing.defaultTeam || "";
39
+ // `change` reflects what the user agreed to: `true` means "open the picker
40
+ // and change the default team", `false` means "leave the existing config
41
+ // alone". Default false makes pressing enter a no-op (keep-as-is).
42
+ const change = await confirm({
43
+ message: currentDefault
44
+ ? `Current default team: ${currentDefault}. Change it?`
45
+ : "Set a default team for new issues?",
46
+ default: false,
47
+ });
48
+ if (!change) {
49
+ return {
50
+ workspaceUrlKey,
51
+ defaultTeam: currentDefault || undefined,
52
+ teams: teamMap,
53
+ };
54
+ }
55
+ if (teams.length === 0) {
56
+ // biome-ignore lint/suspicious/noConsole: wizard
57
+ console.log(" No teams visible to this token. Skipping default-team picker.");
58
+ return {
59
+ workspaceUrlKey,
60
+ defaultTeam: currentDefault || undefined,
61
+ teams: teamMap,
62
+ };
63
+ }
64
+ const choice = await select({
65
+ message: "Default team:",
66
+ choices: [
67
+ ...teams.map((t) => ({
68
+ name: `${t.key}${t.name && t.name !== t.key ? ` — ${t.name}` : ""}`,
69
+ value: t.key,
70
+ })),
71
+ { name: "(no default — pick per-issue)", value: "__skip" },
72
+ ],
73
+ default: currentDefault || teams[0]?.key,
74
+ });
75
+ return {
76
+ workspaceUrlKey,
77
+ defaultTeam: choice === "__skip" ? undefined : choice,
78
+ teams: teamMap,
79
+ };
80
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * `el-linear issue-id` — extract the Linear issue ID from the current
3
+ * git branch (or any branch passed as an argument).
4
+ *
5
+ * Replaces the regex that was duplicated across 4 skills: commit-guard,
6
+ * glab-commit-push-mr, stray-file-triage, git-branch-from-linear. Each
7
+ * had its own copy of `feature/(DEV|ALL|...)-\d+-...` parsing.
8
+ *
9
+ * Output (when issueId found):
10
+ * { branch, issueId: "DEV-3900", team: "DEV", number: 3900, slug: "..." }
11
+ * Output (when no match):
12
+ * { branch, issueId: null, team: null, number: null, slug: null }
13
+ *
14
+ * `--fetch` (optional): also fetches the issue from Linear and includes
15
+ * { title, description, state, assignee }. Skip when you only need the
16
+ * parsed ID — saves an API round-trip.
17
+ */
18
+ import type { Command } from "commander";
19
+ interface ParsedBranch {
20
+ branch: string;
21
+ issueId: string | null;
22
+ number: number | null;
23
+ slug: string | null;
24
+ team: string | null;
25
+ }
26
+ export declare function parseBranchName(branch: string): ParsedBranch;
27
+ export declare function setupIssueIdCommand(program: Command): void;
28
+ export {};
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `el-linear issue-id` — extract the Linear issue ID from the current
3
+ * git branch (or any branch passed as an argument).
4
+ *
5
+ * Replaces the regex that was duplicated across 4 skills: commit-guard,
6
+ * glab-commit-push-mr, stray-file-triage, git-branch-from-linear. Each
7
+ * had its own copy of `feature/(DEV|ALL|...)-\d+-...` parsing.
8
+ *
9
+ * Output (when issueId found):
10
+ * { branch, issueId: "DEV-3900", team: "DEV", number: 3900, slug: "..." }
11
+ * Output (when no match):
12
+ * { branch, issueId: null, team: null, number: null, slug: null }
13
+ *
14
+ * `--fetch` (optional): also fetches the issue from Linear and includes
15
+ * { title, description, state, assignee }. Skip when you only need the
16
+ * parsed ID — saves an API round-trip.
17
+ */
18
+ import { spawnSync } from "node:child_process";
19
+ import { createGraphQLService } from "../utils/graphql-service.js";
20
+ import { handleAsyncCommand, outputSuccess } from "../utils/output.js";
21
+ // Branch naming conventions — kept in sync with branch-name validators
22
+ // elsewhere in the workspace. Any addition here is the single point of
23
+ // truth that all skills rely on.
24
+ const BRANCH_RE = /^(?:feature|fix|chore|refactor|dev)[-/]([A-Z]{2,4})-(\d+)(?:[-/](.*))?$/i;
25
+ export function parseBranchName(branch) {
26
+ const m = branch.match(BRANCH_RE);
27
+ if (!m) {
28
+ return { branch, issueId: null, team: null, number: null, slug: null };
29
+ }
30
+ const team = m[1].toUpperCase();
31
+ const number = Number.parseInt(m[2], 10);
32
+ return {
33
+ branch,
34
+ issueId: `${team}-${number}`,
35
+ team,
36
+ number,
37
+ slug: m[3] ?? null,
38
+ };
39
+ }
40
+ function getCurrentBranch() {
41
+ const result = spawnSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
42
+ encoding: "utf8",
43
+ });
44
+ if (result.status !== 0) {
45
+ throw new Error(`git rev-parse failed: ${result.stderr.trim() || "non-zero exit"}`);
46
+ }
47
+ return result.stdout.trim();
48
+ }
49
+ const ISSUE_QUERY = /* GraphQL */ `
50
+ query IssueByIdentifier($id: String!) {
51
+ issue(id: $id) {
52
+ id
53
+ identifier
54
+ title
55
+ description
56
+ branchName
57
+ state { name type }
58
+ assignee { id name email }
59
+ }
60
+ }
61
+ `;
62
+ export function setupIssueIdCommand(program) {
63
+ program
64
+ .command("issue-id [branch]")
65
+ .description("Extract the Linear issue ID from a git branch name (defaults to current branch). Use --fetch to also pull the issue from Linear.")
66
+ .option("--fetch", "also fetch issue title/description/state from Linear")
67
+ .action(handleAsyncCommand(async (branchArg, options, command) => {
68
+ const branch = branchArg ?? getCurrentBranch();
69
+ const parsed = parseBranchName(branch);
70
+ if (!(options.fetch && parsed.issueId)) {
71
+ outputSuccess(parsed);
72
+ return;
73
+ }
74
+ const rootOpts = command.parent?.opts() ?? {};
75
+ const service = createGraphQLService({ apiToken: rootOpts.apiToken });
76
+ const result = (await service.rawRequest(ISSUE_QUERY, {
77
+ id: parsed.issueId,
78
+ }));
79
+ outputSuccess({ ...parsed, issue: result.issue ?? null });
80
+ }));
81
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function setupIssuesCommands(program: Command): void;