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/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 toRequirement(task, description = "") {
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(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/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 fetchWikiContent(wikiUuid) {
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 url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
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
- return (await response.json()).content ?? "";
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. "#95945" or "95945").
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 wikiPages = task.relatedWikiPages ?? [];
626
- const wikiContents = await Promise.all(wikiPages.filter((w) => !w.errorMessage).map(async (wiki) => {
627
- const content = await this.fetchWikiContent(wiki.uuid);
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
- return toRequirement(task, parts.join("\n"));
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. \"6W9vW3y8J9DO66Pu\" or \"task-6W9vW3y8J9DO66Pu\")"),
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. \"HRL2p8rTX4mQ9xMv\" or \"task-HRL2p8rTX4mQ9xMv\")"),
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
- return { content: [{
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(await adapter.getRequirement({ id: input.id }))
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
  }