ai-dev-requirements 0.1.2
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 +227 -0
- package/README.zh-CN.md +227 -0
- package/dist/index.cjs +785 -0
- package/dist/index.d.cts +2 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.mjs +759 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
- package/skills/dev-workflow/SKILL.md +215 -0
- package/skills/dev-workflow/references/requirement-validation.md +114 -0
- package/skills/dev-workflow/references/service-transform.md +109 -0
- package/skills/dev-workflow/references/task-types.md +123 -0
- package/skills/dev-workflow/references/templates/code-dev-task.md +29 -0
- package/skills/dev-workflow/references/templates/code-fix-task.md +28 -0
- package/skills/dev-workflow/references/templates/code-refactor-task.md +30 -0
- package/skills/dev-workflow/references/templates/doc-write-task.md +29 -0
- package/skills/dev-workflow/references/templates/research-task.md +30 -0
- package/skills/dev-workflow/references/templates/test-task.md +30 -0
- package/skills/dev-workflow/references/workflow.md +162 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,785 @@
|
|
|
1
|
+
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
12
|
+
key = keys[i];
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
14
|
+
__defProp(to, key, {
|
|
15
|
+
get: ((k) => from[k]).bind(null, key),
|
|
16
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return to;
|
|
22
|
+
};
|
|
23
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
24
|
+
value: mod,
|
|
25
|
+
enumerable: true
|
|
26
|
+
}) : target, mod));
|
|
27
|
+
|
|
28
|
+
//#endregion
|
|
29
|
+
let node_fs = require("node:fs");
|
|
30
|
+
let node_path = require("node:path");
|
|
31
|
+
let _modelcontextprotocol_sdk_server_mcp_js = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
32
|
+
let _modelcontextprotocol_sdk_server_stdio_js = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
33
|
+
let node_crypto = require("node:crypto");
|
|
34
|
+
node_crypto = __toESM(node_crypto);
|
|
35
|
+
let zod_v4 = require("zod/v4");
|
|
36
|
+
|
|
37
|
+
//#region src/utils/map-status.ts
|
|
38
|
+
const ONES_STATUS_MAP = {
|
|
39
|
+
to_do: "open",
|
|
40
|
+
in_progress: "in_progress",
|
|
41
|
+
done: "done",
|
|
42
|
+
closed: "closed"
|
|
43
|
+
};
|
|
44
|
+
const ONES_PRIORITY_MAP = {
|
|
45
|
+
urgent: "critical",
|
|
46
|
+
high: "high",
|
|
47
|
+
normal: "medium",
|
|
48
|
+
medium: "medium",
|
|
49
|
+
low: "low"
|
|
50
|
+
};
|
|
51
|
+
const ONES_TYPE_MAP = {
|
|
52
|
+
demand: "feature",
|
|
53
|
+
需求: "feature",
|
|
54
|
+
task: "task",
|
|
55
|
+
任务: "task",
|
|
56
|
+
bug: "bug",
|
|
57
|
+
缺陷: "bug",
|
|
58
|
+
story: "story",
|
|
59
|
+
子任务: "task",
|
|
60
|
+
工单: "task",
|
|
61
|
+
测试任务: "task"
|
|
62
|
+
};
|
|
63
|
+
function mapOnesStatus(status) {
|
|
64
|
+
return ONES_STATUS_MAP[status.toLowerCase()] ?? "open";
|
|
65
|
+
}
|
|
66
|
+
function mapOnesPriority(priority) {
|
|
67
|
+
return ONES_PRIORITY_MAP[priority.toLowerCase()] ?? "medium";
|
|
68
|
+
}
|
|
69
|
+
function mapOnesType(type) {
|
|
70
|
+
return ONES_TYPE_MAP[type.toLowerCase()] ?? "task";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
//#endregion
|
|
74
|
+
//#region src/adapters/base.ts
|
|
75
|
+
/**
|
|
76
|
+
* Abstract base class for source adapters.
|
|
77
|
+
* Each adapter implements platform-specific logic for fetching requirements.
|
|
78
|
+
*/
|
|
79
|
+
var BaseAdapter = class {
|
|
80
|
+
sourceType;
|
|
81
|
+
config;
|
|
82
|
+
resolvedAuth;
|
|
83
|
+
constructor(sourceType, config, resolvedAuth) {
|
|
84
|
+
this.sourceType = sourceType;
|
|
85
|
+
this.config = config;
|
|
86
|
+
this.resolvedAuth = resolvedAuth;
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/adapters/ones.ts
|
|
92
|
+
const TASK_DETAIL_QUERY = `
|
|
93
|
+
query Task($key: Key) {
|
|
94
|
+
task(key: $key) {
|
|
95
|
+
key uuid number name
|
|
96
|
+
issueType { uuid name }
|
|
97
|
+
status { uuid name category }
|
|
98
|
+
priority { value }
|
|
99
|
+
assign { uuid name }
|
|
100
|
+
owner { uuid name }
|
|
101
|
+
project { uuid name }
|
|
102
|
+
parent { uuid number issueType { uuid name } }
|
|
103
|
+
relatedTasks {
|
|
104
|
+
uuid number name
|
|
105
|
+
issueType { uuid name }
|
|
106
|
+
status { uuid name category }
|
|
107
|
+
assign { uuid name }
|
|
108
|
+
}
|
|
109
|
+
relatedWikiPages {
|
|
110
|
+
uuid
|
|
111
|
+
title
|
|
112
|
+
referenceType
|
|
113
|
+
subReferenceType
|
|
114
|
+
errorMessage
|
|
115
|
+
}
|
|
116
|
+
relatedWikiPagesCount
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
`;
|
|
120
|
+
const SEARCH_TASKS_QUERY = `
|
|
121
|
+
query GROUP_TASK_DATA($groupBy: GroupBy, $groupOrderBy: OrderBy, $orderBy: OrderBy, $filterGroup: [Filter!], $search: Search, $pagination: Pagination, $limit: Int) {
|
|
122
|
+
buckets(groupBy: $groupBy, orderBy: $groupOrderBy, pagination: $pagination, filter: $search) {
|
|
123
|
+
key
|
|
124
|
+
tasks(filterGroup: $filterGroup, orderBy: $orderBy, limit: $limit, includeAncestors: { pathField: "path" }) {
|
|
125
|
+
key uuid number name
|
|
126
|
+
issueType { uuid name }
|
|
127
|
+
status { uuid name category }
|
|
128
|
+
priority { value }
|
|
129
|
+
assign { uuid name }
|
|
130
|
+
project { uuid name }
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
`;
|
|
135
|
+
const TASK_BY_NUMBER_QUERY = SEARCH_TASKS_QUERY;
|
|
136
|
+
const DEFAULT_STATUS_NOT_IN = [
|
|
137
|
+
"FgMGkcaq",
|
|
138
|
+
"NvRwHBSo",
|
|
139
|
+
"Dn3k8ffK",
|
|
140
|
+
"TbmY2So5"
|
|
141
|
+
];
|
|
142
|
+
function base64Url(buffer) {
|
|
143
|
+
return buffer.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
|
144
|
+
}
|
|
145
|
+
function getSetCookies(response) {
|
|
146
|
+
const headers = response.headers;
|
|
147
|
+
if (headers.getSetCookie) return headers.getSetCookie();
|
|
148
|
+
const raw = response.headers.get("set-cookie");
|
|
149
|
+
return raw ? [raw] : [];
|
|
150
|
+
}
|
|
151
|
+
function toRequirement(task, description = "") {
|
|
152
|
+
return {
|
|
153
|
+
id: task.uuid,
|
|
154
|
+
source: "ones",
|
|
155
|
+
title: `#${task.number} ${task.name}`,
|
|
156
|
+
description,
|
|
157
|
+
status: mapOnesStatus(task.status?.category ?? "to_do"),
|
|
158
|
+
priority: mapOnesPriority(task.priority?.value ?? "normal"),
|
|
159
|
+
type: mapOnesType(task.issueType?.name ?? "任务"),
|
|
160
|
+
labels: [],
|
|
161
|
+
reporter: "",
|
|
162
|
+
assignee: task.assign?.name ?? null,
|
|
163
|
+
createdAt: "",
|
|
164
|
+
updatedAt: "",
|
|
165
|
+
dueDate: null,
|
|
166
|
+
attachments: [],
|
|
167
|
+
raw: task
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
var OnesAdapter = class extends BaseAdapter {
|
|
171
|
+
session = null;
|
|
172
|
+
constructor(sourceType, config, resolvedAuth) {
|
|
173
|
+
super(sourceType, config, resolvedAuth);
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* ONES OAuth2 PKCE login flow.
|
|
177
|
+
* Reference: D:\company code\ones\packages\core\src\auth.ts
|
|
178
|
+
*/
|
|
179
|
+
async login() {
|
|
180
|
+
if (this.session && Date.now() < this.session.expiresAt) return this.session;
|
|
181
|
+
const baseUrl = this.config.apiBase;
|
|
182
|
+
const email = this.resolvedAuth.email;
|
|
183
|
+
const password = this.resolvedAuth.password;
|
|
184
|
+
if (!email || !password) throw new Error("ONES auth requires email and password (ones-pkce auth type)");
|
|
185
|
+
const certRes = await fetch(`${baseUrl}/identity/api/encryption_cert`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: { "Content-Type": "application/json" },
|
|
188
|
+
body: "{}"
|
|
189
|
+
});
|
|
190
|
+
if (!certRes.ok) throw new Error(`ONES: Failed to get encryption cert: ${certRes.status}`);
|
|
191
|
+
const cert = await certRes.json();
|
|
192
|
+
const encryptedPassword = node_crypto.default.publicEncrypt({
|
|
193
|
+
key: cert.public_key,
|
|
194
|
+
padding: node_crypto.default.constants.RSA_PKCS1_PADDING
|
|
195
|
+
}, Buffer.from(password, "utf-8")).toString("base64");
|
|
196
|
+
const loginRes = await fetch(`${baseUrl}/identity/api/login`, {
|
|
197
|
+
method: "POST",
|
|
198
|
+
headers: { "Content-Type": "application/json" },
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
email,
|
|
201
|
+
password: encryptedPassword
|
|
202
|
+
})
|
|
203
|
+
});
|
|
204
|
+
if (!loginRes.ok) {
|
|
205
|
+
const text = await loginRes.text().catch(() => "");
|
|
206
|
+
throw new Error(`ONES: Login failed: ${loginRes.status} ${text}`);
|
|
207
|
+
}
|
|
208
|
+
const cookies = getSetCookies(loginRes).map((cookie) => cookie.split(";")[0]).join("; ");
|
|
209
|
+
const loginData = await loginRes.json();
|
|
210
|
+
const orgUuid = this.config.options?.orgUuid;
|
|
211
|
+
let orgUser = loginData.org_users[0];
|
|
212
|
+
if (orgUuid) {
|
|
213
|
+
const match = loginData.org_users.find((u) => u.org_uuid === orgUuid);
|
|
214
|
+
if (match) orgUser = match;
|
|
215
|
+
}
|
|
216
|
+
const codeVerifier = base64Url(node_crypto.default.randomBytes(32));
|
|
217
|
+
const codeChallenge = base64Url(node_crypto.default.createHash("sha256").update(codeVerifier).digest());
|
|
218
|
+
const authorizeParams = new URLSearchParams({
|
|
219
|
+
client_id: "ones.v1",
|
|
220
|
+
scope: `openid offline_access ones:org:${orgUser.region_uuid}:${orgUser.org_uuid}:${orgUser.org_user.org_user_uuid}`,
|
|
221
|
+
response_type: "code",
|
|
222
|
+
code_challenge_method: "S256",
|
|
223
|
+
code_challenge: codeChallenge,
|
|
224
|
+
redirect_uri: `${baseUrl}/auth/authorize/callback`,
|
|
225
|
+
state: `org_uuid=${orgUser.org_uuid}`
|
|
226
|
+
});
|
|
227
|
+
const authorizeLocation = (await fetch(`${baseUrl}/identity/authorize`, {
|
|
228
|
+
method: "POST",
|
|
229
|
+
headers: {
|
|
230
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
231
|
+
"Cookie": cookies
|
|
232
|
+
},
|
|
233
|
+
body: authorizeParams.toString(),
|
|
234
|
+
redirect: "manual"
|
|
235
|
+
})).headers.get("location");
|
|
236
|
+
if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
|
|
237
|
+
const authRequestId = new URL(authorizeLocation).searchParams.get("id");
|
|
238
|
+
if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
|
|
239
|
+
const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
|
|
240
|
+
method: "POST",
|
|
241
|
+
headers: {
|
|
242
|
+
"Content-Type": "application/json;charset=UTF-8",
|
|
243
|
+
"Cookie": cookies
|
|
244
|
+
},
|
|
245
|
+
body: JSON.stringify({
|
|
246
|
+
auth_request_id: authRequestId,
|
|
247
|
+
region_uuid: orgUser.region_uuid,
|
|
248
|
+
org_uuid: orgUser.org_uuid,
|
|
249
|
+
org_user_uuid: orgUser.org_user.org_user_uuid
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
if (!finalizeRes.ok) {
|
|
253
|
+
const text = await finalizeRes.text().catch(() => "");
|
|
254
|
+
throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
|
|
255
|
+
}
|
|
256
|
+
const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
|
|
257
|
+
method: "GET",
|
|
258
|
+
headers: { Cookie: cookies },
|
|
259
|
+
redirect: "manual"
|
|
260
|
+
})).headers.get("location");
|
|
261
|
+
if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
|
|
262
|
+
const code = new URL(callbackLocation).searchParams.get("code");
|
|
263
|
+
if (!code) throw new Error("ONES: Cannot parse authorization code from callback redirect");
|
|
264
|
+
const tokenRes = await fetch(`${baseUrl}/identity/oauth/token`, {
|
|
265
|
+
method: "POST",
|
|
266
|
+
headers: {
|
|
267
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
268
|
+
"Cookie": cookies
|
|
269
|
+
},
|
|
270
|
+
body: new URLSearchParams({
|
|
271
|
+
grant_type: "authorization_code",
|
|
272
|
+
client_id: "ones.v1",
|
|
273
|
+
code,
|
|
274
|
+
code_verifier: codeVerifier,
|
|
275
|
+
redirect_uri: `${baseUrl}/auth/authorize/callback`
|
|
276
|
+
}).toString()
|
|
277
|
+
});
|
|
278
|
+
if (!tokenRes.ok) {
|
|
279
|
+
const text = await tokenRes.text().catch(() => "");
|
|
280
|
+
throw new Error(`ONES: Token exchange failed: ${tokenRes.status} ${text}`);
|
|
281
|
+
}
|
|
282
|
+
const token = await tokenRes.json();
|
|
283
|
+
const teamsRes = await fetch(`${baseUrl}/project/api/project/organization/${orgUser.org_uuid}/stamps/data?t=org_my_team`, {
|
|
284
|
+
method: "POST",
|
|
285
|
+
headers: {
|
|
286
|
+
"Authorization": `Bearer ${token.access_token}`,
|
|
287
|
+
"Content-Type": "application/json;charset=UTF-8"
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify({ org_my_team: 0 })
|
|
290
|
+
});
|
|
291
|
+
if (!teamsRes.ok) throw new Error(`ONES: Failed to fetch teams: ${teamsRes.status}`);
|
|
292
|
+
const teams = (await teamsRes.json()).org_my_team?.teams ?? [];
|
|
293
|
+
const configTeamUuid = this.config.options?.teamUuid;
|
|
294
|
+
let teamUuid = teams[0]?.uuid;
|
|
295
|
+
if (configTeamUuid) {
|
|
296
|
+
const match = teams.find((t) => t.uuid === configTeamUuid);
|
|
297
|
+
if (match) teamUuid = match.uuid;
|
|
298
|
+
}
|
|
299
|
+
if (!teamUuid) throw new Error("ONES: No teams found for this user");
|
|
300
|
+
this.session = {
|
|
301
|
+
accessToken: token.access_token,
|
|
302
|
+
teamUuid,
|
|
303
|
+
orgUuid: orgUser.org_uuid,
|
|
304
|
+
expiresAt: Date.now() + (token.expires_in - 60) * 1e3
|
|
305
|
+
};
|
|
306
|
+
return this.session;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Execute a GraphQL query against ONES project API.
|
|
310
|
+
*/
|
|
311
|
+
async graphql(query, variables, tag) {
|
|
312
|
+
const session = await this.login();
|
|
313
|
+
const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/items/graphql${tag ? `?t=${encodeURIComponent(tag)}` : ""}`;
|
|
314
|
+
const response = await fetch(url, {
|
|
315
|
+
method: "POST",
|
|
316
|
+
headers: {
|
|
317
|
+
"Authorization": `Bearer ${session.accessToken}`,
|
|
318
|
+
"Content-Type": "application/json"
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify({
|
|
321
|
+
query,
|
|
322
|
+
variables
|
|
323
|
+
})
|
|
324
|
+
});
|
|
325
|
+
if (!response.ok) {
|
|
326
|
+
const text = await response.text().catch(() => "");
|
|
327
|
+
throw new Error(`ONES GraphQL error: ${response.status} ${text}`);
|
|
328
|
+
}
|
|
329
|
+
return response.json();
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Fetch task info via REST API (includes description/rich fields not available in GraphQL).
|
|
333
|
+
* Reference: ones/packages/core/src/tasks.ts → fetchTaskInfo
|
|
334
|
+
*/
|
|
335
|
+
async fetchTaskInfo(taskUuid) {
|
|
336
|
+
const session = await this.login();
|
|
337
|
+
const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/task/${taskUuid}/info`;
|
|
338
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
339
|
+
if (!response.ok) return {};
|
|
340
|
+
return response.json();
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Fetch wiki page content via REST API.
|
|
344
|
+
* Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
|
|
345
|
+
*/
|
|
346
|
+
async fetchWikiContent(wikiUuid) {
|
|
347
|
+
const session = await this.login();
|
|
348
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
|
|
349
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
350
|
+
if (!response.ok) return "";
|
|
351
|
+
return (await response.json()).content ?? "";
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Fetch a single task by UUID or number (e.g. "#95945" or "95945").
|
|
355
|
+
* If a number is given, searches first to resolve the UUID.
|
|
356
|
+
*/
|
|
357
|
+
async getRequirement(params) {
|
|
358
|
+
let taskUuid = params.id;
|
|
359
|
+
const numMatch = taskUuid.match(/^#?(\d+)$/);
|
|
360
|
+
if (numMatch) {
|
|
361
|
+
const taskNumber = Number.parseInt(numMatch[1], 10);
|
|
362
|
+
const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
|
|
363
|
+
groupBy: { tasks: {} },
|
|
364
|
+
groupOrderBy: null,
|
|
365
|
+
orderBy: { createTime: "DESC" },
|
|
366
|
+
filterGroup: [{ number_in: [taskNumber] }],
|
|
367
|
+
search: null,
|
|
368
|
+
pagination: {
|
|
369
|
+
limit: 10,
|
|
370
|
+
preciseCount: false
|
|
371
|
+
},
|
|
372
|
+
limit: 10
|
|
373
|
+
}, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === taskNumber);
|
|
374
|
+
if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
|
|
375
|
+
taskUuid = found.uuid;
|
|
376
|
+
}
|
|
377
|
+
const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
|
|
378
|
+
if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
|
|
379
|
+
const wikiPages = task.relatedWikiPages ?? [];
|
|
380
|
+
const wikiContents = await Promise.all(wikiPages.filter((w) => !w.errorMessage).map(async (wiki) => {
|
|
381
|
+
const content = await this.fetchWikiContent(wiki.uuid);
|
|
382
|
+
return {
|
|
383
|
+
title: wiki.title,
|
|
384
|
+
uuid: wiki.uuid,
|
|
385
|
+
content
|
|
386
|
+
};
|
|
387
|
+
}));
|
|
388
|
+
const parts = [];
|
|
389
|
+
parts.push(`# #${task.number} ${task.name}`);
|
|
390
|
+
parts.push("");
|
|
391
|
+
parts.push(`- **Type**: ${task.issueType?.name ?? "Unknown"}`);
|
|
392
|
+
parts.push(`- **Status**: ${task.status?.name ?? "Unknown"}`);
|
|
393
|
+
parts.push(`- **Assignee**: ${task.assign?.name ?? "Unassigned"}`);
|
|
394
|
+
if (task.owner?.name) parts.push(`- **Owner**: ${task.owner.name}`);
|
|
395
|
+
if (task.project?.name) parts.push(`- **Project**: ${task.project.name}`);
|
|
396
|
+
parts.push(`- **UUID**: ${task.uuid}`);
|
|
397
|
+
if (task.relatedTasks?.length) {
|
|
398
|
+
parts.push("");
|
|
399
|
+
parts.push("## Related Tasks");
|
|
400
|
+
for (const related of task.relatedTasks) parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name})`);
|
|
401
|
+
}
|
|
402
|
+
if (task.parent?.uuid) {
|
|
403
|
+
parts.push("");
|
|
404
|
+
parts.push("## Parent Task");
|
|
405
|
+
parts.push(`- UUID: ${task.parent.uuid}`);
|
|
406
|
+
if (task.parent.number) parts.push(`- Number: #${task.parent.number}`);
|
|
407
|
+
}
|
|
408
|
+
if (wikiContents.length > 0) {
|
|
409
|
+
parts.push("");
|
|
410
|
+
parts.push("---");
|
|
411
|
+
parts.push("");
|
|
412
|
+
parts.push("## Requirement Documents");
|
|
413
|
+
for (const wiki of wikiContents) {
|
|
414
|
+
parts.push("");
|
|
415
|
+
parts.push(`### ${wiki.title}`);
|
|
416
|
+
parts.push("");
|
|
417
|
+
if (wiki.content) parts.push(wiki.content);
|
|
418
|
+
else parts.push("(No content available)");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
return toRequirement(task, parts.join("\n"));
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Search tasks assigned to current user via GraphQL.
|
|
425
|
+
* Uses keyword-based local filtering (matching ONES reference implementation).
|
|
426
|
+
*/
|
|
427
|
+
async searchRequirements(params) {
|
|
428
|
+
const page = params.page ?? 1;
|
|
429
|
+
const pageSize = params.pageSize ?? 50;
|
|
430
|
+
let tasks = (await this.graphql(SEARCH_TASKS_QUERY, {
|
|
431
|
+
groupBy: { tasks: {} },
|
|
432
|
+
groupOrderBy: null,
|
|
433
|
+
orderBy: {
|
|
434
|
+
position: "ASC",
|
|
435
|
+
createTime: "DESC"
|
|
436
|
+
},
|
|
437
|
+
filterGroup: [{
|
|
438
|
+
assign_in: ["${currentUser}"],
|
|
439
|
+
status_notIn: DEFAULT_STATUS_NOT_IN
|
|
440
|
+
}],
|
|
441
|
+
search: null,
|
|
442
|
+
pagination: {
|
|
443
|
+
limit: pageSize * page,
|
|
444
|
+
preciseCount: false
|
|
445
|
+
},
|
|
446
|
+
limit: 1e3
|
|
447
|
+
}, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? [];
|
|
448
|
+
if (params.query) {
|
|
449
|
+
const keyword = params.query;
|
|
450
|
+
const lower = keyword.toLowerCase();
|
|
451
|
+
const numMatch = keyword.match(/^#?(\d+)$/);
|
|
452
|
+
if (numMatch) tasks = tasks.filter((t) => t.number === Number.parseInt(numMatch[1], 10));
|
|
453
|
+
else tasks = tasks.filter((t) => t.name.toLowerCase().includes(lower));
|
|
454
|
+
}
|
|
455
|
+
const total = tasks.length;
|
|
456
|
+
const start = (page - 1) * pageSize;
|
|
457
|
+
return {
|
|
458
|
+
items: tasks.slice(start, start + pageSize).map((t) => toRequirement(t)),
|
|
459
|
+
total,
|
|
460
|
+
page,
|
|
461
|
+
pageSize
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
};
|
|
465
|
+
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/adapters/index.ts
|
|
468
|
+
const ADAPTER_MAP = { ones: OnesAdapter };
|
|
469
|
+
/**
|
|
470
|
+
* Factory function to create the appropriate adapter based on source type.
|
|
471
|
+
*/
|
|
472
|
+
function createAdapter(sourceType, config, resolvedAuth) {
|
|
473
|
+
const AdapterClass = ADAPTER_MAP[sourceType];
|
|
474
|
+
if (!AdapterClass) throw new Error(`Unsupported source type: "${sourceType}". Supported: ${Object.keys(ADAPTER_MAP).join(", ")}`);
|
|
475
|
+
return new AdapterClass(sourceType, config, resolvedAuth);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
//#endregion
|
|
479
|
+
//#region src/config/loader.ts
|
|
480
|
+
const AuthSchema = zod_v4.z.discriminatedUnion("type", [
|
|
481
|
+
zod_v4.z.object({
|
|
482
|
+
type: zod_v4.z.literal("token"),
|
|
483
|
+
tokenEnv: zod_v4.z.string()
|
|
484
|
+
}),
|
|
485
|
+
zod_v4.z.object({
|
|
486
|
+
type: zod_v4.z.literal("basic"),
|
|
487
|
+
usernameEnv: zod_v4.z.string(),
|
|
488
|
+
passwordEnv: zod_v4.z.string()
|
|
489
|
+
}),
|
|
490
|
+
zod_v4.z.object({
|
|
491
|
+
type: zod_v4.z.literal("oauth2"),
|
|
492
|
+
clientIdEnv: zod_v4.z.string(),
|
|
493
|
+
clientSecretEnv: zod_v4.z.string(),
|
|
494
|
+
tokenUrl: zod_v4.z.string().url()
|
|
495
|
+
}),
|
|
496
|
+
zod_v4.z.object({
|
|
497
|
+
type: zod_v4.z.literal("cookie"),
|
|
498
|
+
cookieEnv: zod_v4.z.string()
|
|
499
|
+
}),
|
|
500
|
+
zod_v4.z.object({
|
|
501
|
+
type: zod_v4.z.literal("custom"),
|
|
502
|
+
headerName: zod_v4.z.string(),
|
|
503
|
+
valueEnv: zod_v4.z.string()
|
|
504
|
+
}),
|
|
505
|
+
zod_v4.z.object({
|
|
506
|
+
type: zod_v4.z.literal("ones-pkce"),
|
|
507
|
+
emailEnv: zod_v4.z.string(),
|
|
508
|
+
passwordEnv: zod_v4.z.string()
|
|
509
|
+
})
|
|
510
|
+
]);
|
|
511
|
+
const SourceConfigSchema = zod_v4.z.object({
|
|
512
|
+
enabled: zod_v4.z.boolean(),
|
|
513
|
+
apiBase: zod_v4.z.string().url(),
|
|
514
|
+
auth: AuthSchema,
|
|
515
|
+
headers: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.string()).optional(),
|
|
516
|
+
options: zod_v4.z.record(zod_v4.z.string(), zod_v4.z.unknown()).optional()
|
|
517
|
+
});
|
|
518
|
+
const SourcesSchema = zod_v4.z.object({ ones: SourceConfigSchema.optional() });
|
|
519
|
+
const McpConfigSchema = zod_v4.z.object({
|
|
520
|
+
sources: SourcesSchema,
|
|
521
|
+
defaultSource: zod_v4.z.enum(["ones"]).optional()
|
|
522
|
+
});
|
|
523
|
+
const CONFIG_FILENAME = ".requirements-mcp.json";
|
|
524
|
+
/**
|
|
525
|
+
* Search for config file starting from `startDir` and walking up to the root.
|
|
526
|
+
*/
|
|
527
|
+
function findConfigFile(startDir) {
|
|
528
|
+
let dir = (0, node_path.resolve)(startDir);
|
|
529
|
+
while (true) {
|
|
530
|
+
const candidate = (0, node_path.resolve)(dir, CONFIG_FILENAME);
|
|
531
|
+
if ((0, node_fs.existsSync)(candidate)) return candidate;
|
|
532
|
+
const parent = (0, node_path.dirname)(dir);
|
|
533
|
+
if (parent === dir) break;
|
|
534
|
+
dir = parent;
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Resolve environment variable references in auth config.
|
|
540
|
+
* Reads actual env var values for fields ending with "Env".
|
|
541
|
+
*/
|
|
542
|
+
function resolveAuthEnv(auth) {
|
|
543
|
+
const resolved = {};
|
|
544
|
+
for (const [key, value] of Object.entries(auth)) {
|
|
545
|
+
if (key === "type") continue;
|
|
546
|
+
if (key.endsWith("Env") && typeof value === "string") {
|
|
547
|
+
const envValue = process.env[value];
|
|
548
|
+
if (!envValue) throw new Error(`Environment variable "${value}" is not set (required by auth.${key})`);
|
|
549
|
+
const resolvedKey = key.slice(0, -3);
|
|
550
|
+
resolved[resolvedKey] = envValue;
|
|
551
|
+
} else if (typeof value === "string") resolved[key] = value;
|
|
552
|
+
}
|
|
553
|
+
return resolved;
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Load and validate the MCP config file.
|
|
557
|
+
* Searches from `startDir` (defaults to cwd) upward.
|
|
558
|
+
*/
|
|
559
|
+
function loadConfig(startDir) {
|
|
560
|
+
const dir = startDir ?? process.cwd();
|
|
561
|
+
const configPath = findConfigFile(dir);
|
|
562
|
+
if (!configPath) throw new Error(`Config file "${CONFIG_FILENAME}" not found. Searched from "${dir}" to root. Create one based on .requirements-mcp.json.example`);
|
|
563
|
+
const raw = (0, node_fs.readFileSync)(configPath, "utf-8");
|
|
564
|
+
let parsed;
|
|
565
|
+
try {
|
|
566
|
+
parsed = JSON.parse(raw);
|
|
567
|
+
} catch {
|
|
568
|
+
throw new Error(`Invalid JSON in ${configPath}`);
|
|
569
|
+
}
|
|
570
|
+
const result = McpConfigSchema.safeParse(parsed);
|
|
571
|
+
if (!result.success) throw new Error(`Invalid config in ${configPath}:\n${result.error.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`);
|
|
572
|
+
const config = result.data;
|
|
573
|
+
const sources = [];
|
|
574
|
+
for (const [type, sourceConfig] of Object.entries(config.sources)) if (sourceConfig && sourceConfig.enabled) {
|
|
575
|
+
const resolvedAuth = resolveAuthEnv(sourceConfig.auth);
|
|
576
|
+
sources.push({
|
|
577
|
+
type,
|
|
578
|
+
config: sourceConfig,
|
|
579
|
+
resolvedAuth
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
if (sources.length === 0) throw new Error("No enabled sources found in config. Enable at least one source.");
|
|
583
|
+
return {
|
|
584
|
+
config,
|
|
585
|
+
sources,
|
|
586
|
+
configPath
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/tools/get-requirement.ts
|
|
592
|
+
const GetRequirementSchema = zod_v4.z.object({
|
|
593
|
+
id: zod_v4.z.string().describe("The requirement/issue ID"),
|
|
594
|
+
source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
595
|
+
});
|
|
596
|
+
async function handleGetRequirement(input, adapters, defaultSource) {
|
|
597
|
+
const sourceType = input.source ?? defaultSource;
|
|
598
|
+
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
599
|
+
const adapter = adapters.get(sourceType);
|
|
600
|
+
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
601
|
+
return { content: [{
|
|
602
|
+
type: "text",
|
|
603
|
+
text: formatRequirement(await adapter.getRequirement({ id: input.id }))
|
|
604
|
+
}] };
|
|
605
|
+
}
|
|
606
|
+
function formatRequirement(req) {
|
|
607
|
+
const lines = [
|
|
608
|
+
`# ${req.title}`,
|
|
609
|
+
"",
|
|
610
|
+
`- **ID**: ${req.id}`,
|
|
611
|
+
`- **Source**: ${req.source}`,
|
|
612
|
+
`- **Status**: ${req.status}`,
|
|
613
|
+
`- **Priority**: ${req.priority}`,
|
|
614
|
+
`- **Type**: ${req.type}`,
|
|
615
|
+
`- **Assignee**: ${req.assignee ?? "Unassigned"}`,
|
|
616
|
+
`- **Reporter**: ${req.reporter || "Unknown"}`
|
|
617
|
+
];
|
|
618
|
+
if (req.createdAt) lines.push(`- **Created**: ${req.createdAt}`);
|
|
619
|
+
if (req.updatedAt) lines.push(`- **Updated**: ${req.updatedAt}`);
|
|
620
|
+
if (req.dueDate) lines.push(`- **Due**: ${req.dueDate}`);
|
|
621
|
+
if (req.labels.length > 0) lines.push(`- **Labels**: ${req.labels.join(", ")}`);
|
|
622
|
+
lines.push("", "## Description", "", req.description || "_No description_");
|
|
623
|
+
if (req.attachments.length > 0) {
|
|
624
|
+
lines.push("", "## Attachments");
|
|
625
|
+
for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
|
|
626
|
+
}
|
|
627
|
+
return lines.join("\n");
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/tools/list-sources.ts
|
|
632
|
+
async function handleListSources(adapters, config) {
|
|
633
|
+
const lines = ["# Configured Sources", ""];
|
|
634
|
+
if (adapters.size === 0) {
|
|
635
|
+
lines.push("No sources configured.");
|
|
636
|
+
return { content: [{
|
|
637
|
+
type: "text",
|
|
638
|
+
text: lines.join("\n")
|
|
639
|
+
}] };
|
|
640
|
+
}
|
|
641
|
+
for (const [type, adapter] of adapters) {
|
|
642
|
+
const isDefault = config.defaultSource === type;
|
|
643
|
+
const sourceConfig = config.sources[adapter.sourceType];
|
|
644
|
+
lines.push(`## ${type}${isDefault ? " (default)" : ""}`);
|
|
645
|
+
lines.push(`- **API Base**: ${sourceConfig?.apiBase ?? "N/A"}`);
|
|
646
|
+
lines.push(`- **Auth Type**: ${sourceConfig?.auth.type ?? "N/A"}`);
|
|
647
|
+
lines.push("");
|
|
648
|
+
}
|
|
649
|
+
if (config.defaultSource) lines.push(`> Default source: **${config.defaultSource}**`);
|
|
650
|
+
return { content: [{
|
|
651
|
+
type: "text",
|
|
652
|
+
text: lines.join("\n")
|
|
653
|
+
}] };
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/tools/search-requirements.ts
|
|
658
|
+
const SearchRequirementsSchema = zod_v4.z.object({
|
|
659
|
+
query: zod_v4.z.string().describe("Search keywords"),
|
|
660
|
+
source: zod_v4.z.string().optional().describe("Source to search. If omitted, searches the default source."),
|
|
661
|
+
page: zod_v4.z.number().int().min(1).optional().describe("Page number (default: 1)"),
|
|
662
|
+
pageSize: zod_v4.z.number().int().min(1).max(50).optional().describe("Results per page (default: 20, max: 50)")
|
|
663
|
+
});
|
|
664
|
+
async function handleSearchRequirements(input, adapters, defaultSource) {
|
|
665
|
+
const sourceType = input.source ?? defaultSource;
|
|
666
|
+
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
667
|
+
const adapter = adapters.get(sourceType);
|
|
668
|
+
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
669
|
+
const result = await adapter.searchRequirements({
|
|
670
|
+
query: input.query,
|
|
671
|
+
page: input.page,
|
|
672
|
+
pageSize: input.pageSize
|
|
673
|
+
});
|
|
674
|
+
const lines = [`Found **${result.total}** results (page ${result.page}/${Math.ceil(result.total / result.pageSize) || 1}):`, ""];
|
|
675
|
+
for (const item of result.items) {
|
|
676
|
+
lines.push(`### ${item.id}: ${item.title}`);
|
|
677
|
+
lines.push(`- Status: ${item.status} | Priority: ${item.priority} | Type: ${item.type}`);
|
|
678
|
+
lines.push(`- Assignee: ${item.assignee ?? "Unassigned"}`);
|
|
679
|
+
if (item.description) {
|
|
680
|
+
const desc = item.description.length > 200 ? `${item.description.slice(0, 200)}...` : item.description;
|
|
681
|
+
lines.push(`- ${desc}`);
|
|
682
|
+
}
|
|
683
|
+
lines.push("");
|
|
684
|
+
}
|
|
685
|
+
return { content: [{
|
|
686
|
+
type: "text",
|
|
687
|
+
text: lines.join("\n")
|
|
688
|
+
}] };
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
//#endregion
|
|
692
|
+
//#region src/index.ts
|
|
693
|
+
/**
|
|
694
|
+
* Load .env file into process.env (if it exists).
|
|
695
|
+
* Searches from cwd upward, same as config loader.
|
|
696
|
+
*/
|
|
697
|
+
function loadEnvFile() {
|
|
698
|
+
let dir = process.cwd();
|
|
699
|
+
while (true) {
|
|
700
|
+
const envPath = (0, node_path.resolve)(dir, ".env");
|
|
701
|
+
if ((0, node_fs.existsSync)(envPath)) {
|
|
702
|
+
const content = (0, node_fs.readFileSync)(envPath, "utf-8");
|
|
703
|
+
for (const line of content.split("\n")) {
|
|
704
|
+
const trimmed = line.trim();
|
|
705
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
706
|
+
const eqIndex = trimmed.indexOf("=");
|
|
707
|
+
if (eqIndex === -1) continue;
|
|
708
|
+
const key = trimmed.slice(0, eqIndex).trim();
|
|
709
|
+
let value = trimmed.slice(eqIndex + 1).trim();
|
|
710
|
+
if (value.startsWith("\"") && value.endsWith("\"") || value.startsWith("'") && value.endsWith("'")) value = value.slice(1, -1);
|
|
711
|
+
if (!process.env[key]) process.env[key] = value;
|
|
712
|
+
}
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const parent = (0, node_path.dirname)(dir);
|
|
716
|
+
if (parent === dir) break;
|
|
717
|
+
dir = parent;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
async function main() {
|
|
721
|
+
loadEnvFile();
|
|
722
|
+
let config;
|
|
723
|
+
try {
|
|
724
|
+
config = loadConfig();
|
|
725
|
+
} catch (err) {
|
|
726
|
+
console.error(`[requirements-mcp] ${err.message}`);
|
|
727
|
+
process.exit(1);
|
|
728
|
+
}
|
|
729
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
730
|
+
for (const source of config.sources) {
|
|
731
|
+
const adapter = createAdapter(source.type, source.config, source.resolvedAuth);
|
|
732
|
+
adapters.set(source.type, adapter);
|
|
733
|
+
}
|
|
734
|
+
const server = new _modelcontextprotocol_sdk_server_mcp_js.McpServer({
|
|
735
|
+
name: "ai-dev-requirements",
|
|
736
|
+
version: "0.1.0"
|
|
737
|
+
});
|
|
738
|
+
server.tool("get_requirement", "Fetch a single requirement/issue by its ID from a configured source (ONES)", GetRequirementSchema.shape, async (params) => {
|
|
739
|
+
try {
|
|
740
|
+
return await handleGetRequirement(params, adapters, config.config.defaultSource);
|
|
741
|
+
} catch (err) {
|
|
742
|
+
return {
|
|
743
|
+
content: [{
|
|
744
|
+
type: "text",
|
|
745
|
+
text: `Error: ${err.message}`
|
|
746
|
+
}],
|
|
747
|
+
isError: true
|
|
748
|
+
};
|
|
749
|
+
}
|
|
750
|
+
});
|
|
751
|
+
server.tool("search_requirements", "Search for requirements/issues by keywords across a configured source", SearchRequirementsSchema.shape, async (params) => {
|
|
752
|
+
try {
|
|
753
|
+
return await handleSearchRequirements(params, adapters, config.config.defaultSource);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
return {
|
|
756
|
+
content: [{
|
|
757
|
+
type: "text",
|
|
758
|
+
text: `Error: ${err.message}`
|
|
759
|
+
}],
|
|
760
|
+
isError: true
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
});
|
|
764
|
+
server.tool("list_sources", "List all configured requirement sources and their status", {}, async () => {
|
|
765
|
+
try {
|
|
766
|
+
return await handleListSources(adapters, config.config);
|
|
767
|
+
} catch (err) {
|
|
768
|
+
return {
|
|
769
|
+
content: [{
|
|
770
|
+
type: "text",
|
|
771
|
+
text: `Error: ${err.message}`
|
|
772
|
+
}],
|
|
773
|
+
isError: true
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
|
|
778
|
+
await server.connect(transport);
|
|
779
|
+
}
|
|
780
|
+
main().catch((err) => {
|
|
781
|
+
console.error("[requirements-mcp] Fatal error:", err);
|
|
782
|
+
process.exit(1);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
//#endregion
|