@agentuity/runtime 2.0.6 → 2.0.8

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.
@@ -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+)([mhdw])$/);
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) {
@@ -236,8 +236,8 @@ function createSandboxMethods(client: APIClient, sandboxId: string) {
236
236
  );
237
237
  },
238
238
 
239
- async rmFile(path: string): Promise<void> {
240
- await withSpan(
239
+ async rmFile(path: string): Promise<{ found: boolean }> {
240
+ return withSpan(
241
241
  'agentuity.sandbox.rmFile',
242
242
  {
243
243
  'sandbox.id': sandboxId,
@@ -247,8 +247,8 @@ function createSandboxMethods(client: APIClient, sandboxId: string) {
247
247
  );
248
248
  },
249
249
 
250
- async rmDir(path: string, recursive?: boolean): Promise<void> {
251
- await withSpan(
250
+ async rmDir(path: string, recursive?: boolean): Promise<{ found: boolean }> {
251
+ return withSpan(
252
252
  'agentuity.sandbox.rmDir',
253
253
  {
254
254
  'sandbox.id': sandboxId,