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.mjs CHANGED
@@ -116,6 +116,26 @@ const ISSUE_TYPES_QUERY = `
116
116
  }
117
117
  }
118
118
  `;
119
+ const PROJECTS_QUERY = `
120
+ query Projects($groupBy: GroupBy, $orderBy: OrderBy, $pagination: Pagination, $projectOrderBy: OrderBy, $projectFilterGroup: [Filter!]) {
121
+ buckets(groupBy: $groupBy, orderBy: $orderBy, pagination: $pagination) {
122
+ key
123
+ projects(limit: 10000, orderBy: $projectOrderBy, filterGroup: $projectFilterGroup) {
124
+ key
125
+ uuid
126
+ name
127
+ identifier
128
+ }
129
+ }
130
+ }
131
+ `;
132
+ const ADD_MANHOUR_MUTATION = `
133
+ mutation AddManhour {
134
+ addManhour(mode: $mode, owner: $owner, task: $task, type: $type, start_time: $start_time, hours: $hours, description: $description, customData: $customData) {
135
+ key
136
+ }
137
+ }
138
+ `;
119
139
  const TASK_BY_NUMBER_QUERY = SEARCH_TASKS_QUERY;
120
140
  const RELATED_TASKS_QUERY = `
121
141
  query Task($key: Key) {
@@ -303,6 +323,93 @@ function extractWikiPageUuidsFromText(text) {
303
323
  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
324
  return [...uuids];
305
325
  }
326
+ function parseOnesWikiPageRoute(input) {
327
+ if (!input.includes("/wiki/")) return null;
328
+ const match = (() => {
329
+ try {
330
+ const parsed = new URL(input);
331
+ return `${parsed.pathname}${parsed.hash}${parsed.search}`;
332
+ } catch {
333
+ return input;
334
+ }
335
+ })().match(/\/team\/([^/?#]+)\/space\/[^/?#]+\/page\/([^/?#]+)/);
336
+ if (!match?.[1] || !match[2]) return null;
337
+ return {
338
+ teamUuid: decodeURIComponent(match[1]),
339
+ wikiUuid: decodeURIComponent(match[2])
340
+ };
341
+ }
342
+ function isOnesWikiUrlInput(input) {
343
+ return input.includes("/wiki/");
344
+ }
345
+ function parseAuthorizeRequestId(location) {
346
+ try {
347
+ const parsed = new URL(location);
348
+ return parsed.searchParams.get("auth_request_id") ?? parsed.searchParams.get("id");
349
+ } catch {
350
+ const match = location.match(/[?&](?:auth_request_id|id)=([^&#]+)/);
351
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
352
+ }
353
+ }
354
+ function parseAuthorizationCode(location) {
355
+ try {
356
+ return new URL(location).searchParams.get("code");
357
+ } catch {
358
+ const match = location.match(/[?&]code=([^&#]+)/);
359
+ return match?.[1] ? decodeURIComponent(match[1]) : null;
360
+ }
361
+ }
362
+ function parseDisplayId(input) {
363
+ const match = input.trim().match(/^([a-z]\w*)-(\d+)$/i);
364
+ if (!match?.[1] || !match[2]) return null;
365
+ return {
366
+ identifier: match[1],
367
+ number: Number.parseInt(match[2], 10)
368
+ };
369
+ }
370
+ function isValidOnesDate(value) {
371
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
372
+ const [yearText, monthText, dayText] = value.split("-");
373
+ const year = Number.parseInt(yearText, 10);
374
+ const month = Number.parseInt(monthText, 10);
375
+ const day = Number.parseInt(dayText, 10);
376
+ const date = new Date(Date.UTC(year, month - 1, day));
377
+ return date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day;
378
+ }
379
+ function toOnesHours(hours) {
380
+ if (!Number.isFinite(hours) || hours <= 0) throw new Error("ONES: hours must be a positive number");
381
+ return Math.round(hours * 1e5);
382
+ }
383
+ function getTodayStartUnixSeconds() {
384
+ const now = /* @__PURE__ */ new Date();
385
+ const localStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
386
+ return Math.floor(localStart.getTime() / 1e3);
387
+ }
388
+ function toLocalDateString(date) {
389
+ return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
390
+ }
391
+ function getLocalStartUnixSeconds(year, month, day) {
392
+ return Math.floor(new Date(year, month - 1, day).getTime() / 1e3);
393
+ }
394
+ function parseManhourDate(input) {
395
+ const value = input?.trim();
396
+ if (!value) return {
397
+ date: null,
398
+ startTime: getTodayStartUnixSeconds()
399
+ };
400
+ const fullDateMatch = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
401
+ const dayOnlyMatch = value.match(/^(\d{1,2})号?$/);
402
+ const now = /* @__PURE__ */ new Date();
403
+ const year = fullDateMatch ? Number.parseInt(fullDateMatch[1], 10) : now.getFullYear();
404
+ const month = fullDateMatch ? Number.parseInt(fullDateMatch[2], 10) : now.getMonth() + 1;
405
+ const day = fullDateMatch ? Number.parseInt(fullDateMatch[3], 10) : dayOnlyMatch ? Number.parseInt(dayOnlyMatch[1], 10) : NaN;
406
+ const parsed = new Date(year, month - 1, day);
407
+ 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");
408
+ return {
409
+ date: toLocalDateString(parsed),
410
+ startTime: getLocalStartUnixSeconds(year, month, day)
411
+ };
412
+ }
306
413
  function htmlToPlainText(html) {
307
414
  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
415
  }
@@ -497,32 +604,35 @@ var OnesAdapter = class extends BaseAdapter {
497
604
  redirect: "manual"
498
605
  })).headers.get("location");
499
606
  if (!authorizeLocation) throw new Error("ONES: Authorize response missing location header");
500
- const authRequestId = new URL(authorizeLocation).searchParams.get("id");
501
- if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
502
- const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
503
- method: "POST",
504
- headers: {
505
- "Content-Type": "application/json;charset=UTF-8",
506
- "Cookie": cookies
507
- },
508
- body: JSON.stringify({
509
- auth_request_id: authRequestId,
510
- region_uuid: orgUser.region_uuid,
511
- org_uuid: orgUser.org_uuid,
512
- org_user_uuid: orgUser.org_user.org_user_uuid
513
- })
514
- });
515
- if (!finalizeRes.ok) {
516
- const text = await finalizeRes.text().catch(() => "");
517
- throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
607
+ let code = parseAuthorizationCode(authorizeLocation);
608
+ if (!code) {
609
+ const authRequestId = parseAuthorizeRequestId(authorizeLocation);
610
+ if (!authRequestId) throw new Error("ONES: Cannot parse auth_request_id from authorize redirect");
611
+ const finalizeRes = await fetch(`${baseUrl}/identity/api/auth_request/finalize`, {
612
+ method: "POST",
613
+ headers: {
614
+ "Content-Type": "application/json;charset=UTF-8",
615
+ "Cookie": cookies
616
+ },
617
+ body: JSON.stringify({
618
+ auth_request_id: authRequestId,
619
+ region_uuid: orgUser.region_uuid,
620
+ org_uuid: orgUser.org_uuid,
621
+ org_user_uuid: orgUser.org_user.org_user_uuid
622
+ })
623
+ });
624
+ if (!finalizeRes.ok) {
625
+ const text = await finalizeRes.text().catch(() => "");
626
+ throw new Error(`ONES: Finalize failed: ${finalizeRes.status} ${text}`);
627
+ }
628
+ const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
629
+ method: "GET",
630
+ headers: { Cookie: cookies },
631
+ redirect: "manual"
632
+ })).headers.get("location");
633
+ if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
634
+ code = parseAuthorizationCode(callbackLocation);
518
635
  }
519
- const callbackLocation = (await fetch(`${baseUrl}/identity/authorize/callback?id=${authRequestId}&lang=zh`, {
520
- method: "GET",
521
- headers: { Cookie: cookies },
522
- redirect: "manual"
523
- })).headers.get("location");
524
- if (!callbackLocation) throw new Error("ONES: Callback response missing location header");
525
- const code = new URL(callbackLocation).searchParams.get("code");
526
636
  if (!code) throw new Error("ONES: Cannot parse authorization code from callback redirect");
527
637
  const tokenRes = await fetch(`${baseUrl}/identity/oauth/token`, {
528
638
  method: "POST",
@@ -592,11 +702,107 @@ var OnesAdapter = class extends BaseAdapter {
592
702
  }
593
703
  return response.json();
594
704
  }
705
+ async searchTaskByNumber(taskNumber) {
706
+ const session = await this.login();
707
+ const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/search?q=${encodeURIComponent(String(taskNumber))}&start=0&limit=10&types=task`;
708
+ const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
709
+ if (!response.ok) return null;
710
+ const found = ((await response.json()).datas?.task ?? []).map((item) => item.fields).find((fields) => fields?.uuid && fields.number === taskNumber);
711
+ if (!found?.uuid) return null;
712
+ return {
713
+ key: `task-${found.uuid}`,
714
+ uuid: found.uuid,
715
+ number: found.number ?? taskNumber,
716
+ name: found.summary ?? "",
717
+ status: {
718
+ uuid: "",
719
+ name: "",
720
+ category: void 0
721
+ },
722
+ issueType: found.issue_type_uuid || found.issue_type_name ? {
723
+ uuid: found.issue_type_uuid ?? "",
724
+ name: found.issue_type_name ?? ""
725
+ } : void 0,
726
+ project: found.project_uuid || found.project_name ? {
727
+ uuid: found.project_uuid ?? "",
728
+ name: found.project_name ?? ""
729
+ } : void 0
730
+ };
731
+ }
595
732
  async fetchIssueTypes() {
596
733
  if (this.issueTypesCache) return this.issueTypesCache;
597
734
  this.issueTypesCache = (await this.graphql(ISSUE_TYPES_QUERY, { orderBy: { namePinyin: "ASC" } }, "issueTypes")).data?.issueTypes ?? [];
598
735
  return this.issueTypesCache;
599
736
  }
