ai-dev-requirements 0.1.9 → 0.1.11
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 +288 -21
- package/dist/index.mjs +288 -21
- 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,139 @@ 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 parseOnesWikiPageRoute(input) {
|
|
307
|
+
if (!input.includes("/wiki/")) return null;
|
|
308
|
+
const match = (() => {
|
|
309
|
+
try {
|
|
310
|
+
const parsed = new URL(input);
|
|
311
|
+
return `${parsed.pathname}${parsed.hash}${parsed.search}`;
|
|
312
|
+
} catch {
|
|
313
|
+
return input;
|
|
314
|
+
}
|
|
315
|
+
})().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
|
|
316
|
+
if (!match?.[1] || !match[2]) return null;
|
|
317
|
+
return {
|
|
318
|
+
teamUuid: decodeURIComponent(match[1]),
|
|
319
|
+
wikiUuid: decodeURIComponent(match[2])
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function isOnesWikiUrlInput(input) {
|
|
323
|
+
return input.includes("/wiki/");
|
|
324
|
+
}
|
|
325
|
+
function htmlToPlainText(html) {
|
|
326
|
+
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();
|
|
327
|
+
}
|
|
328
|
+
function getTaskDetailText(task) {
|
|
329
|
+
return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
|
|
330
|
+
}
|
|
331
|
+
function isRecord(value) {
|
|
332
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
333
|
+
}
|
|
334
|
+
function parseJsonRecord(value) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(value);
|
|
337
|
+
return isRecord(parsed) ? parsed : null;
|
|
338
|
+
} catch {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function asWikiBlocks(value) {
|
|
343
|
+
if (!Array.isArray(value)) return [];
|
|
344
|
+
return value.filter(isRecord);
|
|
345
|
+
}
|
|
346
|
+
function renderWikiTextRuns(value) {
|
|
347
|
+
if (!Array.isArray(value)) return "";
|
|
348
|
+
return value.map((run) => {
|
|
349
|
+
if (!isRecord(run)) return "";
|
|
350
|
+
const attributes = isRecord(run.attributes) ? run.attributes : {};
|
|
351
|
+
const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
|
|
352
|
+
const link = typeof attributes.link === "string" ? attributes.link : "";
|
|
353
|
+
if (link && insert.trim()) return `[${insert}](${link})`;
|
|
354
|
+
const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
|
|
355
|
+
if (link && taskName) return `[${taskName}](${link})`;
|
|
356
|
+
return insert;
|
|
357
|
+
}).join("").replace(/\n{3,}/g, "\n\n").trim();
|
|
358
|
+
}
|
|
359
|
+
function renderWikiHeading(text, heading) {
|
|
360
|
+
if (!heading) return text;
|
|
361
|
+
const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
|
|
362
|
+
return `${"#".repeat(level)} ${text}`;
|
|
363
|
+
}
|
|
364
|
+
function getWikiImageSource(block) {
|
|
365
|
+
const embedData = isRecord(block.embedData) ? block.embedData : {};
|
|
366
|
+
return typeof embedData.src === "string" ? embedData.src.trim() : "";
|
|
367
|
+
}
|
|
368
|
+
function renderWikiEmbed(block, context) {
|
|
369
|
+
if (block.embedType === "image") {
|
|
370
|
+
const src = getWikiImageSource(block);
|
|
371
|
+
if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
|
|
372
|
+
return src ? `[Image: ${src}]` : "[Image]";
|
|
373
|
+
}
|
|
374
|
+
return block.embedType ? `[Embed: ${block.embedType}]` : "";
|
|
375
|
+
}
|
|
376
|
+
function escapeWikiTableCell(value) {
|
|
377
|
+
return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
378
|
+
}
|
|
379
|
+
function renderWikiCell(value, document, context) {
|
|
380
|
+
const blocks = asWikiBlocks(value);
|
|
381
|
+
if (!blocks.length) return "";
|
|
382
|
+
return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
|
|
383
|
+
}
|
|
384
|
+
function renderWikiTable(block, document, context) {
|
|
385
|
+
const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
|
|
386
|
+
const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
|
|
387
|
+
if (!cols || !children.length) return "";
|
|
388
|
+
const rows = [];
|
|
389
|
+
for (let index = 0; index < children.length; index += cols) {
|
|
390
|
+
const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
|
|
391
|
+
while (cells.length < cols) cells.push("");
|
|
392
|
+
rows.push(`| ${cells.join(" | ")} |`);
|
|
393
|
+
}
|
|
394
|
+
if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
|
|
395
|
+
return rows.join("\n");
|
|
396
|
+
}
|
|
397
|
+
function renderWikiBlock(block, document, context) {
|
|
398
|
+
if (block.type === "table") return renderWikiTable(block, document, context);
|
|
399
|
+
if (block.type === "embed") return renderWikiEmbed(block, context);
|
|
400
|
+
const text = renderWikiTextRuns(block.text);
|
|
401
|
+
if (!text) return "";
|
|
402
|
+
if (block.type === "list") {
|
|
403
|
+
const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
|
|
404
|
+
return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
|
|
405
|
+
}
|
|
406
|
+
return renderWikiHeading(text, block.heading);
|
|
407
|
+
}
|
|
408
|
+
function renderWikiContent(content, context = { imageSources: [] }) {
|
|
409
|
+
const trimmed = content.trim();
|
|
410
|
+
if (!trimmed) return "";
|
|
411
|
+
const document = parseJsonRecord(trimmed);
|
|
412
|
+
if (!document) return trimmed;
|
|
413
|
+
if (!("blocks" in document)) return trimmed;
|
|
414
|
+
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();
|
|
415
|
+
}
|
|
416
|
+
function mimeTypeFromFileName(fileName) {
|
|
417
|
+
const normalized = fileName.toLowerCase();
|
|
418
|
+
if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
|
|
419
|
+
if (normalized.endsWith(".gif")) return "image/gif";
|
|
420
|
+
if (normalized.endsWith(".webp")) return "image/webp";
|
|
421
|
+
if (normalized.endsWith(".svg")) return "image/svg+xml";
|
|
422
|
+
return "image/png";
|
|
423
|
+
}
|
|
424
|
+
function attachmentNameFromPath(path) {
|
|
425
|
+
const name = path.split("/").pop() || path;
|
|
426
|
+
try {
|
|
427
|
+
return decodeURIComponent(name);
|
|
428
|
+
} catch {
|
|
429
|
+
return name;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
function toRequirement(task, description = "", attachments = []) {
|
|
298
433
|
return {
|
|
299
434
|
id: task.uuid,
|
|
300
435
|
source: "ones",
|
|
@@ -309,7 +444,7 @@ function toRequirement(task, description = "") {
|
|
|
309
444
|
createdAt: "",
|
|
310
445
|
updatedAt: "",
|
|
311
446
|
dueDate: null,
|
|
312
|
-
attachments
|
|
447
|
+
attachments,
|
|
313
448
|
raw: task
|
|
314
449
|
};
|
|
315
450
|
}
|
|
@@ -589,18 +724,86 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
589
724
|
* Fetch wiki page content via REST API.
|
|
590
725
|
* Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
|
|
591
726
|
*/
|
|
592
|
-
async
|
|
727
|
+
async fetchWikiPageDetail(wikiUuid, teamUuid) {
|
|
728
|
+
const session = await this.login();
|
|
729
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
730
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/page/${wikiUuid}/detail`;
|
|
731
|
+
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
732
|
+
if (!response.ok) return {};
|
|
733
|
+
return response.json();
|
|
734
|
+
}
|
|
735
|
+
buildWikiImageUrl(session, refUuid, source, token, teamUuid) {
|
|
736
|
+
const encodedRefUuid = encodeURIComponent(refUuid);
|
|
737
|
+
const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
|
|
738
|
+
const encodedToken = encodeURIComponent(token);
|
|
739
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
740
|
+
return `${this.config.apiBase}/wiki/api/wiki/editor/${wikiTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
|
|
741
|
+
}
|
|
742
|
+
async fetchWikiContent(wikiUuid, teamUuid) {
|
|
593
743
|
const session = await this.login();
|
|
594
|
-
const
|
|
744
|
+
const wikiTeamUuid = teamUuid ?? session.teamUuid;
|
|
745
|
+
const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/online_page/${wikiUuid}/content`;
|
|
595
746
|
const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
|
|
596
|
-
if (!response.ok) return
|
|
597
|
-
|
|
747
|
+
if (!response.ok) return {
|
|
748
|
+
content: "",
|
|
749
|
+
attachments: []
|
|
750
|
+
};
|
|
751
|
+
const data = await response.json();
|
|
752
|
+
const renderContext = { imageSources: [] };
|
|
753
|
+
const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
|
|
754
|
+
const token = typeof data.token === "string" ? data.token : "";
|
|
755
|
+
if (!renderContext.imageSources.length || !token) return {
|
|
756
|
+
content,
|
|
757
|
+
attachments: []
|
|
758
|
+
};
|
|
759
|
+
const detail = await this.fetchWikiPageDetail(wikiUuid, wikiTeamUuid);
|
|
760
|
+
const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
|
|
761
|
+
if (!refUuid) return {
|
|
762
|
+
content,
|
|
763
|
+
attachments: []
|
|
764
|
+
};
|
|
765
|
+
return {
|
|
766
|
+
content,
|
|
767
|
+
attachments: renderContext.imageSources.map((source, index) => ({
|
|
768
|
+
id: `${wikiUuid}-image-${index + 1}`,
|
|
769
|
+
name: attachmentNameFromPath(source),
|
|
770
|
+
url: this.buildWikiImageUrl(session, refUuid, source, token, wikiTeamUuid),
|
|
771
|
+
mimeType: mimeTypeFromFileName(source),
|
|
772
|
+
size: 0
|
|
773
|
+
}))
|
|
774
|
+
};
|
|
598
775
|
}
|
|
599
776
|
/**
|
|
600
|
-
* Fetch a single task by UUID or number (e.g. "#
|
|
777
|
+
* Fetch a single task by UUID or number (e.g. "#1001" or "1001").
|
|
601
778
|
* If a number is given, searches first to resolve the UUID.
|
|
602
779
|
*/
|
|
603
780
|
async getRequirement(params) {
|
|
781
|
+
const wikiRoute = parseOnesWikiPageRoute(params.id);
|
|
782
|
+
if (wikiRoute) {
|
|
783
|
+
const rendered = await this.fetchWikiContent(wikiRoute.wikiUuid, wikiRoute.teamUuid);
|
|
784
|
+
return {
|
|
785
|
+
id: wikiRoute.wikiUuid,
|
|
786
|
+
source: "ones",
|
|
787
|
+
title: `Wiki ${wikiRoute.wikiUuid}`,
|
|
788
|
+
description: rendered.content,
|
|
789
|
+
status: "open",
|
|
790
|
+
priority: "medium",
|
|
791
|
+
type: "feature",
|
|
792
|
+
labels: [],
|
|
793
|
+
reporter: "",
|
|
794
|
+
assignee: null,
|
|
795
|
+
createdAt: "",
|
|
796
|
+
updatedAt: "",
|
|
797
|
+
dueDate: null,
|
|
798
|
+
attachments: rendered.attachments,
|
|
799
|
+
raw: {
|
|
800
|
+
input: params.id,
|
|
801
|
+
teamUuid: wikiRoute.teamUuid,
|
|
802
|
+
wikiUuid: wikiRoute.wikiUuid
|
|
803
|
+
}
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
|
|
604
807
|
let taskUuid = params.id;
|
|
605
808
|
const numMatch = taskUuid.match(/^#?(\d+)$/);
|
|
606
809
|
if (numMatch) {
|
|
@@ -622,13 +825,27 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
622
825
|
}
|
|
623
826
|
const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
|
|
624
827
|
if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
|
|
625
|
-
const
|
|
626
|
-
const
|
|
627
|
-
|
|
828
|
+
const wikiRefs = /* @__PURE__ */ new Map();
|
|
829
|
+
for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
|
|
830
|
+
title: wiki.title,
|
|
831
|
+
uuid: wiki.uuid
|
|
832
|
+
});
|
|
833
|
+
const detailForLinkExtraction = [
|
|
834
|
+
task.description,
|
|
835
|
+
task.descriptionText,
|
|
836
|
+
task.desc_rich
|
|
837
|
+
].filter(Boolean).join("\n");
|
|
838
|
+
for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
|
|
839
|
+
title: `Wiki ${wikiUuid}`,
|
|
840
|
+
uuid: wikiUuid
|
|
841
|
+
});
|
|
842
|
+
const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
|
|
843
|
+
const rendered = await this.fetchWikiContent(wiki.uuid);
|
|
628
844
|
return {
|
|
629
845
|
title: wiki.title,
|
|
630
846
|
uuid: wiki.uuid,
|
|
631
|
-
content
|
|
847
|
+
content: rendered.content,
|
|
848
|
+
attachments: rendered.attachments
|
|
632
849
|
};
|
|
633
850
|
}));
|
|
634
851
|
const parts = [];
|
|
@@ -667,7 +884,18 @@ var OnesAdapter = class extends BaseAdapter {
|
|
|
667
884
|
else parts.push("(No content available)");
|
|
668
885
|
}
|
|
669
886
|
}
|
|
670
|
-
|
|
887
|
+
const detailText = getTaskDetailText(task);
|
|
888
|
+
const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
|
|
889
|
+
if (detailText && !hasWikiContent) {
|
|
890
|
+
parts.push("");
|
|
891
|
+
parts.push("---");
|
|
892
|
+
parts.push("");
|
|
893
|
+
parts.push("## Requirement Detail");
|
|
894
|
+
parts.push("");
|
|
895
|
+
parts.push(detailText);
|
|
896
|
+
}
|
|
897
|
+
const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
|
|
898
|
+
return toRequirement(task, parts.join("\n"), wikiAttachments);
|
|
671
899
|
}
|
|
672
900
|
/**
|
|
673
901
|
* Search tasks assigned to current user via GraphQL.
|
|
@@ -1081,14 +1309,14 @@ function loadConfig(startDir) {
|
|
|
1081
1309
|
//#endregion
|
|
1082
1310
|
//#region src/tools/get-issue-detail.ts
|
|
1083
1311
|
const GetIssueDetailSchema = z.object({
|
|
1084
|
-
issueId: z.string().describe("The issue task ID or key (e.g. \"
|
|
1312
|
+
issueId: z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
|
|
1085
1313
|
source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1086
1314
|
});
|
|
1087
1315
|
/**
|
|
1088
1316
|
* Download an image from URL and return as base64 data URI.
|
|
1089
1317
|
* Returns null if download fails.
|
|
1090
1318
|
*/
|
|
1091
|
-
async function downloadImageAsBase64(url) {
|
|
1319
|
+
async function downloadImageAsBase64$1(url) {
|
|
1092
1320
|
try {
|
|
1093
1321
|
const res = await fetch(url, { redirect: "follow" });
|
|
1094
1322
|
if (!res.ok) return null;
|
|
@@ -1114,7 +1342,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
|
|
|
1114
1342
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1115
1343
|
const detail = await adapter.getIssueDetail({ issueId: input.issueId });
|
|
1116
1344
|
const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
|
|
1117
|
-
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
|
|
1345
|
+
const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
|
|
1118
1346
|
const content = [{
|
|
1119
1347
|
type: "text",
|
|
1120
1348
|
text: formatIssueDetail(detail)
|
|
@@ -1156,7 +1384,7 @@ function formatIssueDetail(detail) {
|
|
|
1156
1384
|
//#endregion
|
|
1157
1385
|
//#region src/tools/get-related-issues.ts
|
|
1158
1386
|
const GetRelatedIssuesSchema = z.object({
|
|
1159
|
-
taskId: z.string().describe("The parent task ID or key (e.g. \"
|
|
1387
|
+
taskId: z.string().describe("The parent task ID or key (e.g. \"mock-task-uuid\" or \"task-mock-task-uuid\")"),
|
|
1160
1388
|
source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1161
1389
|
});
|
|
1162
1390
|
async function handleGetRelatedIssues(input, adapters, defaultSource) {
|
|
@@ -1197,18 +1425,57 @@ function formatRelatedIssues(issues) {
|
|
|
1197
1425
|
//#endregion
|
|
1198
1426
|
//#region src/tools/get-requirement.ts
|
|
1199
1427
|
const GetRequirementSchema = z.object({
|
|
1200
|
-
id: z.string().describe("The requirement/issue ID"),
|
|
1428
|
+
id: z.string().describe("The requirement/issue ID, task number, or ONES wiki page URL"),
|
|
1201
1429
|
source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
|
|
1202
1430
|
});
|
|
1431
|
+
async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
|
|
1432
|
+
try {
|
|
1433
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
1434
|
+
if (!res.ok) return null;
|
|
1435
|
+
const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
|
|
1436
|
+
return {
|
|
1437
|
+
base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
|
|
1438
|
+
mimeType
|
|
1439
|
+
};
|
|
1440
|
+
} catch {
|
|
1441
|
+
return null;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
function isImageAttachment(attachment) {
|
|
1445
|
+
if (attachment.mimeType.startsWith("image/")) return true;
|
|
1446
|
+
return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
|
|
1447
|
+
}
|
|
1448
|
+
function displayAttachmentUrl(url) {
|
|
1449
|
+
try {
|
|
1450
|
+
const parsed = new URL(url);
|
|
1451
|
+
parsed.search = "";
|
|
1452
|
+
parsed.hash = "";
|
|
1453
|
+
return parsed.toString();
|
|
1454
|
+
} catch {
|
|
1455
|
+
return url.replace(/[?#].*$/, "");
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1203
1458
|
async function handleGetRequirement(input, adapters, defaultSource) {
|
|
1204
1459
|
const sourceType = input.source ?? defaultSource;
|
|
1205
1460
|
if (!sourceType) throw new Error("No source specified and no default source configured");
|
|
1206
1461
|
const adapter = adapters.get(sourceType);
|
|
1207
1462
|
if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
|
|
1208
|
-
|
|
1463
|
+
const requirement = await adapter.getRequirement({ id: input.id });
|
|
1464
|
+
const imageAttachments = requirement.attachments.filter(isImageAttachment);
|
|
1465
|
+
const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
|
|
1466
|
+
const content = [{
|
|
1209
1467
|
type: "text",
|
|
1210
|
-
text: formatRequirement(
|
|
1211
|
-
}]
|
|
1468
|
+
text: formatRequirement(requirement)
|
|
1469
|
+
}];
|
|
1470
|
+
for (const image of imageResults) {
|
|
1471
|
+
if (!image) continue;
|
|
1472
|
+
content.push({
|
|
1473
|
+
type: "image",
|
|
1474
|
+
data: image.base64,
|
|
1475
|
+
mimeType: image.mimeType
|
|
1476
|
+
});
|
|
1477
|
+
}
|
|
1478
|
+
return { content };
|
|
1212
1479
|
}
|
|
1213
1480
|
function formatRequirement(req) {
|
|
1214
1481
|
const lines = [
|
|
@@ -1229,7 +1496,7 @@ function formatRequirement(req) {
|
|
|
1229
1496
|
lines.push("", "## Description", "", req.description || "_No description_");
|
|
1230
1497
|
if (req.attachments.length > 0) {
|
|
1231
1498
|
lines.push("", "## Attachments");
|
|
1232
|
-
for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1499
|
+
for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
|
|
1233
1500
|
}
|
|
1234
1501
|
return lines.join("\n");
|
|
1235
1502
|
}
|