@plandesk/api 0.7.0 → 0.9.0

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 (44) hide show
  1. package/dist/events.d.ts +20 -1
  2. package/dist/index.d.ts +8 -1
  3. package/dist/index.js +5 -1
  4. package/dist/routes/agent-runs.d.ts +1 -1
  5. package/dist/routes/agent-runs.js +20 -0
  6. package/dist/routes/documents.js +2 -0
  7. package/dist/routes/folders.d.ts +4 -0
  8. package/dist/routes/folders.js +69 -0
  9. package/dist/routes/goals.d.ts +4 -0
  10. package/dist/routes/goals.js +138 -0
  11. package/dist/routes/projects.d.ts +1 -1
  12. package/dist/routes/projects.js +23 -3
  13. package/dist/routes/submissions.d.ts +5 -0
  14. package/dist/routes/submissions.js +58 -0
  15. package/dist/routes/tags.d.ts +4 -0
  16. package/dist/routes/tags.js +62 -0
  17. package/dist/routes/tasks.d.ts +1 -0
  18. package/dist/routes/tasks.js +9 -1
  19. package/dist/serialize.d.ts +51 -2
  20. package/dist/serialize.js +101 -1
  21. package/dist/server.js +9 -1
  22. package/dist/services/canvas.d.ts +4 -0
  23. package/dist/services/canvas.js +3 -2
  24. package/dist/services/documents.d.ts +5 -1
  25. package/dist/services/documents.js +31 -2
  26. package/dist/services/folders.d.ts +27 -0
  27. package/dist/services/folders.js +107 -0
  28. package/dist/services/goals.d.ts +239 -0
  29. package/dist/services/goals.js +323 -0
  30. package/dist/services/index.d.ts +6 -0
  31. package/dist/services/index.js +9 -0
  32. package/dist/services/projects.js +8 -2
  33. package/dist/services/sync.d.ts +5 -3
  34. package/dist/services/sync.js +45 -2
  35. package/dist/services/tags.d.ts +27 -0
  36. package/dist/services/tags.js +79 -0
  37. package/dist/services/tasks.d.ts +20 -2
  38. package/dist/services/tasks.js +98 -26
  39. package/package.json +3 -3
  40. package/web/assets/index-DcD7-VGi.css +1 -0
  41. package/web/assets/index-Dcaq3wR-.js +318 -0
  42. package/web/index.html +2 -2
  43. package/web/assets/index-CUxSE_WZ.css +0 -1
  44. package/web/assets/index-cyyf9-Uw.js +0 -234
package/dist/events.d.ts CHANGED
@@ -24,6 +24,20 @@ export type CommentUpdatedEvent = {
24
24
  documentId: string;
25
25
  projectId: string;
26
26
  };