737
+ async fetchProjects() {
738
+ return (await this.graphql(PROJECTS_QUERY, {
739
+ projectOrderBy: {
740
+ isPin: "DESC",
741
+ namePinyin: "ASC",
742
+ createTime: "DESC"
743
+ },
744
+ projectFilterGroup: [{
745
+ visibleInProject_equal: true,
746
+ isArchive_equal: false
747
+ }],
748
+ groupBy: { projects: {} },
749
+ orderBy: null,
750
+ pagination: {
751
+ limit: 50,
752
+ after: "",
753
+ preciseCount: true
754
+ }
755
+ }, "projects-group-list-for-project-view")).data?.buckets?.flatMap((bucket) => bucket.projects ?? []) ?? [];
756
+ }
757
+ async findTaskByNumber(taskNumber, projectUuid) {
758
+ const filter = { number_in: [taskNumber] };
759
+ if (projectUuid) filter.project_in = [projectUuid];
760
+ const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
761
+ groupBy: { tasks: {} },
762
+ groupOrderBy: null,
763
+ orderBy: { createTime: "DESC" },
764
+ filterGroup: [filter],
765
+ search: null,
766
+ pagination: {
767
+ limit: 10,
768
+ preciseCount: false
769
+ },
770
+ limit: 10
771
+ }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((task) => task.number === taskNumber && (!projectUuid || task.project?.uuid === projectUuid));
772
+ if (found) return found;
773
+ if (projectUuid) return null;
774
+ return this.searchTaskByNumber(taskNumber);
775
+ }
776
+ async resolveTaskRef(input) {
777
+ const taskId = input.trim();
778
+ if (!taskId) throw new Error("ONES: taskId is required");
779
+ const numMatch = taskId.match(/^#?(\d+)$/);
780
+ if (numMatch) {
781
+ const taskNumber = Number.parseInt(numMatch[1], 10);
782
+ const found = await this.findTaskByNumber(taskNumber);
783
+ if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
784
+ return {
785
+ key: found.key ?? `task-${found.uuid}`,
786
+ uuid: found.uuid
787
+ };
788
+ }
789
+ const displayId = parseDisplayId(taskId);
790
+ if (displayId) {
791
+ const project = (await this.fetchProjects()).find((item) => item.identifier?.toLowerCase() === displayId.identifier.toLowerCase());
792
+ if (!project) throw new Error(`ONES: Project identifier "${displayId.identifier}" not found in current team`);
793
+ const found = await this.findTaskByNumber(displayId.number, project.uuid);
794
+ if (!found) throw new Error(`ONES: Task "${taskId}" not found in current team`);
795
+ return {
796
+ key: found.key ?? `task-${found.uuid}`,
797
+ uuid: found.uuid
798
+ };
799
+ }
800
+ const key = taskId.startsWith("task-") ? taskId : `task-${taskId}`;
801
+ return {
802
+ key,
803
+ uuid: key.slice(5)
804
+ };
805
+ }
600
806
  async searchTeamUsers(keyword) {
601
807
  const session = await this.login();
602
808
  const url = `${this.config.apiBase}/project/api/project/team/${session.teamUuid}/users/search`;
@@ -705,22 +911,25 @@ var OnesAdapter = class extends BaseAdapter {
705
911
  * Fetch wiki page content via REST API.
706
912
  * Endpoint: /wiki/api/wiki/team/{teamUuid}/online_page/{wikiUuid}/content
707
913
  */
708
- async fetchWikiPageDetail(wikiUuid) {
914
+ async fetchWikiPageDetail(wikiUuid, teamUuid) {
709
915
  const session = await this.login();
710
- const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/page/${wikiUuid}/detail`;
916
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
917
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/page/${wikiUuid}/detail`;
711
918
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
712
919
  if (!response.ok) return {};
713
920
  return response.json();
714
921
  }
715
- buildWikiImageUrl(session, refUuid, source, token) {
922
+ buildWikiImageUrl(session, refUuid, source, token, teamUuid) {
716
923
  const encodedRefUuid = encodeURIComponent(refUuid);
717
924
  const encodedSource = source.split("/").map((part) => encodeURIComponent(part)).join("/");
718
925
  const encodedToken = encodeURIComponent(token);
719
- return `${this.config.apiBase}/wiki/api/wiki/editor/${session.teamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
926
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
927
+ return `${this.config.apiBase}/wiki/api/wiki/editor/${wikiTeamUuid}/${encodedRefUuid}/resources/${encodedSource}?token=${encodedToken}`;
720
928
  }
721
- async fetchWikiContent(wikiUuid) {
929
+ async fetchWikiContent(wikiUuid, teamUuid) {
722
930
  const session = await this.login();
723
- const url = `${this.config.apiBase}/wiki/api/wiki/team/${session.teamUuid}/online_page/${wikiUuid}/content`;
931
+ const wikiTeamUuid = teamUuid ?? session.teamUuid;
932
+ const url = `${this.config.apiBase}/wiki/api/wiki/team/${wikiTeamUuid}/online_page/${wikiUuid}/content`;
724
933
  const response = await fetch(url, { headers: { Authorization: `Bearer ${session.accessToken}` } });
725
934
  if (!response.ok) return {
726
935
  content: "",
@@ -734,7 +943,7 @@ var OnesAdapter = class extends BaseAdapter {
734
943
  content,
735
944
  attachments: []
736
945
  };
737
- const detail = await this.fetchWikiPageDetail(wikiUuid);
946
+ const detail = await this.fetchWikiPageDetail(wikiUuid, wikiTeamUuid);
738
947
  const refUuid = typeof detail.ref_uuid === "string" ? detail.ref_uuid : "";
739
948
  if (!refUuid) return {
740
949
  content,
@@ -745,38 +954,46 @@ var OnesAdapter = class extends BaseAdapter {
745
954
  attachments: renderContext.imageSources.map((source, index) => ({
746
955
  id: `${wikiUuid}-image-${index + 1}`,
747
956
  name: attachmentNameFromPath(source),
748
- url: this.buildWikiImageUrl(session, refUuid, source, token),
957
+ url: this.buildWikiImageUrl(session, refUuid, source, token, wikiTeamUuid),
749
958
  mimeType: mimeTypeFromFileName(source),
750
959
  size: 0
751
960
  }))
752
961
  };
753
962
  }
754
963
  /**
755
- * Fetch a single task by UUID or number (e.g. "#95945" or "95945").
964
+ * Fetch a single task by UUID or number (e.g. "#1001" or "1001").
756
965
  * If a number is given, searches first to resolve the UUID.
757
966
  */
758
967
  async getRequirement(params) {
759
- let taskUuid = params.id;
760
- const numMatch = taskUuid.match(/^#?(\d+)$/);
761
- if (numMatch) {
762
- const taskNumber = Number.parseInt(numMatch[1], 10);
763
- const found = ((await this.graphql(TASK_BY_NUMBER_QUERY, {
764
- groupBy: { tasks: {} },
765
- groupOrderBy: null,
766
- orderBy: { createTime: "DESC" },
767
- filterGroup: [{ number_in: [taskNumber] }],
768
- search: null,
769
- pagination: {
770
- limit: 10,
771
- preciseCount: false
772
- },
773
- limit: 10
774
- }, "group-task-data")).data?.buckets?.flatMap((b) => b.tasks ?? []) ?? []).find((t) => t.number === taskNumber);
775
- if (!found) throw new Error(`ONES: Task #${taskNumber} not found in current team`);
776
- taskUuid = found.uuid;
968
+ const wikiRoute = parseOnesWikiPageRoute(params.id);
969
+ if (wikiRoute) {
970
+ const rendered = await this.fetchWikiContent(wikiRoute.wikiUuid, wikiRoute.teamUuid);
971
+ return {
972
+ id: wikiRoute.wikiUuid,
973
+ source: "ones",
974
+ title: `Wiki ${wikiRoute.wikiUuid}`,
975
+ description: rendered.content,
976
+ status: "open",
977
+ priority: "medium",
978
+ type: "feature",
979
+ labels: [],
980
+ reporter: "",
981
+ assignee: null,
982
+ createdAt: "",
983
+ updatedAt: "",
984
+ dueDate: null,
985
+ attachments: rendered.attachments,
986
+ raw: {
987
+ input: params.id,
988
+ teamUuid: wikiRoute.teamUuid,
989
+ wikiUuid: wikiRoute.wikiUuid
990
+ }
991
+ };
777
992
  }
778
- const task = (await this.graphql(TASK_DETAIL_QUERY, { key: `task-${taskUuid}` }, "Task")).data?.task;
779
- if (!task) throw new Error(`ONES: Task "${taskUuid}" not found`);
993
+ if (isOnesWikiUrlInput(params.id)) throw new Error("ONES: Unsupported wiki page URL. Expected /wiki/#/team/{teamUuid}/space/{spaceUuid}/page/{wikiUuid}");
994
+ const taskRef = await this.resolveTaskRef(params.id);
995
+ const task = (await this.graphql(TASK_DETAIL_QUERY, { key: taskRef.key }, "Task")).data?.task;
996
+ if (!task) throw new Error(`ONES: Task "${params.id}" not found`);
780
997
  const wikiRefs = /* @__PURE__ */ new Map();
781
998
  for (const wiki of task.relatedWikiPages ?? []) if (!wiki.errorMessage) wikiRefs.set(wiki.uuid, {
782
999
  title: wiki.title,
@@ -911,6 +1128,69 @@ var OnesAdapter = class extends BaseAdapter {
911
1128
  pageSize
912
1129
  };
913
1130
  }
1131
+ async addManhour(params) {
1132
+ const description = params.description.trim();
1133
+ if (!description) throw new Error("ONES: description is required");
1134
+ const taskRef = await this.resolveTaskRef(params.taskId);
1135
+ const onesHours = toOnesHours(params.hours);
1136
+ const workDate = parseManhourDate(params.date);
1137
+ const key = (await this.graphql(ADD_MANHOUR_MUTATION, {
1138
+ mode: "simple",
1139
+ type: "recorded",
1140
+ customData: {},
1141
+ owner: (await this.login()).userUuid,
1142
+ task: taskRef.uuid,
1143
+ start_time: workDate.startTime,
1144
+ hours: onesHours,
1145
+ description
1146
+ }, "add-manhour")).data?.addManhour?.key;
1147
+ if (!key) throw new Error("ONES: Failed to add manhour");
1148
+ return {
1149
+ key,
1150
+ taskUuid: taskRef.uuid,
1151
+ hours: params.hours,
1152
+ description,
1153
+ date: workDate.date
1154
+ };
1155
+ }
1156
+ async updateTaskPlanDates(params) {
1157
+ const planStartDate = params.planStartDate?.trim();
1158
+ const planEndDate = params.planEndDate?.trim();
1159
+ if (!planStartDate && !planEndDate) throw new Error("ONES: planStartDate or planEndDate is required");
1160
+ if (planStartDate && !isValidOnesDate(planStartDate)) throw new Error("ONES: planStartDate must be a valid YYYY-MM-DD date");
1161
+ if (planEndDate && !isValidOnesDate(planEndDate)) throw new Error("ONES: planEndDate must be a valid YYYY-MM-DD date");
1162
+ const taskRef = await this.resolveTaskRef(params.taskId);
1163
+ const session = await this.login();
1164
+ const fieldValues = [];
1165
+ if (planStartDate) fieldValues.push({
1166
+ field_uuid: "field027",
1167
+ value: planStartDate
1168
+ });
1169
+ if (planEndDate) fieldValues.push({
1170
+ field_uuid: "field028",
1171
+ value: planEndDate
1172
+ });
1173
+ const response = await fetch(`${this.config.apiBase}/project/api/project/team/${session.teamUuid}/tasks/update3`, {
1174
+ method: "POST",
1175
+ headers: {
1176
+ "Authorization": `Bearer ${session.accessToken}`,
1177
+ "Content-Type": "application/json"
1178
+ },
1179
+ body: JSON.stringify({ tasks: [{
1180
+ uuid: taskRef.uuid,
1181
+ field_values: fieldValues
1182
+ }] })
1183
+ });
1184
+ if (!response.ok) {
1185
+ const text = await response.text().catch(() => "");
1186
+ throw new Error(`ONES: Failed to update task plan dates: ${response.status} ${text}`);
1187
+ }
1188
+ return {
1189
+ taskUuid: taskRef.uuid,
1190
+ planStartDate: planStartDate ?? null,
1191
+ planEndDate: planEndDate ?? null
1192
+ };
1193
+ }
914
1194
  async getRelatedIssues(params) {
915
1195
  const session = await this.login();
916
1196
  const taskKey = params.taskId.startsWith("task-") ? params.taskId : `task-${params.taskId}`;
@@ -1258,10 +1538,46 @@ function loadConfig(startDir) {
1258
1538
  };
1259
1539
  }
1260
1540
 
1541
+ //#endregion
1542
+ //#region src/tools/add-manhour.ts
1543
+ const AddManhourSchema = z.object({
1544
+ 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\")"),
1545
+ hours: z.number().positive().describe("Work hours to record. Natural hours are converted to ONES internal units."),
1546
+ description: z.string().min(1).describe("Work log description."),
1547
+ 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."),
1548
+ source: z.string().optional().describe("Source to update. If omitted, uses the default source.")
1549
+ });
1550
+ async function handleAddManhour(input, adapters, defaultSource) {
1551
+ const sourceType = input.source ?? defaultSource;
1552
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1553
+ const adapter = adapters.get(sourceType);
1554
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1555
+ return { content: [{
1556
+ type: "text",
1557
+ text: formatAddManhourResult(await adapter.addManhour({
1558
+ taskId: input.taskId,
1559
+ hours: input.hours,
1560
+ description: input.description,
1561
+ date: input.date
1562
+ }))
1563
+ }] };
1564
+ }
1565
+ function formatAddManhourResult(result) {
1566
+ return [
1567
+ "Added manhour.",
1568
+ "",
1569
+ `- **Key**: ${result.key}`,
1570
+ `- **Task UUID**: ${result.taskUuid}`,
1571
+ `- **Hours**: ${result.hours}`,
1572
+ `- **Date**: ${result.date ?? "today"}`,
1573
+ `- **Description**: ${result.description}`
1574
+ ].join("\n");
1575
+ }
1576
+
1261
1577
  //#endregion
1262
1578
  //#region src/tools/get-issue-detail.ts
1263
1579
  const GetIssueDetailSchema = z.object({
1264
- issueId: z.string().describe("The issue task ID or key (e.g. \"6W9vW3y8J9DO66Pu\" or \"task-6W9vW3y8J9DO66Pu\")"),
1580
+ issueId: z.string().describe("The issue task ID or key (e.g. \"mock-issue-uuid\" or \"task-mock-issue-uuid\")"),
1265
1581
  source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1266
1582
  });
1267
1583
  /**
@@ -1336,7 +1652,7 @@ function formatIssueDetail(detail) {
1336
1652
  //#endregion
1337
1653
  //#region src/tools/get-related-issues.ts
1338
1654
  const GetRelatedIssuesSchema = z.object({
1339
- taskId: z.string().describe("The parent task ID or key (e.g. \"HRL2p8rTX4mQ9xMv\" or \"task-HRL2p8rTX4mQ9xMv\")"),
1655
+ taskId: z.string().describe("The parent task ID or key (e.g. \"mock-task-uuid\" or \"task-mock-task-uuid\")"),
1340
1656
  source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1341
1657
  });
1342
1658
  async function handleGetRelatedIssues(input, adapters, defaultSource) {
@@ -1377,7 +1693,7 @@ function formatRelatedIssues(issues) {
1377
1693
  //#endregion
1378
1694
  //#region src/tools/get-requirement.ts
1379
1695
  const GetRequirementSchema = z.object({
1380
- id: z.string().describe("The requirement/issue ID"),
1696
+ id: z.string().describe("The requirement/issue ID, task number, or ONES wiki page URL"),
1381
1697
  source: z.string().optional().describe("Source to fetch from. If omitted, uses the default source.")
1382
1698
  });
1383
1699
  async function downloadImageAsBase64(url, fallbackMimeType = "image/png") {
@@ -1572,6 +1888,40 @@ async function handleSearchRequirements(input, adapters, defaultSource) {
1572
1888
  }] };
1573
1889
  }
1574
1890
 
1891
+ //#endregion
1892
+ //#region src/tools/update-task-plan-dates.ts
1893
+ const DateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Expected YYYY-MM-DD");
1894
+ const UpdateTaskPlanDatesSchema = z.object({
1895
+ 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\")"),
1896
+ planStartDate: DateSchema.optional().describe("Plan start date in YYYY-MM-DD format."),
1897
+ planEndDate: DateSchema.optional().describe("Plan end date in YYYY-MM-DD format."),
1898
+ source: z.string().optional().describe("Source to update. If omitted, uses the default source.")
1899
+ });
1900
+ async function handleUpdateTaskPlanDates(input, adapters, defaultSource) {
1901
+ const sourceType = input.source ?? defaultSource;
1902
+ if (!sourceType) throw new Error("No source specified and no default source configured");
1903
+ const adapter = adapters.get(sourceType);
1904
+ if (!adapter) throw new Error(`Source "${sourceType}" is not configured. Available: ${[...adapters.keys()].join(", ")}`);
1905
+ return { content: [{
1906
+ type: "text",
1907
+ text: formatUpdateTaskPlanDatesResult(await adapter.updateTaskPlanDates({
1908
+ taskId: input.taskId,
1909
+ planStartDate: input.planStartDate,
1910
+ planEndDate: input.planEndDate
1911
+ }))
1912
+ }] };
1913
+ }
1914
+ function formatUpdateTaskPlanDatesResult(result) {
1915
+ const lines = [
1916
+ "Updated task plan dates.",
1917
+ "",
1918
+ `- **Task UUID**: ${result.taskUuid}`
1919
+ ];
1920
+ if (result.planStartDate) lines.push(`- **Plan Start Date**: ${result.planStartDate}`);
1921
+ if (result.planEndDate) lines.push(`- **Plan End Date**: ${result.planEndDate}`);
1922
+ return lines.join("\n");
1923
+ }
1924
+
1575
1925
  //#endregion
1576
1926
  //#region src/index.ts
1577
1927
  /**
@@ -1697,6 +2047,32 @@ async function main() {
1697
2047
  };
1698
2048
  }
1699
2049
  });
2050
+ 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) => {
2051
+ try {
2052
+ return await handleAddManhour(params, adapters, config.config.defaultSource);
2053
+ } catch (err) {
2054
+ return {
2055
+ content: [{
2056
+ type: "text",
2057
+ text: `Error: ${err.message}`
2058
+ }],
2059
+ isError: true
2060
+ };
2061
+ }
2062
+ });
2063
+ 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) => {
2064
+ try {
2065
+ return await handleUpdateTaskPlanDates(params, adapters, config.config.defaultSource);
2066
+ } catch (err) {
2067
+ return {
2068
+ content: [{
2069
+ type: "text",
2070
+ text: `Error: ${err.message}`
2071
+ }],
2072
+ isError: true
2073
+ };
2074
+ }
2075
+ });
1700
2076
  const transport = new StdioServerTransport();
1701
2077
  await server.connect(transport);
1702
2078
  }