@agentuity/runtime 1.0.24 → 1.0.26
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/dev-patches/aisdk.d.ts +17 -0
- package/dist/dev-patches/aisdk.d.ts.map +1 -0
- package/dist/dev-patches/aisdk.js +154 -0
- package/dist/dev-patches/aisdk.js.map +1 -0
- package/dist/dev-patches/gateway.d.ts +16 -0
- package/dist/dev-patches/gateway.d.ts.map +1 -0
- package/dist/dev-patches/gateway.js +55 -0
- package/dist/dev-patches/gateway.js.map +1 -0
- package/dist/dev-patches/index.d.ts +21 -0
- package/dist/dev-patches/index.d.ts.map +1 -0
- package/dist/dev-patches/index.js +33 -0
- package/dist/dev-patches/index.js.map +1 -0
- package/dist/dev-patches/otel-llm.d.ts +12 -0
- package/dist/dev-patches/otel-llm.d.ts.map +1 -0
- package/dist/dev-patches/otel-llm.js +345 -0
- package/dist/dev-patches/otel-llm.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/services/local/_db.d.ts.map +1 -1
- package/dist/services/local/_db.js +78 -1
- package/dist/services/local/_db.js.map +1 -1
- package/dist/services/local/email.d.ts +4 -1
- package/dist/services/local/email.d.ts.map +1 -1
- package/dist/services/local/email.js +9 -0
- package/dist/services/local/email.js.map +1 -1
- package/dist/services/local/task.d.ts +26 -1
- package/dist/services/local/task.d.ts.map +1 -1
- package/dist/services/local/task.js +304 -4
- package/dist/services/local/task.js.map +1 -1
- package/package.json +7 -7
- package/src/dev-patches/aisdk.ts +172 -0
- package/src/dev-patches/gateway.ts +70 -0
- package/src/dev-patches/index.ts +37 -0
- package/src/dev-patches/otel-llm.ts +408 -0
- package/src/index.ts +3 -0
- package/src/services/local/_db.ts +98 -5
- package/src/services/local/email.ts +14 -4
- package/src/services/local/task.ts +448 -4
|
@@ -115,12 +115,20 @@ function initializeTables(db: Database): void {
|
|
|
115
115
|
created_id TEXT NOT NULL,
|
|
116
116
|
assigned_id TEXT,
|
|
117
117
|
closed_id TEXT,
|
|
118
|
+
deleted INTEGER NOT NULL DEFAULT 0,
|
|
118
119
|
created_at INTEGER NOT NULL,
|
|
119
120
|
updated_at INTEGER NOT NULL,
|
|
120
121
|
PRIMARY KEY (project_path, id)
|
|
121
122
|
)
|
|
122
123
|
`);
|
|
123
124
|
|
|
125
|
+
// Migration: add deleted column for existing databases
|
|
126
|
+
try {
|
|
127
|
+
db.run('ALTER TABLE task_storage ADD COLUMN deleted INTEGER NOT NULL DEFAULT 0');
|
|
128
|
+
} catch {
|
|
129
|
+
// Column already exists
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
// Task Changelog table
|
|
125
133
|
db.run(`
|
|
126
134
|
CREATE TABLE IF NOT EXISTS task_changelog_storage (
|
|
@@ -139,6 +147,57 @@ function initializeTables(db: Database): void {
|
|
|
139
147
|
CREATE INDEX IF NOT EXISTS idx_task_changelog_lookup
|
|
140
148
|
ON task_changelog_storage(project_path, task_id)
|
|
141
149
|
`);
|
|
150
|
+
|
|
151
|
+
// Task Comment table
|
|
152
|
+
db.run(`
|
|
153
|
+
CREATE TABLE IF NOT EXISTS task_comment_storage (
|
|
154
|
+
project_path TEXT NOT NULL,
|
|
155
|
+
id TEXT NOT NULL,
|
|
156
|
+
task_id TEXT NOT NULL,
|
|
157
|
+
user_id TEXT NOT NULL,
|
|
158
|
+
body TEXT NOT NULL,
|
|
159
|
+
created_at INTEGER NOT NULL,
|
|
160
|
+
updated_at INTEGER NOT NULL,
|
|
161
|
+
PRIMARY KEY (project_path, id)
|
|
162
|
+
)
|
|
163
|
+
`);
|
|
164
|
+
|
|
165
|
+
db.run(`
|
|
166
|
+
CREATE INDEX IF NOT EXISTS idx_task_comment_lookup
|
|
167
|
+
ON task_comment_storage(project_path, task_id)
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
// Task Tag table
|
|
171
|
+
db.run(`
|
|
172
|
+
CREATE TABLE IF NOT EXISTS task_tag_storage (
|
|
173
|
+
project_path TEXT NOT NULL,
|
|
174
|
+
id TEXT NOT NULL,
|
|
175
|
+
name TEXT NOT NULL,
|
|
176
|
+
color TEXT,
|
|
177
|
+
created_at INTEGER NOT NULL,
|
|
178
|
+
PRIMARY KEY (project_path, id)
|
|
179
|
+
)
|
|
180
|
+
`);
|
|
181
|
+
|
|
182
|
+
// Task-Tag association table
|
|
183
|
+
db.run(`
|
|
184
|
+
CREATE TABLE IF NOT EXISTS task_tag_association_storage (
|
|
185
|
+
project_path TEXT NOT NULL,
|
|
186
|
+
task_id TEXT NOT NULL,
|
|
187
|
+
tag_id TEXT NOT NULL,
|
|
188
|
+
PRIMARY KEY (project_path, task_id, tag_id)
|
|
189
|
+
)
|
|
190
|
+
`);
|
|
191
|
+
|
|
192
|
+
db.run(`
|
|
193
|
+
CREATE INDEX IF NOT EXISTS idx_task_tag_assoc_task
|
|
194
|
+
ON task_tag_association_storage(project_path, task_id)
|
|
195
|
+
`);
|
|
196
|
+
|
|
197
|
+
db.run(`
|
|
198
|
+
CREATE INDEX IF NOT EXISTS idx_task_tag_assoc_tag
|
|
199
|
+
ON task_tag_association_storage(project_path, tag_id)
|
|
200
|
+
`);
|
|
142
201
|
}
|
|
143
202
|
|
|
144
203
|
function cleanupOrphanedProjects(db: Database): void {
|
|
@@ -163,14 +222,36 @@ function cleanupOrphanedProjects(db: Database): void {
|
|
|
163
222
|
.all() as Array<{
|
|
164
223
|
project_path: string;
|
|
165
224
|
}>;
|
|
225
|
+
const taskCommentPaths = db
|
|
226
|
+
.query('SELECT DISTINCT project_path FROM task_comment_storage')
|
|
227
|
+
.all() as Array<{
|
|
228
|
+
project_path: string;
|
|
229
|
+
}>;
|
|
230
|
+
const taskTagPaths = db
|
|
231
|
+
.query('SELECT DISTINCT project_path FROM task_tag_storage')
|
|
232
|
+
.all() as Array<{
|
|
233
|
+
project_path: string;
|
|
234
|
+
}>;
|
|
235
|
+
const taskTagAssocPaths = db
|
|
236
|
+
.query('SELECT DISTINCT project_path FROM task_tag_association_storage')
|
|
237
|
+
.all() as Array<{
|
|
238
|
+
project_path: string;
|
|
239
|
+
}>;
|
|
166
240
|
|
|
167
241
|
// Combine and deduplicate all project paths
|
|
168
242
|
const allPaths = new Set<string>();
|
|
169
|
-
[
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
243
|
+
[
|
|
244
|
+
...kvPaths,
|
|
245
|
+
...streamPaths,
|
|
246
|
+
...vectorPaths,
|
|
247
|
+
...taskPaths,
|
|
248
|
+
...taskChangelogPaths,
|
|
249
|
+
...taskCommentPaths,
|
|
250
|
+
...taskTagPaths,
|
|
251
|
+
...taskTagAssocPaths,
|
|
252
|
+
].forEach((row) => {
|
|
253
|
+
allPaths.add(row.project_path);
|
|
254
|
+
});
|
|
174
255
|
|
|
175
256
|
// Check which paths no longer exist and are not the current project
|
|
176
257
|
const pathsToDelete: string[] = [];
|
|
@@ -198,12 +279,24 @@ function cleanupOrphanedProjects(db: Database): void {
|
|
|
198
279
|
const deleteTaskChangelog = db.prepare(
|
|
199
280
|
`DELETE FROM task_changelog_storage WHERE project_path IN (${placeholders})`
|
|
200
281
|
);
|
|
282
|
+
const deleteTaskComments = db.prepare(
|
|
283
|
+
`DELETE FROM task_comment_storage WHERE project_path IN (${placeholders})`
|
|
284
|
+
);
|
|
285
|
+
const deleteTaskTags = db.prepare(
|
|
286
|
+
`DELETE FROM task_tag_storage WHERE project_path IN (${placeholders})`
|
|
287
|
+
);
|
|
288
|
+
const deleteTaskTagAssoc = db.prepare(
|
|
289
|
+
`DELETE FROM task_tag_association_storage WHERE project_path IN (${placeholders})`
|
|
290
|
+
);
|
|
201
291
|
|
|
202
292
|
deleteKv.run(...pathsToDelete);
|
|
203
293
|
deleteStream.run(...pathsToDelete);
|
|
204
294
|
deleteVector.run(...pathsToDelete);
|
|
205
295
|
deleteTasks.run(...pathsToDelete);
|
|
206
296
|
deleteTaskChangelog.run(...pathsToDelete);
|
|
297
|
+
deleteTaskComments.run(...pathsToDelete);
|
|
298
|
+
deleteTaskTags.run(...pathsToDelete);
|
|
299
|
+
deleteTaskTagAssoc.run(...pathsToDelete);
|
|
207
300
|
|
|
208
301
|
console.log(`[LocalDB] Cleaned up data for ${pathsToDelete.length} orphaned project(s)`);
|
|
209
302
|
}
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
type EmailService,
|
|
4
4
|
type EmailAddress,
|
|
5
5
|
type EmailDestination,
|
|
6
|
+
type EmailConnectionConfig,
|
|
6
7
|
type EmailInbound,
|
|
7
8
|
type EmailOutbound,
|
|
8
9
|
type EmailSendParams,
|
|
@@ -11,10 +12,7 @@ import {
|
|
|
11
12
|
const ERROR_MESSAGE =
|
|
12
13
|
'Email service is not available in local development mode. Deploy to Agentuity Cloud to use email.';
|
|
13
14
|
|
|
14
|
-
const LocalEmailNotAvailableError = StructuredError(
|
|
15
|
-
'LocalEmailNotAvailableError',
|
|
16
|
-
ERROR_MESSAGE
|
|
17
|
-
);
|
|
15
|
+
const LocalEmailNotAvailableError = StructuredError('LocalEmailNotAvailableError', ERROR_MESSAGE);
|
|
18
16
|
|
|
19
17
|
/**
|
|
20
18
|
* Local development stub for the email service.
|
|
@@ -33,6 +31,10 @@ export class LocalEmailStorage implements EmailService {
|
|
|
33
31
|
throw new LocalEmailNotAvailableError();
|
|
34
32
|
}
|
|
35
33
|
|
|
34
|
+
async getConnectionConfig(_id: string): Promise<EmailConnectionConfig | null> {
|
|
35
|
+
throw new LocalEmailNotAvailableError();
|
|
36
|
+
}
|
|
37
|
+
|
|
36
38
|
async deleteAddress(_id: string): Promise<void> {
|
|
37
39
|
throw new LocalEmailNotAvailableError();
|
|
38
40
|
}
|
|
@@ -65,6 +67,10 @@ export class LocalEmailStorage implements EmailService {
|
|
|
65
67
|
throw new LocalEmailNotAvailableError();
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
async deleteInbound(_id: string): Promise<void> {
|
|
71
|
+
throw new LocalEmailNotAvailableError();
|
|
72
|
+
}
|
|
73
|
+
|
|
68
74
|
async listOutbound(_addressId?: string): Promise<EmailOutbound[]> {
|
|
69
75
|
throw new LocalEmailNotAvailableError();
|
|
70
76
|
}
|
|
@@ -72,4 +78,8 @@ export class LocalEmailStorage implements EmailService {
|
|
|
72
78
|
async getOutbound(_id: string): Promise<EmailOutbound | null> {
|
|
73
79
|
throw new LocalEmailNotAvailableError();
|
|
74
80
|
}
|
|
81
|
+
|
|
82
|
+
async deleteOutbound(_id: string): Promise<void> {
|
|
83
|
+
throw new LocalEmailNotAvailableError();
|
|
84
|
+
}
|
|
75
85
|
}
|
|
@@ -11,6 +11,19 @@ import type {
|
|
|
11
11
|
ListTasksParams,
|
|
12
12
|
ListTasksResult,
|
|
13
13
|
TaskChangelogResult,
|
|
14
|
+
Comment,
|
|
15
|
+
Tag,
|
|
16
|
+
ListCommentsResult,
|
|
17
|
+
ListTagsResult,
|
|
18
|
+
ListUsersResult,
|
|
19
|
+
ListProjectsResult,
|
|
20
|
+
Attachment,
|
|
21
|
+
CreateAttachmentParams,
|
|
22
|
+
PresignUploadResponse,
|
|
23
|
+
PresignDownloadResponse,
|
|
24
|
+
ListAttachmentsResult,
|
|
25
|
+
TaskActivityParams,
|
|
26
|
+
TaskActivityResult,
|
|
14
27
|
} from '@agentuity/core';
|
|
15
28
|
import { StructuredError } from '@agentuity/core';
|
|
16
29
|
import { now } from './_util';
|
|
@@ -24,6 +37,46 @@ const TaskNotFoundError = StructuredError('TaskNotFoundError', 'Task not found')
|
|
|
24
37
|
|
|
25
38
|
const TaskAlreadyClosedError = StructuredError('TaskAlreadyClosedError', 'Task is already closed');
|
|
26
39
|
|
|
40
|
+
const CommentNotFoundError = StructuredError('CommentNotFoundError', 'Comment not found');
|
|
41
|
+
|
|
42
|
+
const TagNotFoundError = StructuredError('TagNotFoundError', 'Tag not found');
|
|
43
|
+
|
|
44
|
+
const CommentBodyRequiredError = StructuredError(
|
|
45
|
+
'CommentBodyRequiredError',
|
|
46
|
+
'Comment body is required and must be a non-empty string'
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const CommentUserRequiredError = StructuredError(
|
|
50
|
+
'CommentUserRequiredError',
|
|
51
|
+
'Comment user ID is required and must be a non-empty string'
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const TagNameRequiredError = StructuredError(
|
|
55
|
+
'TagNameRequiredError',
|
|
56
|
+
'Tag name is required and must be a non-empty string'
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const AttachmentNotSupportedError = StructuredError(
|
|
60
|
+
'AttachmentNotSupportedError',
|
|
61
|
+
'Attachments are not supported in local task storage'
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
type CommentRow = {
|
|
65
|
+
id: string;
|
|
66
|
+
created_at: number;
|
|
67
|
+
updated_at: number;
|
|
68
|
+
task_id: string;
|
|
69
|
+
user_id: string;
|
|
70
|
+
body: string;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type TagRow = {
|
|
74
|
+
id: string;
|
|
75
|
+
created_at: number;
|
|
76
|
+
name: string;
|
|
77
|
+
color: string | null;
|
|
78
|
+
};
|
|
79
|
+
|
|
27
80
|
type TaskRow = {
|
|
28
81
|
id: string;
|
|
29
82
|
created_at: number;
|
|
@@ -41,6 +94,7 @@ type TaskRow = {
|
|
|
41
94
|
created_id: string;
|
|
42
95
|
assigned_id: string | null;
|
|
43
96
|
closed_id: string | null;
|
|
97
|
+
deleted: number;
|
|
44
98
|
};
|
|
45
99
|
|
|
46
100
|
type TaskChangelogRow = {
|
|
@@ -92,9 +146,38 @@ function toTask(row: TaskRow): Task {
|
|
|
92
146
|
created_id: row.created_id,
|
|
93
147
|
assigned_id: row.assigned_id ?? undefined,
|
|
94
148
|
closed_id: row.closed_id ?? undefined,
|
|
149
|
+
deleted: Boolean(row.deleted),
|
|
95
150
|
};
|
|
96
151
|
}
|
|
97
152
|
|
|
153
|
+
function toComment(row: CommentRow): Comment {
|
|
154
|
+
return {
|
|
155
|
+
id: row.id,
|
|
156
|
+
created_at: new Date(row.created_at).toISOString(),
|
|
157
|
+
updated_at: new Date(row.updated_at).toISOString(),
|
|
158
|
+
task_id: row.task_id,
|
|
159
|
+
user_id: row.user_id,
|
|
160
|
+
body: row.body,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function toTag(row: TagRow): Tag {
|
|
165
|
+
return {
|
|
166
|
+
id: row.id,
|
|
167
|
+
created_at: new Date(row.created_at).toISOString(),
|
|
168
|
+
name: row.name,
|
|
169
|
+
color: row.color ?? undefined,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function generateCommentId(): string {
|
|
174
|
+
return `comment_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function generateTagId(): string {
|
|
178
|
+
return `tag_${crypto.randomUUID().replace(/-/g, '').slice(0, 24)}`;
|
|
179
|
+
}
|
|
180
|
+
|
|
98
181
|
function toChangelogEntry(row: TaskChangelogRow): TaskChangelogEntry {
|
|
99
182
|
return {
|
|
100
183
|
id: row.id,
|
|
@@ -168,6 +251,7 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
168
251
|
created_id: params.created_id,
|
|
169
252
|
assigned_id: params.assigned_id ?? null,
|
|
170
253
|
closed_id: null,
|
|
254
|
+
deleted: 0,
|
|
171
255
|
};
|
|
172
256
|
|
|
173
257
|
stmt.run(
|
|
@@ -211,7 +295,8 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
211
295
|
closed_date,
|
|
212
296
|
created_id,
|
|
213
297
|
assigned_id,
|
|
214
|
-
closed_id
|
|
298
|
+
closed_id,
|
|
299
|
+
deleted
|
|
215
300
|
FROM task_storage
|
|
216
301
|
WHERE project_path = ? AND id = ?
|
|
217
302
|
`);
|
|
@@ -278,7 +363,8 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
278
363
|
closed_date,
|
|
279
364
|
created_id,
|
|
280
365
|
assigned_id,
|
|
281
|
-
closed_id
|
|
366
|
+
closed_id,
|
|
367
|
+
deleted
|
|
282
368
|
FROM task_storage
|
|
283
369
|
${whereClause}
|
|
284
370
|
ORDER BY ${sortField} ${sortOrder}
|
|
@@ -314,7 +400,8 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
314
400
|
closed_date,
|
|
315
401
|
created_id,
|
|
316
402
|
assigned_id,
|
|
317
|
-
closed_id
|
|
403
|
+
closed_id,
|
|
404
|
+
deleted
|
|
318
405
|
FROM task_storage
|
|
319
406
|
WHERE project_path = ? AND id = ?
|
|
320
407
|
`);
|
|
@@ -493,7 +580,8 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
493
580
|
closed_date,
|
|
494
581
|
created_id,
|
|
495
582
|
assigned_id,
|
|
496
|
-
closed_id
|
|
583
|
+
closed_id,
|
|
584
|
+
deleted
|
|
497
585
|
FROM task_storage
|
|
498
586
|
WHERE project_path = ? AND id = ?
|
|
499
587
|
`);
|
|
@@ -592,4 +680,360 @@ export class LocalTaskStorage implements TaskStorage {
|
|
|
592
680
|
offset,
|
|
593
681
|
};
|
|
594
682
|
}
|
|
683
|
+
|
|
684
|
+
async softDelete(id: string): Promise<Task> {
|
|
685
|
+
const task = await this.get(id);
|
|
686
|
+
if (!task) {
|
|
687
|
+
throw new TaskNotFoundError();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const timestamp = now();
|
|
691
|
+
|
|
692
|
+
const updateStmt = this.#db.prepare(`
|
|
693
|
+
UPDATE task_storage
|
|
694
|
+
SET status = 'closed', deleted = 1, closed_date = COALESCE(closed_date, ?), updated_at = ?
|
|
695
|
+
WHERE project_path = ? AND id = ?
|
|
696
|
+
`);
|
|
697
|
+
|
|
698
|
+
updateStmt.run(new Date(timestamp).toISOString(), timestamp, this.#projectPath, id);
|
|
699
|
+
|
|
700
|
+
const changelogStmt = this.#db.prepare(`
|
|
701
|
+
INSERT INTO task_changelog_storage (
|
|
702
|
+
project_path, id, task_id, field, old_value, new_value, created_at
|
|
703
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
704
|
+
`);
|
|
705
|
+
|
|
706
|
+
changelogStmt.run(
|
|
707
|
+
this.#projectPath,
|
|
708
|
+
generateChangelogId(),
|
|
709
|
+
id,
|
|
710
|
+
'deleted',
|
|
711
|
+
'false',
|
|
712
|
+
'true',
|
|
713
|
+
timestamp
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
const updated = await this.get(id);
|
|
717
|
+
return { ...updated!, deleted: true };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
async createComment(taskId: string, body: string, userId: string): Promise<Comment> {
|
|
721
|
+
const trimmedBody = body?.trim();
|
|
722
|
+
if (!trimmedBody) {
|
|
723
|
+
throw new CommentBodyRequiredError();
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
const trimmedUserId = userId?.trim();
|
|
727
|
+
if (!trimmedUserId) {
|
|
728
|
+
throw new CommentUserRequiredError();
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const task = await this.get(taskId);
|
|
732
|
+
if (!task) {
|
|
733
|
+
throw new TaskNotFoundError();
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const id = generateCommentId();
|
|
737
|
+
const timestamp = now();
|
|
738
|
+
|
|
739
|
+
const stmt = this.#db.prepare(`
|
|
740
|
+
INSERT INTO task_comment_storage (
|
|
741
|
+
project_path, id, task_id, user_id, body, created_at, updated_at
|
|
742
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
743
|
+
`);
|
|
744
|
+
|
|
745
|
+
stmt.run(this.#projectPath, id, taskId, trimmedUserId, trimmedBody, timestamp, timestamp);
|
|
746
|
+
|
|
747
|
+
return toComment({
|
|
748
|
+
id,
|
|
749
|
+
created_at: timestamp,
|
|
750
|
+
updated_at: timestamp,
|
|
751
|
+
task_id: taskId,
|
|
752
|
+
user_id: trimmedUserId,
|
|
753
|
+
body: trimmedBody,
|
|
754
|
+
});
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
async getComment(commentId: string): Promise<Comment> {
|
|
758
|
+
const query = this.#db.query(`
|
|
759
|
+
SELECT id, created_at, updated_at, task_id, user_id, body
|
|
760
|
+
FROM task_comment_storage
|
|
761
|
+
WHERE project_path = ? AND id = ?
|
|
762
|
+
`);
|
|
763
|
+
|
|
764
|
+
const row = query.get(this.#projectPath, commentId) as CommentRow | null;
|
|
765
|
+
if (!row) {
|
|
766
|
+
throw new CommentNotFoundError();
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
return toComment(row);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
async updateComment(commentId: string, body: string): Promise<Comment> {
|
|
773
|
+
const trimmedBody = body?.trim();
|
|
774
|
+
if (!trimmedBody) {
|
|
775
|
+
throw new CommentBodyRequiredError();
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
const existing = await this.getComment(commentId);
|
|
779
|
+
const timestamp = now();
|
|
780
|
+
|
|
781
|
+
const stmt = this.#db.prepare(`
|
|
782
|
+
UPDATE task_comment_storage
|
|
783
|
+
SET body = ?, updated_at = ?
|
|
784
|
+
WHERE project_path = ? AND id = ?
|
|
785
|
+
`);
|
|
786
|
+
|
|
787
|
+
stmt.run(trimmedBody, timestamp, this.#projectPath, commentId);
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
...existing,
|
|
791
|
+
body: trimmedBody,
|
|
792
|
+
updated_at: new Date(timestamp).toISOString(),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
async deleteComment(commentId: string): Promise<void> {
|
|
797
|
+
const stmt = this.#db.prepare(`
|
|
798
|
+
DELETE FROM task_comment_storage
|
|
799
|
+
WHERE project_path = ? AND id = ?
|
|
800
|
+
`);
|
|
801
|
+
|
|
802
|
+
stmt.run(this.#projectPath, commentId);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
async listComments(
|
|
806
|
+
taskId: string,
|
|
807
|
+
params?: { limit?: number; offset?: number }
|
|
808
|
+
): Promise<ListCommentsResult> {
|
|
809
|
+
const limit = params?.limit ?? DEFAULT_LIMIT;
|
|
810
|
+
const offset = params?.offset ?? 0;
|
|
811
|
+
|
|
812
|
+
const totalQuery = this.#db.query(
|
|
813
|
+
`SELECT COUNT(*) as count FROM task_comment_storage WHERE project_path = ? AND task_id = ?`
|
|
814
|
+
);
|
|
815
|
+
const totalRow = totalQuery.get(this.#projectPath, taskId) as { count: number };
|
|
816
|
+
|
|
817
|
+
const query = this.#db.query(`
|
|
818
|
+
SELECT id, created_at, updated_at, task_id, user_id, body
|
|
819
|
+
FROM task_comment_storage
|
|
820
|
+
WHERE project_path = ? AND task_id = ?
|
|
821
|
+
ORDER BY created_at DESC
|
|
822
|
+
LIMIT ? OFFSET ?
|
|
823
|
+
`);
|
|
824
|
+
|
|
825
|
+
const rows = query.all(this.#projectPath, taskId, limit, offset) as CommentRow[];
|
|
826
|
+
|
|
827
|
+
return {
|
|
828
|
+
comments: rows.map(toComment),
|
|
829
|
+
total: totalRow.count,
|
|
830
|
+
limit,
|
|
831
|
+
offset,
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
async createTag(name: string, color?: string): Promise<Tag> {
|
|
836
|
+
const trimmedName = name?.trim();
|
|
837
|
+
if (!trimmedName) {
|
|
838
|
+
throw new TagNameRequiredError();
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const id = generateTagId();
|
|
842
|
+
const timestamp = now();
|
|
843
|
+
|
|
844
|
+
const stmt = this.#db.prepare(`
|
|
845
|
+
INSERT INTO task_tag_storage (
|
|
846
|
+
project_path, id, name, color, created_at
|
|
847
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
848
|
+
`);
|
|
849
|
+
|
|
850
|
+
stmt.run(this.#projectPath, id, trimmedName, color ?? null, timestamp);
|
|
851
|
+
|
|
852
|
+
return toTag({
|
|
853
|
+
id,
|
|
854
|
+
created_at: timestamp,
|
|
855
|
+
name: trimmedName,
|
|
856
|
+
color: color ?? null,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async getTag(tagId: string): Promise<Tag> {
|
|
861
|
+
const query = this.#db.query(`
|
|
862
|
+
SELECT id, created_at, name, color
|
|
863
|
+
FROM task_tag_storage
|
|
864
|
+
WHERE project_path = ? AND id = ?
|
|
865
|
+
`);
|
|
866
|
+
|
|
867
|
+
const row = query.get(this.#projectPath, tagId) as TagRow | null;
|
|
868
|
+
if (!row) {
|
|
869
|
+
throw new TagNotFoundError();
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
return toTag(row);
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async updateTag(tagId: string, name: string, color?: string): Promise<Tag> {
|
|
876
|
+
const trimmedName = name?.trim();
|
|
877
|
+
if (!trimmedName) {
|
|
878
|
+
throw new TagNameRequiredError();
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Verify exists
|
|
882
|
+
await this.getTag(tagId);
|
|
883
|
+
|
|
884
|
+
const stmt = this.#db.prepare(`
|
|
885
|
+
UPDATE task_tag_storage
|
|
886
|
+
SET name = ?, color = ?
|
|
887
|
+
WHERE project_path = ? AND id = ?
|
|
888
|
+
`);
|
|
889
|
+
|
|
890
|
+
stmt.run(trimmedName, color ?? null, this.#projectPath, tagId);
|
|
891
|
+
|
|
892
|
+
return this.getTag(tagId);
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
async deleteTag(tagId: string): Promise<void> {
|
|
896
|
+
// Also remove tag associations
|
|
897
|
+
const deleteAssocStmt = this.#db.prepare(`
|
|
898
|
+
DELETE FROM task_tag_association_storage
|
|
899
|
+
WHERE project_path = ? AND tag_id = ?
|
|
900
|
+
`);
|
|
901
|
+
deleteAssocStmt.run(this.#projectPath, tagId);
|
|
902
|
+
|
|
903
|
+
const stmt = this.#db.prepare(`
|
|
904
|
+
DELETE FROM task_tag_storage
|
|
905
|
+
WHERE project_path = ? AND id = ?
|
|
906
|
+
`);
|
|
907
|
+
stmt.run(this.#projectPath, tagId);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
async listTags(): Promise<ListTagsResult> {
|
|
911
|
+
const query = this.#db.query(`
|
|
912
|
+
SELECT id, created_at, name, color
|
|
913
|
+
FROM task_tag_storage
|
|
914
|
+
WHERE project_path = ?
|
|
915
|
+
ORDER BY name ASC
|
|
916
|
+
`);
|
|
917
|
+
|
|
918
|
+
const rows = query.all(this.#projectPath) as TagRow[];
|
|
919
|
+
|
|
920
|
+
return {
|
|
921
|
+
tags: rows.map(toTag),
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
async addTagToTask(taskId: string, tagId: string): Promise<void> {
|
|
926
|
+
// Verify task and tag exist
|
|
927
|
+
const task = await this.get(taskId);
|
|
928
|
+
if (!task) {
|
|
929
|
+
throw new TaskNotFoundError();
|
|
930
|
+
}
|
|
931
|
+
await this.getTag(tagId);
|
|
932
|
+
|
|
933
|
+
const stmt = this.#db.prepare(`
|
|
934
|
+
INSERT OR IGNORE INTO task_tag_association_storage (
|
|
935
|
+
project_path, task_id, tag_id
|
|
936
|
+
) VALUES (?, ?, ?)
|
|
937
|
+
`);
|
|
938
|
+
|
|
939
|
+
stmt.run(this.#projectPath, taskId, tagId);
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
async removeTagFromTask(taskId: string, tagId: string): Promise<void> {
|
|
943
|
+
const stmt = this.#db.prepare(`
|
|
944
|
+
DELETE FROM task_tag_association_storage
|
|
945
|
+
WHERE project_path = ? AND task_id = ? AND tag_id = ?
|
|
946
|
+
`);
|
|
947
|
+
|
|
948
|
+
stmt.run(this.#projectPath, taskId, tagId);
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
async listTagsForTask(taskId: string): Promise<Tag[]> {
|
|
952
|
+
const query = this.#db.query(`
|
|
953
|
+
SELECT t.id, t.created_at, t.name, t.color
|
|
954
|
+
FROM task_tag_storage t
|
|
955
|
+
INNER JOIN task_tag_association_storage a ON t.id = a.tag_id AND t.project_path = a.project_path
|
|
956
|
+
WHERE a.project_path = ? AND a.task_id = ?
|
|
957
|
+
ORDER BY t.name ASC
|
|
958
|
+
`);
|
|
959
|
+
|
|
960
|
+
const rows = query.all(this.#projectPath, taskId) as TagRow[];
|
|
961
|
+
|
|
962
|
+
return rows.map(toTag);
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// Attachment methods — not supported in local storage
|
|
966
|
+
|
|
967
|
+
async uploadAttachment(
|
|
968
|
+
_taskId: string,
|
|
969
|
+
_params: CreateAttachmentParams
|
|
970
|
+
): Promise<PresignUploadResponse> {
|
|
971
|
+
throw new AttachmentNotSupportedError();
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
async confirmAttachment(_attachmentId: string): Promise<Attachment> {
|
|
975
|
+
throw new AttachmentNotSupportedError();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
async downloadAttachment(_attachmentId: string): Promise<PresignDownloadResponse> {
|
|
979
|
+
throw new AttachmentNotSupportedError();
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
async listAttachments(_taskId: string): Promise<ListAttachmentsResult> {
|
|
983
|
+
throw new AttachmentNotSupportedError();
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
async deleteAttachment(_attachmentId: string): Promise<void> {
|
|
987
|
+
throw new AttachmentNotSupportedError();
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
async listUsers(): Promise<ListUsersResult> {
|
|
991
|
+
return { users: [] };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
async listProjects(): Promise<ListProjectsResult> {
|
|
995
|
+
return { projects: [] };
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
async getActivity(params?: TaskActivityParams): Promise<TaskActivityResult> {
|
|
999
|
+
const days = Math.min(365, Math.max(7, params?.days ?? 90));
|
|
1000
|
+
const activity = [];
|
|
1001
|
+
const now = new Date();
|
|
1002
|
+
|
|
1003
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
1004
|
+
const date = new Date(now);
|
|
1005
|
+
date.setDate(date.getDate() - i);
|
|
1006
|
+
const dateStr = date.toISOString().slice(0, 10);
|
|
1007
|
+
|
|
1008
|
+
const row = this.#db
|
|
1009
|
+
.prepare(
|
|
1010
|
+
`SELECT
|
|
1011
|
+
COALESCE(SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END), 0) as open,
|
|
1012
|
+
COALESCE(SUM(CASE WHEN status = 'in_progress' THEN 1 ELSE 0 END), 0) as in_progress,
|
|
1013
|
+
COALESCE(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END), 0) as done,
|
|
1014
|
+
COALESCE(SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END), 0) as closed,
|
|
1015
|
+
COALESCE(SUM(CASE WHEN status = 'cancelled' THEN 1 ELSE 0 END), 0) as cancelled
|
|
1016
|
+
FROM task_storage
|
|
1017
|
+
WHERE project_path = ? AND date(created_at) = ?`
|
|
1018
|
+
)
|
|
1019
|
+
.get(this.#projectPath, dateStr) as {
|
|
1020
|
+
open: number;
|
|
1021
|
+
in_progress: number;
|
|
1022
|
+
done: number;
|
|
1023
|
+
closed: number;
|
|
1024
|
+
cancelled: number;
|
|
1025
|
+
};
|
|
1026
|
+
|
|
1027
|
+
activity.push({
|
|
1028
|
+
date: dateStr,
|
|
1029
|
+
open: row.open,
|
|
1030
|
+
inProgress: row.in_progress,
|
|
1031
|
+
done: row.done,
|
|
1032
|
+
closed: row.closed,
|
|
1033
|
+
cancelled: row.cancelled,
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return { activity, days };
|
|
1038
|
+
}
|
|
595
1039
|
}
|