ai-dev-requirements 0.1.9 → 0.1.10
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 +68 -14
- package/README.zh-CN.md +68 -14
- package/dist/index.cjs +234 -15
- package/dist/index.mjs +234 -15
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/skills/dev-workflow/SKILL.md +186 -123
- package/skills/dev-workflow/references/task-types.md +148 -97
- package/skills/dev-workflow/references/templates/code-dev-task.md +34 -21
- package/skills/dev-workflow/references/templates/code-fix-task.md +34 -20
- package/skills/dev-workflow/references/templates/code-refactor-task.md +34 -22
- package/skills/dev-workflow/references/templates/doc-write-task.md +33 -21
- package/skills/dev-workflow/references/templates/research-task.md +34 -22
- package/skills/dev-workflow/references/templates/test-task.md +33 -22
- package/skills/dev-workflow/references/workflow.md +290 -118
package/dist/index.mjs
CHANGED
|
@@ -65,6 +65,9 @@ const TASK_DETAIL_QUERY = `
|
|
|
65
65
|
query Task($key: Key) {
|
|
66
66
|
task(key: $key) {
|
|
67
67
|
key uuid number name
|
|
68
|
+
description
|
|
69
|
+
descriptionText
|
|
70
|
+
desc_rich: description
|
|
68
71
|
issueType { uuid name }
|
|
69
72
|
status { uuid name category }
|
|
70
73
|
priority { value }
|
|
@@ -294,7 +297,120 @@ function getSetCookies(response) {
|
|
|
294
297
|
const raw = response.headers.get("set-cookie");
|
|
295
298
|
return raw ? [raw] : [];
|
|
296
299
|
}
|
|
297
|
-
function
|
|
300
|
+
function extractWikiPageUuidsFromText(text) {
|
|
301
|
+
if (!text) return [];
|
|
302
|
+
const uuids = /* @__PURE__ */ new Set();
|
|
303
|
+
for (const pattern of [/\/page\/([\w-]+)/g, /page=([\w-]+)/g]) for (const match of text.matchAll(pattern)) if (match[1]) uuids.add(match[1]);
|
|
304
|
+
return [...uuids];
|
|
305
|
+
}
|
|
306
|
+
function htmlToPlainText(html) {
|
|
307
|
+
return html.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>/gi, "\n").replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\n{3,}/g, "\n\n").trim();
|
|
308
|
+
}
|
|
309
|
+
function getTaskDetailText(task) {
|
|
310
|
+
return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
|
|
311
|
+
}
|
|
312
|
+
function isRecord(value) {
|
|
313
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
314
|
+
}
|
|
315
|
+
function parseJsonRecord(value) {
|
|
316
|
+
try {
|
|
317
|
+
const parsed = JSON.parse(value);
|
|
318
|
+
return isRecord(parsed) ? parsed : null;
|
|
319
|
+
} catch {
|
|
320
|
+
return null;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
function asWikiBlocks(value) {
|
|
324
|
+
if (!Array.isArray(value)) return [];
|
|
325
|
+
return value.filter(isRecord);
|
|
326
|
+
}
|
|
327
|
+
function renderWikiTextRuns(value) {
|
|
328
|
+
if (!Array.isArray(value)) return "";
|
|
329
|
+
return value.map((run) => {
|
|
330
|
+
if (!isRecord(run)) return "";
|
|
331
|
+
const attributes = isRecord(run.attributes) ? run.attributes : {};
|
|
332
|
+
const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
|
|
333
|
+
const link = typeof attributes.link === "string" ? attributes.link : "";
|
|
334
|
+
if (link && insert.trim()) return `[${insert}](${link})`;
|
|
335
|
+
const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
|
|
336
|
+
if (link && taskName) return `[${taskName}](${link})`;
|
|
337
|
+
return insert;
|
|
338
|
+
}).join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
339
|
+
}
|
|
340
|
+
function renderWikiHeading(text, heading) {
|
|
341
|
+
if (!heading) return text;
|
|
342
|
+
const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
|
|
343
|
+
return `${"#".repeat(level)} ${text}`;
|
|
344
|
+
}
|
|
345
|
+
function getWikiImageSource(block) {
|
|
346
|
+
const embedData = isRecord(block.embedData) ? block.embedData : {};
|
|
347
|
+
return typeof embedData.src === "string" ? embedData.src.trim() : "";
|
|
348
|
+
}
|
|
349
|
+
function renderWikiEmbed(block, context) {
|
|
350
|
+
if (block.embedType === "image") {
|
|
351
|
+
const src = getWikiImageSource(block);
|
|
352
|
+
if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
|
|
353
|
+
return src ? `[Image: ${src}]` : "[Image]";
|
|
354
|
+
}
|
|
355
|
+
return block.embedType ? `[Embed: ${block.embedType}]` : "";
|
|
356
|
+
}
|
|
357
|
+
function escapeWikiTableCell(value) {
|
|
358
|
+
return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
359
|
+
}
|
|
360
|
+
function renderWikiCell(value, document, context) {
|
|
361
|
+
const blocks = asWikiBlocks(value);
|
|
362
|
+
if (!blocks.length) return "";
|
|
363
|
+
return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
364
|
+
}
|
|
365
|
+
function renderWikiTable(block, document, context) {
|
|
366
|
+
const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
|
|
367
|
+
const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
|
|
368
|
+
if (!cols || !children.length) return "";
|
|
369
|
+
const rows = [];
|
|
370
|
+
for (let index = 0; index < children.length; index += cols) {
|
|
371
|
+
const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
|
|
372
|
+
while (cells.length < cols) cells.push("");
|
|
373
|
+
rows.push(`| ${cells.join(" | ")} |`);
|
|
374
|
+
}
|
|
375
|
+
if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
376
|
+
return rows.join("\n");
|
|
377
|
+
}
|
|
378
|
+
function renderWikiBlock(block, document, context) {
|
|
379
|
+
if (block.type === "table") return renderWikiTable(block, document, context);
|
|
380
|
+
if (block.type === "embed") return renderWikiEmbed(block, context);
|
|
381
|
+
const text = renderWikiTextRuns(block.text);
|
|
382
|
+
if (!text) return "";
|
|
383
|
+
if (block.type === "list") {
|
|
384
|
+
const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
|
|
385
|
+
return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
|
|
386
|
+
}
|
|
387
|
+
return renderWikiHeading(text, block.heading);
|
|
388
|
+
}
|
|
389
|
+
function renderWikiContent(content, context = { imageSources: [] }) {
|
|
390
|
+
const trimmed = content.trim();
|
|
391
|
+
if (!trimmed) return "";
|
|
392
|
+
const document = parseJsonRecord(trimmed);
|
|
393
|
+
if (!document) return trimmed;
|
|
394
|
+
if (!("blocks" in document)) return trimmed;
|
|
395
|
+
return asWikiBlocks(document.blocks).map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join("\n\n").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
|
|
396
|
+
}
|
|
397
|
+
function mimeTypeFromFileName(fileName) {
|
|
398
|
+
const normalized = fileName.toLowerCase();
|
|
399
|
+
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
|
|
400
|
+
if (normalized.endsWith(".gif")) return "image/gif";
|
|
401
|
+
if (normalized.endsWith(".webp")) return "image/webp";
|
|
402
|
+
if (normalized.endsWith(".svg")) return "image/svg+xml";
|
|
403
|
+
return "image/png";
|
|
404
|
+
}
|
|
405
|
+
function attachmentNameFromPath(path) {
|
|
406
|
+
const name = path.split("/").pop() || path;
|
|
407
|
+
try {
|
|
408
|
+
return decodeURIComponent(name);
|
|
409
|
+
} catch {
|
|
410
|
+
return name;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
function toRequirement(task, description = "", attachments = []) {
|
|
298
414
|
return {
|
|
299
415
|
id: task.uuid,
|
|
300
416
|
source: "ones",
|
|
@@ -309,7 +425,7 @@ function toRequirement(task, description = "") {
|
|
|
309
425
|
createdAt: "",
|
|
310
426
|
updatedAt: "",
|
|
311
427
|
dueDate: null,
|
|
312
|
-
attachments
|
|
428
|
+
attachments,
|
|
313
429
|
raw: task
|
|
314
430
|
};
|
|
315
431
|
}
|
|
@@ -589,12 +705,51 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
589
705
|
* Fetch wiki page content via REST API.
|
|
590
706
|
* Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
|
|
591
707
|
*/
|
|
708
|
+
async fetchWikiPageDetail(wikiUuid) {
|
|
709
|
+
const session = await this.login();
|
|
710
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/page/${wikiUuid}/detail`;
|
|
711
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
712
|
+
if (!response.ok) return {};
|
|
713
|
+
return response.json();
|
|
714
|
+
}
|
|
715
|
+
buildWikiImageUrl(session, refUuid, source, token) {
|
|
716
|
+
const encodedRefUuid = encodeURIComponent(refUuid);
|
|
717
|
+
const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
718
|
+
const encodedToken = encodeURIComponent(token);
|
|
719
|
+
return `${this.config.apiBase}/wiki/api/wiki/editor/${session.teamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
|
|
720
|
+
}
|
|
592
721
|
async fetchWikiContent(wikiUuid) {
|
|
593
722
|
const session = await this.login();
|
|
594
723
|
const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
|
|
595
724
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
596
|
-
if (!response.ok) return
|
|
597
|
-
|
|
725
|
+
if (!response.ok) return {
|
|
726
|
+
content: "",
|
|
727
|
+
attachments: []
|
|
728
|
+
};
|
|
729
|
+
const data = await response.json();
|
|
730
|
+
const renderContext = { imageSources: [] };
|
|
731
|
+
const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
|
|
732
|
+
const token = typeof data.token === "string" ? data.token : "";
|
|
733
|
+
if (!renderContext.imageSources.length || !token) return {
|
|
734
|
+
content,
|
|
735
|
+
attachments: []
|
|
736
|
+
};
|
|
737
|
+
const detail = await this.fetchWikiPageDetail(wikiUuid);
|
|
738
|
+
const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
|
|
739
|
+
if (!refUuid) return {
|
|
740
|
+
content,
|
|
741
|
+
attachments: []
|
|
742
|
+
};
|
|
743
|
+
return {
|
|
744
|
+
content,
|
|
745
|
+
attachments: renderContext.imageSources.map((source, index) => ({
|
|
746
|
+
id: `${wikiUuid}-image-${index + 1}`,
|
|
747
|
+
name: attachmentNameFromPath(source),
|
|
748
|
+
url: this.buildWikiImageUrl(session, refUuid, source, token),
|
|
749
|
+
mimeType: mimeTypeFromFileName(source),
|
|
750
|
+
size: 0
|
|
751
|
+
}))
|
|
752
|
+
};
|
|
598
753
|
}
|
|
599
754
|
/**
|
|
600
755
|
* Fetch a single task by UUID or number (e.g. "#95945" or "95945").
|
|
@@ -622,13 +777,27 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
622
777
|
}
|
|
623
778
|
const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
|
|
624
779
|
if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
|
|
780
|
+
const wikiRefs = /* @__PURE__ */ new Map();
|
|
781
|
+
for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
|
|
782
|
+
title: wiki.title,
|
|
783
|
+
uuid: wiki.uuid
|
|
784
|
+
});
|
|
785
|
+
const detailForLinkExtraction = [
|
|
786
|
+
task.description,
|
|
787
|
+
task.descriptionText,
|
|
788
|
+
task.desc_rich
|
|
789
|
+
].filter(Boolean).join("\n");
|
|
790
|
+
for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
|
|
791
|
+
title: `Wiki ${wikiUuid}`,
|
|
792
|
+
uuid: wikiUuid
|
|
793
|
+
});
|
|
794
|
+
const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
|
|
795
|
+
const rendered = await this.fetchWikiContent(wiki.uuid);
|
|
628
796
|
return {
|
|
629
797
|
title: wiki.title,
|
|
630
798
|
uuid: wiki.uuid,
|
|
631
|
-
content
|
|
799
|
+
content: rendered.content,
|
|
800
|
+
attachments: rendered.attachments
|
|
632
801
|
};
|
|
633
802
|
}));
|
|
634
803
|
const parts = [];
|
|
@@ -667,7 +836,18 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
667
836
|
else parts.push("(No content available)");
|
|
668
837
|
}
|
|
669
838
|
}
|
|
670
|
-
|
|
839
|
+
const detailText = getTaskDetailText(task);
|
|
840
|
+
const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
|
|
841
|
+
if (detailText && !hasWikiContent) {
|
|
842
|
+
parts.push("");
|
|
843
|
+
parts.push("---");
|
|
844
|
+
parts.push("");
|
|
845
|
+
parts.push("## Requirement Detail");
|
|
846
|
+
parts.push("");
|
|
847
|
+
parts.push(detailText);
|
|
848
|
+
}
|
|
849
|
+
const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
|
|
850
|
+
return toRequirement(task, parts.join("\n"), wikiAttachments);
|
|
671
851
|
}
|
|
672
852
|
/**
|
|
673
853
|
* Search tasks assigned to current user via GraphQL.
|
|
@@ -1088,7 +1268,7 @@ const GetIssueDetailSchema = z.object({
|
|
|
1088
1268
|
* Download an image from URL and return as base64 data URI.
|
|
1089
1269
|
* Returns null if download fails.
|
|
1090
1270
|
*/
|
|
1091
|
-
async function downloadImageAsBase64(url) {
|
|
1271
|
+
async function downloadImageAsBase64$1(url) {
|
|
1092
1272
|
try {
|
|
1093
1273
|
const res = await fetch(url, { redirect: "follow" });
|
|
1094
1274
|
if (!res.ok) return null;
|
|
@@ -1114,7 +1294,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
|
|
|
1114
1294
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1115
1295
|
const detail = await adapter.getIssueDetail({ issueId: input.issueId });
|
|
1116
1296
|
const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
|
|
1117
|
-
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
|
|
1297
|
+
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
|
|
1118
1298
|
const content = [{
|
|
1119
1299
|
type: "text",
|
|
1120
1300
|
text: formatIssueDetail(detail)
|
|
@@ -1200,15 +1380,54 @@ const GetRequirementSchema = z.object({
|
|
|
1200
1380
|
id: z.string().describe("The requirement/issue ID"),
|
|
1201
1381
|
source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1202
1382
|
});
|
|
1383
|
+
async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
|
|
1384
|
+
try {
|
|
1385
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1386
|
+
if (!res.ok) return null;
|
|
1387
|
+
const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
|
|
1388
|
+
return {
|
|
1389
|
+
base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
|
|
1390
|
+
mimeType
|
|
1391
|
+
};
|
|
1392
|
+
} catch {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
function isImageAttachment(attachment) {
|
|
1397
|
+
if (attachment.mimeType.startsWith("image/")) return true;
|
|
1398
|
+
return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
|
|
1399
|
+
}
|
|
1400
|
+
function displayAttachmentUrl(url) {
|
|
1401
|
+
try {
|
|
1402
|
+
const parsed = new URL(url);
|
|
1403
|
+
parsed.search = "";
|
|
1404
|
+
parsed.hash = "";
|
|
1405
|
+
return parsed.toString();
|
|
1406
|
+
} catch {
|
|
1407
|
+
return url.replace(/[?#].*$/, "");
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1203
1410
|
async function handleGetRequirement(input, adapters, defaultSource) {
|
|
1204
1411
|
const sourceType = input.source ?? defaultSource;
|
|
1205
1412
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
1206
1413
|
const adapter = adapters.get(sourceType);
|
|
1207
1414
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1208
|
-
|
|
1415
|
+
const requirement = await adapter.getRequirement({ id: input.id });
|
|
1416
|
+
const imageAttachments = requirement.attachments.filter(isImageAttachment);
|
|
1417
|
+
const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
|
|
1418
|
+
const content = [{
|
|
1209
1419
|
type: "text",
|
|
1210
|
-
text: formatRequirement(
|
|
1211
|
-
}]
|
|
1420
|
+
text: formatRequirement(requirement)
|
|
1421
|
+
}];
|
|
1422
|
+
for (const image of imageResults) {
|
|
1423
|
+
if (!image) continue;
|
|
1424
|
+
content.push({
|
|
1425
|
+
type: "image",
|
|
1426
|
+
data: image.base64,
|
|
1427
|
+
mimeType: image.mimeType
|
|
1428
|
+
});
|
|
1429
|
+
}
|
|
1430
|
+
return { content };
|
|
1212
1431
|
}
|
|
1213
1432
|
function formatRequirement(req) {
|
|
1214
1433
|
const lines = [
|
|
@@ -1229,7 +1448,7 @@ function formatRequirement(req) {
|
|
|
1229
1448
|
lines.push("", "## Description", "", req.description || "_No description_");
|
|
1230
1449
|
if (req.attachments.length > 0) {
|
|
1231
1450
|
lines.push("", "## Attachments");
|
|
1232
|
-
for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1451
|
+
for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1233
1452
|
}
|
|
1234
1453
|
return lines.join("\n");
|
|
1235
1454
|
}
|