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.cjs CHANGED
@@ -93,6 +93,9 @@ const TASK_DETAIL_QUERY = `
93
93
  query Task($key: Key) {
94
94
  task(key: $key) {
95
95
  key uuid number name
96
+ description
97
+ descriptionText
98
+ desc_rich: description
96
99
  issueType { uuid name }
97
100
  status { uuid name category }
98
101
  priority { value }
@@ -322,7 +325,139 @@ function getSetCookies(response) {
322
325
  const raw = response.headers.get("set-cookie");
323
326
  return raw ? [raw] : [];
324
327
  }
325
- function toRequirement(task, description = "") {
328
+ function extractWikiPageUuidsFromText(text) {
329
+ if (!text) return [];
330
+ const uuids = /* @__PURE__ */ new Set();
331
+ for (const pattern of [/\/page\/([\w-]+)/g, /page=([\w-]+)/g]) for (const match of text.matchAll(pattern)) if (match[1]) uuids.add(match[1]);
332
+ return [...uuids];
333
+ }
334
+ function parseOnesWikiPageRoute(input) {
335
+ if (!input.includes("/wiki/")) return null;
336
+ const match = (() => {
337
+ try {
338
+ const parsed = new URL(input);
339
+ return `${parsed.pathname}${parsed.hash}${parsed.search}`;
340
+ } catch {
341
+ return input;
342
+ }
343
+ })().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
344
+ if (!match?.[1] || !match[2]) return null;
345
+ return {
346
+ teamUuid: decodeURIComponent(match[1]),
347
+ wikiUuid: decodeURIComponent(match[2])
348
+ };
349
+ }
350
+ function isOnesWikiUrlInput(input) {
351
+ return input.includes("/wiki/");
352
+ }
353
+ function htmlToPlainText(html) {
354
+ 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();
355
+ }
356
+ function getTaskDetailText(task) {
357
+ return task.descriptionText?.trim() || htmlToPlainText(task.desc_rich ?? task.description ?? "");
358
+ }
359
+ function isRecord(value) {
360
+ return value !== null && typeof value === "object" && !Array.isArray(value);
361
+ }
362
+ function parseJsonRecord(value) {
363
+ try {
364
+ const parsed = JSON.parse(value);
365
+ return isRecord(parsed) ? parsed : null;
366
+ } catch {
367
+ return null;
368
+ }
369
+ }
370
+ function asWikiBlocks(value) {
371
+ if (!Array.isArray(value)) return [];
372
+ return value.filter(isRecord);
373
+ }
374
+ function renderWikiTextRuns(value) {
375
+ if (!Array.isArray(value)) return "";
376
+ return value.map((run) => {
377
+ if (!isRecord(run)) return "";
378
+ const attributes = isRecord(run.attributes) ? run.attributes : {};
379
+ const insert = typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "";
380
+ const link = typeof attributes.link === "string" ? attributes.link : "";
381
+ if (link && insert.trim()) return `[${insert}](${link})`;
382
+ const taskName = typeof attributes.taskName === "string" ? attributes.taskName : "";
383
+ if (link && taskName) return `[${taskName}](${link})`;
384
+ return insert;
385
+ }).join("").replace(/\n{3,}/g, "\n\n").trim();
386
+ }
387
+ function renderWikiHeading(text, heading) {
388
+ if (!heading) return text;
389
+ const level = Math.min(Math.max(Math.trunc(heading), 1), 6);
390
+ return `${"#".repeat(level)} ${text}`;
391
+ }
392
+ function getWikiImageSource(block) {
393
+ const embedData = isRecord(block.embedData) ? block.embedData : {};
394
+ return typeof embedData.src === "string" ? embedData.src.trim() : "";
395
+ }
396
+ function renderWikiEmbed(block, context) {
397
+ if (block.embedType === "image") {
398
+ const src = getWikiImageSource(block);
399
+ if (src && !context.imageSources.includes(src)) context.imageSources.push(src);
400
+ return src ? `[Image: ${src}]` : "[Image]";
401
+ }
402
+ return block.embedType ? `[Embed: ${block.embedType}]` : "";
403
+ }
404
+ function escapeWikiTableCell(value) {
405
+ return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
406
+ }
407
+ function renderWikiCell(value, document, context) {
408
+ const blocks = asWikiBlocks(value);
409
+ if (!blocks.length) return "";
410
+ return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
411
+ }
412
+ function renderWikiTable(block, document, context) {
413
+ const cols = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
414
+ const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
415
+ if (!cols || !children.length) return "";
416
+ const rows = [];
417
+ for (let index = 0; index < children.length; index += cols) {
418
+ const cells = children.slice(index, index + cols).map((childId) => escapeWikiTableCell(renderWikiCell(document[childId], document, context)));
419
+ while (cells.length < cols) cells.push("");
420
+ rows.push(`| ${cells.join(" | ")} |`);
421
+ }
422
+ if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
423
+ return rows.join("\n");
424
+ }
425
+ function renderWikiBlock(block, document, context) {
426
+ if (block.type === "table") return renderWikiTable(block, document, context);
427
+ if (block.type === "embed") return renderWikiEmbed(block, context);
428
+ const text = renderWikiTextRuns(block.text);
429
+ if (!text) return "";
430
+ if (block.type === "list") {
431
+ const level = typeof block.level === "number" ? Math.max(Math.trunc(block.level), 1) : 1;
432
+ return `${" ".repeat(level - 1)}${block.ordered ? `${block.start ?? 1}.` : "-"} ${text}`;
433
+ }
434
+ return renderWikiHeading(text, block.heading);
435
+ }
436
+ function renderWikiContent(content, context = { imageSources: [] }) {
437
+ const trimmed = content.trim();
438
+ if (!trimmed) return "";
439
+ const document = parseJsonRecord(trimmed);
440
+ if (!document) return trimmed;
441
+ if (!("blocks" in document)) return trimmed;
442
+ 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();
443
+ }
444
+ function mimeTypeFromFileName(fileName) {
445
+ const normalized = fileName.toLowerCase();
446
+ if (normalized.endsWith(".jpg") || normalized.endsWith(".jpeg")) return "image/jpeg";
447
+ if (normalized.endsWith(".gif")) return "image/gif";
448
+ if (normalized.endsWith(".webp")) return "image/webp";
449
+ if (normalized.endsWith(".svg")) return "image/svg+xml";
450
+ return "image/png";
451
+ }
452
+ function attachmentNameFromPath(path) {
453
+ const name = path.split("/").pop() || path;
454
+ try {
455
+ return decodeURIComponent(name);
456
+ } catch {
457
+ return name;
458
+ }
459
+ }
460
+ function toRequirement(task, description = "", attachments = []) {
326
461
  return {
327
462
  id: task.uuid,
328
463
  source: "ones",
@@ -337,7 +472,7 @@ function toRequirement(task, description = "") {
337
472
  createdAt: "",
338
473
  updatedAt: "",
339
474
  dueDate: null,
340
- attachments: [],
475
+ attachments,
341
476
  raw: task
342
477
  };
343
478
  }
@@ -617,18 +752,86 @@ var OnesAdapter = class extends BaseAdapter {
617
752
  * Fetch wiki page content via REST API.
618
753
  * Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
619
754
  */
620
- async fetchWikiContent(wikiUuid) {
755
+ async fetchWikiPageDetail(wikiUuid, teamUuid) {
756
+ const session = await this.login();
757
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
758
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/page/${wikiUuid}/detail`;
759
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
760
+ if (!response.ok) return {};
761
+ return response.json();
762
+ }
763
+ buildWikiImageUrl(session, refUuid, source, token, teamUuid) {
764
+ const encodedRefUuid = encodeURIComponent(refUuid);
765
+ const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
766
+ const encodedToken = encodeURIComponent(token);
767
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
768
+ return `${this.config.apiBase}/wiki/api/wiki/editor/${wikiTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
769
+ }
770
+ async fetchWikiContent(wikiUuid, teamUuid) {
621
771
  const session = await this.login();
622
- const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
772
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
773
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/online_page/${wikiUuid}/content`;
623
774
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
624
- if (!response.ok) return "";
625
- return (await response.json()).content ?? "";
775
+ if (!response.ok) return {
776
+ content: "",
777
+ attachments: []
778
+ };
779
+ const data = await response.json();
780
+ const renderContext = { imageSources: [] };
781
+ const content = renderWikiContent(typeof data.content === "string" ? data.content : "", renderContext);
782
+ const token = typeof data.token === "string" ? data.token : "";
783
+ if (!renderContext.imageSources.length || !token) return {
784
+ content,
785
+ attachments: []
786
+ };
787
+ const detail = await this.fetchWikiPageDetail(wikiUuid, wikiTeamUuid);
788
+ const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
789
+ if (!refUuid) return {
790
+ content,
791
+ attachments: []
792
+ };
793
+ return {
794
+ content,
795
+ attachments: renderContext.imageSources.map((source, index) => ({
796
+ id: `${wikiUuid}-image-${index + 1}`,
797
+ name: attachmentNameFromPath(source),
798
+ url: this.buildWikiImageUrl(session, refUuid, source, token, wikiTeamUuid),
799
+ mimeType: mimeTypeFromFileName(source),
800
+ size: 0
801
+ }))
802
+ };
626
803
  }
