@agentuity/runtime 1.0.22 → 1.0.23

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.
Files changed (48) hide show
  1. package/dist/_context.d.ts +4 -1
  2. package/dist/_context.d.ts.map +1 -1
  3. package/dist/_context.js +3 -0
  4. package/dist/_context.js.map +1 -1
  5. package/dist/_services.d.ts +4 -1
  6. package/dist/_services.d.ts.map +1 -1
  7. package/dist/_services.js +28 -3
  8. package/dist/_services.js.map +1 -1
  9. package/dist/_standalone.d.ts +4 -1
  10. package/dist/_standalone.d.ts.map +1 -1
  11. package/dist/_standalone.js +3 -0
  12. package/dist/_standalone.js.map +1 -1
  13. package/dist/agent.d.ts +43 -1
  14. package/dist/agent.d.ts.map +1 -1
  15. package/dist/agent.js.map +1 -1
  16. package/dist/app.d.ts +12 -1
  17. package/dist/app.d.ts.map +1 -1
  18. package/dist/app.js.map +1 -1
  19. package/dist/middleware.d.ts +1 -1
  20. package/dist/middleware.d.ts.map +1 -1
  21. package/dist/middleware.js +6 -0
  22. package/dist/middleware.js.map +1 -1
  23. package/dist/services/local/_db.d.ts.map +1 -1
  24. package/dist/services/local/_db.js +49 -1
  25. package/dist/services/local/_db.js.map +1 -1
  26. package/dist/services/local/email.d.ts +20 -0
  27. package/dist/services/local/email.d.ts.map +1 -0
  28. package/dist/services/local/email.js +46 -0
  29. package/dist/services/local/email.js.map +1 -0
  30. package/dist/services/local/index.d.ts +2 -0
  31. package/dist/services/local/index.d.ts.map +1 -1
  32. package/dist/services/local/index.js +2 -0
  33. package/dist/services/local/index.js.map +1 -1
  34. package/dist/services/local/task.d.ts +16 -0
  35. package/dist/services/local/task.d.ts.map +1 -0
  36. package/dist/services/local/task.js +425 -0
  37. package/dist/services/local/task.js.map +1 -0
  38. package/package.json +7 -7
  39. package/src/_context.ts +6 -0
  40. package/src/_services.ts +33 -1
  41. package/src/_standalone.ts +6 -0
  42. package/src/agent.ts +48 -0
  43. package/src/app.ts +14 -0
  44. package/src/middleware.ts +7 -3
  45. package/src/services/local/_db.ts +64 -3
  46. package/src/services/local/email.ts +75 -0
  47. package/src/services/local/index.ts +2 -0
  48. package/src/services/local/task.ts +595 -0
