ai-dev-requirements 0.1.10 → 0.1.12

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
@@ -144,6 +144,26 @@ const ISSUE_TYPES_QUERY = `
144
144
  }
145
145
  }
146
146
  `;
147
+ const PROJECTS_QUERY = `
148
+ query Projects($groupBy: GroupBy, $orderBy: OrderBy, $pagination: Pagination, $projectOrderBy: OrderBy, $projectFilterGroup: [Filter!]) {
149
+ buckets(groupBy: $groupBy, orderBy: $orderBy, pagination: $pagination) {
150
+ key
151
+ projects(limit: 10000, orderBy: $projectOrderBy, filterGroup: $projectFilterGroup) {
152
+ key
153
+ uuid
154
+ name
155
+ identifier
156
+ }
157
+ }
158
+ }
159
+ `;
160
+ const ADD_MANHOUR_MUTATION = `
161
+ mutation AddManhour {
162
+ addManhour(mode: $mode, owner: $owner, task: $task, type: $type, start_time: $start_time, hours: $hours, description: $description, customData: $customData) {
163
+ key
164
+ }
165
+ }
166
+ `;
147
167
  const TASK_BY_NUMBER_QUERY = SEARCH_TASKS_QUERY;
148
168
  const RELATED_TASKS_QUERY = `
149
169
  query Task($key: Key) {
@@ -331,6 +351,93 @@ function extractWikiPageUuidsFromText(text) {
331
351
  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
352
  return [...uuids];
333
353
  }
354
+ function parseOnesWikiPageRoute(input) {
355
+ if (!input.includes("/wiki/")) return null;
356
+ const match = (() => {
357
+ try {
358
+ const parsed = new URL(input);
359
+ return `${parsed.pathname}${parsed.hash}${parsed.search}`;
360
+ } catch {
361
+ return input;
362
+ }
363
+ })().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
364
+ if (!match?.[1] || !match[2]) return null;
365
+ return {
366
+ teamUuid: decodeURIComponent(match[1]),
367
+ wikiUuid: decodeURIComponent(match[2])
368
+ };
369
+ }
370
+ function isOnesWikiUrlInput(input) {
371
+ return input.includes("/wiki/");
372
+ }
373
+ function parseAuthorizeRequestId(location) {
374
+ try {
375
+ const parsed = new URL(location);
376
+ return parsed.searchParams.get("auth_request_id") ?? parsed.searchParams.get("id");
377
+ } catch {
378
+ const match = location.match(/[?&](?:auth_request_id|id)=([^&#]+)/);
379
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
380
+ }
381
+ }
382
+ function parseAuthorizationCode(location) {
383
+ try {
384
+ return new URL(location).searchParams.get("code");
385
+ } catch {
386
+ const match = location.match(/[?&]code=([^&#]+)/);
387
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
388
+ }
389
+ }
390
+ function parseDisplayId(input) {
391
+ const match = input.trim().match(/^([a-z]\w*)-(\d+)$/i);
392
+ if (!match?.[1] || !match[2]) return null;
393
+ return {
394
+ identifier: match[1],
395
+ number: Number.parseInt(match[2], 10)
396
+ };
397
+ }
398
+ function isValidOnesDate(value) {
399
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
400
+ const [yearText, monthText, dayText] = value.split("-");
401
+ const year = Number.parseInt(yearText, 10);
402
+ const month = Number.parseInt(monthText, 10);
403
+ const day = Number.parseInt(dayText, 10);
404
+ const date = new Date(Date.UTC(year, month - 1, day));
405
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
406
+ }
407
+ function toOnesHours(hours) {
408
+ if (!Number.isFinite(hours) || hours <= 0) throw new Error("ONES: hours must be a positive number");
409
+ return Math.round(hours * 1e5);
410
+ }
411
+ function getTodayStartUnixSeconds() {
412
+ const now = /* @__PURE__ */ new Date();
413
+ const localStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
414
+ return Math.floor(localStart.getTime() / 1e3);
415
+ }
416
+ function toLocalDateString(date) {
417
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
418
+ }
419
+ function getLocalStartUnixSeconds(year, month, day) {
420
+ return Math.floor(new Date(year, month - 1, day).getTime() / 1e3);
421
+ }
422
+ function parseManhourDate(input) {
423
+ const value = input?.trim();
424
+ if (!value) return {
425
+ date: null,
426
+ startTime: getTodayStartUnixSeconds()
427
+ };
428
+ const fullDateMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
429
+ const dayOnlyMatch = value.match(/^(\d{1,2})号?$/);
430
+ const now = /* @__PURE__ */ new Date();
431
+ const year = fullDateMatch ? Number.parseInt(fullDateMatch[1], 10) : now.getFullYear();
432
+ const month = fullDateMatch ? Number.parseInt(fullDateMatch[2], 10) : now.getMonth() + 1;
433
+ const day = fullDateMatch ? Number.parseInt(fullDateMatch[3], 10) : dayOnlyMatch ? Number.parseInt(dayOnlyMatch[1], 10) : NaN;
434
+ const parsed = new Date(year, month - 1, day);
435
+ 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");
436
+ return {
437
+ date: toLocalDateString(parsed),
438
+ startTime: getLocalStartUnixSeconds(year, month, day)
439
+ };
440
+ }
334
441
  function htmlToPlainText(html) {
335
442
  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();
336
443
  }
@@ -525,32 +632,35 @@ var OnesAdapter = class extends BaseAdapter {
525
632
  redirect: "manual"
526
633
  })).headers.get("location");
527
634
  if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
528
- const authRequestId = new URL(authorizeLocation).searchParams.get("id");
529
- if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
530
- const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
531
- method: "POST",
532
- headers: {
533
- "Content-Type": "application/json;charset=UTF-8",
534
- "Cookie": cookies
535
- },
536
- body: JSON.stringify({
537
- auth_request_id: authRequestId,
538
- region_uuid: orgUser.region_uuid,
539
- org_uuid: orgUser.org_uuid,
540
- org_user_uuid: orgUser.org_user.org_user_uuid
541
- })
542
- });
543
- if (!finalizeRes.ok) {
544
- const text = await finalizeRes.text().catch(() => "");
545
- throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
635
+ let code = parseAuthorizationCode(authorizeLocation);
636
+ if (!code) {
637
+ const authRequestId = parseAuthorizeRequestId(authorizeLocation);
638
+ if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
639
+ const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
640
+ method: "POST",
641
+ headers: {
642
+ "Content-Type": "application/json;charset=UTF-8",
643
+ "Cookie": cookies
644
+ },
645
+ body: JSON.stringify({
646
+ auth_request_id: authRequestId,
647
+ region_uuid: orgUser.region_uuid,
648
+ org_uuid: orgUser.org_uuid,
649
+ org_user_uuid: orgUser.org_user.org_user_uuid
650
+ })
651
+ });
652
+ if (!finalizeRes.ok) {
653
+ const text = await finalizeRes.text().catch(() => "");
654
+ throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
655
+ }
656
+ const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
657
+ method: "GET",
658
+ headers: { Cookie: cookies },
659
+ redirect: "manual"
660
+ })).headers.get("location");
661
+ if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
662
+ code = parseAuthorizationCode(callbackLocation);
546
663
  }
547
- const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
548
- method: "GET",
549
- headers: { Cookie: cookies },
550
- redirect: "manual"
551
- })).headers.get("location");
552
- if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
553
- const code = new URL(callbackLocation).searchParams.get("code");
554
664
  if (!code) throw new Error("ONES: Cannot parse authorization code from callback redirect");
555
665
  const tokenRes = await fetch(`${baseUrl}/identity/oauth/token`, {
556
666
  method: "POST",
@@ -620,11 +730,107 @@ var OnesAdapter = class extends BaseAdapter {
620
730
  }
621
731
  return response.json();
622
732
  }
733
+ async searchTaskByNumber(taskNumber) {
734
+ const session = await this.login();
735
+ const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/search?q=${encodeURIComponent(String(taskNumber))}&start=0&limit=10&types=task`;
736
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
737
+ if (!response.ok) return null;
738
+ const found = ((await response.json()).datas?.task ?? []).map((item) => item.fields).find((fields) => fields?.uuid && fields.number === taskNumber);
739
+ if (!found?.uuid) return null;
740
+ return {
741
+ key: `task-${found.uuid}`,
742
+ uuid: found.uuid,
743
+ number: found.number ?? taskNumber,
744
+ name: found.summary ?? "",
745
+ status: {
746
+ uuid: "",
747
+ name: "",
748
+ category: void 0
749
+ },
750
+ issueType: found.issue_type_uuid || found.issue_type_name ? {
751
+ uuid: found.issue_type_uuid ?? "",
752
+ name: found.issue_type_name ?? ""
753
+ } : void 0,
754
+ project: found.project_uuid || found.project_name ? {
755
+ uuid: found.project_uuid ?? "",
756
+ name: found.project_name ?? ""
757
+ } : void 0
758
+ };
759
+ }
623
760
  async fetchIssueTypes() {
624
761
  if (this.issueTypesCache) return this.issueTypesCache;
625
762
  this.issueTypesCache = (await this.graphql(ISSUE_TYPES_QUERY, { orderBy: { namePinyin: "ASC" } }, "issueTypes")).data?.issueTypes ?? [];
626
763
  return this.issueTypesCache;
627
764
  }
