specvector 0.1.8 → 0.1.9
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.
- package/package.json +1 -1
- package/src/context/linear.ts +62 -45
package/package.json
CHANGED
package/src/context/linear.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linear Context Provider - Fetches ticket context for reviews.
|
|
3
3
|
*
|
|
4
|
-
* Extracts ticket ID from branch name/PR and fetches details via
|
|
4
|
+
* Extracts ticket ID from branch name/PR and fetches details via Linear GraphQL API.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { createMCPClient } from "../mcp";
|
|
8
7
|
import type { Result } from "../types/result";
|
|
9
8
|
import { ok, err } from "../types/result";
|
|
10
9
|
|
|
@@ -13,12 +12,12 @@ import { ok, err } from "../types/result";
|
|
|
13
12
|
// ============================================================================
|
|
14
13
|
|
|
15
14
|
export interface LinearTicket {
|
|
16
|
-
|
|
17
|
-
title
|
|
18
|
-
description
|
|
19
|
-
state
|
|
20
|
-
priority
|
|
21
|
-
labels
|
|
15
|
+
identifier?: string;
|
|
16
|
+
title?: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
state?: { name?: string };
|
|
19
|
+
priority?: number;
|
|
20
|
+
labels?: { nodes?: Array<{ name?: string }> };
|
|
22
21
|
}
|
|
23
22
|
|
|
24
23
|
export interface LinearContextError {
|
|
@@ -76,7 +75,7 @@ export function extractTicketId(
|
|
|
76
75
|
/**
|
|
77
76
|
* Fetch Linear ticket context for a review.
|
|
78
77
|
*
|
|
79
|
-
*
|
|
78
|
+
* Uses Linear's GraphQL API directly - simpler and more reliable than MCP.
|
|
80
79
|
*/
|
|
81
80
|
export async function fetchLinearContext(
|
|
82
81
|
ticketId: string
|
|
@@ -90,67 +89,85 @@ export async function fetchLinearContext(
|
|
|
90
89
|
});
|
|
91
90
|
}
|
|
92
91
|
|
|
93
|
-
// Create MCP client
|
|
94
|
-
const clientResult = await createMCPClient({
|
|
95
|
-
name: "linear",
|
|
96
|
-
command: "npx",
|
|
97
|
-
args: ["linear-mcp-server"],
|
|
98
|
-
env: {
|
|
99
|
-
LINEAR_API_KEY: apiToken,
|
|
100
|
-
},
|
|
101
|
-
timeout: 30000,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
if (!clientResult.ok) {
|
|
105
|
-
return err({
|
|
106
|
-
code: "CONNECTION_FAILED",
|
|
107
|
-
message: clientResult.error.message,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const client = clientResult.value;
|
|
112
|
-
|
|
113
92
|
try {
|
|
114
|
-
//
|
|
115
|
-
const
|
|
93
|
+
// Use Linear GraphQL API directly
|
|
94
|
+
const response = await fetch("https://api.linear.app/graphql", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
headers: {
|
|
97
|
+
"Content-Type": "application/json",
|
|
98
|
+
Authorization: apiToken,
|
|
99
|
+
},
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
query: `
|
|
102
|
+
query GetIssue($id: String!) {
|
|
103
|
+
issue(id: $id) {
|
|
104
|
+
identifier
|
|
105
|
+
title
|
|
106
|
+
description
|
|
107
|
+
state { name }
|
|
108
|
+
priority
|
|
109
|
+
labels { nodes { name } }
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
`,
|
|
113
|
+
variables: { id: ticketId },
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
116
|
|
|
117
|
-
if (!
|
|
117
|
+
if (!response.ok) {
|
|
118
118
|
return err({
|
|
119
119
|
code: "FETCH_FAILED",
|
|
120
|
-
message:
|
|
120
|
+
message: `Linear API error: ${response.status}`,
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
|
|
125
|
-
const content = ticketResult.value.content;
|
|
126
|
-
const textContent = content.find(c => c.type === "text");
|
|
124
|
+
const data = await response.json() as { data?: { issue?: LinearTicket | null }; errors?: Array<{ message: string }> };
|
|
127
125
|
|
|
128
|
-
if (
|
|
126
|
+
if (data.errors?.length) {
|
|
127
|
+
return err({
|
|
128
|
+
code: "FETCH_FAILED",
|
|
129
|
+
message: data.errors[0]?.message ?? "GraphQL error",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const issue = data.data?.issue;
|
|
134
|
+
if (!issue) {
|
|
129
135
|
return err({
|
|
130
136
|
code: "FETCH_FAILED",
|
|
131
|
-
message:
|
|
137
|
+
message: `Issue ${ticketId} not found`,
|
|
132
138
|
});
|
|
133
139
|
}
|
|
134
140
|
|
|
135
141
|
// Format the context for the review prompt
|
|
136
|
-
const contextBlock = formatLinearContext(ticketId,
|
|
142
|
+
const contextBlock = formatLinearContext(ticketId, issue);
|
|
137
143
|
return ok(contextBlock);
|
|
138
144
|
|
|
139
|
-
}
|
|
140
|
-
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return err({
|
|
147
|
+
code: "FETCH_FAILED",
|
|
148
|
+
message: error instanceof Error ? error.message : "Unknown error",
|
|
149
|
+
});
|
|
141
150
|
}
|
|
142
151
|
}
|
|
143
152
|
|
|
144
153
|
/**
|
|
145
|
-
* Format Linear
|
|
154
|
+
* Format Linear issue data as context for review prompt.
|
|
146
155
|
*/
|
|
147
|
-
function formatLinearContext(ticketId: string,
|
|
156
|
+
function formatLinearContext(ticketId: string, issue: { title?: string; description?: string; state?: { name?: string }; priority?: number; labels?: { nodes?: Array<{ name?: string }> } }): string {
|
|
157
|
+
const labels = issue.labels?.nodes?.map(l => l.name).filter(Boolean).join(", ") || "none";
|
|
158
|
+
const priority = issue.priority ?? 0;
|
|
159
|
+
const priorityLabels = ["", "Urgent", "High", "Medium", "Low", "No Priority"];
|
|
160
|
+
|
|
148
161
|
return `
|
|
149
162
|
## Linear Ticket Context: ${ticketId}
|
|
150
163
|
|
|
151
|
-
|
|
164
|
+
**Title:** ${issue.title ?? "Untitled"}
|
|
165
|
+
**Status:** ${issue.state?.name ?? "Unknown"}
|
|
166
|
+
**Priority:** ${priorityLabels[priority] ?? "Unknown"}
|
|
167
|
+
**Labels:** ${labels}
|
|
152
168
|
|
|
153
|
-
|
|
169
|
+
### Description
|
|
170
|
+
${issue.description ?? "No description provided."}
|
|
154
171
|
|
|
155
172
|
---
|
|
156
173
|
When reviewing, check that the PR changes align with the ticket requirements above.
|