27
+ export type FolderCreatedEvent = {
28
+ type: 'folder_created';
29
+ folderId: string;
30
+ projectId: string;
31
+ };
32
+ export type FolderUpdatedEvent = {
33
+ type: 'folder_updated';
34
+ folderId: string;
35
+ projectId: string;
36
+ };
37
+ export type TagUpdatedEvent = {
38
+ type: 'tag_updated';
39
+ projectId: string;
40
+ };
27
41
  export type NoteCreatedEvent = {
28
42
  type: 'note_created';
29
43
  noteId: string;
@@ -53,7 +67,12 @@ export type SubmissionsPulledEvent = {
53
67
  type: 'submissions_pulled';
54
68
  projectId: string;
55
69
  };
56
- export type PlankDeskEvent = TaskUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent;
70
+ export type GoalUpdatedEvent = {
71
+ type: 'goal_updated';
72
+ goalId: string;
73
+ projectId: string;
74
+ };
75
+ export type PlankDeskEvent = TaskUpdatedEvent | TagUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | FolderCreatedEvent | FolderUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent | GoalUpdatedEvent;
57
76
  export type EventListener = (event: PlankDeskEvent) => void;
58
77
  export type EventBus = {
59
78
  subscribe(listener: EventListener): () => void;
package/dist/index.d.ts CHANGED
@@ -5,19 +5,26 @@ export { mountStatic } from './static.js';
5
5
  export { createServices, type Services, type ServicesDeps } from './services/index.js';
6
6
  export { createEventBus, type EventBus, type PlankDeskEvent } from './events.js';
7
7
  export type { ProjectService } from './services/projects.js';
8
+ export type { GoalService, VerificationEvidence } from './services/goals.js';
9
+ export { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from './services/goals.js';
8
10
  export type { TaskService } from './services/tasks.js';
11
+ export { InvalidGoalReferenceError } from './services/tasks.js';
12
+ export type { TagService } from './services/tags.js';
9
13
  export type { CanvasService } from './services/canvas.js';
10
14
  export type { DocumentService } from './services/documents.js';
15
+ export type { FolderService } from './services/folders.js';
11
16
  export type { NoteService } from './services/notes.js';
12
17
  export type { CommentService } from './services/comments.js';
13
18
  export type { AgentRunService } from './services/agent-runs.js';
14
19
  export { InvalidDocumentError } from './services/documents.js';
20
+ export { InvalidFolderError } from './services/folders.js';
15
21
  export { InvalidNoteError } from './services/notes.js';
22
+ export { InvalidTagError } from './services/tags.js';
16
23
  export { InvalidCommentError } from './services/comments.js';
17
24
  export { InvalidCanvasError } from './services/canvas.js';
18
25
  export { InvalidAgentRunError } from './services/agent-runs.js';
19
26
  export { InvalidScaffoldError } from './services/projects.js';
20
27
  export { InvalidShareError } from './services/share.js';
21
- export { InvalidTriageError, SyncUnavailableError, SyncUnauthorizedError, type SyncRemote, type SyncService, } from './services/sync.js';
28
+ export { InvalidTriageError, InvalidTriageInputError, SyncUnavailableError, SyncUnauthorizedError, type SyncRemote, type SyncService, } from './services/sync.js';
22
29
  export declare const version: () => string;
23
30
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,14 +4,18 @@ export { healthRouter } from './routes/health.js';
4
4
  export { mountStatic } from './static.js';
5
5
  export { createServices } from './services/index.js';
6
6
  export { createEventBus } from './events.js';
7
+ export { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from './services/goals.js';
8
+ export { InvalidGoalReferenceError } from './services/tasks.js';
7
9
  export { InvalidDocumentError } from './services/documents.js';
10
+ export { InvalidFolderError } from './services/folders.js';
8
11
  export { InvalidNoteError } from './services/notes.js';
12
+ export { InvalidTagError } from './services/tags.js';
9
13
  export { InvalidCommentError } from './services/comments.js';
10
14
  export { InvalidCanvasError } from './services/canvas.js';
11
15
  export { InvalidAgentRunError } from './services/agent-runs.js';
12
16
  export { InvalidScaffoldError } from './services/projects.js';
13
17
  export { InvalidShareError } from './services/share.js';
14
- export { InvalidTriageError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
18
+ export { InvalidTriageError, InvalidTriageInputError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
15
19
  import { readFileSync } from 'node:fs';
16
20
  import { dirname, join } from 'node:path';
17
21
  import { fileURLToPath } from 'node:url';
@@ -1,4 +1,4 @@
1
1
  import { Hono } from 'hono';
2
- import type { AgentRunService } from '../services/agent-runs.js';
2
+ import { type AgentRunService } from '../services/agent-runs.js';
3
3
  export declare function createAgentRunsRouter(agentRunService: AgentRunService): Hono;
4
4
  //# sourceMappingURL=agent-runs.d.ts.map
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
+ import { InvalidAgentRunError } from '../services/agent-runs.js';
2
3
  import { parsePaginationParams } from '../serialize.js';
3
4
  export function createAgentRunsRouter(agentRunService) {
4
5
  const router = new Hono();
@@ -13,6 +14,25 @@ export function createAgentRunsRouter(agentRunService) {
13
14
  }
14
15
  return c.json(runs);
15
16
  });
17
+ router.post('/agent-runs/:id/progress', async (c) => {
18
+ const body = await c.req.json();
19
+ if (typeof body.message !== 'string' || body.message.trim() === '') {
20
+ return c.json({ error: 'invalid_argument' }, 400);
21
+ }
22
+ try {
23
+ const event = agentRunService.recordProgress(c.req.param('id'), body.message);
24
+ if (!event) {
25
+ return c.json({ error: 'not_found' }, 404);
26
+ }
27
+ return c.json(event, 201);
28
+ }
29
+ catch (error) {
30
+ if (error instanceof InvalidAgentRunError) {
31
+ return c.json({ error: 'invalid_argument' }, 400);
32
+ }
33
+ throw error;
34
+ }
35
+ });
16
36
  return router;
17
37
  }
18
38
  //# sourceMappingURL=agent-runs.js.map
@@ -37,6 +37,7 @@ export function createDocumentsRouter(documentService) {
37
37
  body: body.body,
38
38
  statusLine: body.status_line,
39
39
  parentId: body.parent_id,
40
+ folderId: body.folder_id,
40
41
  linkedTaskId: resolveLinkedTaskId(body),
41
42
  });
42
43
  if (!document) {
@@ -67,6 +68,7 @@ export function createDocumentsRouter(documentService) {
67
68
  ...(body.body !== undefined ? { body: body.body } : {}),
68
69
  ...(body.status_line !== undefined ? { statusLine: body.status_line } : {}),
69
70
  ...(body.parent_id !== undefined ? { parentId: body.parent_id } : {}),
71
+ ...(body.folder_id !== undefined ? { folderId: body.folder_id } : {}),
70
72
  ...(linkedTaskId !== undefined ? { linkedTaskId } : {}),
71
73
  });
72
74
  if (!document) {
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import { type FolderService } from '../services/folders.js';
3
+ export declare function createFoldersRouter(folderService: FolderService): Hono;
4
+ //# sourceMappingURL=folders.d.ts.map
@@ -0,0 +1,69 @@
1
+ import { Hono } from 'hono';
2
+ import { InvalidFolderError } from '../services/folders.js';
3
+ export function createFoldersRouter(folderService) {
4
+ const router = new Hono();
5
+ router.get('/projects/:id/folders', (c) => {
6
+ const folders = folderService.list(c.req.param('id'));
7
+ if (!folders) {
8
+ return c.json({ error: 'not_found' }, 404);
9
+ }
10
+ return c.json(folders);
11
+ });
12
+ router.post('/projects/:id/folders', async (c) => {
13
+ const body = await c.req.json();
14
+ if (typeof body.name !== 'string' || body.name.trim() === '') {
15
+ return c.json({ error: 'invalid_argument' }, 400);
16
+ }
17
+ try {
18
+ const folder = folderService.create(c.req.param('id'), {
19
+ name: body.name,
20
+ parentFolderId: body.parent_folder_id,
21
+ });
22
+ if (!folder) {
23
+ return c.json({ error: 'not_found' }, 404);
24
+ }
25
+ return c.json(folder, 201);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof InvalidFolderError) {
29
+ return c.json({ error: 'invalid_argument' }, 400);
30
+ }
31
+ throw error;
32
+ }
33
+ });
34
+ router.get('/folders/:id', (c) => {
35
+ const folder = folderService.get(c.req.param('id'));
36
+ if (!folder) {
37
+ return c.json({ error: 'not_found' }, 404);
38
+ }
39
+ return c.json(folder);
40
+ });
41
+ router.patch('/folders/:id', async (c) => {
42
+ const body = await c.req.json();
43
+ try {
44
+ const folder = folderService.update(c.req.param('id'), {
45
+ ...(body.name !== undefined ? { name: body.name } : {}),
46
+ ...(body.parent_folder_id !== undefined ? { parentFolderId: body.parent_folder_id } : {}),
47
+ });
48
+ if (!folder) {
49
+ return c.json({ error: 'not_found' }, 404);
50
+ }
51
+ return c.json(folder);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof InvalidFolderError) {
55
+ return c.json({ error: 'invalid_argument' }, 400);
56
+ }
57
+ throw error;
58
+ }
59
+ });
60
+ router.delete('/folders/:id', (c) => {
61
+ const deleted = folderService.delete(c.req.param('id'));
62
+ if (!deleted) {
63
+ return c.json({ error: 'not_found' }, 404);
64
+ }
65
+ return c.body(null, 204);
66
+ });
67
+ return router;
68
+ }
69
+ //# sourceMappingURL=folders.js.map
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import { type GoalService } from '../services/goals.js';
3
+ export declare function createGoalsRouter(goalService: GoalService): Hono;
4
+ //# sourceMappingURL=goals.d.ts.map
@@ -0,0 +1,138 @@
1
+ import { Hono } from 'hono';
2
+ import { InvalidGoalStatusError, isGoalStatus } from '@plandesk/db';
3
+ import { GoalCompletionBlockedError, GoalVerificationRequiredError, InvalidGoalTransitionError, InvalidVerificationSurfaceError, } from '../services/goals.js';
4
+ function mapGoalInput(body) {
5
+ return {
6
+ ...(body.objective !== undefined ? { objective: body.objective } : {}),
7
+ ...(body.verification_surface !== undefined
8
+ ? { verificationSurface: body.verification_surface }
9
+ : {}),
10
+ ...(body.constraints !== undefined ? { constraints: body.constraints } : {}),
11
+ ...(body.boundaries !== undefined ? { boundaries: body.boundaries } : {}),
12
+ ...(body.iteration_policy !== undefined ? { iterationPolicy: body.iteration_policy } : {}),
13
+ ...(body.stop_condition !== undefined ? { stopCondition: body.stop_condition } : {}),
14
+ ...(body.budget !== undefined ? { budget: body.budget } : {}),
15
+ };
16
+ }
17
+ function handleGoalError(c, error) {
18
+ if (error instanceof InvalidGoalStatusError || error instanceof InvalidGoalTransitionError) {
19
+ return c.json({ error: 'invalid_argument' }, 400);
20
+ }
21
+ if (error instanceof InvalidVerificationSurfaceError) {
22
+ return c.json({ error: 'invalid_argument' }, 400);
23
+ }
24
+ if (error instanceof GoalVerificationRequiredError) {
25
+ return c.json({
26
+ error: 'verification_required',
27
+ required_kind: error.requiredKind,
28
+ }, 400);
29
+ }
30
+ if (error instanceof GoalCompletionBlockedError) {
31
+ return c.json({
32
+ error: 'blocked_by_incomplete_tasks',
33
+ incomplete_task_ids: error.incompleteTaskIds,
34
+ }, 400);
35
+ }
36
+ throw error;
37
+ }
38
+ export function createGoalsRouter(goalService) {
39
+ const router = new Hono();
40
+ router.post('/projects/:id/goals', async (c) => {
41
+ const body = await c.req.json();
42
+ if (typeof body.objective !== 'string' || body.objective.trim() === '') {
43
+ return c.json({ error: 'invalid_argument' }, 400);
44
+ }
45
+ if (body.status !== undefined && !isGoalStatus(body.status)) {
46
+ return c.json({ error: 'invalid_argument' }, 400);
47
+ }
48
+ try {
49
+ const goal = goalService.create(c.req.param('id'), {
50
+ objective: body.objective,
51
+ ...(body.status !== undefined ? { status: body.status } : {}),
52
+ ...mapGoalInput(body),
53
+ });
54
+ if (!goal) {
55
+ return c.json({ error: 'not_found' }, 404);
56
+ }
57
+ return c.json(goal, 201);
58
+ }
59
+ catch (error) {
60
+ return handleGoalError(c, error);
61
+ }
62
+ });
63
+ router.get('/projects/:id/goals', (c) => {
64
+ const goals = goalService.listByProject(c.req.param('id'));
65
+ if (!goals) {
66
+ return c.json({ error: 'not_found' }, 404);
67
+ }
68
+ return c.json(goals);
69
+ });
70
+ router.get('/goals/:id', (c) => {
71
+ const goal = goalService.get(c.req.param('id'));
72
+ if (!goal) {
73
+ return c.json({ error: 'not_found' }, 404);
74
+ }
75
+ return c.json(goal);
76
+ });
77
+ router.patch('/goals/:id', async (c) => {
78
+ const body = await c.req.json();
79
+ if (body.objective !== undefined && body.objective.trim() === '') {
80
+ return c.json({ error: 'invalid_argument' }, 400);
81
+ }
82
+ try {
83
+ const goal = goalService.update(c.req.param('id'), mapGoalInput(body));
84
+ if (!goal) {
85
+ return c.json({ error: 'not_found' }, 404);
86
+ }
87
+ return c.json(goal);
88
+ }
89
+ catch (error) {
90
+ return handleGoalError(c, error);
91
+ }
92
+ });
93
+ router.post('/goals/:id/pause', (c) => {
94
+ try {
95
+ const goal = goalService.pause(c.req.param('id'));
96
+ if (!goal) {
97
+ return c.json({ error: 'not_found' }, 404);
98
+ }
99
+ return c.json(goal);
100
+ }
101
+ catch (error) {
102
+ return handleGoalError(c, error);
103
+ }
104
+ });
105
+ router.post('/goals/:id/resume', (c) => {
106
+ try {
107
+ const goal = goalService.resume(c.req.param('id'));
108
+ if (!goal) {
109
+ return c.json({ error: 'not_found' }, 404);
110
+ }
111
+ return c.json(goal);
112
+ }
113
+ catch (error) {
114
+ return handleGoalError(c, error);
115
+ }
116
+ });
117
+ router.post('/goals/:id/complete', async (c) => {
118
+ let body = {};
119
+ try {
120
+ body = await c.req.json();
121
+ }
122
+ catch {
123
+ // empty body is valid when the goal has no verification_surface
124
+ }
125
+ try {
126
+ const goal = goalService.complete(c.req.param('id'), body.evidence);
127
+ if (!goal) {
128
+ return c.json({ error: 'not_found' }, 404);
129
+ }
130
+ return c.json(goal);
131
+ }
132
+ catch (error) {
133
+ return handleGoalError(c, error);
134
+ }
135
+ });
136
+ return router;
137
+ }
138
+ //# sourceMappingURL=goals.js.map
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { ProjectService } from '../services/projects.js';
3
- import type { TaskService } from '../services/tasks.js';
3
+ import { type TaskService } from '../services/tasks.js';
4
4
  export declare function createProjectsRouter(projectService: ProjectService, taskService: TaskService): Hono;
5
5
  //# sourceMappingURL=projects.d.ts.map
@@ -1,5 +1,8 @@
1
1
  import { Hono } from 'hono';
2
2
  import { InvalidTaskStatusError, isTaskStatus } from '@plandesk/db';
3
+ import { InvalidGoalReferenceError } from '../services/tasks.js';
4
+ import { InvalidTagError } from '../services/tags.js';
5
+ import { isStringArray } from './tasks.js';
3
6
  import { parsePaginationParams } from '../serialize.js';
4
7
  export function createProjectsRouter(projectService, taskService) {
5
8
  const router = new Hono();
@@ -57,6 +60,9 @@ export function createProjectsRouter(projectService, taskService) {
57
60
  if (body.status !== undefined && !isTaskStatus(body.status)) {
58
61
  return c.json({ error: 'invalid_argument' }, 400);
59
62
  }
63
+ if (body.tags !== undefined && !isStringArray(body.tags)) {
64
+ return c.json({ error: 'invalid_argument' }, 400);
65
+ }
60
66
  let dueDate;
61
67
  if (body.due_date !== undefined && body.due_date !== null) {
62
68
  dueDate = new Date(body.due_date);
@@ -76,6 +82,8 @@ export function createProjectsRouter(projectService, taskService) {
76
82
  ...(body.y !== undefined ? { y: body.y } : {}),
77
83
  ...(body.assignee !== undefined ? { assignee: body.assignee } : {}),
78
84
  ...(dueDate !== undefined ? { dueDate } : {}),
85
+ ...(body.goal_id !== undefined ? { goalId: body.goal_id } : {}),
86
+ ...(body.tags !== undefined ? { tags: body.tags } : {}),
79
87
  });
80
88
  if (!task) {
81
89
  return c.json({ error: 'not_found' }, 404);
@@ -83,7 +91,9 @@ export function createProjectsRouter(projectService, taskService) {
83
91
  return c.json(task, 201);
84
92
  }
85
93
  catch (error) {
86
- if (error instanceof InvalidTaskStatusError) {
94
+ if (error instanceof InvalidTaskStatusError ||
95
+ error instanceof InvalidTagError ||
96
+ error instanceof InvalidGoalReferenceError) {
87
97
  return c.json({ error: 'invalid_argument' }, 400);
88
98
  }
89
99
  throw error;
@@ -96,19 +106,29 @@ export function createProjectsRouter(projectService, taskService) {
96
106
  return c.json({ error: 'invalid_argument' }, 400);
97
107
  }
98
108
  const status = c.req.query('status');
99
- const tasks = taskService.listByProject(c.req.param('id'), { status }, pagination);
109
+ // Repeated ?tag= params filter with OR semantics (task matches if it has
110
+ // ANY of the given tags).
111
+ const tags = c.req.queries('tag');
112
+ const tasks = taskService.listByProject(c.req.param('id'), { status, ...(tags !== undefined && tags.length > 0 ? { tags } : {}) }, pagination);
100
113
  if (!tasks) {
101
114
  return c.json({ error: 'not_found' }, 404);
102
115
  }
103
116
  return c.json(tasks);
104
117
  }
105
118
  catch (error) {
106
- if (error instanceof InvalidTaskStatusError) {
119
+ if (error instanceof InvalidTaskStatusError || error instanceof InvalidTagError) {
107
120
  return c.json({ error: 'invalid_argument' }, 400);
108
121
  }
109
122
  throw error;
110
123
  }
111
124
  });
125
+ router.get('/projects/:id/next-task', (c) => {
126
+ const result = taskService.nextActionable(c.req.param('id'));
127
+ if (!result) {
128
+ return c.json({ error: 'not_found' }, 404);
129
+ }
130
+ return c.json(result);
131
+ });
112
132
  return router;
113
133
  }
114
134
  //# sourceMappingURL=projects.js.map
@@ -0,0 +1,5 @@
1
+ import { Hono } from 'hono';
2
+ import { type SyncService } from '../services/sync.js';
3
+ import type { ProjectService } from '../services/projects.js';
4
+ export declare function createSubmissionsRouter(syncService: SyncService, projectService: ProjectService): Hono;
5
+ //# sourceMappingURL=submissions.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { Hono } from 'hono';
2
+ import { shareSubmissionStatuses } from '@plandesk/db';
3
+ import { InvalidTriageError, SyncUnauthorizedError, SyncUnavailableError, } from '../services/sync.js';
4
+ function isSubmissionStatus(value) {
5
+ return shareSubmissionStatuses.includes(value);
6
+ }
7
+ export function createSubmissionsRouter(syncService, projectService) {
8
+ const router = new Hono();
9
+ router.get('/projects/:id/submissions', (c) => {
10
+ const projectId = c.req.param('id');
11
+ if (!projectService.get(projectId)) {
12
+ return c.json({ error: 'not_found' }, 404);
13
+ }
14
+ const statusParam = c.req.query('status') ?? 'pending';
15
+ if (!isSubmissionStatus(statusParam)) {
16
+ return c.json({ error: 'invalid_argument' }, 400);
17
+ }
18
+ return c.json(syncService.listTriage(projectId, statusParam));
19
+ });
20
+ router.post('/submissions/:id/triage', async (c) => {
21
+ const submissionId = c.req.param('id');
22
+ const body = await c.req.json();
23
+ if (body.action !== 'accept' && body.action !== 'reject') {
24
+ return c.json({ error: 'invalid_argument' }, 400);
25
+ }
26
+ const submission = syncService.getSubmission(submissionId);
27
+ if (submission === undefined) {
28
+ return c.json({ error: 'not_found' }, 404);
29
+ }
30
+ const remote = syncService.getRemote(submission.project_id);
31
+ if (remote === undefined) {
32
+ return c.json({ error: 'not_published' }, 400);
33
+ }
34
+ try {
35
+ // Triage never creates a `todo` task — the scope->todo release is the human's own
36
+ // board action, enforced in syncService.triage(). A merge (link_task_id) links to
37
+ // an existing task instead of creating one; the two are mutually exclusive, so
38
+ // only one is forwarded.
39
+ const linkTaskId = body.action === 'accept' ? body.link_task_id : undefined;
40
+ const result = await syncService.triage(submissionId, body.action, remote, body.action === 'accept' && linkTaskId === undefined ? body.as_task : undefined, linkTaskId);
41
+ return c.json(result);
42
+ }
43
+ catch (error) {
44
+ if (error instanceof InvalidTriageError) {
45
+ return c.json({ error: 'not_found' }, 404);
46
+ }
47
+ if (error instanceof SyncUnauthorizedError) {
48
+ return c.json({ error: 'sync_unauthorized' }, 400);
49
+ }
50
+ if (error instanceof SyncUnavailableError) {
51
+ return c.json({ error: 'sync_unavailable' }, 502);
52
+ }
53
+ throw error;
54
+ }
55
+ });
56
+ return router;
57
+ }
58
+ //# sourceMappingURL=submissions.js.map
@@ -0,0 +1,4 @@
1
+ import { Hono } from 'hono';
2
+ import { type TagService } from '../services/tags.js';
3
+ export declare function createTagsRouter(tagService: TagService): Hono;
4
+ //# sourceMappingURL=tags.d.ts.map
@@ -0,0 +1,62 @@
1
+ import { Hono } from 'hono';
2
+ import { InvalidTagError } from '../services/tags.js';
3
+ export function createTagsRouter(tagService) {
4
+ const router = new Hono();
5
+ router.get('/projects/:id/tags', (c) => {
6
+ const tags = tagService.list(c.req.param('id'));
7
+ if (!tags) {
8
+ return c.json({ error: 'not_found' }, 404);
9
+ }
10
+ return c.json(tags);
11
+ });
12
+ router.post('/projects/:id/tags', async (c) => {
13
+ const body = await c.req.json();
14
+ if (typeof body.name !== 'string' || body.name.trim() === '') {
15
+ return c.json({ error: 'invalid_argument' }, 400);
16
+ }
17
+ try {
18
+ const tag = tagService.create(c.req.param('id'), {
19
+ name: body.name,
20
+ color: body.color,
21
+ });
22
+ if (!tag) {
23
+ return c.json({ error: 'not_found' }, 404);
24
+ }
25
+ return c.json(tag, 201);
26
+ }
27
+ catch (error) {
28
+ if (error instanceof InvalidTagError) {
29
+ return c.json({ error: 'invalid_argument' }, 400);
30
+ }
31
+ throw error;
32
+ }
33
+ });
34
+ router.patch('/tags/:id', async (c) => {
35
+ const body = await c.req.json();
36
+ try {
37
+ const tag = tagService.update(c.req.param('id'), {
38
+ ...(body.name !== undefined ? { name: body.name } : {}),
39
+ ...(body.color !== undefined ? { color: body.color } : {}),
40
+ });
41
+ if (!tag) {
42
+ return c.json({ error: 'not_found' }, 404);
43
+ }
44
+ return c.json(tag);
45
+ }
46
+ catch (error) {
47
+ if (error instanceof InvalidTagError) {
48
+ return c.json({ error: 'invalid_argument' }, 400);
49
+ }
50
+ throw error;
51
+ }
52
+ });
53
+ router.delete('/tags/:id', (c) => {
54
+ const deleted = tagService.delete(c.req.param('id'));
55
+ if (!deleted) {
56
+ return c.json({ error: 'not_found' }, 404);
57
+ }
58
+ return c.body(null, 204);
59
+ });
60
+ return router;
61
+ }
62
+ //# sourceMappingURL=tags.js.map
@@ -1,4 +1,5 @@
1
1
  import { Hono } from 'hono';
2
2
  import type { TaskService } from '../services/tasks.js';
3
+ export declare function isStringArray(value: unknown): value is string[];
3
4
  export declare function createTasksRouter(taskService: TaskService): Hono;
4
5
  //# sourceMappingURL=tasks.d.ts.map
@@ -1,5 +1,9 @@
1
1
  import { Hono } from 'hono';
2
2
  import { InvalidTaskStatusError, isTaskStatus } from '@plandesk/db';
3
+ import { InvalidTagError } from '../services/tags.js';
4
+ export function isStringArray(value) {
5
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
6
+ }
3
7
  export function createTasksRouter(taskService) {
4
8
  const router = new Hono();
5
9
  router.patch('/tasks/:id', async (c) => {
@@ -7,6 +11,9 @@ export function createTasksRouter(taskService) {
7
11
  if (body.status !== undefined && !isTaskStatus(body.status)) {
8
12
  return c.json({ error: 'invalid_argument' }, 400);
9
13
  }
14
+ if (body.tags !== undefined && !isStringArray(body.tags)) {
15
+ return c.json({ error: 'invalid_argument' }, 400);
16
+ }
10
17
  try {
11
18
  const task = taskService.update(c.req.param('id'), {
12
19
  ...(body.label !== undefined ? { label: body.label } : {}),
@@ -14,6 +21,7 @@ export function createTasksRouter(taskService) {
14
21
  ...(body.description !== undefined ? { description: body.description } : {}),
15
22
  ...(body.x !== undefined ? { x: body.x } : {}),
16
23
  ...(body.y !== undefined ? { y: body.y } : {}),
24
+ ...(body.tags !== undefined ? { tags: body.tags } : {}),
17
25
  });
18
26
  if (!task) {
19
27
  return c.json({ error: 'not_found' }, 404);
@@ -21,7 +29,7 @@ export function createTasksRouter(taskService) {
21
29
  return c.json(task);
22
30
  }
23
31
  catch (error) {
24
- if (error instanceof InvalidTaskStatusError) {
32
+ if (error instanceof InvalidTaskStatusError || error instanceof InvalidTagError) {
25
33
  return c.json({ error: 'invalid_argument' }, 400);
26
34
  }
27
35
  throw error;