@@ -0,0 +1,595 @@
1
+ import type { Database } from 'bun:sqlite';
2
+ import type {
3
+ TaskStorage,
4
+ Task,
5
+ TaskChangelogEntry,
6
+ TaskPriority,
7
+ TaskStatus,
8
+ TaskType,
9
+ CreateTaskParams,
10
+ UpdateTaskParams,
11
+ ListTasksParams,
12
+ ListTasksResult,
13
+ TaskChangelogResult,
14
+ } from '@agentuity/core';
15
+ import { StructuredError } from '@agentuity/core';
16
+ import { now } from './_util';
17
+
18
+ const TaskTitleRequiredError = StructuredError(
19
+ 'TaskTitleRequiredError',
20
+ 'Task title is required and must be a non-empty string'
21
+ );
22
+
23
+ const TaskNotFoundError = StructuredError('TaskNotFoundError', 'Task not found');
24
+
25
+ const TaskAlreadyClosedError = StructuredError('TaskAlreadyClosedError', 'Task is already closed');
26
+
27
+ type TaskRow = {
28
+ id: string;
29
+ created_at: number;
30
+ updated_at: number;
31
+ title: string;
32
+ description: string | null;
33
+ metadata: string | null;
34
+ priority: TaskPriority;
35
+ parent_id: string | null;
36
+ type: TaskType;
37
+ status: TaskStatus;
38
+ open_date: string | null;
39
+ in_progress_date: string | null;
40
+ closed_date: string | null;
41
+ created_id: string;
42
+ assigned_id: string | null;
43
+ closed_id: string | null;
44
+ };
45
+
46
+ type TaskChangelogRow = {
47
+ id: string;
48
+ created_at: number;
49
+ task_id: string;
50
+ field: string;
51
+ old_value: string | null;
52
+ new_value: string | null;
53
+ };
54
+
55
+ const DEFAULT_LIMIT = 100;
56
+
57
+ const SORT_FIELDS: Record<string, string> = {
58
+ created_at: 'created_at',
59
+ updated_at: 'updated_at',
60
+ title: 'title',
61
+ priority: 'priority',
62
+ status: 'status',
63
+ type: 'type',
64
+ open_date: 'open_date',
65
+ in_progress_date: 'in_progress_date',
66
+ closed_date: 'closed_date',
67
+ };
68
+
69
+ function generateTaskId(): string {
70
+ return `task_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
71
+ }
72
+
73
+ function generateChangelogId(): string {
74
+ return `taskch_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
75
+ }
76
+
77
+ function toTask(row: TaskRow): Task {
78
+ return {
79
+ id: row.id,
80
+ created_at: new Date(row.created_at).toISOString(),
81
+ updated_at: new Date(row.updated_at).toISOString(),
82
+ title: row.title,
83
+ description: row.description ?? undefined,
84
+ metadata: row.metadata ? (JSON.parse(row.metadata) as Record<string, unknown>) : undefined,
85
+ priority: row.priority,
86
+ parent_id: row.parent_id ?? undefined,
87
+ type: row.type,
88
+ status: row.status,
89
+ open_date: row.open_date ?? undefined,
90
+ in_progress_date: row.in_progress_date ?? undefined,
91
+ closed_date: row.closed_date ?? undefined,
92
+ created_id: row.created_id,
93
+ assigned_id: row.assigned_id ?? undefined,
94
+ closed_id: row.closed_id ?? undefined,
95
+ };
96
+ }
97
+
98
+ function toChangelogEntry(row: TaskChangelogRow): TaskChangelogEntry {
99
+ return {
100
+ id: row.id,
101
+ created_at: new Date(row.created_at).toISOString(),
102
+ task_id: row.task_id,
103
+ field: row.field,
104
+ old_value: row.old_value ?? undefined,
105
+ new_value: row.new_value ?? undefined,
106
+ };
107
+ }
108
+
109
+ export class LocalTaskStorage implements TaskStorage {
110
+ #db: Database;
111
+ #projectPath: string;
112
+
113
+ constructor(db: Database, projectPath: string) {
114
+ this.#db = db;
115
+ this.#projectPath = projectPath;
116
+ }
117
+
118
+ async create(params: CreateTaskParams): Promise<Task> {
119
+ const trimmedTitle = params?.title?.trim();
120
+ if (!trimmedTitle) {
121
+ throw new TaskTitleRequiredError();
122
+ }
123
+
124
+ const id = generateTaskId();
125
+ const timestamp = now();
126
+ const status: TaskStatus = params.status ?? 'open';
127
+ const priority: TaskPriority = params.priority ?? 'none';
128
+ const openDate = status === 'open' ? new Date(timestamp).toISOString() : null;
129
+ const inProgressDate = status === 'in_progress' ? new Date(timestamp).toISOString() : null;
130
+ const closedDate = status === 'closed' ? new Date(timestamp).toISOString() : null;
131
+
132
+ const stmt = this.#db.prepare(`
133
+ INSERT INTO task_storage (
134
+ project_path,
135
+ id,
136
+ title,
137
+ description,
138
+ metadata,
139
+ priority,
140
+ parent_id,
141
+ type,
142
+ status,
143
+ open_date,
144
+ in_progress_date,
145
+ closed_date,
146
+ created_id,
147
+ assigned_id,
148
+ closed_id,
149
+ created_at,
150
+ updated_at
151
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
152
+ `);
153
+
154
+ const row: TaskRow = {
155
+ id,
156
+ created_at: timestamp,
157
+ updated_at: timestamp,
158
+ title: trimmedTitle,
159
+ description: params.description ?? null,
160
+ metadata: params.metadata ? JSON.stringify(params.metadata) : null,
161
+ priority,
162
+ parent_id: params.parent_id ?? null,
163
+ type: params.type,
164
+ status,
165
+ open_date: openDate,
166
+ in_progress_date: inProgressDate,
167
+ closed_date: closedDate,
168
+ created_id: params.created_id,
169
+ assigned_id: params.assigned_id ?? null,
170
+ closed_id: null,
171
+ };
172
+
173
+ stmt.run(
174
+ this.#projectPath,
175
+ row.id,
176
+ row.title,
177
+ row.description,
178
+ row.metadata,
179
+ row.priority,
180
+ row.parent_id,
181
+ row.type,
182
+ row.status,
183
+ row.open_date,
184
+ row.in_progress_date,
185
+ row.closed_date,
186
+ row.created_id,
187
+ row.assigned_id,
188
+ row.closed_id,
189
+ row.created_at,
190
+ row.updated_at
191
+ );
192
+
193
+ return toTask(row);
194
+ }
195
+
196
+ async get(id: string): Promise<Task | null> {
197
+ const query = this.#db.query(`
198
+ SELECT
199
+ id,
200
+ created_at,
201
+ updated_at,
202
+ title,
203
+ description,
204
+ metadata,
205
+ priority,
206
+ parent_id,
207
+ type,
208
+ status,
209
+ open_date,
210
+ in_progress_date,
211
+ closed_date,
212
+ created_id,
213
+ assigned_id,
214
+ closed_id
215
+ FROM task_storage
216
+ WHERE project_path = ? AND id = ?
217
+ `);
218
+
219
+ const row = query.get(this.#projectPath, id) as TaskRow | null;
220
+ if (!row) {
221
+ return null;
222
+ }
223
+
224
+ return toTask(row);
225
+ }
226
+
227
+ async list(params?: ListTasksParams): Promise<ListTasksResult> {
228
+ const filters: string[] = ['project_path = ?'];
229
+ const values: Array<string | number> = [this.#projectPath];
230
+
231
+ if (params?.status) {
232
+ filters.push('status = ?');
233
+ values.push(params.status);
234
+ }
235
+ if (params?.type) {
236
+ filters.push('type = ?');
237
+ values.push(params.type);
238
+ }
239
+ if (params?.priority) {
240
+ filters.push('priority = ?');
241
+ values.push(params.priority);
242
+ }
243
+ if (params?.assigned_id) {
244
+ filters.push('assigned_id = ?');
245
+ values.push(params.assigned_id);
246
+ }
247
+ if (params?.parent_id) {
248
+ filters.push('parent_id = ?');
249
+ values.push(params.parent_id);
250
+ }
251
+
252
+ const whereClause = filters.length > 0 ? `WHERE ${filters.join(' AND ')}` : '';
253
+ const sortField =
254
+ params?.sort && SORT_FIELDS[params.sort] ? SORT_FIELDS[params.sort] : 'created_at';
255
+ const sortOrder = params?.order === 'asc' ? 'ASC' : 'DESC';
256
+ const limit = params?.limit ?? DEFAULT_LIMIT;
257
+ const offset = params?.offset ?? 0;
258
+
259
+ const totalQuery = this.#db.query(
260
+ `SELECT COUNT(*) as count FROM task_storage ${whereClause}`
261
+ );
262
+ const totalRow = totalQuery.get(...values) as { count: number };
263
+
264
+ const query = this.#db.query(`
265
+ SELECT
266
+ id,
267
+ created_at,
268
+ updated_at,
269
+ title,
270
+ description,
271
+ metadata,
272
+ priority,
273
+ parent_id,
274
+ type,
275
+ status,
276
+ open_date,
277
+ in_progress_date,
278
+ closed_date,
279
+ created_id,
280
+ assigned_id,
281
+ closed_id
282
+ FROM task_storage
283
+ ${whereClause}
284
+ ORDER BY ${sortField} ${sortOrder}
285
+ LIMIT ? OFFSET ?
286
+ `);
287
+
288
+ const rows = query.all(...values, limit, offset) as TaskRow[];
289
+
290
+ return {
291
+ tasks: rows.map(toTask),
292
+ total: totalRow.count,
293
+ limit,
294
+ offset,
295
+ };
296
+ }
297
+
298
+ async update(id: string, params: UpdateTaskParams): Promise<Task> {
299
+ const updateInTransaction = this.#db.transaction(() => {
300
+ const existingQuery = this.#db.query(`
301
+ SELECT
302
+ id,
303
+ created_at,
304
+ updated_at,
305
+ title,
306
+ description,
307
+ metadata,
308
+ priority,
309
+ parent_id,
310
+ type,
311
+ status,
312
+ open_date,
313
+ in_progress_date,
314
+ closed_date,
315
+ created_id,
316
+ assigned_id,
317
+ closed_id
318
+ FROM task_storage
319
+ WHERE project_path = ? AND id = ?
320
+ `);
321
+
322
+ const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
323
+ if (!existing) {
324
+ throw new TaskNotFoundError();
325
+ }
326
+ const trimmedTitle = params.title !== undefined ? params.title?.trim() : undefined;
327
+ if (params.title !== undefined && !trimmedTitle) {
328
+ throw new TaskTitleRequiredError();
329
+ }
330
+ const timestamp = now();
331
+ const nowIso = new Date(timestamp).toISOString();
332
+
333
+ const updated: TaskRow = {
334
+ ...existing,
335
+ title: trimmedTitle ?? existing.title,
336
+ description:
337
+ params.description !== undefined ? params.description : existing.description,
338
+ metadata:
339
+ params.metadata !== undefined
340
+ ? params.metadata
341
+ ? JSON.stringify(params.metadata)
342
+ : null
343
+ : existing.metadata,
344
+ priority: params.priority ?? existing.priority,
345
+ parent_id: params.parent_id !== undefined ? params.parent_id : existing.parent_id,
346
+ type: params.type ?? existing.type,
347
+ status: params.status ?? existing.status,
348
+ assigned_id:
349
+ params.assigned_id !== undefined ? params.assigned_id : existing.assigned_id,
350
+ closed_id: params.closed_id !== undefined ? params.closed_id : existing.closed_id,
351
+ updated_at: timestamp,
352
+ };
353
+
354
+ if (params.status && params.status !== existing.status) {
355
+ if (params.status === 'open' && !existing.open_date) {
356
+ updated.open_date = nowIso;
357
+ }
358
+ if (params.status === 'in_progress' && !existing.in_progress_date) {
359
+ updated.in_progress_date = nowIso;
360
+ }
361
+ if (params.status === 'closed' && !existing.closed_date) {
362
+ updated.closed_date = nowIso;
363
+ }
364
+ }
365
+
366
+ const changelogEntries: Array<{
367
+ field: string;
368
+ oldValue: string | null;
369
+ newValue: string | null;
370
+ }> = [];
371
+
372
+ const compare = (field: string, oldValue: string | null, newValue: string | null) => {
373
+ if (oldValue !== newValue) {
374
+ changelogEntries.push({ field, oldValue, newValue });
375
+ }
376
+ };
377
+
378
+ if (params.title !== undefined) {
379
+ compare('title', existing.title, updated.title);
380
+ }
381
+ if (params.description !== undefined) {
382
+ compare('description', existing.description, updated.description);
383
+ }
384
+ if (params.metadata !== undefined) {
385
+ compare('metadata', existing.metadata, updated.metadata);
386
+ }
387
+ if (params.priority !== undefined) {
388
+ compare('priority', existing.priority, updated.priority);
389
+ }
390
+ if (params.parent_id !== undefined) {
391
+ compare('parent_id', existing.parent_id, updated.parent_id);
392
+ }
393
+ if (params.type !== undefined) {
394
+ compare('type', existing.type, updated.type);
395
+ }
396
+ if (params.status !== undefined) {
397
+ compare('status', existing.status, updated.status);
398
+ }
399
+ if (params.assigned_id !== undefined) {
400
+ compare('assigned_id', existing.assigned_id, updated.assigned_id);
401
+ }
402
+ if (params.closed_id !== undefined) {
403
+ compare('closed_id', existing.closed_id, updated.closed_id);
404
+ }
405
+
406
+ const updateStmt = this.#db.prepare(`
407
+ UPDATE task_storage
408
+ SET
409
+ title = ?,
410
+ description = ?,
411
+ metadata = ?,
412
+ priority = ?,
413
+ parent_id = ?,
414
+ type = ?,
415
+ status = ?,
416
+ open_date = ?,
417
+ in_progress_date = ?,
418
+ closed_date = ?,
419
+ created_id = ?,
420
+ assigned_id = ?,
421
+ closed_id = ?,
422
+ updated_at = ?
423
+ WHERE project_path = ? AND id = ?
424
+ `);
425
+
426
+ updateStmt.run(
427
+ updated.title,
428
+ updated.description,
429
+ updated.metadata,
430
+ updated.priority,
431
+ updated.parent_id,
432
+ updated.type,
433
+ updated.status,
434
+ updated.open_date,
435
+ updated.in_progress_date,
436
+ updated.closed_date,
437
+ updated.created_id,
438
+ updated.assigned_id,
439
+ updated.closed_id,
440
+ updated.updated_at,
441
+ this.#projectPath,
442
+ id
443
+ );
444
+
445
+ if (changelogEntries.length > 0) {
446
+ const changelogStmt = this.#db.prepare(`
447
+ INSERT INTO task_changelog_storage (
448
+ project_path,
449
+ id,
450
+ task_id,
451
+ field,
452
+ old_value,
453
+ new_value,
454
+ created_at
455
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
456
+ `);
457
+
458
+ for (const entry of changelogEntries) {
459
+ changelogStmt.run(
460
+ this.#projectPath,
461
+ generateChangelogId(),
462
+ id,
463
+ entry.field,
464
+ entry.oldValue,
465
+ entry.newValue,
466
+ timestamp
467
+ );
468
+ }
469
+ }
470
+
471
+ return toTask(updated);
472
+ });
473
+
474
+ return updateInTransaction.immediate();
475
+ }
476
+
477
+ async close(id: string): Promise<Task> {
478
+ const closeInTransaction = this.#db.transaction(() => {
479
+ const existingQuery = this.#db.query(`
480
+ SELECT
481
+ id,
482
+ created_at,
483
+ updated_at,
484
+ title,
485
+ description,
486
+ metadata,
487
+ priority,
488
+ parent_id,
489
+ type,
490
+ status,
491
+ open_date,
492
+ in_progress_date,
493
+ closed_date,
494
+ created_id,
495
+ assigned_id,
496
+ closed_id
497
+ FROM task_storage
498
+ WHERE project_path = ? AND id = ?
499
+ `);
500
+
501
+ const existing = existingQuery.get(this.#projectPath, id) as TaskRow | null;
502
+ if (!existing) {
503
+ throw new TaskNotFoundError();
504
+ }
505
+
506
+ if (existing.status === 'closed') {
507
+ throw new TaskAlreadyClosedError();
508
+ }
509
+ const timestamp = now();
510
+ const nowIso = new Date(timestamp).toISOString();
511
+ const updated: TaskRow = {
512
+ ...existing,
513
+ status: 'closed',
514
+ closed_date: existing.closed_date ?? nowIso,
515
+ updated_at: timestamp,
516
+ };
517
+
518
+ const updateStmt = this.#db.prepare(`
519
+ UPDATE task_storage
520
+ SET status = ?, closed_date = ?, updated_at = ?
521
+ WHERE project_path = ? AND id = ?
522
+ `);
523
+
524
+ updateStmt.run(
525
+ updated.status,
526
+ updated.closed_date,
527
+ updated.updated_at,
528
+ this.#projectPath,
529
+ id
530
+ );
531
+
532
+ const changelogStmt = this.#db.prepare(`
533
+ INSERT INTO task_changelog_storage (
534
+ project_path,
535
+ id,
536
+ task_id,
537
+ field,
538
+ old_value,
539
+ new_value,
540
+ created_at
541
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
542
+ `);
543
+
544
+ changelogStmt.run(
545
+ this.#projectPath,
546
+ generateChangelogId(),
547
+ id,
548
+ 'status',
549
+ existing.status,
550
+ updated.status,
551
+ timestamp
552
+ );
553
+
554
+ return toTask(updated);
555
+ });
556
+
557
+ return closeInTransaction.immediate();
558
+ }
559
+
560
+ async changelog(
561
+ id: string,
562
+ params?: { limit?: number; offset?: number }
563
+ ): Promise<TaskChangelogResult> {
564
+ const limit = params?.limit ?? DEFAULT_LIMIT;
565
+ const offset = params?.offset ?? 0;
566
+
567
+ const totalQuery = this.#db.query(
568
+ `SELECT COUNT(*) as count FROM task_changelog_storage WHERE project_path = ? AND task_id = ?`
569
+ );
570
+ const totalRow = totalQuery.get(this.#projectPath, id) as { count: number };
571
+
572
+ const query = this.#db.query(`
573
+ SELECT
574
+ id,
575
+ created_at,
576
+ task_id,
577
+ field,
578
+ old_value,
579
+ new_value
580
+ FROM task_changelog_storage
581
+ WHERE project_path = ? AND task_id = ?
582
+ ORDER BY created_at DESC
583
+ LIMIT ? OFFSET ?
584
+ `);
585
+
586
+ const rows = query.all(this.#projectPath, id, limit, offset) as TaskChangelogRow[];
587
+
588
+ return {
589
+ changelog: rows.map(toChangelogEntry),
590
+ total: totalRow.count,
591
+ limit,
592
+ offset,
593
+ };
594
+ }
595
+ }