ai-dev-requirements 0.1.11 → 0.1.13

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
@@ -5,7 +5,6 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
5
5
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
6
  import crypto from "node:crypto";
7
7
  import { z } from "zod/v4";
8
-
9
8
  //#region src/utils/map-status.ts
10
9
  const ONES_STATUS_MAP = {
11
10
  to_do: "open",
@@ -41,7 +40,6 @@ function mapOnesPriority(priority) {
41
40
  function mapOnesType(type) {
42
41
  return ONES_TYPE_MAP[type.toLowerCase()] ?? "task";
43
42
  }
44
-
45
43
  //#endregion
46
44
  //#region src/adapters/base.ts
47
45
  /**
@@ -58,7 +56,6 @@ var BaseAdapter = class {
58
56
  this.resolvedAuth = resolvedAuth;
59
57
  }
60
58
  };
61
-
62
59
  //#endregion
63
60
  //#region src/adapters/ones.ts
64
61
  const TASK_DETAIL_QUERY = `
@@ -92,6 +89,26 @@ const TASK_DETAIL_QUERY = `
92
89
  }
93
90
  }
94
91
  `;
92
+ const RELATED_ACTIVITIES_QUERY = `
93
+ query Task($key: Key) {
94
+ task(key: $key) {
95
+ key
96
+ ...RelatedActivities_task1
97
+ }
98
+ }
99
+
100
+ fragment RelatedActivities_task1 on Task {
101
+ relatedActivities {
102
+ uuid
103
+ name
104
+ projectUUID
105
+ project_uuid: projectUUID
106
+ relatedChild
107
+ related_child_uuid: relatedChild
108
+ }
109
+ relatedActivitiesCount
110
+ }
111
+ `;
95
112
  const SEARCH_TASKS_QUERY = `
96
113
  query GROUP_TASK_DATA($groupBy: GroupBy, $groupOrderBy: OrderBy, $orderBy: OrderBy, $filterGroup: [Filter!], $search: Search, $pagination: Pagination, $limit: Int) {
97
114
  buckets(groupBy: $groupBy, orderBy: $groupOrderBy, pagination: $pagination, filter: $search) {
@@ -116,6 +133,26 @@ const ISSUE_TYPES_QUERY = `
116
133
  }
117
134
  }
118
135
  `;
136
+ const PROJECTS_QUERY = `
137
+ query Projects($groupBy: GroupBy, $orderBy: OrderBy, $pagination: Pagination, $projectOrderBy: OrderBy, $projectFilterGroup: [Filter!]) {
138
+ buckets(groupBy: $groupBy, orderBy: $orderBy, pagination: $pagination) {
139
+ key
140
+ projects(limit: 10000, orderBy: $projectOrderBy, filterGroup: $projectFilterGroup) {
141
+ key
142
+ uuid
143
+ name
144
+ identifier
145
+ }
146
+ }
147
+ }
148
+ `;
149
+ const ADD_MANHOUR_MUTATION = `
150
+ mutation AddManhour {
151
+ addManhour(mode: $mode, owner: $owner, task: $task, type: $type, start_time: $start_time, hours: $hours, description: $description, customData: $customData) {
152
+ key
153
+ }
154
+ }
155
+ `;
119
156
  const TASK_BY_NUMBER_QUERY = SEARCH_TASKS_QUERY;
120
157
  const RELATED_TASKS_QUERY = `
121
158
  query Task($key: Key) {
@@ -304,7 +341,7 @@ function extractWikiPageUuidsFromText(text) {
304
341
  return [...uuids];
305
342
  }
306
343
  function parseOnesWikiPageRoute(input) {
307
- if (!input.includes("/wiki/")) return null;
344
+ if (!isOnesWikiUrlInput(input)) return null;
308
345
  const match = (() => {
309
346
  try {
310
347
  const parsed = new URL(input);
@@ -312,7 +349,7 @@ function parseOnesWikiPageRoute(input) {
312
349
  } catch {
313
350
  return input;
314
351
  }
315
- })().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
352
+ })().match(/\/team\/([^/?#]+)\/(?:space\/[^/?#]+\/)?page\/([^/?#]+)/);
316
353
  if (!match?.[1] || !match[2]) return null;
317
354
  return {
318
355
  teamUuid: decodeURIComponent(match[1]),
@@ -320,7 +357,75 @@ function parseOnesWikiPageRoute(input) {
320
357
  };
321
358
  }
322
359
  function isOnesWikiUrlInput(input) {
323
- return input.includes("/wiki/");
360
+ return /\/wiki(?:\/|(?=[#?]|$))/.test(input);
361
+ }
362
+ function parseAuthorizeRequestId(location) {
363
+ try {
364
+ const parsed = new URL(location);
365
+ return parsed.searchParams.get("auth_request_id") ?? parsed.searchParams.get("id");
366
+ } catch {
367
+ const match = location.match(/[?&](?:auth_request_id|id)=([^&#]+)/);
368
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
369
+ }
370
+ }
371
+ function parseAuthorizationCode(location) {
372
+ try {
373
+ return new URL(location).searchParams.get("code");
374
+ } catch {
375
+ const match = location.match(/[?&]code=([^&#]+)/);
376
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
377
+ }
378
+ }
379
+ function parseDisplayId(input) {
380
+ const match = input.trim().match(/^([a-z]\w*)-(\d+)$/i);
381
+ if (!match?.[1] || !match[2]) return null;
382
+ return {
383
+ identifier: match[1],
384
+ number: Number.parseInt(match[2], 10)
385
+ };
386
+ }
387
+ function isValidOnesDate(value) {
388
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
389
+ const [yearText, monthText, dayText] = value.split("-");
390
+ const year = Number.parseInt(yearText, 10);
391
+ const month = Number.parseInt(monthText, 10);
392
+ const day = Number.parseInt(dayText, 10);
393
+ const date = new Date(Date.UTC(year, month - 1, day));
394
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
395
+ }
396
+ function toOnesHours(hours) {
397
+ if (!Number.isFinite(hours) || hours <= 0) throw new Error("ONES: hours must be a positive number");
398
+ return Math.round(hours * 1e5);
399
+ }
400
+ function getTodayStartUnixSeconds() {
401
+ const now = /* @__PURE__ */ new Date();
402
+ const localStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
403
+ return Math.floor(localStart.getTime() / 1e3);
404
+ }
405
+ function toLocalDateString(date) {
406
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
407
+ }
408
+ function getLocalStartUnixSeconds(year, month, day) {
409
+ return Math.floor(new Date(year, month - 1, day).getTime() / 1e3);
410
+ }
411
+ function parseManhourDate(input) {
412
+ const value = input?.trim();
413
+ if (!value) return {
414
+ date: null,
415
+ startTime: getTodayStartUnixSeconds()
416
+ };
417
+ const fullDateMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
418
+ const dayOnlyMatch = value.match(/^(\d{1,2})号?$/);
419
+ const now = /* @__PURE__ */ new Date();
420
+ const year = fullDateMatch ? Number.parseInt(fullDateMatch[1], 10) : now.getFullYear();
421
+ const month = fullDateMatch ? Number.parseInt(fullDateMatch[2], 10) : now.getMonth() + 1;
422
+ const day = fullDateMatch ? Number.parseInt(fullDateMatch[3], 10) : dayOnlyMatch ? Number.parseInt(dayOnlyMatch[1], 10) : NaN;
423
+ const parsed = new Date(year, month - 1, day);
424
+ if (!(Number.isInteger(day) && parsed.getFullYear() === year && parsed.getMonth() === month - 1 && parsed.getDate() === day)) throw new Error("ONES: date must be a valid YYYY-MM-DD date or day of current month");
425
+ return {
426
+ date: toLocalDateString(parsed),
427
+ startTime: getLocalStartUnixSeconds(year, month, day)
428
+ };
324
429
  }
325
430
  function htmlToPlainText(html) {
326
431
  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();
@@ -376,22 +481,126 @@ function renderWikiEmbed(block, context) {
376
481
  function escapeWikiTableCell(value) {
377
482
  return value.replace(/\|/g, "\\|").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
378
483
  }
484
+ function escapeWikiHtml(value) {
485
+ return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
486
+ }
487
+ function parseWikiTableSpan(value) {
488
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return 1;
489
+ return Math.max(Math.trunc(value), 1);
490
+ }
491
+ function buildWikiTableLayout(block) {
492
+ const columnCount = typeof block.cols === "number" && block.cols > 0 ? Math.trunc(block.cols) : 0;
493
+ const children = Array.isArray(block.children) ? block.children.filter((child) => typeof child === "string") : [];
494
+ if (!columnCount || !children.length) return null;
495
+ const hasDeclaredRows = typeof block.rows === "number" && block.rows > 0;
496
+ const initialRowCount = hasDeclaredRows ? Math.trunc(block.rows) : Math.max(Math.ceil(children.length / columnCount), 1);
497
+ const occupied = [];
498
+ const rows = [];
499
+ const ensureRowCount = (count) => {
500
+ while (occupied.length < count) {
501
+ occupied.push(Array.from({ length: columnCount }).fill(false));
502
+ rows.push([]);
503
+ }
504
+ };
505
+ ensureRowCount(initialRowCount);
506
+ let cursor = 0;
507
+ let hasMergedCells = false;
508
+ for (const childId of children) {
509
+ while (true) {
510
+ const row = Math.floor(cursor / columnCount);
511
+ const column = cursor % columnCount;
512
+ ensureRowCount(row + 1);
513
+ if (!occupied[row][column]) break;
514
+ cursor += 1;
515
+ }
516
+ const row = Math.floor(cursor / columnCount);
517
+ const column = cursor % columnCount;
518
+ const requestedRowSpan = parseWikiTableSpan(block[`${childId}_rowSpan`]);
519
+ if (!hasDeclaredRows) ensureRowCount(row + requestedRowSpan);
520
+ const rowSpan = Math.min(requestedRowSpan, occupied.length - row);
521
+ const colSpan = Math.min(parseWikiTableSpan(block[`${childId}_colSpan`]), columnCount - column);
522
+ hasMergedCells ||= rowSpan > 1 || colSpan > 1;
523
+ rows[row].push({
524
+ childId,
525
+ row,
526
+ column,
527
+ rowSpan,
528
+ colSpan
529
+ });
530
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset += 1) for (let columnOffset = 0; columnOffset < colSpan; columnOffset += 1) occupied[row + rowOffset][column + columnOffset] = true;
531
+ cursor += 1;
532
+ }
533
+ return {
534
+ columnCount,
535
+ rows,
536
+ hasMergedCells
537
+ };
538
+ }
539
+ function wikiCellContainsTable(value) {
540
+ return asWikiBlocks(value).some((block) => block.type === "table");
541
+ }
542
+ function renderWikiTextRunsHtml(value) {
543
+ if (!Array.isArray(value)) return "";
544
+ return value.map((run) => {
545
+ if (!isRecord(run)) return "";
546
+ const attributes = isRecord(run.attributes) ? run.attributes : {};
547
+ let content = escapeWikiHtml(typeof run.insert === "string" ? run.insert.replace(/\u00A0/g, " ") : "").replace(/\n/g, "<br>");
548
+ if (attributes.code) content = `<code>${content}</code>`;
549
+ if (attributes.bold) content = `<strong>${content}</strong>`;
550
+ if (attributes.italic) content = `<em>${content}</em>`;
551
+ if (attributes.underline) content = `<u>${content}</u>`;
552
+ if (attributes.strike) content = `<s>${content}</s>`;
553
+ const link = typeof attributes.link === "string" ? attributes.link : "";
554
+ return link ? `<a href="${escapeWikiHtml(link)}">${content}</a>` : content;
555
+ }).join("");
556
+ }
557
+ function renderWikiCellHtml(value, document, context) {
558
+ return asWikiBlocks(value).map((block) => renderWikiBlockHtml(block, document, context)).filter(Boolean).join("");
559
+ }
560
+ function renderWikiBlockHtml(block, document, context) {
561
+ if (block.type === "table") {
562
+ const layout = buildWikiTableLayout(block);
563
+ return layout ? renderWikiTableHtml(layout, document, context) : "";
564
+ }
565
+ if (block.type === "embed") return `<p>${escapeWikiHtml(renderWikiEmbed(block, context))}</p>`;
566
+ const text = renderWikiTextRunsHtml(block.text);
567
+ if (!text) return "";
568
+ if (block.type === "list") {
569
+ const tag = block.ordered ? "ol" : "ul";
570
+ return `<${tag}><li>${text}</li></${tag}>`;
571
+ }
572
+ if (block.heading) {
573
+ const level = Math.min(Math.max(Math.trunc(block.heading), 1), 6);
574
+ return `<h${level}>${text}</h${level}>`;
575
+ }
576
+ return `<p>${text}</p>`;
577
+ }
578
+ function renderWikiTableHtml(layout, document, context) {
579
+ return `<table>\n<tbody>\n${layout.rows.map((row) => {
580
+ return `<tr>\n${row.map((cell) => {
581
+ const attributes = [cell.rowSpan > 1 ? `rowspan="${cell.rowSpan}"` : "", cell.colSpan > 1 ? `colspan="${cell.colSpan}"` : ""].filter(Boolean);
582
+ const content = renderWikiCellHtml(document[cell.childId], document, context);
583
+ return `<td${attributes.length ? ` ${attributes.join(" ")}` : ""}>${content}</td>`;
584
+ }).join("\n")}\n</tr>`;
585
+ }).join("\n")}\n</tbody>\n</table>`;
586
+ }
379
587
  function renderWikiCell(value, document, context) {
380
588
  const blocks = asWikiBlocks(value);
381
589
  if (!blocks.length) return "";
382
590
  return blocks.map((block) => renderWikiBlock(block, document, context)).filter(Boolean).join(" ").replace(/[ \t]*\n+[ \t]*/g, " ").trim();
383
591
  }
384
592
  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 "";
593
+ const layout = buildWikiTableLayout(block);
594
+ if (!layout) return "";
595
+ const hasNestedTable = layout.rows.some((row) => row.some((cell) => wikiCellContainsTable(document[cell.childId])));
596
+ if (layout.hasMergedCells || hasNestedTable) return renderWikiTableHtml(layout, document, context);
388
597
  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("");
598
+ for (const row of layout.rows) {
599
+ const cells = Array.from({ length: layout.columnCount }).fill("");
600
+ for (const cell of row) cells[cell.column] = escapeWikiTableCell(renderWikiCell(document[cell.childId], document, context));
392
601
  rows.push(`| ${cells.join(" | ")} |`);
393
602
  }
394
- if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: cols }, () => "---").join(" | ")} |`);
603
+ if (rows.length > 1) rows.splice(1, 0, `| ${Array.from({ length: layout.columnCount }).fill("---").join(" | ")} |`);
395
604
  return rows.join("\n");
396
605
  }
397
606
  function renderWikiBlock(block, document, context) {
@@ -516,32 +725,35 @@ var OnesAdapter = class extends BaseAdapter {
516
725
  redirect: "manual"
517
726
  })).headers.get("location");
518
727
  if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
519
- const authRequestId = new URL(authorizeLocation).searchParams.get("id");
520
- if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
521
- const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
522
- method: "POST",
523
- headers: {
524
- "Content-Type": "application/json;charset=UTF-8",
525
- "Cookie": cookies
526
- },
527
- body: JSON.stringify({
528
- auth_request_id: authRequestId,
529
- region_uuid: orgUser.region_uuid,
530
- org_uuid: orgUser.org_uuid,
531
- org_user_uuid: orgUser.org_user.org_user_uuid
532
- })
533
- });
534
- if (!finalizeRes.ok) {
535
- const text = await finalizeRes.text().catch(() => "");
536
- throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
728
+ let code = parseAuthorizationCode(authorizeLocation);
729
+ if (!code) {
730
+ const authRequestId = parseAuthorizeRequestId(authorizeLocation);
731
+ if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
732
+ const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
733
+ method: "POST",
734
+ headers: {
735
+ "Content-Type": "application/json;charset=UTF-8",
736
+ "Cookie": cookies
737
+ },
738
+ body: JSON.stringify({
739
+ auth_request_id: authRequestId,
740
+ region_uuid: orgUser.region_uuid,
741
+ org_uuid: orgUser.org_uuid,
742
+ org_user_uuid: orgUser.org_user.org_user_uuid
743
+ })
744
+ });
745
+ if (!finalizeRes.ok) {
746
+ const text = await finalizeRes.text().catch(() => "");
747
+ throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
748
+ }
749
+ const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
750
+ method: "GET",
751
+ headers: { Cookie: cookies },
752
+ redirect: "manual"
753
+ })).headers.get("location");
754
+ if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
755
+ code = parseAuthorizationCode(callbackLocation);
537
756
  }
538
- const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
539
- method: "GET",
540
- headers: { Cookie: cookies },
541
- redirect: "manual"
542
- })).headers.get("location");
543
- if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
544
- const code = new URL(callbackLocation).searchParams.get("code");
545
757
  if (!code) throw new Error("ONES: Cannot parse authorization code from callback redirect");
546
758
  const tokenRes = await fetch(`${baseUrl}/identity/oauth/token`, {
547
759
  method: "POST",
@@ -611,11 +823,136 @@ var OnesAdapter = class extends BaseAdapter {
611
823
  }
612
824
  return response.json();
613
825
  }
826
+ async onesql(query, variables, workItemType) {
827
+ const session = await this.login();
828
+ const url = `${this.config.apiBase}/project/api/ones-project/team/${session.teamUuid}/workitems/onesql`;
829
+ const response = await fetch(url, {
830
+ method: "POST",
831
+ headers: {
832
+ "Authorization": `Bearer ${session.accessToken}`,
833
+ "Content-Type": "application/json"
834
+ },
835
+ body: JSON.stringify({
836
+ query,
837
+ variables: [
838
+ variables,
839
+ workItemType,
840
+ null,
841
+ null
842
+ ]
843
+ })
844
+ });
845
+ if (!response.ok) {
846
+ const text = await response.text().catch(() => "");
847
+ throw new Error(`ONES OneSQL error: ${response.status} ${text}`);
848
+ }
849
+ return response.json();
850
+ }
851
+ async fetchRelatedActivities(taskKey) {
852
+ return (await this.onesql(RELATED_ACTIVITIES_QUERY, { key: taskKey }, "Task")).data?.task?.relatedActivities ?? [];
853
+ }
854
+ async searchTaskByNumber(taskNumber) {
855
+ const session = await this.login();
856
+ const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/search?q=${encodeURIComponent(String(taskNumber))}&start=0&limit=10&types=task`;
857
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
858
+ if (!response.ok) return null;
859
+ const found = ((await response.json()).datas?.task ?? []).map((item) => item.fields).find((fields) => fields?.uuid && fields.number === taskNumber);
860
+ if (!found?.uuid) return null;
861
+ return {
862
+ key: `task-${found.uuid}`,
863
+ uuid: found.uuid,
864
+ number: found.number ?? taskNumber,
865
+ name: found.summary ?? "",
866
+ status: {
867
+ uuid: "",
868
+ name: "",
869
+ category: void 0
870
+ },
871
+ issueType: found.issue_type_uuid || found.issue_type_name ? {
872
+ uuid: found.issue_type_uuid ?? "",
873
+ name: found.issue_type_name ?? ""
874
+ } : void 0,
875
+ project: found.project_uuid || found.project_name ? {
876
+ uuid: found.project_uuid ?? "",
877
+ name: found.project_name ?? ""
878
+ } : void 0
879
+ };
880
+ }
614
881
  async fetchIssueTypes() {
615
882
  if (this.issueTypesCache) return this.issueTypesCache;
616
- this.issueTypesCache = (await this.graphql(ISSUE_TYPES_QUERY, { orderBy: { namePinyin: "ASC" } }, "issueTypes")).data?.issueTypes ?? [];
883
+ const data = await this.graphql(ISSUE_TYPES_QUERY, { orderBy: { namePinyin: "ASC" } }, "issueTypes");
884
+ this.issueTypesCache = data.data?.issueTypes ?? [];
617
885
  return this.issueTypesCache;
618
886
  }
