@agentuity/runtime 2.0.5 → 2.0.7
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/agent.js +1 -1
- package/dist/agent.js.map +1 -1
- package/dist/dev-patches/otel-llm.d.ts.map +1 -1
- package/dist/dev-patches/otel-llm.js +0 -1
- package/dist/dev-patches/otel-llm.js.map +1 -1
- package/dist/logger/console.d.ts.map +1 -1
- package/dist/logger/console.js +4 -0
- package/dist/logger/console.js.map +1 -1
- package/dist/services/local/task.d.ts +3 -1
- package/dist/services/local/task.d.ts.map +1 -1
- package/dist/services/local/task.js +332 -2
- package/dist/services/local/task.js.map +1 -1
- package/package.json +6 -6
- package/src/agent.ts +3 -3
- package/src/dev-patches/otel-llm.ts +0 -3
- package/src/logger/console.ts +5 -0
- package/src/services/local/task.ts +396 -2
|
@@ -12,6 +12,10 @@ import type {
|
|
|
12
12
|
ListTasksResult,
|
|
13
13
|
BatchDeleteTasksParams,
|
|
14
14
|
BatchDeleteTasksResult,
|
|
15
|
+
BatchUpdateTasksParams,
|
|
16
|
+
BatchUpdateTasksResult,
|
|
17
|
+
BatchCloseTasksParams,
|
|
18
|
+
BatchCloseTasksResult,
|
|
15
19
|
TaskChangelogResult,
|
|
16
20
|
Comment,
|
|
17
21
|
Tag,
|
|
@@ -139,6 +143,7 @@ const SORT_FIELDS: Record<string, string> = {
|
|
|
139
143
|
};
|
|
140
144
|
|
|
141
145
|
const DURATION_UNITS: Record<string, number> = {
|
|
146
|
+
s: 1000,
|
|
142
147
|
m: 60 * 1000,
|
|
143
148
|
h: 60 * 60 * 1000,
|
|
144
149
|
d: 24 * 60 * 60 * 1000,
|
|
@@ -147,11 +152,11 @@ const DURATION_UNITS: Record<string, number> = {
|
|
|
147
152
|
|
|
148
153
|
const InvalidDurationError = StructuredError(
|
|
149
154
|
'InvalidDurationError',
|
|
150
|
-
'Invalid duration format: use a number followed by m (minutes), h (hours), d (days), or w (weeks)'
|
|
155
|
+
'Invalid duration format: use a number followed by s (seconds), m (minutes), h (hours), d (days), or w (weeks)'
|
|
151
156
|
);
|
|
152
157
|
|
|
153
158
|
function parseDurationMs(duration: string): number {
|
|
154
|
-
const match = duration.match(/^(\d+)([
|
|
159
|
+
const match = duration.match(/^(\d+)([smhdw])$/);
|
|
155
160
|
if (!match) {
|
|
156
161
|
throw new InvalidDurationError();
|
|
157
162
|
}
|
|
@@ -190,6 +195,7 @@ function toTask(row: TaskRow): Task {
|
|
|
190
195
|
created_id: row.created_id,
|
|
191
196
|
assigned_id: row.assigned_id ?? undefined,
|
|
192
197
|
closed_id: row.closed_id ?? undefined,
|
|
198
|
+
deleted: row.deleted === 1,
|
|
193
199
|
};
|
|
194
200
|
}
|
|
195
201
|
|
|
@@ -384,6 +390,27 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
384
390
|
filters.push('parent_id = ?');
|
|
385
391
|
values.push(params.parent_id);
|
|
386
392
|
}
|
|
393
|
+
if (params?.created_id) {
|
|
394
|
+
filters.push('created_id = ?');
|
|
395
|
+
values.push(params.created_id);
|
|
396
|
+
}
|
|
397
|
+
if (params?.project_id) {
|
|
398
|
+
filters.push('project_id = ?');
|
|
399
|
+
values.push(params.project_id);
|
|
400
|
+
}
|
|
401
|
+
if (params?.deleted === undefined) {
|
|
402
|
+
filters.push('deleted = 0');
|
|
403
|
+
} else {
|
|
404
|
+
filters.push('deleted = ?');
|
|
405
|
+
values.push(params.deleted ? 1 : 0);
|
|
406
|
+
}
|
|
407
|
+
if (params?.tag_id) {
|
|
408
|
+
filters.push(
|
|
409
|
+
'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
|
|
410
|
+
);
|
|
411
|
+
values.push(params.tag_id);
|
|
412
|
+
values.push(this.#projectPath);
|
|
413
|
+
}
|
|
387
414
|
|
|
388
415
|
const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
|
|
389
416
|
const sortField =
|
|
@@ -864,6 +891,373 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
864
891
|
};
|
|
865
892
|
}
|
|
866
893
|
|
|
894
|
+
async batchUpdate(params: BatchUpdateTasksParams): Promise<BatchUpdateTasksResult> {
|
|
895
|
+
const conditions: string[] = ['project_path = ?', 'deleted = 0'];
|
|
896
|
+
const args: (string | number)[] = [this.#projectPath];
|
|
897
|
+
|
|
898
|
+
// Handle explicit IDs
|
|
899
|
+
if (params.ids && params.ids.length > 0) {
|
|
900
|
+
const placeholders = params.ids.map(() => '?').join(', ');
|
|
901
|
+
conditions.push(`id IN (${placeholders})`);
|
|
902
|
+
args.push(...params.ids);
|
|
903
|
+
} else {
|
|
904
|
+
// Build filter conditions
|
|
905
|
+
if (params.status) {
|
|
906
|
+
conditions.push('status = ?');
|
|
907
|
+
args.push(normalizeTaskStatus(params.status));
|
|
908
|
+
}
|
|
909
|
+
if (params.type) {
|
|
910
|
+
conditions.push('type = ?');
|
|
911
|
+
args.push(params.type);
|
|
912
|
+
}
|
|
913
|
+
if (params.priority) {
|
|
914
|
+
conditions.push('priority = ?');
|
|
915
|
+
args.push(params.priority);
|
|
916
|
+
}
|
|
917
|
+
if (params.parent_id) {
|
|
918
|
+
conditions.push('parent_id = ?');
|
|
919
|
+
args.push(params.parent_id);
|
|
920
|
+
}
|
|
921
|
+
if (params.created_id) {
|
|
922
|
+
conditions.push('created_id = ?');
|
|
923
|
+
args.push(params.created_id);
|
|
924
|
+
}
|
|
925
|
+
if (params.assigned_id) {
|
|
926
|
+
conditions.push('assigned_id = ?');
|
|
927
|
+
args.push(params.assigned_id);
|
|
928
|
+
}
|
|
929
|
+
if (params.older_than) {
|
|
930
|
+
const ms = parseDurationMs(params.older_than);
|
|
931
|
+
const cutoff = Date.now() - ms;
|
|
932
|
+
conditions.push('created_at < ?');
|
|
933
|
+
args.push(cutoff);
|
|
934
|
+
}
|
|
935
|
+
if (params.project_id) {
|
|
936
|
+
conditions.push('project_id = ?');
|
|
937
|
+
args.push(params.project_id);
|
|
938
|
+
}
|
|
939
|
+
if (params.tag_id) {
|
|
940
|
+
conditions.push(
|
|
941
|
+
'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
|
|
942
|
+
);
|
|
943
|
+
args.push(params.tag_id);
|
|
944
|
+
args.push(this.#projectPath);
|
|
945
|
+
}
|
|
946
|
+
if (params.newer_than) {
|
|
947
|
+
const ms = parseDurationMs(params.newer_than);
|
|
948
|
+
const cutoff = Date.now() - ms;
|
|
949
|
+
conditions.push('created_at > ?');
|
|
950
|
+
args.push(cutoff);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
// Require at least one filter or IDs
|
|
955
|
+
if (params.ids && params.ids.length > 0) {
|
|
956
|
+
// IDs provided, OK
|
|
957
|
+
} else if (conditions.length < 3) {
|
|
958
|
+
throw new Error('At least one filter or ids is required for batch update');
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// Check for update fields
|
|
962
|
+
const hasUpdate =
|
|
963
|
+
params.new_status ||
|
|
964
|
+
params.new_priority ||
|
|
965
|
+
params.new_assigned_id ||
|
|
966
|
+
params.new_assignee ||
|
|
967
|
+
params.new_title ||
|
|
968
|
+
params.new_description ||
|
|
969
|
+
params.new_metadata ||
|
|
970
|
+
params.new_type;
|
|
971
|
+
if (!hasUpdate) {
|
|
972
|
+
throw new Error('At least one update field is required for batch update');
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit <= 0)) {
|
|
976
|
+
throw new Error('Batch update limit must be a positive integer');
|
|
977
|
+
}
|
|
978
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
979
|
+
|
|
980
|
+
const whereClause = conditions.join(' AND ');
|
|
981
|
+
const selectQuery = `SELECT id, title, status, priority FROM task_storage WHERE ${whereClause} ORDER BY created_at ASC LIMIT ?`;
|
|
982
|
+
const selectStmt = this.#db.prepare(selectQuery);
|
|
983
|
+
const rows = selectStmt.all(...args, limit) as Array<{
|
|
984
|
+
id: string;
|
|
985
|
+
title: string;
|
|
986
|
+
status: string;
|
|
987
|
+
priority: string;
|
|
988
|
+
}>;
|
|
989
|
+
|
|
990
|
+
if (rows.length === 0) {
|
|
991
|
+
return { updated: [], count: 0, dry_run: params.dry_run ?? false };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Dry run - return preview without updating
|
|
995
|
+
if (params.dry_run) {
|
|
996
|
+
const normalizedStatus = params.new_status
|
|
997
|
+
? normalizeTaskStatus(params.new_status)
|
|
998
|
+
: undefined;
|
|
999
|
+
return {
|
|
1000
|
+
updated: rows.map((r) => ({
|
|
1001
|
+
id: r.id,
|
|
1002
|
+
title: params.new_title ?? r.title,
|
|
1003
|
+
status: (normalizedStatus ?? r.status) as TaskStatus,
|
|
1004
|
+
priority: (params.new_priority ?? r.priority) as TaskPriority,
|
|
1005
|
+
})),
|
|
1006
|
+
count: rows.length,
|
|
1007
|
+
dry_run: true,
|
|
1008
|
+
};
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const timestamp = now();
|
|
1012
|
+
const ids = rows.map((r) => r.id);
|
|
1013
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
1014
|
+
|
|
1015
|
+
const updateFields: string[] = ['updated_at = ?'];
|
|
1016
|
+
const updateArgs: (string | number)[] = [timestamp];
|
|
1017
|
+
|
|
1018
|
+
if (params.new_status) {
|
|
1019
|
+
updateFields.push('status = ?');
|
|
1020
|
+
updateArgs.push(normalizeTaskStatus(params.new_status));
|
|
1021
|
+
}
|
|
1022
|
+
if (params.new_priority) {
|
|
1023
|
+
updateFields.push('priority = ?');
|
|
1024
|
+
updateArgs.push(params.new_priority);
|
|
1025
|
+
}
|
|
1026
|
+
if (params.new_assigned_id) {
|
|
1027
|
+
updateFields.push('assigned_id = ?');
|
|
1028
|
+
updateArgs.push(params.new_assigned_id);
|
|
1029
|
+
}
|
|
1030
|
+
if (params.new_title) {
|
|
1031
|
+
updateFields.push('title = ?');
|
|
1032
|
+
updateArgs.push(params.new_title);
|
|
1033
|
+
}
|
|
1034
|
+
if (params.new_description) {
|
|
1035
|
+
updateFields.push('description = ?');
|
|
1036
|
+
updateArgs.push(params.new_description);
|
|
1037
|
+
}
|
|
1038
|
+
if (params.new_metadata) {
|
|
1039
|
+
updateFields.push('metadata = ?');
|
|
1040
|
+
updateArgs.push(JSON.stringify(params.new_metadata));
|
|
1041
|
+
}
|
|
1042
|
+
if (params.new_type) {
|
|
1043
|
+
updateFields.push('type = ?');
|
|
1044
|
+
updateArgs.push(params.new_type);
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Set lifecycle timestamps based on new status (only when transitioning)
|
|
1048
|
+
if (params.new_status) {
|
|
1049
|
+
const newStatus = normalizeTaskStatus(params.new_status);
|
|
1050
|
+
if (newStatus === 'open') {
|
|
1051
|
+
updateFields.push('open_date = COALESCE(open_date, ?)');
|
|
1052
|
+
updateArgs.push(new Date(timestamp).toISOString());
|
|
1053
|
+
} else if (newStatus === 'in_progress') {
|
|
1054
|
+
updateFields.push('in_progress_date = COALESCE(in_progress_date, ?)');
|
|
1055
|
+
updateArgs.push(new Date(timestamp).toISOString());
|
|
1056
|
+
} else if (newStatus === 'done') {
|
|
1057
|
+
updateFields.push('closed_date = COALESCE(closed_date, ?)');
|
|
1058
|
+
updateArgs.push(new Date(timestamp).toISOString());
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
const txn = this.#db.transaction(() => {
|
|
1063
|
+
const updateStmt = this.#db.prepare(`
|
|
1064
|
+
UPDATE task_storage SET ${updateFields.join(', ')}
|
|
1065
|
+
WHERE project_path = ? AND id IN (${placeholders})
|
|
1066
|
+
`);
|
|
1067
|
+
updateStmt.run(...updateArgs, this.#projectPath, ...ids);
|
|
1068
|
+
|
|
1069
|
+
const changelogStmt = this.#db.prepare(`
|
|
1070
|
+
INSERT INTO task_changelog_storage (
|
|
1071
|
+
project_path, id, task_id, field, old_value, new_value, created_at
|
|
1072
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1073
|
+
`);
|
|
1074
|
+
for (const row of rows) {
|
|
1075
|
+
if (params.new_status && row.status !== params.new_status) {
|
|
1076
|
+
changelogStmt.run(
|
|
1077
|
+
this.#projectPath,
|
|
1078
|
+
generateChangelogId(),
|
|
1079
|
+
row.id,
|
|
1080
|
+
'status',
|
|
1081
|
+
row.status,
|
|
1082
|
+
params.new_status,
|
|
1083
|
+
timestamp
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1086
|
+
if (params.new_priority && row.priority !== params.new_priority) {
|
|
1087
|
+
changelogStmt.run(
|
|
1088
|
+
this.#projectPath,
|
|
1089
|
+
generateChangelogId(),
|
|
1090
|
+
row.id,
|
|
1091
|
+
'priority',
|
|
1092
|
+
row.priority,
|
|
1093
|
+
params.new_priority,
|
|
1094
|
+
timestamp
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
});
|
|
1099
|
+
txn();
|
|
1100
|
+
|
|
1101
|
+
return {
|
|
1102
|
+
updated: rows.map((r) => ({
|
|
1103
|
+
id: r.id,
|
|
1104
|
+
title: params.new_title ?? r.title,
|
|
1105
|
+
status: (params.new_status ?? r.status) as TaskStatus,
|
|
1106
|
+
priority: (params.new_priority ?? r.priority) as TaskPriority,
|
|
1107
|
+
})),
|
|
1108
|
+
count: rows.length,
|
|
1109
|
+
dry_run: false,
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
async batchClose(params: BatchCloseTasksParams): Promise<BatchCloseTasksResult> {
|
|
1114
|
+
// Resolve closer ID from either closed_id or closer entity ref
|
|
1115
|
+
const closerId = params.closed_id ?? params.closer?.id ?? null;
|
|
1116
|
+
|
|
1117
|
+
const conditions: string[] = ['project_path = ?', 'deleted = 0', "status != 'done'"];
|
|
1118
|
+
const args: (string | number)[] = [this.#projectPath];
|
|
1119
|
+
|
|
1120
|
+
// Handle explicit IDs
|
|
1121
|
+
if (params.ids && params.ids.length > 0) {
|
|
1122
|
+
const placeholders = params.ids.map(() => '?').join(', ');
|
|
1123
|
+
conditions.push(`id IN (${placeholders})`);
|
|
1124
|
+
args.push(...params.ids);
|
|
1125
|
+
} else {
|
|
1126
|
+
// Build filter conditions
|
|
1127
|
+
if (params.status) {
|
|
1128
|
+
conditions.push('status = ?');
|
|
1129
|
+
args.push(normalizeTaskStatus(params.status));
|
|
1130
|
+
}
|
|
1131
|
+
if (params.type) {
|
|
1132
|
+
conditions.push('type = ?');
|
|
1133
|
+
args.push(params.type);
|
|
1134
|
+
}
|
|
1135
|
+
if (params.priority) {
|
|
1136
|
+
conditions.push('priority = ?');
|
|
1137
|
+
args.push(params.priority);
|
|
1138
|
+
}
|
|
1139
|
+
if (params.parent_id) {
|
|
1140
|
+
conditions.push('parent_id = ?');
|
|
1141
|
+
args.push(params.parent_id);
|
|
1142
|
+
}
|
|
1143
|
+
if (params.created_id) {
|
|
1144
|
+
conditions.push('created_id = ?');
|
|
1145
|
+
args.push(params.created_id);
|
|
1146
|
+
}
|
|
1147
|
+
if (params.assigned_id) {
|
|
1148
|
+
conditions.push('assigned_id = ?');
|
|
1149
|
+
args.push(params.assigned_id);
|
|
1150
|
+
}
|
|
1151
|
+
if (params.older_than) {
|
|
1152
|
+
const ms = parseDurationMs(params.older_than);
|
|
1153
|
+
const cutoff = Date.now() - ms;
|
|
1154
|
+
conditions.push('created_at < ?');
|
|
1155
|
+
args.push(cutoff);
|
|
1156
|
+
}
|
|
1157
|
+
if (params.project_id) {
|
|
1158
|
+
conditions.push('project_id = ?');
|
|
1159
|
+
args.push(params.project_id);
|
|
1160
|
+
}
|
|
1161
|
+
if (params.tag_id) {
|
|
1162
|
+
conditions.push(
|
|
1163
|
+
'id IN (SELECT task_id FROM task_tag_association_storage WHERE tag_id = ? AND project_path = ?)'
|
|
1164
|
+
);
|
|
1165
|
+
args.push(params.tag_id);
|
|
1166
|
+
args.push(this.#projectPath);
|
|
1167
|
+
}
|
|
1168
|
+
if (params.newer_than) {
|
|
1169
|
+
const ms = parseDurationMs(params.newer_than);
|
|
1170
|
+
const cutoff = Date.now() - ms;
|
|
1171
|
+
conditions.push('created_at > ?');
|
|
1172
|
+
args.push(cutoff);
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
// Require at least one filter or IDs
|
|
1177
|
+
if (params.ids && params.ids.length > 0) {
|
|
1178
|
+
// IDs provided, OK
|
|
1179
|
+
} else if (conditions.length < 4) {
|
|
1180
|
+
throw new Error('At least one filter or ids is required for batch close');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
if (params.limit !== undefined && (!Number.isInteger(params.limit) || params.limit <= 0)) {
|
|
1184
|
+
throw new Error('Batch close limit must be a positive integer');
|
|
1185
|
+
}
|
|
1186
|
+
const limit = Math.min(params.limit ?? 50, 200);
|
|
1187
|
+
|
|
1188
|
+
const whereClause = conditions.join(' AND ');
|
|
1189
|
+
const selectQuery = `SELECT id, title, status FROM task_storage WHERE ${whereClause} ORDER BY created_at ASC LIMIT ?`;
|
|
1190
|
+
const selectStmt = this.#db.prepare(selectQuery);
|
|
1191
|
+
const rows = selectStmt.all(...args, limit) as Array<{
|
|
1192
|
+
id: string;
|
|
1193
|
+
title: string;
|
|
1194
|
+
status: string;
|
|
1195
|
+
}>;
|
|
1196
|
+
|
|
1197
|
+
if (rows.length === 0) {
|
|
1198
|
+
return { closed: [], count: 0, dry_run: params.dry_run ?? false };
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
// Dry run - return preview without closing
|
|
1202
|
+
if (params.dry_run) {
|
|
1203
|
+
const nowTs = new Date().toISOString();
|
|
1204
|
+
return {
|
|
1205
|
+
closed: rows.map((r) => ({
|
|
1206
|
+
id: r.id,
|
|
1207
|
+
title: r.title,
|
|
1208
|
+
status: 'done',
|
|
1209
|
+
closed_date: nowTs,
|
|
1210
|
+
})),
|
|
1211
|
+
count: rows.length,
|
|
1212
|
+
dry_run: true,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
const timestamp = now();
|
|
1217
|
+
const ids = rows.map((r) => r.id);
|
|
1218
|
+
const placeholders = ids.map(() => '?').join(', ');
|
|
1219
|
+
const closedDate = new Date(timestamp).toISOString();
|
|
1220
|
+
|
|
1221
|
+
const txn = this.#db.transaction(() => {
|
|
1222
|
+
const updateStmt = this.#db.prepare(`
|
|
1223
|
+
UPDATE task_storage SET status = 'done', closed_date = COALESCE(closed_date, ?), closed_id = ?, updated_at = ?
|
|
1224
|
+
WHERE project_path = ? AND id IN (${placeholders})
|
|
1225
|
+
`);
|
|
1226
|
+
updateStmt.run(closedDate, closerId, timestamp, this.#projectPath, ...ids);
|
|
1227
|
+
|
|
1228
|
+
const changelogStmt = this.#db.prepare(`
|
|
1229
|
+
INSERT INTO task_changelog_storage (
|
|
1230
|
+
project_path, id, task_id, field, old_value, new_value, created_at
|
|
1231
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
1232
|
+
`);
|
|
1233
|
+
for (const row of rows) {
|
|
1234
|
+
if (row.status !== 'done') {
|
|
1235
|
+
changelogStmt.run(
|
|
1236
|
+
this.#projectPath,
|
|
1237
|
+
generateChangelogId(),
|
|
1238
|
+
row.id,
|
|
1239
|
+
'status',
|
|
1240
|
+
row.status,
|
|
1241
|
+
'done',
|
|
1242
|
+
timestamp
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
});
|
|
1247
|
+
txn();
|
|
1248
|
+
|
|
1249
|
+
return {
|
|
1250
|
+
closed: rows.map((r) => ({
|
|
1251
|
+
id: r.id,
|
|
1252
|
+
title: r.title,
|
|
1253
|
+
status: 'done',
|
|
1254
|
+
closed_date: closedDate,
|
|
1255
|
+
})),
|
|
1256
|
+
count: rows.length,
|
|
1257
|
+
dry_run: false,
|
|
1258
|
+
};
|
|
1259
|
+
}
|
|
1260
|
+
|
|
867
1261
|
async createComment(taskId: string, body: string, userId: string): Promise<Comment> {
|
|
868
1262
|
const trimmedBody = body?.trim();
|
|
869
1263
|
if (!trimmedBody) {
|