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/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 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 htmlToPlainText(html) {
307
+ 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();
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
- return (await response.json()).content ?? "";
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 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);
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
- return toRequirement(task, parts.join("\n"));
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
- return { content: [{
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(await adapter.getRequirement({ id: input.id }))
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
  }