887
+ async fetchProjects() {
888
+ return (await this.graphql(PROJECTS_QUERY, {
889
+ projectOrderBy: {
890
+ isPin: "DESC",
891
+ namePinyin: "ASC",
892
+ createTime: "DESC"
893
+ },
894
+ projectFilterGroup: [{
895
+ visibleInProject_equal: true,
896
+ isArchive_equal: false
897
+ }],
898
+ groupBy: { projects: {} },
899
+ orderBy: null,
900
+ pagination: {
901
+ limit: 50,
902
+ after: "",
903
+ preciseCount: true
904
+ }
905
+ }, "projects-group-list-for-project-view")).data?.buckets?.flatMap((bucket) => bucket.projects ?? []) ?? [];
906
+ }
907
+ async findTaskByNumber(taskNumber, projectUuid) {
908
+ const filter = { number_in: [taskNumber] };
909
+ if (projectUuid) filter.project_in = [projectUuid];
910
+ const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
911
+ groupBy: { tasks: {} },
912
+ groupOrderBy: null,
913
+ orderBy: { createTime: "DESC" },
914
+ filterGroup: [filter],
915
+ search: null,
916
+ pagination: {
917
+ limit: 10,
918
+ preciseCount: false
919
+ },
920
+ limit: 10
921
+ }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((task) => task.number === taskNumber && (!projectUuid || task.project?.uuid === projectUuid));
922
+ if (found) return found;
923
+ if (projectUuid) return null;
924
+ return this.searchTaskByNumber(taskNumber);
925
+ }
926
+ async resolveTaskRef(input) {
927
+ const taskId = input.trim();
928
+ if (!taskId) throw new Error("ONES: taskId is required");
929
+ const numMatch = taskId.match(/^#?(\d+)$/);
930
+ if (numMatch) {
931
+ const taskNumber = Number.parseInt(numMatch[1], 10);
932
+ const found = await this.findTaskByNumber(taskNumber);
933
+ if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
934
+ return {
935
+ key: found.key ?? `task-${found.uuid}`,
936
+ uuid: found.uuid
937
+ };
938
+ }
939
+ const displayId = parseDisplayId(taskId);
940
+ if (displayId) {
941
+ const project = (await this.fetchProjects()).find((item) => item.identifier?.toLowerCase() === displayId.identifier.toLowerCase());
942
+ if (!project) throw new Error(`ONES: Project identifier "${displayId.identifier}" not found in current team`);
943
+ const found = await this.findTaskByNumber(displayId.number, project.uuid);
944
+ if (!found) throw new Error(`ONES: Task "${taskId}" not found in current team`);
945
+ return {
946
+ key: found.key ?? `task-${found.uuid}`,
947
+ uuid: found.uuid
948
+ };
949
+ }
950
+ const key = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
951
+ return {
952
+ key,
953
+ uuid: key.slice(5)
954
+ };
955
+ }
619
956
  async searchTeamUsers(keyword) {
620
957
  const session = await this.login();
621
958
  const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/users/search`;
@@ -804,27 +1141,11 @@ var OnesAdapter = class extends BaseAdapter {
804
1141
  };
805
1142
  }
806
1143
  if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
807
- let taskUuid = params.id;
808
- const numMatch = taskUuid.match(/^#?(\d+)$/);
809
- if (numMatch) {
810
- const taskNumber = Number.parseInt(numMatch[1], 10);
811
- const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
812
- groupBy: { tasks: {} },
813
- groupOrderBy: null,
814
- orderBy: { createTime: "DESC" },
815
- filterGroup: [{ number_in: [taskNumber] }],
816
- search: null,
817
- pagination: {
818
- limit: 10,
819
- preciseCount: false
820
- },
821
- limit: 10
822
- }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === taskNumber);
823
- if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
824
- taskUuid = found.uuid;
825
- }
826
- const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
827
- if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
1144
+ const taskRef = await this.resolveTaskRef(params.id);
1145
+ const shouldFetchRelatedActivities = parseDisplayId(params.id.trim()) !== null;
1146
+ const task = (await this.graphql(TASK_DETAIL_QUERY, { key: taskRef.key }, "Task")).data?.task;
1147
+ if (!task) throw new Error(`ONES: Task "${params.id}" not found`);
1148
+ const relatedActivities = shouldFetchRelatedActivities ? await this.fetchRelatedActivities(taskRef.key) : [];
828
1149
  const wikiRefs = /* @__PURE__ */ new Map();
829
1150
  for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
830
1151
  title: wiki.title,
@@ -865,6 +1186,18 @@ var OnesAdapter = class extends BaseAdapter {
865
1186
  parts.push(`- #${related.number} ${related.name} [${related.issueType?.name}] (${related.status?.name}) — ${assignee}`);
866
1187
  }
867
1188
  }
