@m1heng-clawd/feishu 0.1.15 → 0.1.16

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.
@@ -5,6 +5,13 @@ import type { FeishuDriveParams } from "./schemas.js";
5
5
  type DriveMoveType = "doc" | "docx" | "sheet" | "bitable" | "folder" | "file" | "mindnote" | "slides";
6
6
  type DriveDeleteType = DriveMoveType | "shortcut";
7
7
 
8
+ function requireString(value: unknown, field: string): string {
9
+ if (typeof value !== "string" || value.trim().length === 0) {
10
+ throw new Error(`${field} is required`);
11
+ }
12
+ return value;
13
+ }
14
+
8
15
  async function getFileType(client: DriveClient, fileToken: string): Promise<string> {
9
16
  let pageToken: string | undefined;
10
17
 
@@ -196,18 +203,23 @@ export async function runDriveAction(
196
203
  case "list":
197
204
  return listFolder(client, params.folder_token);
198
205
  case "info":
199
- return getFileInfo(client, params.file_token);
206
+ return getFileInfo(client, requireString(params.file_token, "file_token"));
200
207
  case "create_folder":
201
- return createFolder(client, params.name, params.folder_token);
208
+ return createFolder(client, requireString(params.name, "name"), params.folder_token);
202
209
  case "move":
203
- return moveFile(client, params.file_token, params.type, params.folder_token);
210
+ return moveFile(
211
+ client,
212
+ requireString(params.file_token, "file_token"),
213
+ requireString(params.type, "type"),
214
+ requireString(params.folder_token, "folder_token"),
215
+ );
204
216
  case "delete":
205
- return deleteFile(client, params.file_token, params.type);
217
+ return deleteFile(client, requireString(params.file_token, "file_token"), params.type);
206
218
  case "import_document":
207
219
  return importDocument(
208
220
  client,
209
- params.title,
210
- params.content,
221
+ requireString(params.title, "title"),
222
+ requireString(params.content, "content"),
211
223
  mediaMaxBytes,
212
224
  params.folder_token,
213
225
  params.doc_type || "docx",
@@ -1,67 +1,54 @@
1
1
  import { Type, type Static } from "@sinclair/typebox";
2
2
 
3
- const FileType = Type.Union([
4
- Type.Literal("doc"),
5
- Type.Literal("docx"),
6
- Type.Literal("sheet"),
7
- Type.Literal("bitable"),
8
- Type.Literal("folder"),
9
- Type.Literal("file"),
10
- Type.Literal("mindnote"),
11
- Type.Literal("shortcut"),
12
- ]);
3
+ function stringEnum<T extends readonly string[]>(
4
+ values: T,
5
+ options: { description?: string; default?: T[number] } = {},
6
+ ) {
7
+ return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
8
+ }
13
9
 
14
- const DocType = Type.Union([
15
- Type.Literal("docx", { description: "New generation document (default)" }),
16
- Type.Literal("doc", { description: "Legacy document" }),
17
- ]);
10
+ const FILE_TYPE_VALUES = [
11
+ "doc",
12
+ "docx",
13
+ "sheet",
14
+ "bitable",
15
+ "folder",
16
+ "file",
17
+ "mindnote",
18
+ "shortcut",
19
+ ] as const;
18
20
 
19
- export const FeishuDriveSchema = Type.Union([
20
- Type.Object({
21
- action: Type.Literal("list"),
22
- folder_token: Type.Optional(
23
- Type.String({ description: "Folder token (optional, omit for root directory)" }),
24
- ),
25
- }),
26
- Type.Object({
27
- action: Type.Literal("info"),
28
- file_token: Type.String({ description: "File or folder token" }),
29
- type: Type.Optional(FileType),
30
- }),
31
- Type.Object({
32
- action: Type.Literal("create_folder"),
33
- name: Type.String({ description: "Folder name" }),
34
- folder_token: Type.Optional(
35
- Type.String({ description: "Parent folder token (optional, omit for root)" }),
36
- ),
37
- }),
38
- Type.Object({
39
- action: Type.Literal("move"),
40
- file_token: Type.String({ description: "File token to move" }),
41
- type: FileType,
42
- folder_token: Type.String({ description: "Target folder token" }),
43
- }),
44
- Type.Object({
45
- action: Type.Literal("delete"),
46
- file_token: Type.String({ description: "File token to delete" }),
47
- type: Type.Optional(FileType),
48
- }),
49
- Type.Object({
50
- action: Type.Literal("import_document"),
51
- title: Type.String({
52
- description: "Document title",
53
- }),
54
- content: Type.String({
21
+ const DOC_TYPE_VALUES = ["docx", "doc"] as const;
22
+
23
+ const DRIVE_ACTION_VALUES = [
24
+ "list",
25
+ "info",
26
+ "create_folder",
27
+ "move",
28
+ "delete",
29
+ "import_document",
30
+ ] as const;
31
+
32
+ export const FeishuDriveSchema = Type.Object({
33
+ action: stringEnum(DRIVE_ACTION_VALUES, { description: "Drive action" }),
34
+ folder_token: Type.Optional(
35
+ Type.String({ description: "Folder token (optional, omit for root directory)" }),
36
+ ),
37
+ file_token: Type.Optional(Type.String({ description: "File or folder token" })),
38
+ type: Type.Optional(stringEnum(FILE_TYPE_VALUES, { description: "File type" })),
39
+ name: Type.Optional(Type.String({ description: "Folder name (for create_folder)" })),
40
+ title: Type.Optional(Type.String({ description: "Document title (for import_document)" })),
41
+ content: Type.Optional(
42
+ Type.String({
55
43
  description:
56
44
  "Markdown content to import. Supports full Markdown syntax including tables, lists, code blocks, etc.",
57
45
  }),
58
- folder_token: Type.Optional(
59
- Type.String({
60
- description: "Target folder token (optional, defaults to root). Use 'list' to find folder tokens.",
61
- }),
62
- ),
63
- doc_type: Type.Optional(DocType),
64
- }),
65
- ]);
46
+ ),
47
+ doc_type: Type.Optional(
48
+ stringEnum(DOC_TYPE_VALUES, {
49
+ description: "Document type for import_document (docx default, doc legacy)",
50
+ }),
51
+ ),
52
+ });
66
53
 
67
54
  export type FeishuDriveParams = Static<typeof FeishuDriveSchema>;
package/src/media.ts CHANGED
@@ -237,23 +237,6 @@ export async function uploadImageFeishu(params: {
237
237
  return { imageKey };
238
238
  }
239
239
 
240
- /**
241
- * Encode a filename for safe use in Feishu multipart/form-data uploads.
242
- * Non-ASCII characters (Chinese, em-dash, full-width brackets, etc.) cause
243
- * the upload to silently fail when passed raw through the SDK's form-data
244
- * serialization. RFC 5987 percent-encoding keeps headers 7-bit clean while
245
- * Feishu's server decodes and preserves the original display name.
246
- */
247
- export function sanitizeFileNameForUpload(fileName: string): string {
248
- const ASCII_ONLY = /^[\x20-\x7E]+$/;
249
- if (ASCII_ONLY.test(fileName)) {
250
- return fileName;
251
- }
252
- return encodeURIComponent(fileName)
253
- .replace(/'/g, "%27")
254
- .replace(/\(/g, "%28")
255
- .replace(/\)/g, "%29");
256
- }
257
240
 
258
241
  /**
259
242
  * Upload a file to Feishu and get a file_key for sending.
@@ -280,12 +263,10 @@ export async function uploadFileFeishu(params: {
280
263
  const fileStream =
281
264
  typeof file === "string" ? fs.createReadStream(file) : Readable.from(file);
282
265
 
283
- const safeFileName = sanitizeFileNameForUpload(fileName);
284
-
285
266
  const response = await client.im.file.create({
286
267
  data: {
287
268
  file_type: fileType,
288
- file_name: safeFileName,
269
+ file_name: fileName,
289
270
  file: fileStream as any,
290
271
  ...(duration !== undefined && { duration }),
291
272
  },
@@ -496,9 +477,10 @@ export async function sendMediaFeishu(params: {
496
477
  mediaBuffer?: Buffer;
497
478
  fileName?: string;
498
479
  replyToMessageId?: string;
480
+ mediaLocalRoots?: readonly string[];
499
481
  accountId?: string;
500
482
  }): Promise<SendMediaResult> {
501
- const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, accountId } = params;
483
+ const { cfg, to, mediaUrl, mediaBuffer, fileName, replyToMessageId, mediaLocalRoots, accountId } = params;
502
484
  const account = resolveFeishuAccount({ cfg, accountId });
503
485
  if (!account.configured) {
504
486
  throw new Error(`Feishu account "${account.accountId}" not configured`);
@@ -513,30 +495,16 @@ export async function sendMediaFeishu(params: {
513
495
  buffer = mediaBuffer;
514
496
  name = fileName ?? "file";
515
497
  } else if (mediaUrl) {
516
- const mediaLocalRoots = (account.config?.mediaLocalRoots ?? [])
498
+ const configLocalRoots = (account.config?.mediaLocalRoots ?? [])
517
499
  .map((root) => root.trim())
518
500
  .filter((root) => root.length > 0);
519
- const loaded = await (async () => {
520
- try {
521
- return await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
522
- maxBytes: mediaMaxBytes,
523
- optimizeImages: false,
524
- });
525
- } catch (err) {
526
- const shouldRetryWithCustomRoots =
527
- mediaLocalRoots.length > 0 &&
528
- err instanceof Error &&
529
- err.message.includes("Local media path is not under an allowed directory");
530
- if (!shouldRetryWithCustomRoots) {
531
- throw err;
532
- }
533
- return await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
534
- maxBytes: mediaMaxBytes,
535
- optimizeImages: false,
536
- localRoots: mediaLocalRoots,
537
- });
538
- }
539
- })();
501
+ // Merge context-provided roots (includes agent workspace) with config roots.
502
+ const mergedLocalRoots = [...(mediaLocalRoots ?? []), ...configLocalRoots];
503
+ const loaded = await getFeishuRuntime().media.loadWebMedia(mediaUrl, {
504
+ maxBytes: mediaMaxBytes,
505
+ optimizeImages: false,
506
+ ...(mergedLocalRoots.length > 0 ? { localRoots: mergedLocalRoots } : {}),
507
+ });
540
508
  buffer = loaded.buffer;
541
509
  name = fileName ?? loaded.fileName ?? "file";
542
510
  contentType = loaded.contentType;
package/src/outbound.ts CHANGED
@@ -12,7 +12,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
12
12
  const result = await sendMessageFeishu({ cfg, to, text, accountId });
13
13
  return { channel: "feishu", ...result };
14
14
  },
15
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
15
+ sendMedia: async ({ cfg, to, text, mediaUrl, mediaLocalRoots, accountId }) => {
16
16
  // Send text first if provided
17
17
  if (text?.trim()) {
18
18
  await sendMessageFeishu({ cfg, to, text, accountId });
@@ -21,7 +21,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
21
21
  // Upload and send media if URL provided
22
22
  if (mediaUrl) {
23
23
  try {
24
- const result = await sendMediaFeishu({ cfg, to, mediaUrl, accountId });
24
+ const result = await sendMediaFeishu({ cfg, to, mediaUrl, mediaLocalRoots, accountId });
25
25
  return { channel: "feishu", ...result };
26
26
  } catch (err) {
27
27
  // Log the error for debugging
@@ -33,6 +33,13 @@ type MemberType =
33
33
  | "wikispaceid";
34
34
  type PermType = "view" | "edit" | "full_access";
35
35
 
36
+ function requireString(value: unknown, field: string): string {
37
+ if (typeof value !== "string" || value.trim().length === 0) {
38
+ throw new Error(`${field} is required`);
39
+ }
40
+ return value;
41
+ }
42
+
36
43
  async function listMembers(client: PermClient, token: string, type: string) {
37
44
  const res = await runPermApiCall("drive.permissionMember.list", () =>
38
45
  client.drive.permissionMember.list({
@@ -100,11 +107,28 @@ async function removeMember(
100
107
  export async function runPermAction(client: PermClient, params: FeishuPermParams) {
101
108
  switch (params.action) {
102
109
  case "list":
103
- return listMembers(client, params.token, params.type);
110
+ return listMembers(
111
+ client,
112
+ requireString(params.token, "token"),
113
+ requireString(params.type, "type"),
114
+ );
104
115
  case "add":
105
- return addMember(client, params.token, params.type, params.member_type, params.member_id, params.perm);
116
+ return addMember(
117
+ client,
118
+ requireString(params.token, "token"),
119
+ requireString(params.type, "type"),
120
+ requireString(params.member_type, "member_type"),
121
+ requireString(params.member_id, "member_id"),
122
+ requireString(params.perm, "perm"),
123
+ );
106
124
  case "remove":
107
- return removeMember(client, params.token, params.type, params.member_type, params.member_id);
125
+ return removeMember(
126
+ client,
127
+ requireString(params.token, "token"),
128
+ requireString(params.type, "type"),
129
+ requireString(params.member_type, "member_type"),
130
+ requireString(params.member_id, "member_id"),
131
+ );
108
132
  default:
109
133
  return { error: `Unknown action: ${(params as any).action}` };
110
134
  }
@@ -1,52 +1,46 @@
1
1
  import { Type, type Static } from "@sinclair/typebox";
2
2
 
3
- const TokenType = Type.Union([
4
- Type.Literal("doc"),
5
- Type.Literal("docx"),
6
- Type.Literal("sheet"),
7
- Type.Literal("bitable"),
8
- Type.Literal("folder"),
9
- Type.Literal("file"),
10
- Type.Literal("wiki"),
11
- Type.Literal("mindnote"),
12
- ]);
3
+ function stringEnum<T extends readonly string[]>(
4
+ values: T,
5
+ options: { description?: string; default?: T[number] } = {},
6
+ ) {
7
+ return Type.Unsafe<T[number]>({ type: "string", enum: [...values], ...options });
8
+ }
13
9
 
14
- const MemberType = Type.Union([
15
- Type.Literal("email"),
16
- Type.Literal("openid"),
17
- Type.Literal("userid"),
18
- Type.Literal("unionid"),
19
- Type.Literal("openchat"),
20
- Type.Literal("opendepartmentid"),
21
- ]);
10
+ const TOKEN_TYPE_VALUES = [
11
+ "doc",
12
+ "docx",
13
+ "sheet",
14
+ "bitable",
15
+ "folder",
16
+ "file",
17
+ "wiki",
18
+ "mindnote",
19
+ ] as const;
22
20
 
23
- const Permission = Type.Union([
24
- Type.Literal("view"),
25
- Type.Literal("edit"),
26
- Type.Literal("full_access"),
27
- ]);
21
+ const MEMBER_TYPE_VALUES = [
22
+ "email",
23
+ "openid",
24
+ "userid",
25
+ "unionid",
26
+ "openchat",
27
+ "opendepartmentid",
28
+ ] as const;
28
29
 
29
- export const FeishuPermSchema = Type.Union([
30
- Type.Object({
31
- action: Type.Literal("list"),
32
- token: Type.String({ description: "File token" }),
33
- type: TokenType,
34
- }),
35
- Type.Object({
36
- action: Type.Literal("add"),
37
- token: Type.String({ description: "File token" }),
38
- type: TokenType,
39
- member_type: MemberType,
40
- member_id: Type.String({ description: "Member ID (email, open_id, user_id, etc.)" }),
41
- perm: Permission,
42
- }),
43
- Type.Object({
44
- action: Type.Literal("remove"),
45
- token: Type.String({ description: "File token" }),
46
- type: TokenType,
47
- member_type: MemberType,
48
- member_id: Type.String({ description: "Member ID to remove" }),
49
- }),
50
- ]);
30
+ const PERMISSION_VALUES = ["view", "edit", "full_access"] as const;
31
+ const PERM_ACTION_VALUES = ["list", "add", "remove"] as const;
32
+
33
+ export const FeishuPermSchema = Type.Object({
34
+ action: stringEnum(PERM_ACTION_VALUES, { description: "Permission action" }),
35
+ token: Type.Optional(Type.String({ description: "File token" })),
36
+ type: Type.Optional(stringEnum(TOKEN_TYPE_VALUES, { description: "File token type" })),
37
+ member_type: Type.Optional(
38
+ stringEnum(MEMBER_TYPE_VALUES, {
39
+ description: "Member ID type (email/openid/userid/unionid/openchat/opendepartmentid)",
40
+ }),
41
+ ),
42
+ member_id: Type.Optional(Type.String({ description: "Member ID" })),
43
+ perm: Type.Optional(stringEnum(PERMISSION_VALUES, { description: "Permission level" })),
44
+ });
51
45
 
52
46
  export type FeishuPermParams = Static<typeof FeishuPermSchema>;
@@ -3,26 +3,33 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import type { TaskClient } from "./common.js";
5
5
  import {
6
+ TASK_COMMENT_UPDATE_FIELD_VALUES,
6
7
  TASKLIST_UPDATE_FIELD_VALUES,
7
8
  TASK_UPDATE_FIELD_VALUES,
8
9
  type AddTaskToTasklistParams,
9
10
  type AddTasklistMembersParams,
11
+ type CreateTaskCommentParams,
10
12
  type CreateTasklistParams,
11
- CreateSubtaskParams,
12
- CreateTaskParams,
13
- DeleteTaskAttachmentParams,
14
- GetTaskAttachmentParams,
15
- GetTaskParams,
13
+ type CreateSubtaskParams,
14
+ type CreateTaskParams,
15
+ type DeleteTaskAttachmentParams,
16
+ type DeleteTaskCommentParams,
17
+ type GetTaskAttachmentParams,
18
+ type GetTaskCommentParams,
19
+ type GetTaskParams,
16
20
  type GetTasklistParams,
17
- ListTaskAttachmentsParams,
21
+ type ListTaskAttachmentsParams,
22
+ type ListTaskCommentsParams,
18
23
  type ListTasklistsParams,
19
24
  type RemoveTaskFromTasklistParams,
20
25
  type RemoveTasklistMembersParams,
21
- TaskUpdateTask,
26
+ type TaskCommentPatchComment,
27
+ type TaskUpdateTask,
22
28
  type TasklistPatchTasklist,
23
- UploadTaskAttachmentParams,
29
+ type UploadTaskAttachmentParams,
30
+ type UpdateTaskCommentParams,
24
31
  type UpdateTasklistParams,
25
- UpdateTaskParams,
32
+ type UpdateTaskParams,
26
33
  } from "./schemas.js";
27
34
  import {
28
35
  DEFAULT_TASK_ATTACHMENT_FILENAME,
@@ -36,6 +43,7 @@ import { getFeishuRuntime } from "../runtime.js";
36
43
  import { runTaskApiCall } from "./common.js";
37
44
 
38
45
  const SUPPORTED_PATCH_FIELDS = new Set<string>(TASK_UPDATE_FIELD_VALUES);
46
+ const SUPPORTED_COMMENT_PATCH_FIELDS = new Set<string>(TASK_COMMENT_UPDATE_FIELD_VALUES);
39
47
  const SUPPORTED_TASKLIST_PATCH_FIELDS = new Set<string>(TASKLIST_UPDATE_FIELD_VALUES);
40
48
 
41
49
  function omitUndefined<T extends Record<string, unknown>>(obj: T): T {
@@ -50,10 +58,16 @@ function inferUpdateFields(task: TaskUpdateTask): string[] {
50
58
  );
51
59
  }
52
60
 
61
+ function inferCommentUpdateFields(comment: TaskCommentPatchComment): string[] {
62
+ return Object.keys(comment).filter((field) =>
63
+ SUPPORTED_COMMENT_PATCH_FIELDS.has(field),
64
+ );
65
+ }
66
+
53
67
  function ensureSupportedUpdateFields(
54
68
  updateFields: string[],
55
69
  supported: Set<string>,
56
- resource: "task" | "tasklist",
70
+ resource: "task" | "comment" | "tasklist",
57
71
  ) {
58
72
  const invalid = updateFields.filter((field) => !supported.has(field));
59
73
  if (invalid.length > 0) {
@@ -102,6 +116,20 @@ function formatTasklist(tasklist: Record<string, unknown> | undefined) {
102
116
  };
103
117
  }
104
118
 
119
+ function formatComment(comment: Record<string, unknown> | undefined) {
120
+ if (!comment) return undefined;
121
+ return {
122
+ id: comment.id,
123
+ content: comment.content,
124
+ creator: comment.creator,
125
+ reply_to_comment_id: comment.reply_to_comment_id,
126
+ created_at: comment.created_at,
127
+ updated_at: comment.updated_at,
128
+ resource_type: comment.resource_type,
129
+ resource_id: comment.resource_id,
130
+ };
131
+ }
132
+
105
133
  function formatAttachment(attachment: Record<string, unknown> | undefined) {
106
134
  if (!attachment) return undefined;
107
135
  return {
@@ -307,6 +335,113 @@ export async function getTask(client: TaskClient, params: GetTaskParams) {
307
335
  };
308
336
  }
309
337
 
338
+ export async function createTaskComment(client: TaskClient, params: CreateTaskCommentParams) {
339
+ const res = await runTaskApiCall("task.v2.comment.create", () =>
340
+ client.task.v2.comment.create({
341
+ data: omitUndefined({
342
+ content: params.content,
343
+ reply_to_comment_id: params.reply_to_comment_id,
344
+ resource_type: "task",
345
+ resource_id: params.task_guid,
346
+ }),
347
+ params: omitUndefined({
348
+ user_id_type: params.user_id_type,
349
+ }),
350
+ }),
351
+ );
352
+
353
+ return {
354
+ comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
355
+ };
356
+ }
357
+
358
+ export async function listTaskComments(client: TaskClient, params: ListTaskCommentsParams) {
359
+ const res = await runTaskApiCall("task.v2.comment.list", () =>
360
+ client.task.v2.comment.list({
361
+ params: omitUndefined({
362
+ resource_type: "task",
363
+ resource_id: params.task_guid,
364
+ page_size: params.page_size,
365
+ page_token: params.page_token,
366
+ direction: params.direction,
367
+ user_id_type: params.user_id_type,
368
+ }),
369
+ }),
370
+ );
371
+
372
+ const items = (res.data?.items ?? []) as Record<string, unknown>[];
373
+
374
+ return {
375
+ items: items.map((item) => formatComment(item)),
376
+ page_token: res.data?.page_token,
377
+ has_more: res.data?.has_more,
378
+ };
379
+ }
380
+
381
+ export async function getTaskComment(client: TaskClient, params: GetTaskCommentParams) {
382
+ const res = await runTaskApiCall("task.v2.comment.get", () =>
383
+ client.task.v2.comment.get({
384
+ path: { comment_id: params.comment_id },
385
+ params: omitUndefined({
386
+ user_id_type: params.user_id_type,
387
+ }),
388
+ }),
389
+ );
390
+
391
+ return {
392
+ comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
393
+ };
394
+ }
395
+
396
+ export async function updateTaskComment(client: TaskClient, params: UpdateTaskCommentParams) {
397
+ const comment = omitUndefined(params.comment as Record<string, unknown>) as TaskCommentPatchComment;
398
+ const updateFields = params.update_fields?.length
399
+ ? params.update_fields
400
+ : inferCommentUpdateFields(comment);
401
+
402
+ if (params.update_fields?.length) {
403
+ ensureSupportedUpdateFields(updateFields, SUPPORTED_COMMENT_PATCH_FIELDS, "comment");
404
+ }
405
+
406
+ if (Object.keys(comment).length === 0) {
407
+ throw new Error("comment update payload is empty");
408
+ }
409
+ if (updateFields.length === 0) {
410
+ throw new Error("no valid update_fields provided or inferred from comment payload");
411
+ }
412
+
413
+ const res = await runTaskApiCall("task.v2.comment.patch", () =>
414
+ client.task.v2.comment.patch({
415
+ path: { comment_id: params.comment_id },
416
+ data: {
417
+ comment,
418
+ update_fields: updateFields,
419
+ },
420
+ params: omitUndefined({
421
+ user_id_type: params.user_id_type,
422
+ }),
423
+ }),
424
+ );
425
+
426
+ return {
427
+ comment: formatComment((res.data?.comment ?? undefined) as Record<string, unknown> | undefined),
428
+ update_fields: updateFields,
429
+ };
430
+ }
431
+
432
+ export async function deleteTaskComment(client: TaskClient, params: DeleteTaskCommentParams) {
433
+ await runTaskApiCall("task.v2.comment.delete", () =>
434
+ client.task.v2.comment.delete({
435
+ path: { comment_id: params.comment_id },
436
+ }),
437
+ );
438
+
439
+ return {
440
+ success: true,
441
+ comment_id: params.comment_id,
442
+ };
443
+ }
444
+
310
445
  export async function getTasklist(client: TaskClient, params: GetTasklistParams) {
311
446
  const res = await runTaskApiCall("task.v2.tasklist.get", () =>
312
447
  client.task.v2.tasklist.get({