@cliangdev/flux-plugin 0.2.0 → 0.3.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.
- package/README.md +11 -7
- package/agents/coder.md +150 -25
- package/bin/install.cjs +171 -16
- package/commands/breakdown.md +47 -10
- package/commands/dashboard.md +29 -0
- package/commands/flux.md +92 -12
- package/commands/implement.md +166 -17
- package/commands/linear.md +6 -5
- package/commands/prd.md +996 -82
- package/manifest.json +2 -1
- package/package.json +9 -11
- package/skills/flux-orchestrator/SKILL.md +11 -3
- package/skills/prd-writer/SKILL.md +761 -0
- package/skills/ux-ui-design/SKILL.md +346 -0
- package/skills/ux-ui-design/references/design-tokens.md +359 -0
- package/src/__tests__/version.test.ts +37 -0
- package/src/adapters/local/.gitkeep +0 -0
- package/src/dashboard/__tests__/api.test.ts +211 -0
- package/src/dashboard/browser.ts +35 -0
- package/src/dashboard/public/app.js +869 -0
- package/src/dashboard/public/index.html +90 -0
- package/src/dashboard/public/styles.css +807 -0
- package/src/dashboard/public/vendor/highlight.css +10 -0
- package/src/dashboard/public/vendor/highlight.min.js +8422 -0
- package/src/dashboard/public/vendor/marked.min.js +2210 -0
- package/src/dashboard/server.ts +296 -0
- package/src/dashboard/watchers.ts +83 -0
- package/src/server/__tests__/config.test.ts +163 -0
- package/src/server/adapters/__tests__/a-client-linear.test.ts +197 -0
- package/src/server/adapters/__tests__/adapter-factory.test.ts +230 -0
- package/src/server/adapters/__tests__/dependency-ops.test.ts +429 -0
- package/src/server/adapters/__tests__/document-ops.test.ts +306 -0
- package/src/server/adapters/__tests__/linear-adapter.test.ts +91 -0
- package/src/server/adapters/__tests__/linear-config.test.ts +425 -0
- package/src/server/adapters/__tests__/linear-criteria-parser.test.ts +287 -0
- package/src/server/adapters/__tests__/linear-description-test.ts +238 -0
- package/src/server/adapters/__tests__/linear-epic-crud.test.ts +496 -0
- package/src/server/adapters/__tests__/linear-mappers-description.test.ts +276 -0
- package/src/server/adapters/__tests__/linear-mappers-epic.test.ts +294 -0
- package/src/server/adapters/__tests__/linear-mappers-prd.test.ts +300 -0
- package/src/server/adapters/__tests__/linear-mappers-task.test.ts +197 -0
- package/src/server/adapters/__tests__/linear-prd-crud.test.ts +620 -0
- package/src/server/adapters/__tests__/linear-stats.test.ts +450 -0
- package/src/server/adapters/__tests__/linear-task-crud.test.ts +534 -0
- package/src/server/adapters/__tests__/linear-types.test.ts +243 -0
- package/src/server/adapters/__tests__/status-ops.test.ts +441 -0
- package/src/server/adapters/factory.ts +90 -0
- package/src/server/adapters/index.ts +9 -0
- package/src/server/adapters/linear/adapter.ts +1141 -0
- package/src/server/adapters/linear/client.ts +169 -0
- package/src/server/adapters/linear/config.ts +152 -0
- package/src/server/adapters/linear/helpers/criteria-parser.ts +197 -0
- package/src/server/adapters/linear/helpers/index.ts +7 -0
- package/src/server/adapters/linear/index.ts +16 -0
- package/src/server/adapters/linear/mappers/description.ts +136 -0
- package/src/server/adapters/linear/mappers/epic.ts +81 -0
- package/src/server/adapters/linear/mappers/index.ts +27 -0
- package/src/server/adapters/linear/mappers/prd.ts +178 -0
- package/src/server/adapters/linear/mappers/task.ts +82 -0
- package/src/server/adapters/linear/types.ts +264 -0
- package/src/server/adapters/local-adapter.ts +1009 -0
- package/src/server/adapters/types.ts +293 -0
- package/src/server/config.ts +73 -0
- package/src/server/db/__tests__/queries.test.ts +473 -0
- package/src/server/db/ids.ts +17 -0
- package/src/server/db/index.ts +69 -0
- package/src/server/db/queries.ts +142 -0
- package/src/server/db/refs.ts +60 -0
- package/src/server/db/schema.ts +97 -0
- package/src/server/db/sqlite.ts +10 -0
- package/src/server/index.ts +81 -0
- package/src/server/tools/__tests__/crud.test.ts +411 -0
- package/src/server/tools/__tests__/get-version.test.ts +27 -0
- package/src/server/tools/__tests__/mcp-interface.test.ts +479 -0
- package/src/server/tools/__tests__/query.test.ts +405 -0
- package/src/server/tools/__tests__/z-configure-linear.test.ts +511 -0
- package/src/server/tools/__tests__/z-get-linear-url.test.ts +108 -0
- package/src/server/tools/configure-linear.ts +373 -0
- package/src/server/tools/create-epic.ts +44 -0
- package/src/server/tools/create-prd.ts +40 -0
- package/src/server/tools/create-task.ts +47 -0
- package/src/server/tools/criteria.ts +50 -0
- package/src/server/tools/delete-entity.ts +76 -0
- package/src/server/tools/dependencies.ts +55 -0
- package/src/server/tools/get-entity.ts +240 -0
- package/src/server/tools/get-linear-url.ts +28 -0
- package/src/server/tools/get-stats.ts +52 -0
- package/src/server/tools/get-version.ts +20 -0
- package/src/server/tools/index.ts +158 -0
- package/src/server/tools/init-project.ts +108 -0
- package/src/server/tools/query-entities.ts +167 -0
- package/src/server/tools/render-status.ts +219 -0
- package/src/server/tools/update-entity.ts +140 -0
- package/src/server/tools/update-status.ts +166 -0
- package/src/server/utils/__tests__/mcp-response.test.ts +331 -0
- package/src/server/utils/logger.ts +9 -0
- package/src/server/utils/mcp-response.ts +254 -0
- package/src/server/utils/status-transitions.ts +160 -0
- package/src/status-line/__tests__/status-line.test.ts +215 -0
- package/src/status-line/index.ts +147 -0
- package/src/utils/__tests__/chalk-import.test.ts +32 -0
- package/src/utils/__tests__/display.test.ts +97 -0
- package/src/utils/__tests__/status-renderer.test.ts +310 -0
- package/src/utils/display.ts +62 -0
- package/src/utils/status-renderer.ts +214 -0
- package/src/version.ts +5 -0
- package/dist/server/index.js +0 -87063
- package/skills/prd-template/SKILL.md +0 -242
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { LinearClient } from "@linear/sdk";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { clearAdapterCache } from "../adapters/index.js";
|
|
5
|
+
import { saveLinearConfig } from "../adapters/linear/config.js";
|
|
6
|
+
import type { LinearConfig } from "../adapters/linear/types.js";
|
|
7
|
+
import { config } from "../config.js";
|
|
8
|
+
import type { ToolDefinition } from "./index.js";
|
|
9
|
+
|
|
10
|
+
const LINEAR_API_KEY_ENV = "LINEAR_API_KEY";
|
|
11
|
+
|
|
12
|
+
const inputSchema = z
|
|
13
|
+
.object({
|
|
14
|
+
apiKey: z.string().optional(),
|
|
15
|
+
teamId: z.string().optional(),
|
|
16
|
+
projectName: z.string().optional(),
|
|
17
|
+
existingProjectId: z.string().optional(),
|
|
18
|
+
prdLabel: z.string().optional().default("prd"),
|
|
19
|
+
epicLabel: z.string().optional().default("epic"),
|
|
20
|
+
taskLabel: z.string().optional().default("task"),
|
|
21
|
+
interactive: z.boolean().optional().default(false),
|
|
22
|
+
})
|
|
23
|
+
.refine(
|
|
24
|
+
(data) => {
|
|
25
|
+
if (data.interactive) {
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
return (
|
|
29
|
+
data.teamId &&
|
|
30
|
+
data.teamId.length > 0 &&
|
|
31
|
+
(data.projectName || data.existingProjectId)
|
|
32
|
+
);
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
message:
|
|
36
|
+
"teamId and either projectName or existingProjectId are required (unless interactive mode)",
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function resolveApiKey(inputApiKey?: string): {
|
|
41
|
+
apiKey: string;
|
|
42
|
+
source: string;
|
|
43
|
+
} {
|
|
44
|
+
if (inputApiKey) {
|
|
45
|
+
return { apiKey: inputApiKey, source: "input" };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const envApiKey = process.env[LINEAR_API_KEY_ENV];
|
|
49
|
+
if (envApiKey) {
|
|
50
|
+
return { apiKey: envApiKey, source: "environment" };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const configPath = `${config.fluxPath}/linear-config.json`;
|
|
54
|
+
if (existsSync(configPath)) {
|
|
55
|
+
try {
|
|
56
|
+
const configData = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
57
|
+
if (configData.apiKey) {
|
|
58
|
+
return { apiKey: configData.apiKey, source: "config" };
|
|
59
|
+
}
|
|
60
|
+
} catch {
|
|
61
|
+
// Ignore parse errors, fall through to error
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(
|
|
66
|
+
`Linear API key not found. Provide it via one of:\n` +
|
|
67
|
+
` 1. Environment variable: export ${LINEAR_API_KEY_ENV}=lin_api_xxx\n` +
|
|
68
|
+
` 2. Existing config: .flux/linear-config.json with "apiKey" field\n` +
|
|
69
|
+
` 3. Tool parameter: apiKey (not recommended - visible in logs)`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function ensureLabelExists(
|
|
74
|
+
client: LinearClient,
|
|
75
|
+
teamId: string,
|
|
76
|
+
labelName: string,
|
|
77
|
+
): Promise<string> {
|
|
78
|
+
const labelsResult = await client.issueLabels({
|
|
79
|
+
filter: {
|
|
80
|
+
name: { eq: labelName },
|
|
81
|
+
team: { id: { eq: teamId } },
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const existingLabel = labelsResult.nodes[0];
|
|
86
|
+
if (existingLabel) {
|
|
87
|
+
return existingLabel.id;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const createResult = await client.createIssueLabel({
|
|
91
|
+
name: labelName,
|
|
92
|
+
teamId,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
if (!createResult.success || !createResult.issueLabel) {
|
|
96
|
+
throw new Error(`Failed to create label '${labelName}'`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const newLabel = await createResult.issueLabel;
|
|
100
|
+
return newLabel.id;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
interface ViewCreationResult {
|
|
104
|
+
created: string | null;
|
|
105
|
+
error?: string;
|
|
106
|
+
skipped?: boolean;
|
|
107
|
+
setup_hint?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function createDefaultView(
|
|
111
|
+
client: LinearClient,
|
|
112
|
+
teamId: string,
|
|
113
|
+
projectId: string,
|
|
114
|
+
projectName: string,
|
|
115
|
+
labels: { prd: string; epic: string; task: string },
|
|
116
|
+
): Promise<ViewCreationResult> {
|
|
117
|
+
try {
|
|
118
|
+
const viewName = `Flux: ${projectName}`;
|
|
119
|
+
const createResult = await client.createCustomView({
|
|
120
|
+
name: viewName,
|
|
121
|
+
description:
|
|
122
|
+
"Flux PRDs, Epics & Tasks. For hierarchy: Display → Grouping → 'Parent issue'",
|
|
123
|
+
teamId,
|
|
124
|
+
projectId,
|
|
125
|
+
color: "#5E6AD2",
|
|
126
|
+
filterData: {
|
|
127
|
+
project: { id: { eq: projectId } },
|
|
128
|
+
labels: {
|
|
129
|
+
some: {
|
|
130
|
+
name: { in: [labels.prd, labels.epic, labels.task] },
|
|
131
|
+
},
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
shared: true,
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (!createResult.success) {
|
|
138
|
+
return { created: null, error: "View creation unsuccessful" };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
created: viewName,
|
|
143
|
+
setup_hint: `For best hierarchy view: Open '${viewName}' view → Click 'Display' → Set Grouping to 'Parent issue' → Click 'Set default for everyone'`,
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
return {
|
|
147
|
+
created: null,
|
|
148
|
+
error: error instanceof Error ? error.message : String(error),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
interface TeamInfo {
|
|
154
|
+
id: string;
|
|
155
|
+
name: string;
|
|
156
|
+
key: string;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
interface ProjectInfo {
|
|
160
|
+
id: string;
|
|
161
|
+
name: string;
|
|
162
|
+
description: string | null;
|
|
163
|
+
state: string;
|
|
164
|
+
updatedAt: string;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function listTeams(client: LinearClient): Promise<TeamInfo[]> {
|
|
168
|
+
const teamsResult = await client.teams();
|
|
169
|
+
return teamsResult.nodes.map((team) => ({
|
|
170
|
+
id: team.id,
|
|
171
|
+
name: team.name,
|
|
172
|
+
key: team.key,
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function listProjects(
|
|
177
|
+
client: LinearClient,
|
|
178
|
+
teamId: string,
|
|
179
|
+
): Promise<ProjectInfo[]> {
|
|
180
|
+
const projectsResult = await client.projects({
|
|
181
|
+
filter: {
|
|
182
|
+
accessibleTeams: { some: { id: { eq: teamId } } },
|
|
183
|
+
state: { nin: ["canceled"] },
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
return projectsResult.nodes.map((project) => ({
|
|
188
|
+
id: project.id,
|
|
189
|
+
name: project.name,
|
|
190
|
+
description: project.description ?? null,
|
|
191
|
+
state: project.state,
|
|
192
|
+
updatedAt: project.updatedAt.toISOString(),
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function handler(input: unknown) {
|
|
197
|
+
const parsed = inputSchema.parse(input);
|
|
198
|
+
|
|
199
|
+
const { apiKey, source: apiKeySource } = resolveApiKey(parsed.apiKey);
|
|
200
|
+
|
|
201
|
+
const client = new LinearClient({ apiKey });
|
|
202
|
+
|
|
203
|
+
let viewerName: string;
|
|
204
|
+
let viewerEmail: string | undefined;
|
|
205
|
+
try {
|
|
206
|
+
const viewer = await client.viewer;
|
|
207
|
+
if (!viewer) {
|
|
208
|
+
throw new Error("Invalid API key");
|
|
209
|
+
}
|
|
210
|
+
viewerName = viewer.name;
|
|
211
|
+
viewerEmail = viewer.email;
|
|
212
|
+
} catch (error) {
|
|
213
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
214
|
+
throw new Error(
|
|
215
|
+
`Invalid Linear API key (source: ${apiKeySource}): ${message}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (parsed.interactive && !parsed.teamId) {
|
|
220
|
+
const teams = await listTeams(client);
|
|
221
|
+
return {
|
|
222
|
+
step: "select_team",
|
|
223
|
+
user: { name: viewerName, email: viewerEmail },
|
|
224
|
+
api_key_source: apiKeySource,
|
|
225
|
+
teams,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (
|
|
230
|
+
parsed.interactive &&
|
|
231
|
+
parsed.teamId &&
|
|
232
|
+
!parsed.projectName &&
|
|
233
|
+
!parsed.existingProjectId
|
|
234
|
+
) {
|
|
235
|
+
const team = await client.team(parsed.teamId);
|
|
236
|
+
if (!team) {
|
|
237
|
+
throw new Error(`Team ${parsed.teamId} not found`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const projects = await listProjects(client, parsed.teamId);
|
|
241
|
+
return {
|
|
242
|
+
step: "select_project",
|
|
243
|
+
user: { name: viewerName, email: viewerEmail },
|
|
244
|
+
api_key_source: apiKeySource,
|
|
245
|
+
team: { id: team.id, name: team.name, key: team.key },
|
|
246
|
+
projects,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!parsed.teamId) {
|
|
251
|
+
throw new Error("teamId is required");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const team = await client.team(parsed.teamId);
|
|
255
|
+
if (!team) {
|
|
256
|
+
throw new Error(`Team ${parsed.teamId} not found`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let projectId: string;
|
|
260
|
+
let projectName: string;
|
|
261
|
+
let isNewProject = false;
|
|
262
|
+
|
|
263
|
+
if (parsed.existingProjectId) {
|
|
264
|
+
const project = await client.project(parsed.existingProjectId);
|
|
265
|
+
if (!project) {
|
|
266
|
+
throw new Error(`Project ${parsed.existingProjectId} not found`);
|
|
267
|
+
}
|
|
268
|
+
projectId = project.id;
|
|
269
|
+
projectName = project.name;
|
|
270
|
+
} else if (parsed.projectName) {
|
|
271
|
+
const createResult = await client.createProject({
|
|
272
|
+
name: parsed.projectName,
|
|
273
|
+
teamIds: [parsed.teamId],
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
if (!createResult.success || !createResult.project) {
|
|
277
|
+
throw new Error(`Failed to create project '${parsed.projectName}'`);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const newProject = await createResult.project;
|
|
281
|
+
projectId = newProject.id;
|
|
282
|
+
projectName = newProject.name;
|
|
283
|
+
isNewProject = true;
|
|
284
|
+
} else {
|
|
285
|
+
throw new Error("Either projectName or existingProjectId must be provided");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const prdLabelId = await ensureLabelExists(
|
|
289
|
+
client,
|
|
290
|
+
parsed.teamId,
|
|
291
|
+
parsed.prdLabel,
|
|
292
|
+
);
|
|
293
|
+
const epicLabelId = await ensureLabelExists(
|
|
294
|
+
client,
|
|
295
|
+
parsed.teamId,
|
|
296
|
+
parsed.epicLabel,
|
|
297
|
+
);
|
|
298
|
+
const taskLabelId = await ensureLabelExists(
|
|
299
|
+
client,
|
|
300
|
+
parsed.teamId,
|
|
301
|
+
parsed.taskLabel,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
let viewResult: ViewCreationResult | undefined;
|
|
305
|
+
if (isNewProject) {
|
|
306
|
+
viewResult = await createDefaultView(
|
|
307
|
+
client,
|
|
308
|
+
parsed.teamId,
|
|
309
|
+
projectId,
|
|
310
|
+
projectName,
|
|
311
|
+
{
|
|
312
|
+
prd: parsed.prdLabel,
|
|
313
|
+
epic: parsed.epicLabel,
|
|
314
|
+
task: parsed.taskLabel,
|
|
315
|
+
},
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const linearConfig: LinearConfig = {
|
|
320
|
+
apiKey,
|
|
321
|
+
teamId: parsed.teamId,
|
|
322
|
+
projectId,
|
|
323
|
+
defaultLabels: {
|
|
324
|
+
prd: parsed.prdLabel,
|
|
325
|
+
epic: parsed.epicLabel,
|
|
326
|
+
task: parsed.taskLabel,
|
|
327
|
+
},
|
|
328
|
+
};
|
|
329
|
+
saveLinearConfig(linearConfig);
|
|
330
|
+
|
|
331
|
+
const projectJsonPath = config.projectJsonPath;
|
|
332
|
+
const projectData = JSON.parse(readFileSync(projectJsonPath, "utf-8"));
|
|
333
|
+
projectData.adapter = { type: "linear" };
|
|
334
|
+
writeFileSync(projectJsonPath, JSON.stringify(projectData, null, 2));
|
|
335
|
+
|
|
336
|
+
clearAdapterCache();
|
|
337
|
+
config.clearCache();
|
|
338
|
+
|
|
339
|
+
return {
|
|
340
|
+
success: true,
|
|
341
|
+
message: "Linear integration configured successfully",
|
|
342
|
+
api_key_source: apiKeySource,
|
|
343
|
+
team: team.name,
|
|
344
|
+
project: {
|
|
345
|
+
id: projectId,
|
|
346
|
+
name: projectName,
|
|
347
|
+
},
|
|
348
|
+
labels: {
|
|
349
|
+
prd: { name: parsed.prdLabel, id: prdLabelId },
|
|
350
|
+
epic: { name: parsed.epicLabel, id: epicLabelId },
|
|
351
|
+
task: { name: parsed.taskLabel, id: taskLabelId },
|
|
352
|
+
},
|
|
353
|
+
view: viewResult ?? { created: null, skipped: true },
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const configureLinearTool: ToolDefinition = {
|
|
358
|
+
name: "configure_linear",
|
|
359
|
+
description:
|
|
360
|
+
"Configure Linear integration for the Flux project. " +
|
|
361
|
+
"Use interactive mode for guided setup: " +
|
|
362
|
+
"1) Call with {interactive: true} to get list of teams. " +
|
|
363
|
+
"2) Call with {interactive: true, teamId: '...'} to get list of projects. " +
|
|
364
|
+
"3) Call with full params to configure. " +
|
|
365
|
+
"API Key Resolution (priority order): 1) LINEAR_API_KEY environment variable (recommended), 2) Existing .flux/linear-config.json, 3) apiKey parameter (not recommended - visible in logs). " +
|
|
366
|
+
"Required (non-interactive): teamId (Linear team ID), and either projectName (to create new) or existingProjectId (to use existing). " +
|
|
367
|
+
"Optional: apiKey, prdLabel (default 'prd'), epicLabel (default 'epic'), taskLabel (default 'task'). " +
|
|
368
|
+
"Creates a Linear Project, ensures labels exist, creates 'Flux' custom view for new projects, saves config to .flux/linear-config.json. " +
|
|
369
|
+
"Returns {success, message, api_key_source, team, project, labels, view} on final configuration. " +
|
|
370
|
+
"Setup: export LINEAR_API_KEY=lin_api_xxx before running.",
|
|
371
|
+
inputSchema,
|
|
372
|
+
handler,
|
|
373
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import { toMcpEpic } from "../utils/mcp-response.js";
|
|
4
|
+
import type { ToolDefinition } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
prd_ref: z.string().min(1, "PRD reference is required"),
|
|
8
|
+
title: z.string().min(1, "Title is required"),
|
|
9
|
+
description: z.string().optional(),
|
|
10
|
+
acceptance_criteria: z.array(z.string()).optional(),
|
|
11
|
+
depends_on: z.array(z.string()).optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
async function handler(input: unknown) {
|
|
15
|
+
const parsed = inputSchema.parse(input);
|
|
16
|
+
const adapter = getAdapter();
|
|
17
|
+
|
|
18
|
+
const epic = await adapter.createEpic({
|
|
19
|
+
prdRef: parsed.prd_ref,
|
|
20
|
+
title: parsed.title,
|
|
21
|
+
description: parsed.description,
|
|
22
|
+
acceptanceCriteria: parsed.acceptance_criteria,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const criteriaCount = parsed.acceptance_criteria?.length || 0;
|
|
26
|
+
|
|
27
|
+
const dependencies: string[] = [];
|
|
28
|
+
if (parsed.depends_on && parsed.depends_on.length > 0) {
|
|
29
|
+
for (const depRef of parsed.depends_on) {
|
|
30
|
+
await adapter.addDependency(epic.ref, depRef);
|
|
31
|
+
dependencies.push(depRef);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ...toMcpEpic(epic), criteria_count: criteriaCount, dependencies };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const createEpicTool: ToolDefinition = {
|
|
39
|
+
name: "create_epic",
|
|
40
|
+
description:
|
|
41
|
+
"Create a new Epic under a PRD. Required: prd_ref (e.g., 'FLUX-P1'), title. Optional: description, acceptance_criteria (string array), depends_on (array of Epic refs this Epic depends on, e.g., ['FLUX-E1']). Returns {id, ref, title, status, criteria_count, dependencies}. Status starts as PENDING.",
|
|
42
|
+
inputSchema,
|
|
43
|
+
handler,
|
|
44
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import { toMcpPrd } from "../utils/mcp-response.js";
|
|
4
|
+
import type { ToolDefinition } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
title: z.string().min(1, "Title is required"),
|
|
8
|
+
description: z.string().optional(),
|
|
9
|
+
tag: z.string().optional(),
|
|
10
|
+
depends_on: z.array(z.string()).optional(),
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
async function handler(input: unknown) {
|
|
14
|
+
const parsed = inputSchema.parse(input);
|
|
15
|
+
const adapter = getAdapter();
|
|
16
|
+
|
|
17
|
+
const prd = await adapter.createPrd({
|
|
18
|
+
title: parsed.title,
|
|
19
|
+
description: parsed.description,
|
|
20
|
+
tag: parsed.tag,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const dependencies: string[] = [];
|
|
24
|
+
if (parsed.depends_on && parsed.depends_on.length > 0) {
|
|
25
|
+
for (const depRef of parsed.depends_on) {
|
|
26
|
+
await adapter.addDependency(prd.ref, depRef);
|
|
27
|
+
dependencies.push(depRef);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { ...toMcpPrd(prd), dependencies };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const createPrdTool: ToolDefinition = {
|
|
35
|
+
name: "create_prd",
|
|
36
|
+
description:
|
|
37
|
+
"Create a new PRD (Product Requirements Document). Required: title. Optional: description, tag, depends_on (array of PRD refs this PRD depends on, e.g., ['FLUX-P1', 'FLUX-P2']). Returns the created PRD with {id, ref, title, description, status, tag, dependencies}. Status starts as DRAFT.",
|
|
38
|
+
inputSchema,
|
|
39
|
+
handler,
|
|
40
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import type { Priority } from "../adapters/types.js";
|
|
4
|
+
import { toMcpTask } from "../utils/mcp-response.js";
|
|
5
|
+
import type { ToolDefinition } from "./index.js";
|
|
6
|
+
|
|
7
|
+
const inputSchema = z.object({
|
|
8
|
+
epic_ref: z.string().min(1, "Epic reference is required"),
|
|
9
|
+
title: z.string().min(1, "Title is required"),
|
|
10
|
+
description: z.string().optional(),
|
|
11
|
+
priority: z.enum(["LOW", "MEDIUM", "HIGH"]).optional().default("MEDIUM"),
|
|
12
|
+
acceptance_criteria: z.array(z.string()).optional(),
|
|
13
|
+
depends_on: z.array(z.string()).optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
async function handler(input: unknown) {
|
|
17
|
+
const parsed = inputSchema.parse(input);
|
|
18
|
+
const adapter = getAdapter();
|
|
19
|
+
|
|
20
|
+
const task = await adapter.createTask({
|
|
21
|
+
epicRef: parsed.epic_ref,
|
|
22
|
+
title: parsed.title,
|
|
23
|
+
description: parsed.description,
|
|
24
|
+
priority: parsed.priority as Priority,
|
|
25
|
+
acceptanceCriteria: parsed.acceptance_criteria,
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const criteriaCount = parsed.acceptance_criteria?.length || 0;
|
|
29
|
+
|
|
30
|
+
const dependencies: string[] = [];
|
|
31
|
+
if (parsed.depends_on && parsed.depends_on.length > 0) {
|
|
32
|
+
for (const depRef of parsed.depends_on) {
|
|
33
|
+
await adapter.addDependency(task.ref, depRef);
|
|
34
|
+
dependencies.push(depRef);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { ...toMcpTask(task), criteria_count: criteriaCount, dependencies };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const createTaskTool: ToolDefinition = {
|
|
42
|
+
name: "create_task",
|
|
43
|
+
description:
|
|
44
|
+
"Create a new Task under an Epic. Required: epic_ref (e.g., 'FLUX-E1'), title. Optional: description, priority (LOW|MEDIUM|HIGH, default MEDIUM), acceptance_criteria (string array), depends_on (array of Task refs this Task depends on, e.g., ['FLUX-T1']). Returns {id, ref, title, status, priority, criteria_count, dependencies}.",
|
|
45
|
+
inputSchema,
|
|
46
|
+
handler,
|
|
47
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import { toMcpCriterion } from "../utils/mcp-response.js";
|
|
4
|
+
import type { ToolDefinition } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const addCriteriaSchema = z.object({
|
|
7
|
+
parent_ref: z.string().min(1, "Parent reference is required"),
|
|
8
|
+
criteria: z.string().min(1, "Criteria text is required"),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const markCriteriaMetSchema = z.object({
|
|
12
|
+
criteria_id: z.string().min(1, "Criteria ID is required"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
async function addCriteriaHandler(input: unknown) {
|
|
16
|
+
const parsed = addCriteriaSchema.parse(input);
|
|
17
|
+
const adapter = getAdapter();
|
|
18
|
+
|
|
19
|
+
const criterion = await adapter.addCriterion({
|
|
20
|
+
parentRef: parsed.parent_ref,
|
|
21
|
+
criteria: parsed.criteria,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return toMcpCriterion(criterion);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function markCriteriaMetHandler(input: unknown) {
|
|
28
|
+
const parsed = markCriteriaMetSchema.parse(input);
|
|
29
|
+
const adapter = getAdapter();
|
|
30
|
+
|
|
31
|
+
const criterion = await adapter.markCriterionMet(parsed.criteria_id);
|
|
32
|
+
|
|
33
|
+
return toMcpCriterion(criterion);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const addCriteriaTool: ToolDefinition = {
|
|
37
|
+
name: "add_criteria",
|
|
38
|
+
description:
|
|
39
|
+
"Add an acceptance criterion to an Epic or Task. Required: parent_ref (e.g., 'FLUX-E1' or 'FLUX-T1'), criteria (text). Returns created criterion {id, parent_type, parent_id, criteria, is_met}. PRDs don't have direct criteria.",
|
|
40
|
+
inputSchema: addCriteriaSchema,
|
|
41
|
+
handler: addCriteriaHandler,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const markCriteriaMetTool: ToolDefinition = {
|
|
45
|
+
name: "mark_criteria_met",
|
|
46
|
+
description:
|
|
47
|
+
"Mark an acceptance criterion as met/completed. Required: criteria_id (not ref, the actual id like 'ac_abc123'). Returns updated criterion with is_met=true. Use get_entity with include=['criteria'] to find criterion IDs.",
|
|
48
|
+
inputSchema: markCriteriaMetSchema,
|
|
49
|
+
handler: markCriteriaMetHandler,
|
|
50
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import { toMcpCascadeResult } from "../utils/mcp-response.js";
|
|
4
|
+
import type { ToolDefinition } from "./index.js";
|
|
5
|
+
|
|
6
|
+
const inputSchema = z.object({
|
|
7
|
+
ref: z.string().min(1, "Entity reference is required"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
function getEntityType(ref: string): "P" | "E" | "T" | null {
|
|
11
|
+
// Try Flux-style ref format first (e.g., FLUX-P1, FLUX-E2, FLUX-T3)
|
|
12
|
+
const match = ref.match(/-([PET])\d+$/);
|
|
13
|
+
if (match) {
|
|
14
|
+
return match[1] as "P" | "E" | "T";
|
|
15
|
+
}
|
|
16
|
+
// For other ref formats (e.g., Linear: CAL-10), return null
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function handler(input: unknown) {
|
|
21
|
+
const parsed = inputSchema.parse(input);
|
|
22
|
+
const adapter = getAdapter();
|
|
23
|
+
|
|
24
|
+
let entityType = getEntityType(parsed.ref);
|
|
25
|
+
|
|
26
|
+
// If ref format is unknown (e.g., Linear refs), try each entity type
|
|
27
|
+
if (!entityType) {
|
|
28
|
+
const prd = await adapter.getPrd(parsed.ref);
|
|
29
|
+
if (prd) {
|
|
30
|
+
entityType = "P";
|
|
31
|
+
} else {
|
|
32
|
+
const epic = await adapter.getEpic(parsed.ref);
|
|
33
|
+
if (epic) {
|
|
34
|
+
entityType = "E";
|
|
35
|
+
} else {
|
|
36
|
+
const task = await adapter.getTask(parsed.ref);
|
|
37
|
+
if (task) {
|
|
38
|
+
entityType = "T";
|
|
39
|
+
} else {
|
|
40
|
+
throw new Error(`Entity not found: ${parsed.ref}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let result: {
|
|
47
|
+
deleted: string;
|
|
48
|
+
cascade: {
|
|
49
|
+
criteria: number;
|
|
50
|
+
tasks: number;
|
|
51
|
+
epics: number;
|
|
52
|
+
dependencies: number;
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (entityType === "P") {
|
|
57
|
+
result = await adapter.deletePrd(parsed.ref);
|
|
58
|
+
} else if (entityType === "E") {
|
|
59
|
+
result = await adapter.deleteEpic(parsed.ref);
|
|
60
|
+
} else {
|
|
61
|
+
result = await adapter.deleteTask(parsed.ref);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
deleted: result.deleted,
|
|
66
|
+
cascade: toMcpCascadeResult(result.cascade),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const deleteEntityTool: ToolDefinition = {
|
|
71
|
+
name: "delete_entity",
|
|
72
|
+
description:
|
|
73
|
+
"Delete any entity by reference with cascade deletion. Deleting a PRD removes all its Epics/Tasks/Criteria. Deleting an Epic removes its Tasks/Criteria. Returns {deleted: ref, cascade: {criteria, tasks, epics, dependencies}}.",
|
|
74
|
+
inputSchema,
|
|
75
|
+
handler,
|
|
76
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getAdapter } from "../adapters/index.js";
|
|
3
|
+
import type { ToolDefinition } from "./index.js";
|
|
4
|
+
|
|
5
|
+
const addDependencySchema = z.object({
|
|
6
|
+
ref: z.string().min(1, "Entity reference is required"),
|
|
7
|
+
depends_on_ref: z.string().min(1, "Dependency reference is required"),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const removeDependencySchema = z.object({
|
|
11
|
+
ref: z.string().min(1, "Entity reference is required"),
|
|
12
|
+
depends_on_ref: z.string().min(1, "Dependency reference is required"),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
async function addDependencyHandler(input: unknown) {
|
|
16
|
+
const parsed = addDependencySchema.parse(input);
|
|
17
|
+
const adapter = getAdapter();
|
|
18
|
+
|
|
19
|
+
await adapter.addDependency(parsed.ref, parsed.depends_on_ref);
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
success: true,
|
|
23
|
+
ref: parsed.ref,
|
|
24
|
+
depends_on: parsed.depends_on_ref,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function removeDependencyHandler(input: unknown) {
|
|
29
|
+
const parsed = removeDependencySchema.parse(input);
|
|
30
|
+
const adapter = getAdapter();
|
|
31
|
+
|
|
32
|
+
await adapter.removeDependency(parsed.ref, parsed.depends_on_ref);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
success: true,
|
|
36
|
+
ref: parsed.ref,
|
|
37
|
+
removed_dependency: parsed.depends_on_ref,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const addDependencyTool: ToolDefinition = {
|
|
42
|
+
name: "add_dependency",
|
|
43
|
+
description:
|
|
44
|
+
"Add a dependency between two entities of the same type. Required: ref (depends on depends_on_ref), depends_on_ref. Both must be PRDs (FLUX-P*), Epics (FLUX-E*), or Tasks (FLUX-T*). Validates no circular dependencies. Returns {success, ref, depends_on}.",
|
|
45
|
+
inputSchema: addDependencySchema,
|
|
46
|
+
handler: addDependencyHandler,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const removeDependencyTool: ToolDefinition = {
|
|
50
|
+
name: "remove_dependency",
|
|
51
|
+
description:
|
|
52
|
+
"Remove a dependency between two entities. Required: ref, depends_on_ref. Both must be same type (PRD, Epic, or Task). Returns {success, ref, removed_dependency}.",
|
|
53
|
+
inputSchema: removeDependencySchema,
|
|
54
|
+
handler: removeDependencyHandler,
|
|
55
|
+
};
|