1189
+ if (relatedActivities.length) {
1190
+ parts.push("");
1191
+ parts.push("## Related Work Items");
1192
+ for (const activity of relatedActivities) {
1193
+ const details = [
1194
+ `UUID: ${activity.uuid}`,
1195
+ activity.projectUUID ? `Project: ${activity.projectUUID}` : null,
1196
+ activity.relatedChild ? `Relation: ${activity.relatedChild}` : null
1197
+ ].filter(Boolean);
1198
+ parts.push(`- ${activity.name} (${details.join(", ")})`);
1199
+ }
1200
+ }
868
1201
  if (task.parent?.uuid) {
869
1202
  parts.push("");
870
1203
  parts.push("## Parent Task");
@@ -895,7 +1228,12 @@ var OnesAdapter = class extends BaseAdapter {
895
1228
  parts.push(detailText);
896
1229
  }
897
1230
  const wikiAttachments = wikiContents.flatMap((wiki) => wiki.attachments);
898
- return toRequirement(task, parts.join("\n"), wikiAttachments);
1231
+ const req = toRequirement(task, parts.join("\n"), wikiAttachments);
1232
+ req.raw = {
1233
+ ...req.raw,
1234
+ relatedActivities
1235
+ };
1236
+ return req;
899
1237
  }