765
+ async fetchProjects() {
766
+ return (await this.graphql(PROJECTS_QUERY, {
767
+ projectOrderBy: {
768
+ isPin: "DESC",
769
+ namePinyin: "ASC",
770
+ createTime: "DESC"
771
+ },
772
+ projectFilterGroup: [{
773
+ visibleInProject_equal: true,
774
+ isArchive_equal: false
775
+ }],
776
+ groupBy: { projects: {} },
777
+ orderBy: null,
778
+ pagination: {
779
+ limit: 50,
780
+ after: "",
781
+ preciseCount: true
782
+ }
783
+ }, "projects-group-list-for-project-view")).data?.buckets?.flatMap((bucket) => bucket.projects ?? []) ?? [];
784
+ }
785
+ async findTaskByNumber(taskNumber, projectUuid) {
786
+ const filter = { number_in: [taskNumber] };
787
+ if (projectUuid) filter.project_in = [projectUuid];
788
+ const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
789
+ groupBy: { tasks: {} },
790
+ groupOrderBy: null,
791
+ orderBy: { createTime: "DESC" },
792
+ filterGroup: [filter],
793
+ search: null,
794
+ pagination: {
795
+ limit: 10,
796
+ preciseCount: false
797
+ },
798
+ limit: 10
799
+ }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((task) => task.number === taskNumber && (!projectUuid || task.project?.uuid === projectUuid));
800
+ if (found) return found;
801
+ if (projectUuid) return null;
802
+ return this.searchTaskByNumber(taskNumber);
803
+ }
804
+ async resolveTaskRef(input) {
805
+ const taskId = input.trim();
806
+ if (!taskId) throw new Error("ONES: taskId is required");
807
+ const numMatch = taskId.match(/^#?(\d+)$/);
808
+ if (numMatch) {
809
+ const taskNumber = Number.parseInt(numMatch[1], 10);
810
+ const found = await this.findTaskByNumber(taskNumber);
811
+ if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
812
+ return {
813
+ key: found.key ?? `task-${found.uuid}`,
814
+ uuid: found.uuid
815
+ };
816
+ }
817
+ const displayId = parseDisplayId(taskId);
818
+ if (displayId) {
819
+ const project = (await this.fetchProjects()).find((item) => item.identifier?.toLowerCase() === displayId.identifier.toLowerCase());
820
+ if (!project) throw new Error(`ONES: Project identifier "${displayId.identifier}" not found in current team`);
821
+ const found = await this.findTaskByNumber(displayId.number, project.uuid);
822
+ if (!found) throw new Error(`ONES: Task "${taskId}" not found in current team`);
823
+ return {
824
+ key: found.key ?? `task-${found.uuid}`,
825
+ uuid: found.uuid
826
+ };
827
+ }
828
+ const key = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
829
+ return {
830
+ key,
831
+ uuid: key.slice(5)
832
+ };
833
+ }
628
834
  async searchTeamUsers(keyword) {
629
835
  const session = await this.login();
630
836
  const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/users/search`;
@@ -733,22 +939,25 @@ var OnesAdapter = class extends BaseAdapter {
733
939
  * Fetch wiki page content via REST API.
734
940
  * Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
735
941
  */
736
- async fetchWikiPageDetail(wikiUuid) {
942
+ async fetchWikiPageDetail(wikiUuid, teamUuid) {
737
943
  const session = await this.login();
738
- const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/page/${wikiUuid}/detail`;
944
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
945
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/page/${wikiUuid}/detail`;
739
946
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
740
947
  if (!response.ok) return {};
741
948
  return response.json();
742
949
  }
743
- buildWikiImageUrl(session, refUuid, source, token) {
950
+ buildWikiImageUrl(session, refUuid, source, token, teamUuid) {
744
951
  const encodedRefUuid = encodeURIComponent(refUuid);
745
952
  const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
746
953
  const encodedToken = encodeURIComponent(token);
747
- return `${this.config.apiBase}/wiki/api/wiki/editor/${session.teamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
954
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
955
+ return `${this.config.apiBase}/wiki/api/wiki/editor/${wikiTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
748
956
  }
749
- async fetchWikiContent(wikiUuid) {
957
+ async fetchWikiContent(wikiUuid, teamUuid) {
750
958
  const session = await this.login();
751
- const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
959
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
960
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/online_page/${wikiUuid}/content`;
752
961
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
753
962
  if (!response.ok) return {
754
963
  content: "",
@@ -762,7 +971,7 @@ var OnesAdapter = class extends BaseAdapter {
762
971
  content,
763
972
  attachments: []
764
973
  };
765
- const detail = await this.fetchWikiPageDetail(wikiUuid);
974
+ const detail = await this.fetchWikiPageDetail(wikiUuid, wikiTeamUuid);
766
975
  const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
767
976
  if (!refUuid) return {
768
977
  content,
@@ -773,38 +982,46 @@ var OnesAdapter = class extends BaseAdapter {
773
982
  attachments: renderContext.imageSources.map((source, index) => ({
774
983
  id: `${wikiUuid}-image-${index + 1}`,
775
984
  name: attachmentNameFromPath(source),
776
- url: this.buildWikiImageUrl(session, refUuid, source, token),
985
+ url: this.buildWikiImageUrl(session, refUuid, source, token, wikiTeamUuid),
777
986
  mimeType: mimeTypeFromFileName(source),
778
987
  size: 0
779
988
  }))
780
989
  };
781
990
  }
782
991
  /**
783
- * Fetch a single task by UUID or number (e.g. "#95945" or "95945").
992
+ * Fetch a single task by UUID or number (e.g. "#1001" or "1001").
784
993
  * If a number is given, searches first to resolve the UUID.
785
994
  */
786
995
  async getRequirement(params) {
787
- let taskUuid = params.id;
788
- const numMatch = taskUuid.match(/^#?(\d+)$/);
789
- if (numMatch) {
790
- const taskNumber = Number.parseInt(numMatch[1], 10);
791
- const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
792
- groupBy: { tasks: {} },
793
- groupOrderBy: null,
794
- orderBy: { createTime: "DESC" },
795
- filterGroup: [{ number_in: [taskNumber] }],
796
- search: null,
797
- pagination: {
798
- limit: 10,
799
- preciseCount: false
800
- },
801
- limit: 10
802
- }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === taskNumber);
803
- if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
804
- taskUuid = found.uuid;
996
+ const wikiRoute = parseOnesWikiPageRoute(params.id);
997
+ if (wikiRoute) {
998
+ const rendered = await this.fetchWikiContent(wikiRoute.wikiUuid, wikiRoute.teamUuid);
999
+ return {
1000
+ id: wikiRoute.wikiUuid,
1001
+ source: "ones",
1002
+ title: `Wiki ${wikiRoute.wikiUuid}`,
1003
+ description: rendered.content,
1004
+ status: "open",
1005
+ priority: "medium",
1006
+ type: "feature",
1007
+ labels: [],
1008
+ reporter: "",
1009
+ assignee: null,
1010
+ createdAt: "",
1011
+ updatedAt: "",
1012
+ dueDate: null,
1013
+ attachments: rendered.attachments,
1014
+ raw: {
1015
+ input: params.id,
1016
+ teamUuid: wikiRoute.teamUuid,
1017
+ wikiUuid: wikiRoute.wikiUuid
1018
+ }
1019
+ };
805
1020
  }
806
- const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
807
- if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
1021
+ if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
1022
+ const taskRef = await this.resolveTaskRef(params.id);
1023
+ const task = (await this.graphql(TASK_DETAIL_QUERY, { key: taskRef.key }, "Task")).data?.task;
1024
+ if (!task) throw new Error(`ONES: Task "${params.id}" not found`);
808
1025
  const wikiRefs = /* @__PURE__ */ new Map();
809
1026
  for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
810
1027
  title: wiki.title,
@@ -939,6 +1156,69 @@ var OnesAdapter = class extends BaseAdapter {
939
1156
  pageSize
940
1157
  };
941
1158
  }
1159
+ async addManhour(params) {
1160
+ const description = params.description.trim();
1161
+ if (!description) throw new Error("ONES: description is required");
1162
+ const taskRef = await this.resolveTaskRef(params.taskId);
1163
+ const onesHours = toOnesHours(params.hours);
1164
+ const workDate = parseManhourDate(params.date);
1165
+ const key = (await this.graphql(ADD_MANHOUR_MUTATION, {
1166
+ mode: "simple",
1167
+ type: "recorded",
1168
+ customData: {},
1169
+ owner: (await this.login()).userUuid,
1170
+ task: taskRef.uuid,
1171
+ start_time: workDate.startTime,
1172
+ hours: onesHours,
1173
+ description
1174
+ }, "add-manhour")).data?.addManhour?.key;
1175
+ if (!key) throw new Error("ONES: Failed to add manhour");
1176
+ return {
1177
+ key,
1178
+ taskUuid: taskRef.uuid,
1179
+ hours: params.hours,
1180
+ description,
1181
+ date: workDate.date
1182
+ };
1183
+ }
1184
+ async updateTaskPlanDates(params) {
1185
+ const planStartDate = params.planStartDate?.trim();
1186
+ const planEndDate = params.planEndDate?.trim();
1187
+ if (!planStartDate && !planEndDate) throw new Error("ONES: planStartDate or planEndDate is required");
1188
+ if (planStartDate && !isValidOnesDate(planStartDate)) throw new Error("ONES: planStartDate must be a valid YYYY-MM-DD date");
1189
+ if (planEndDate && !isValidOnesDate(planEndDate)) throw new Error("ONES: planEndDate must be a valid YYYY-MM-DD date");
1190
+ const taskRef = await this.resolveTaskRef(params.taskId);
1191
+ const session = await this.login();
1192
+ const fieldValues = [];
1193
+ if (planStartDate) fieldValues.push({
1194
+ field_uuid: "field027",
1195
+ value: planStartDate
1196
+ });
1197
+ if (planEndDate) fieldValues.push({
1198
+ field_uuid: "field028",
1199
+ value: planEndDate
1200
+ });
1201
+ const response = await fetch(`${this.config.apiBase}/project/api/project/team/${session.teamUuid}/tasks/update3`, {
1202
+ method: "POST",
1203
+ headers: {
1204
+ "Authorization": `Bearer ${session.accessToken}`,
1205
+ "Content-Type": "application/json"
1206
+ },
1207
+ body: JSON.stringify({ tasks: [{
1208
+ uuid: taskRef.uuid,
1209
+ field_values: fieldValues
1210
+ }] })
1211
+ });
1212
+ if (!response.ok) {
1213
+ const text = await response.text().catch(() => "");
1214
+ throw new Error(`ONES: Failed to update task plan dates: ${response.status} ${text}`);
1215
+ }
1216
+ return {
1217
+ taskUuid: taskRef.uuid,
1218
+ planStartDate: planStartDate ?? null,
1219
+ planEndDate: planEndDate ?? null
1220
+ };
1221
+ }
942
1222
  async getRelatedIssues(params) {
943
1223
  const session = await this.login();
944
1224
  const taskKey = params.taskId.startsWith("task-") ? params.taskId : `task-${params.taskId}`;
@@ -1286,10 +1566,46 @@ function loadConfig(startDir) {
1286
1566
  };
1287
1567
  }
1288
1568
 
1569
+ //#endregion
1570
+ //#region src/tools/add-manhour.ts
1571
+ const AddManhourSchema = zod_v4.z.object({
1572
+ taskId: zod_v4.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\")"),
1573
+ hours: zod_v4.z.number().positive().describe("Work hours to record. Natural hours are converted to ONES internal units."),
1574
+ description: zod_v4.z.string().min(1).describe("Work log description."),
1575
+ date: zod_v4.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."),
1576
+ source: zod_v4.z.string().optional().describe("Source to update. If omitted, uses the default source.")
1577
+ });
1578
+ async function handleAddManhour(input, adapters, defaultSource) {
1579
+ const sourceType = input.source ?? defaultSource;
1580
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1581
+ const adapter = adapters.get(sourceType);
1582
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1583
+ return { content: [{
1584
+ type: "text",
1585
+ text: formatAddManhourResult(await adapter.addManhour({
1586
+ taskId: input.taskId,
1587
+ hours: input.hours,
1588
+ description: input.description,
1589
+ date: input.date
1590
+ }))
1591
+ }] };
1592
+ }
1593
+ function formatAddManhourResult(result) {
1594
+ return [
1595
+ "Added manhour.",
1596
+ "",
1597
+ `- **Key**: ${result.key}`,
1598
+ `- **Task UUID**: ${result.taskUuid}`,
1599
+ `- **Hours**: ${result.hours}`,
1600
+ `- **Date**: ${result.date ?? "today"}`,
1601
+ `- **Description**: ${result.description}`
1602
+ ].join("\n");
1603
+ }
1604
+
1289
1605
  //#endregion
1290
1606
  //#region src/tools/get-issue-detail.ts
1291
1607
  const GetIssueDetailSchema = zod_v4.z.object({
1292
- issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"6W9vW3y8J9DO66Pu\" or \"task-6W9vW3y8J9DO66Pu\")"),
1608
+ issueId: zod_v4.z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
1293
1609
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1294
1610
  });
1295
1611
  /**
@@ -1364,7 +1680,7 @@ function formatIssueDetail(detail) {
1364
1680
  //#endregion
1365
1681
  //#region src/tools/get-related-issues.ts
1366
1682
  const GetRelatedIssuesSchema = zod_v4.z.object({
1367
- taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"HRL2p8rTX4mQ9xMv\" or \"task-HRL2p8rTX4mQ9xMv\")"),
1683
+ taskId: zod_v4.z.string().describe("The parent task ID or key (e.g. \"mock-task-uuid\" or \"task-mock-task-uuid\")"),
1368
1684
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1369
1685
  });
1370
1686
  async function handleGetRelatedIssues(input, adapters, defaultSource) {
@@ -1405,7 +1721,7 @@ function formatRelatedIssues(issues) {
1405
1721
  //#endregion
1406
1722
  //#region src/tools/get-requirement.ts
1407
1723
  const GetRequirementSchema = zod_v4.z.object({
1408
- id: zod_v4.z.string().describe("The requirement/issue ID"),
1724
+ id: zod_v4.z.string().describe("The requirement/issue ID, task number, or ONES wiki page URL"),
1409
1725
  source: zod_v4.z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1410
1726
  });
1411
1727
  async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
@@ -1600,6 +1916,40 @@ async function handleSearchRequirements(input, adapters, defaultSource) {
1600
1916
  }] };
1601
1917
  }
1602
1918
 
1919
+ //#endregion
1920
+ //#region src/tools/update-task-plan-dates.ts
1921
+ const DateSchema = zod_v4.z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD");
1922
+ const UpdateTaskPlanDatesSchema = zod_v4.z.object({
1923
+ taskId: zod_v4.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\")"),
1924
+ planStartDate: DateSchema.optional().describe("Plan start date in YYYY-MM-DD format."),
1925
+ planEndDate: DateSchema.optional().describe("Plan end date in YYYY-MM-DD format."),
1926
+ source: zod_v4.z.string().optional().describe("Source to update. If omitted, uses the default source.")
1927
+ });
1928
+ async function handleUpdateTaskPlanDates(input, adapters, defaultSource) {
1929
+ const sourceType = input.source ?? defaultSource;
1930
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1931
+ const adapter = adapters.get(sourceType);
1932
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1933
+ return { content: [{
1934
+ type: "text",
1935
+ text: formatUpdateTaskPlanDatesResult(await adapter.updateTaskPlanDates({
1936
+ taskId: input.taskId,
1937
+ planStartDate: input.planStartDate,
1938
+ planEndDate: input.planEndDate
1939
+ }))
1940
+ }] };
1941
+ }
1942
+ function formatUpdateTaskPlanDatesResult(result) {
1943
+ const lines = [
1944
+ "Updated task plan dates.",
1945
+ "",
1946
+ `- **Task UUID**: ${result.taskUuid}`
1947
+ ];
1948
+ if (result.planStartDate) lines.push(`- **Plan Start Date**: ${result.planStartDate}`);
1949
+ if (result.planEndDate) lines.push(`- **Plan End Date**: ${result.planEndDate}`);
1950
+ return lines.join("\n");
1951
+ }
1952
+
1603
1953
  //#endregion
1604
1954
  //#region src/index.ts
1605
1955
  /**
@@ -1725,6 +2075,32 @@ async function main() {
1725
2075
  };
1726
2076
  }
1727
2077
  });
2078
+ server.tool("add_manhour", "Add a work-hour record to a ONES task, bug, or requirement. Supports task key, uuid, number, or displayId.", AddManhourSchema.shape, async (params) => {
2079
+ try {
2080
+ return await handleAddManhour(params, adapters, config.config.defaultSource);
2081
+ } catch (err) {
2082
+ return {
2083
+ content: [{
2084
+ type: "text",
2085
+ text: `Error: ${err.message}`
2086
+ }],
2087
+ isError: true
2088
+ };
2089
+ }
2090
+ });
2091
+ server.tool("update_task_plan_dates", "Update plan start and/or plan end dates for a ONES task, bug, or requirement. Supports task key, uuid, number, or displayId.", UpdateTaskPlanDatesSchema.shape, async (params) => {
2092
+ try {
2093
+ return await handleUpdateTaskPlanDates(params, adapters, config.config.defaultSource);
2094
+ } catch (err) {
2095
+ return {
2096
+ content: [{
2097
+ type: "text",
2098
+ text: `Error: ${err.message}`
2099
+ }],
2100
+ isError: true
2101
+ };
2102
+ }
2103
+ });
1728
2104
  const transport = new _modelcontextprotocol_sdk_server_stdio_js.StdioServerTransport();
1729
2105
  await server.connect(transport);
1730
2106
  }