627
804
  /**
628
- * Fetch a single task by UUID or number (e.g. "#95945" or "95945").
805
+ * Fetch a single task by UUID or number (e.g. "#1001" or "1001").
629
806
  * If a number is given, searches first to resolve the UUID.
630
807
  */
631
808
  async getRequirement(params) {
809
+ const wikiRoute = parseOnesWikiPageRoute(params.id);
810
+ if (wikiRoute) {
811
+ const rendered = await this.fetchWikiContent(wikiRoute.wikiUuid, wikiRoute.teamUuid);
812
+ return {
813
+ id: wikiRoute.wikiUuid,
814
+ source: "ones",
815
+ title: `Wiki ${wikiRoute.wikiUuid}`,
816
+ description: rendered.content,
817
+ status: "open",
818
+ priority: "medium",
819
+ type: "feature",
820
+ labels: [],
821
+ reporter: "",
822
+ assignee: null,
823
+ createdAt: "",
824
+ updatedAt: "",
825
+ dueDate: null,
826
+ attachments: rendered.attachments,
827
+ raw: {
828
+ input: params.id,
829
+ teamUuid: wikiRoute.teamUuid,
830
+ wikiUuid: wikiRoute.wikiUuid
831
+ }
832
+ };
833
+ }
834
+ if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
632
835
  let taskUuid = params.id;
633
836
  const numMatch = taskUuid.match(/^#?(\d+)$/);
634
837
  if (numMatch) {
@@ -650,13 +853,27 @@ var OnesAdapter = class extends BaseAdapter {
650
853
  }
651
854
  const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
652
855
  if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
653
- const wikiPages = task.relatedWikiPages ?? [];
654
- const wikiContents = await Promise.all(wikiPages.filter((w) => !w.errorMessage).map(async (wiki) => {
655
- const content = await this.fetchWikiContent(wiki.uuid);
856
+ const wikiRefs = /* @__PURE__ */ new Map();
857
+ for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
858
+ title: wiki.title,
859
+ uuid: wiki.uuid
860
+ });
861
+ const detailForLinkExtraction = [
862
+ task.description,
863
+ task.descriptionText,
864
+ task.desc_rich
865
+ ].filter(Boolean).join("\n");
866
+ for (const wikiUuid of extractWikiPageUuidsFromText(detailForLinkExtraction)) if (!wikiRefs.has(wikiUuid)) wikiRefs.set(wikiUuid, {
867
+ title: `Wiki ${wikiUuid}`,
868
+ uuid: wikiUuid
869
+ });
870
+ const wikiContents = await Promise.all([...wikiRefs.values()].map(async (wiki) => {
871
+ const rendered = await this.fetchWikiContent(wiki.uuid);
656
872
  return {
657
873
  title: wiki.title,
658
874
  uuid: wiki.uuid,
659
- content
875
+ content: rendered.content,
876
+ attachments: rendered.attachments
660
877
  };
661
878
  }));
662
879
  const parts = [];
@@ -695,7 +912,18 @@ var OnesAdapter = class extends BaseAdapter {
695
912
  else parts.push("(No content available)");
696
913
  }
697
914
  }
698
- return toRequirement(task, parts.join("\n"));
915
+ const detailText = getTaskDetailText(task);
916
+ const hasWikiContent = wikiContents.some((wiki) => wiki.content.trim());
917
+ if (detailText && !hasWikiContent) {
918
+ parts.push("");
919
+ parts.push("---");
920
+ parts.push("");
921
+ parts.push("## Requirement Detail");
922
+ parts.push("");
923
+ parts.push(detailText);
924
+ }
925
+ const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
926
+ return toRequirement(task, parts.join("\n"), wikiAttachments);
699
927
  }
700
928
  /**
701
929
  * Search tasks assigned to current user via GraphQL.
@@ -1109,14 +1337,14 @@ function loadConfig(startDir) {
1109
1337
  //#endregion
1110
1338
  //#region src/tools/get-issue-detail.ts
1111
1339
  const GetIssueDetailSchema = zod_v4.z.object({
1112
- issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"6W9vW3y8J9DO66Pu\" or \"task-6W9vW3y8J9DO66Pu\")"),
1340
+ issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
1113
1341
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1114
1342
  });
1115
1343
  /**
1116
1344
  * Download an image from URL and return as base64 data URI.
1117
1345
  * Returns null if download fails.
1118
1346
  */
1119
- async function downloadImageAsBase64(url) {
1347
+ async function downloadImageAsBase64$1(url) {
1120
1348
  try {
1121
1349
  const res = await fetch(url, { redirect: "follow" });
1122
1350
  if (!res.ok) return null;
@@ -1142,7 +1370,7 @@ async function handleGetIssueDetail(input, adapters, defaultSource) {
1142
1370
  if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1143
1371
  const detail = await adapter.getIssueDetail({ issueId: input.issueId });
1144
1372
  const imageUrls = detail.descriptionRich ? extractImageUrls(detail.descriptionRich) : [];
1145
- const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64(url)));
1373
+ const imageResults = await Promise.all(imageUrls.map((url) => downloadImageAsBase64$1(url)));
1146
1374
  const content = [{
1147
1375
  type: "text",
1148
1376
  text: formatIssueDetail(detail)
@@ -1184,7 +1412,7 @@ function formatIssueDetail(detail) {
1184
1412
  //#endregion
1185
1413
  //#region src/tools/get-related-issues.ts
1186
1414
  const GetRelatedIssuesSchema = zod_v4.z.object({
1187
- taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"HRL2p8rTX4mQ9xMv\" or \"task-HRL2p8rTX4mQ9xMv\")"),
1415
+ taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"mock-task-uuid\" or \"task-mock-task-uuid\")"),
1188
1416
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1189
1417
  });
1190
1418
  async function handleGetRelatedIssues(input, adapters, defaultSource) {
@@ -1225,18 +1453,57 @@ function formatRelatedIssues(issues) {
1225
1453
  //#endregion
1226
1454
  //#region src/tools/get-requirement.ts
1227
1455
  const GetRequirementSchema = zod_v4.z.object({
1228
- id: zod_v4.z.string().describe("The requirement/issue ID"),
1456
+ id: zod_v4.z.string().describe("The requirement/issue ID, task number, or ONES wiki page URL"),
1229
1457
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1230
1458
  });
1459
+ async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
1460
+ try {
1461
+ const res = await fetch(url, { redirect: "follow" });
1462
+ if (!res.ok) return null;
1463
+ const mimeType = (res.headers.get("content-type") ?? fallbackMimeType).split(";")[0].trim() || fallbackMimeType;
1464
+ return {
1465
+ base64: Buffer.from(await res.arrayBuffer()).toString("base64"),
1466
+ mimeType
1467
+ };
1468
+ } catch {
1469
+ return null;
1470
+ }
1471
+ }
1472
+ function isImageAttachment(attachment) {
1473
+ if (attachment.mimeType.startsWith("image/")) return true;
1474
+ return /\.(?:png|jpe?g|gif|webp|svg)$/i.test(attachment.url);
1475
+ }
1476
+ function displayAttachmentUrl(url) {
1477
+ try {
1478
+ const parsed = new URL(url);
1479
+ parsed.search = "";
1480
+ parsed.hash = "";
1481
+ return parsed.toString();
1482
+ } catch {
1483
+ return url.replace(/[?#].*$/, "");
1484
+ }
1485
+ }
1231
1486
  async function handleGetRequirement(input, adapters, defaultSource) {
1232
1487
  const sourceType = input.source ?? defaultSource;
1233
1488
  if (!sourceType) throw new Error("No source specified and no default source configured");
1234
1489
  const adapter = adapters.get(sourceType);
1235
1490
  if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1236
- return { content: [{
1491
+ const requirement = await adapter.getRequirement({ id: input.id });
1492
+ const imageAttachments = requirement.attachments.filter(isImageAttachment);
1493
+ const imageResults = await Promise.all(imageAttachments.map((attachment) => downloadImageAsBase64(attachment.url, attachment.mimeType)));
1494
+ const content = [{
1237
1495
  type: "text",
1238
- text: formatRequirement(await adapter.getRequirement({ id: input.id }))
1239
- }] };
1496
+ text: formatRequirement(requirement)
1497
+ }];
1498
+ for (const image of imageResults) {
1499
+ if (!image) continue;
1500
+ content.push({
1501
+ type: "image",
1502
+ data: image.base64,
1503
+ mimeType: image.mimeType
1504
+ });
1505
+ }
1506
+ return { content };
1240
1507
  }
1241
1508
  function formatRequirement(req) {
1242
1509
  const lines = [
@@ -1257,7 +1524,7 @@ function formatRequirement(req) {
1257
1524
  lines.push("", "## Description", "", req.description || "_No description_");
1258
1525
  if (req.attachments.length > 0) {
1259
1526
  lines.push("", "## Attachments");
1260
- for (const att of req.attachments) lines.push(`- [${att.name}](${att.url}) (${att.mimeType}, ${att.size} bytes)`);
1527
+ for (const att of req.attachments) lines.push(`- [${att.name}](${displayAttachmentUrl(att.url)}) (${att.mimeType}, ${att.size} bytes)`);
1261
1528
  }
1262
1529
  return lines.join("\n");
1263
1530
  }