900
1238
  /**
901
1239
  * Search tasks assigned to current user via GraphQL.
@@ -959,6 +1297,69 @@ var OnesAdapter = class extends BaseAdapter {
959
1297
  pageSize
960
1298
  };
961
1299
  }
1300
+ async addManhour(params) {
1301
+ const description = params.description.trim();
1302
+ if (!description) throw new Error("ONES: description is required");
1303
+ const taskRef = await this.resolveTaskRef(params.taskId);
1304
+ const onesHours = toOnesHours(params.hours);
1305
+ const workDate = parseManhourDate(params.date);
1306
+ const key = (await this.graphql(ADD_MANHOUR_MUTATION, {
1307
+ mode: "simple",
1308
+ type: "recorded",
1309
+ customData: {},
1310
+ owner: (await this.login()).userUuid,
1311
+ task: taskRef.uuid,
1312
+ start_time: workDate.startTime,
1313
+ hours: onesHours,
1314
+ description
1315
+ }, "add-manhour")).data?.addManhour?.key;
1316
+ if (!key) throw new Error("ONES: Failed to add manhour");
1317
+ return {
1318
+ key,
1319
+ taskUuid: taskRef.uuid,
1320
+ hours: params.hours,
1321
+ description,
1322
+ date: workDate.date
1323
+ };
1324
+ }
1325
+ async updateTaskPlanDates(params) {
1326
+ const planStartDate = params.planStartDate?.trim();
1327
+ const planEndDate = params.planEndDate?.trim();
1328
+ if (!planStartDate && !planEndDate) throw new Error("ONES: planStartDate or planEndDate is required");
1329
+ if (planStartDate && !isValidOnesDate(planStartDate)) throw new Error("ONES: planStartDate must be a valid YYYY-MM-DD date");
1330
+ if (planEndDate && !isValidOnesDate(planEndDate)) throw new Error("ONES: planEndDate must be a valid YYYY-MM-DD date");
1331
+ const taskRef = await this.resolveTaskRef(params.taskId);
1332
+ const session = await this.login();
1333
+ const fieldValues = [];
1334
+ if (planStartDate) fieldValues.push({
1335
+ field_uuid: "field027",
1336
+ value: planStartDate
1337
+ });
1338
+ if (planEndDate) fieldValues.push({
1339
+ field_uuid: "field028",
1340
+ value: planEndDate
1341
+ });
1342
+ const response = await fetch(`${this.config.apiBase}/project/api/project/team/${session.teamUuid}/tasks/update3`, {
1343
+ method: "POST",
1344
+ headers: {
1345
+ "Authorization": `Bearer ${session.accessToken}`,
1346
+ "Content-Type": "application/json"
1347
+ },
1348
+ body: JSON.stringify({ tasks: [{
1349
+ uuid: taskRef.uuid,
1350
+ field_values: fieldValues
1351
+ }] })
1352
+ });
1353
+ if (!response.ok) {
1354
+ const text = await response.text().catch(() => "");
1355
+ throw new Error(`ONES: Failed to update task plan dates: ${response.status} ${text}`);
1356
+ }
1357
+ return {
1358
+ taskUuid: taskRef.uuid,
1359
+ planStartDate: planStartDate ?? null,
1360
+ planEndDate: planEndDate ?? null
1361
+ };
1362
+ }
962
1363
  async getRelatedIssues(params) {
963
1364
  const session = await this.login();
964
1365
  const taskKey = params.taskId.startsWith("task-") ? params.taskId : `task-${params.taskId}`;
@@ -1135,7 +1536,6 @@ var OnesAdapter = class extends BaseAdapter {
1135
1536
  };
1136
1537
  }
1137
1538
  };
1138
-
1139
1539
  //#endregion
1140
1540
  //#region src/adapters/index.ts
1141
1541
  const ADAPTER_MAP = { ones: OnesAdapter };
@@ -1147,7 +1547,6 @@ function createAdapter(sourceType, config, resolvedAuth) {
1147
1547
  if (!AdapterClass) throw new Error(`Unsupported source type: "${sourceType}". Supported: ${Object.keys(ADAPTER_MAP).join(", ")}`);
1148
1548
  return new AdapterClass(sourceType, config, resolvedAuth);
1149
1549
  }
1150
-
1151
1550
  //#endregion
1152
1551
  //#region src/config/loader.ts
1153
1552
  const AuthSchema = z.discriminatedUnion("type", [
@@ -1305,7 +1704,41 @@ function loadConfig(startDir) {
1305
1704
  configPath
1306
1705
  };
1307
1706
  }
1308
-
1707
+ //#endregion
1708
+ //#region src/tools/add-manhour.ts
1709
+ const AddManhourSchema = z.object({
1710
+ taskId: z.string().min(1).describe("The task or requirement ID, key, number, or displayId (e.g. \"task-mock-uuid\", \"mock-uuid\", \"1001\", or \"DEMO-1001\")"),
1711
+ hours: z.number().positive().describe("Work hours to record. Natural hours are converted to ONES internal units."),
1712
+ description: z.string().min(1).describe("Work log description."),
1713
+ date: z.string().optional().describe("Optional work date. Accepts YYYY-MM-DD, a day number like \"11\", or a day-of-month phrase like \"11号\"; day-only values use the current year and month."),
1714
+ source: z.string().optional().describe("Source to update. If omitted, uses the default source.")
1715
+ });
1716
+ async function handleAddManhour(input, adapters, defaultSource) {
1717
+ const sourceType = input.source ?? defaultSource;
1718
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1719
+ const adapter = adapters.get(sourceType);
1720
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1721
+ return { content: [{
1722
+ type: "text",
1723
+ text: formatAddManhourResult(await adapter.addManhour({
1724
+ taskId: input.taskId,
1725
+ hours: input.hours,
1726
+ description: input.description,
1727
+ date: input.date
1728
+ }))
1729
+ }] };
1730
+ }
1731
+ function formatAddManhourResult(result) {
1732
+ return [
1733
+ "Added manhour.",
1734
+ "",
1735
+ `- **Key**: ${result.key}`,
1736
+ `- **Task UUID**: ${result.taskUuid}`,
1737
+ `- **Hours**: ${result.hours}`,
1738
+ `- **Date**: ${result.date ?? "today"}`,
1739
+ `- **Description**: ${result.description}`
1740
+ ].join("\n");
1741
+ }
1309
1742
  //#endregion
1310
1743
  //#region src/tools/get-issue-detail.ts
1311
1744
  const GetIssueDetailSchema = z.object({
@@ -1380,7 +1813,6 @@ function formatIssueDetail(detail) {
1380
1813
  else lines.push("_No description_");
1381
1814
  return lines.join("\n");
1382
1815
  }
1383
-
1384
1816
  //#endregion
1385
1817
  //#region src/tools/get-related-issues.ts
1386
1818
  const GetRelatedIssuesSchema = z.object({
@@ -1421,7 +1853,6 @@ function formatRelatedIssues(issues) {
1421
1853
  }
1422
1854
  return lines.join("\n");
1423
1855
  }
1424
-
1425
1856
  //#endregion
1426
1857
  //#region src/tools/get-requirement.ts
1427
1858
  const GetRequirementSchema = z.object({
@@ -1500,7 +1931,6 @@ function formatRequirement(req) {
1500
1931
  }
1501
1932
  return lines.join("\n");
1502
1933
  }
1503
-
1504
1934
  //#endregion
1505
1935
  //#region src/tools/get-testcases.ts
1506
1936
  const GetTestcasesSchema = z.object({
@@ -1552,7 +1982,6 @@ function formatTestcases(result) {
1552
1982
  }
1553
1983
  return lines.join("\n");
1554
1984
  }
1555
-
1556
1985
  //#endregion
1557
1986
  //#region src/tools/list-sources.ts
1558
1987
  async function handleListSources(adapters, config) {
@@ -1578,7 +2007,6 @@ async function handleListSources(adapters, config) {
1578
2007
  text: lines.join("\n")
1579
2008
  }] };
1580
2009
  }
1581
-
1582
2010
  //#endregion
1583
2011
  //#region src/tools/search-requirements.ts
1584
2012
  const SearchRequirementsSchema = z.object({
@@ -1619,7 +2047,39 @@ async function handleSearchRequirements(input, adapters, defaultSource) {
1619
2047
  text: lines.join("\n")
1620
2048
  }] };
1621
2049
  }
1622
-
2050
+ //#endregion
2051
+ //#region src/tools/update-task-plan-dates.ts
2052
+ const DateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD");
2053
+ const UpdateTaskPlanDatesSchema = z.object({
2054
+ taskId: z.string().min(1).describe("The task or requirement ID, key, number, or displayId (e.g. \"task-mock-uuid\", \"mock-uuid\", \"1001\", or \"DEMO-1001\")"),
2055
+ planStartDate: DateSchema.optional().describe("Plan start date in YYYY-MM-DD format."),
2056
+ planEndDate: DateSchema.optional().describe("Plan end date in YYYY-MM-DD format."),
2057
+ source: z.string().optional().describe("Source to update. If omitted, uses the default source.")
2058
+ });
2059
+ async function handleUpdateTaskPlanDates(input, adapters, defaultSource) {
2060
+ const sourceType = input.source ?? defaultSource;
2061
+ if (!sourceType) throw new Error("No source specified and no default source configured");
2062
+ const adapter = adapters.get(sourceType);
2063
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
2064
+ return { content: [{
2065
+ type: "text",
2066
+ text: formatUpdateTaskPlanDatesResult(await adapter.updateTaskPlanDates({
2067
+ taskId: input.taskId,
2068
+ planStartDate: input.planStartDate,
2069
+ planEndDate: input.planEndDate
2070
+ }))
2071
+ }] };
2072
+ }
2073
+ function formatUpdateTaskPlanDatesResult(result) {
2074
+ const lines = [
2075
+ "Updated task plan dates.",
2076
+ "",
2077
+ `- **Task UUID**: ${result.taskUuid}`
2078
+ ];
2079
+ if (result.planStartDate) lines.push(`- **Plan Start Date**: ${result.planStartDate}`);
2080
+ if (result.planEndDate) lines.push(`- **Plan End Date**: ${result.planEndDate}`);
2081
+ return lines.join("\n");
2082
+ }
1623
2083
  //#endregion
1624
2084
  //#region src/index.ts
1625
2085
  /**
@@ -1667,7 +2127,10 @@ async function main() {
1667
2127
  name: "ai-dev-requirements",
1668
2128
  version: "0.1.0"
1669
2129
  });
1670
- server.tool("get_requirement", "Fetch a single requirement/issue by its ID from a configured source (ONES)", GetRequirementSchema.shape, async (params) => {
2130
+ server.registerTool("get_requirement", {
2131
+ description: "Fetch a single requirement/issue by its ID from a configured source (ONES)",
2132
+ inputSchema: GetRequirementSchema.shape
2133
+ }, async (params) => {
1671
2134
  try {
1672
2135
  return await handleGetRequirement(params, adapters, config.config.defaultSource);
1673
2136
  } catch (err) {
@@ -1680,7 +2143,10 @@ async function main() {
1680
2143
  };
1681
2144
  }
1682
2145
  });
1683
- server.tool("search_requirements", "Search for requirements/issues by keywords across a configured source", SearchRequirementsSchema.shape, async (params) => {
2146
+ server.registerTool("search_requirements", {
2147
+ description: "Search for requirements/issues by keywords across a configured source",
2148
+ inputSchema: SearchRequirementsSchema.shape
2149
+ }, async (params) => {
1684
2150
  try {
1685
2151
  return await handleSearchRequirements(params, adapters, config.config.defaultSource);
1686
2152
  } catch (err) {
@@ -1693,7 +2159,7 @@ async function main() {
1693
2159
  };
1694
2160
  }
1695
2161
  });
1696
- server.tool("list_sources", "List all configured requirement sources and their status", {}, async () => {
2162
+ server.registerTool("list_sources", { description: "List all configured requirement sources and their status" }, async () => {
1697
2163
  try {
1698
2164
  return await handleListSources(adapters, config.config);
1699
2165
  } catch (err) {
@@ -1706,7 +2172,10 @@ async function main() {
1706
2172
  };
1707
2173
  }
1708
2174
  });
1709
- server.tool("get_related_issues", "Get pending defect issues (bugs) related to a requirement task. Returns all pending defects grouped by assignee (current user first).", GetRelatedIssuesSchema.shape, async (params) => {
2175
+ server.registerTool("get_related_issues", {
2176
+ description: "Get pending defect issues (bugs) related to a requirement task. Returns all pending defects grouped by assignee (current user first).",
2177
+ inputSchema: GetRelatedIssuesSchema.shape
2178
+ }, async (params) => {
1710
2179
  try {
1711
2180
  return await handleGetRelatedIssues(params, adapters, config.config.defaultSource);
1712
2181
  } catch (err) {
@@ -1719,7 +2188,10 @@ async function main() {
1719
2188
  };
1720
2189
  }
1721
2190
  });
1722
- server.tool("get_issue_detail", "Get detailed information about a specific issue/defect including description, rich text, and images", GetIssueDetailSchema.shape, async (params) => {
2191
+ server.registerTool("get_issue_detail", {
2192
+ description: "Get detailed information about a specific issue/defect including description, rich text, and images",
2193
+ inputSchema: GetIssueDetailSchema.shape
2194
+ }, async (params) => {
1723
2195
  try {
1724
2196
  return await handleGetIssueDetail(params, adapters, config.config.defaultSource);
1725
2197
  } catch (err) {
@@ -1732,7 +2204,10 @@ async function main() {
1732
2204
  };
1733
2205
  }
1734
2206
  });
1735
- server.tool("get_testcases", "Get all test cases for a task by its number (e.g. 302). Searches the testcase library for a matching module and returns all cases with steps.", GetTestcasesSchema.shape, async (params) => {
2207
+ server.registerTool("get_testcases", {
2208
+ description: "Get all test cases for a task by its number (e.g. 302). Searches the testcase library for a matching module and returns all cases with steps.",
2209
+ inputSchema: GetTestcasesSchema.shape
2210
+ }, async (params) => {
1736
2211
  try {
1737
2212
  return await handleGetTestcases(params, adapters, config.config.defaultSource);
1738
2213
  } catch (err) {
@@ -1745,6 +2220,38 @@ async function main() {
1745
2220
  };
1746
2221
  }
1747
2222
  });
2223
+ server.registerTool("add_manhour", {
2224
+ description: "Add a work-hour record to a ONES task, bug, or requirement. Supports task key, uuid, number, or displayId.",
2225
+ inputSchema: AddManhourSchema.shape
2226
+ }, async (params) => {
2227
+ try {
2228
+ return await handleAddManhour(params, adapters, config.config.defaultSource);
2229
+ } catch (err) {
2230
+ return {
2231
+ content: [{
2232
+ type: "text",
2233
+ text: `Error: ${err.message}`
2234
+ }],
2235
+ isError: true
2236
+ };
2237
+ }
2238
+ });
2239
+ server.registerTool("update_task_plan_dates", {
2240
+ description: "Update plan start and/or plan end dates for a ONES task, bug, or requirement. Supports task key, uuid, number, or displayId.",
2241
+ inputSchema: UpdateTaskPlanDatesSchema.shape
2242
+ }, async (params) => {
2243
+ try {
2244
+ return await handleUpdateTaskPlanDates(params, adapters, config.config.defaultSource);
2245
+ } catch (err) {
2246
+ return {
2247
+ content: [{
2248
+ type: "text",
2249
+ text: `Error: ${err.message}`
2250
+ }],
2251
+ isError: true
2252
+ };
2253
+ }
2254
+ });
1748
2255
  const transport = new StdioServerTransport();
1749
2256
  await server.connect(transport);
1750
2257
  }
@@ -1752,7 +2259,7 @@ main().catch((err) => {
1752
2259
  console.error("[requirements-mcp] Fatal error:", err);
1753
2260
  process.exit(1);
1754
2261
  });
1755
-
1756
2262
  //#endregion
1757
- export { };
2263
+ export {};
2264
+
1758
2265
  //# sourceMappingURL=index.mjs.map