@plandesk/api 0.8.0 → 0.10.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.
package/dist/events.d.ts CHANGED
@@ -15,14 +15,18 @@ export type DocumentCreatedEvent = {
15
15
  export type CommentCreatedEvent = {
16
16
  type: 'comment_created';
17
17
  commentId: string;
18
- documentId: string;
19
18
  projectId: string;
19
+ target_type: string;
20
+ target_id: string;
21
+ documentId?: string;
20
22
  };
21
23
  export type CommentUpdatedEvent = {
22
24
  type: 'comment_updated';
23
25
  commentId: string;
24
- documentId: string;
25
26
  projectId: string;
27
+ target_type: string;
28
+ target_id: string;
29
+ documentId?: string;
26
30
  };
27
31
  export type FolderCreatedEvent = {
28
32
  type: 'folder_created';
@@ -67,7 +71,12 @@ export type SubmissionsPulledEvent = {
67
71
  type: 'submissions_pulled';
68
72
  projectId: string;
69
73
  };
70
- export type PlankDeskEvent = TaskUpdatedEvent | TagUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | FolderCreatedEvent | FolderUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent;
74
+ export type GoalUpdatedEvent = {
75
+ type: 'goal_updated';
76
+ goalId: string;
77
+ projectId: string;
78
+ };
79
+ export type PlankDeskEvent = TaskUpdatedEvent | TagUpdatedEvent | CanvasUpdatedEvent | DocumentCreatedEvent | CommentCreatedEvent | CommentUpdatedEvent | FolderCreatedEvent | FolderUpdatedEvent | NoteCreatedEvent | NoteUpdatedEvent | AgentRunStartedEvent | AgentRunProgressEvent | AgentRunCompletedEvent | SubmissionsPulledEvent | GoalUpdatedEvent;
71
80
  export type EventListener = (event: PlankDeskEvent) => void;
72
81
  export type EventBus = {
73
82
  subscribe(listener: EventListener): () => void;
package/dist/index.d.ts CHANGED
@@ -5,7 +5,10 @@ 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';
9
12
  export type { TagService } from './services/tags.js';
10
13
  export type { CanvasService } from './services/canvas.js';
11
14
  export type { DocumentService } from './services/documents.js';
@@ -22,6 +25,6 @@ export { InvalidCanvasError } from './services/canvas.js';
22
25
  export { InvalidAgentRunError } from './services/agent-runs.js';
23
26
  export { InvalidScaffoldError } from './services/projects.js';
24
27
  export { InvalidShareError } from './services/share.js';
25
- 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';
26
29
  export declare const version: () => string;
27
30
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -4,6 +4,8 @@ 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';
8
10
  export { InvalidFolderError } from './services/folders.js';
9
11
  export { InvalidNoteError } from './services/notes.js';
@@ -13,7 +15,7 @@ export { InvalidCanvasError } from './services/canvas.js';
13
15
  export { InvalidAgentRunError } from './services/agent-runs.js';
14
16
  export { InvalidScaffoldError } from './services/projects.js';
15
17
  export { InvalidShareError } from './services/share.js';
16
- export { InvalidTriageError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
18
+ export { InvalidTriageError, InvalidTriageInputError, SyncUnavailableError, SyncUnauthorizedError, } from './services/sync.js';
17
19
  import { readFileSync } from 'node:fs';
18
20
  import { dirname, join } from 'node:path';
19
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
@@ -1,40 +1,48 @@
1
1
  import { Hono } from 'hono';
2
- import { InvalidCommentError } from '../services/comments.js';
2
+ import { InvalidCommentError, } from '../services/comments.js';
3
3
  function parseIncludeResolved(value) {
4
4
  return value === 'true';
5
5
  }
6
- export function createCommentsRouter(commentService) {
7
- const router = new Hono();
8
- router.post('/documents/:id/comments', async (c) => {
9
- const body = await c.req.json();
10
- if (typeof body.body !== 'string') {
11
- return c.json({ error: 'invalid_argument' }, 400);
12
- }
13
- try {
14
- const comment = commentService.create(c.req.param('id'), {
15
- body: body.body,
16
- passage: body.passage,
17
- });
18
- if (!comment) {
19
- return c.json({ error: 'not_found' }, 404);
20
- }
21
- return c.json(comment, 201);
22
- }
23
- catch (error) {
24
- if (error instanceof InvalidCommentError) {
25
- return c.json({ error: 'invalid_argument' }, 400);
26
- }
27
- throw error;
28
- }
29
- });
30
- router.get('/documents/:id/comments', (c) => {
31
- const includeResolved = parseIncludeResolved(c.req.query('include_resolved'));
32
- const comments = commentService.listByDocument(c.req.param('id'), { includeResolved });
33
- if (!comments) {
6
+ async function handleCreateComment(c, commentService, target) {
7
+ const body = await c.req.json();
8
+ if (typeof body.body !== 'string') {
9
+ return c.json({ error: 'invalid_argument' }, 400);
10
+ }
11
+ try {
12
+ const comment = commentService.create(target, {
13
+ body: body.body,
14
+ passage: body.passage,
15
+ });
16
+ if (!comment) {
34
17
  return c.json({ error: 'not_found' }, 404);
35
18
  }
36
- return c.json(comments);
37
- });
19
+ return c.json(comment, 201);
20
+ }
21
+ catch (error) {
22
+ if (error instanceof InvalidCommentError) {
23
+ return c.json({ error: 'invalid_argument' }, 400);
24
+ }
25
+ throw error;
26
+ }
27
+ }
28
+ function handleListComments(c, commentService, target) {
29
+ const includeResolved = parseIncludeResolved(c.req.query('include_resolved'));
30
+ const comments = commentService.listByTarget(target, { includeResolved });
31
+ if (!comments) {
32
+ return c.json({ error: 'not_found' }, 404);
33
+ }
34
+ return c.json(comments);
35
+ }
36
+ export function createCommentsRouter(commentService) {
37
+ const router = new Hono();
38
+ router.post('/documents/:id/comments', (c) => handleCreateComment(c, commentService, { type: 'document', id: c.req.param('id') }));
39
+ router.get('/documents/:id/comments', (c) => handleListComments(c, commentService, { type: 'document', id: c.req.param('id') }));
40
+ router.post('/tasks/:id/comments', (c) => handleCreateComment(c, commentService, { type: 'task', id: c.req.param('id') }));
41
+ router.get('/tasks/:id/comments', (c) => handleListComments(c, commentService, { type: 'task', id: c.req.param('id') }));
42
+ router.post('/notes/:id/comments', (c) => handleCreateComment(c, commentService, { type: 'note', id: c.req.param('id') }));
43
+ router.get('/notes/:id/comments', (c) => handleListComments(c, commentService, { type: 'note', id: c.req.param('id') }));
44
+ router.post('/submissions/:id/comments', (c) => handleCreateComment(c, commentService, { type: 'submission', id: c.req.param('id') }));
45
+ router.get('/submissions/:id/comments', (c) => handleListComments(c, commentService, { type: 'submission', id: c.req.param('id') }));
38
46
  router.get('/projects/:id/comments', (c) => {
39
47
  const includeResolved = parseIncludeResolved(c.req.query('include_resolved'));
40
48
  const comments = commentService.listByProject(c.req.param('id'), { includeResolved });
@@ -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,6 @@
1
1
  import { Hono } from 'hono';
2
2
  import { InvalidTaskStatusError, isTaskStatus } from '@plandesk/db';
3
+ import { InvalidGoalReferenceError } from '../services/tasks.js';
3
4
  import { InvalidTagError } from '../services/tags.js';
4
5
  import { isStringArray } from './tasks.js';
5
6
  import { parsePaginationParams } from '../serialize.js';
@@ -81,6 +82,7 @@ export function createProjectsRouter(projectService, taskService) {
81
82
  ...(body.y !== undefined ? { y: body.y } : {}),
82
83
  ...(body.assignee !== undefined ? { assignee: body.assignee } : {}),
83
84
  ...(dueDate !== undefined ? { dueDate } : {}),
85
+ ...(body.goal_id !== undefined ? { goalId: body.goal_id } : {}),
84
86
  ...(body.tags !== undefined ? { tags: body.tags } : {}),
85
87
  });
86
88
  if (!task) {
@@ -89,7 +91,9 @@ export function createProjectsRouter(projectService, taskService) {
89
91
  return c.json(task, 201);
90
92
  }
91
93
  catch (error) {
92
- if (error instanceof InvalidTaskStatusError || error instanceof InvalidTagError) {
94
+ if (error instanceof InvalidTaskStatusError ||
95
+ error instanceof InvalidTagError ||
96
+ error instanceof InvalidGoalReferenceError) {
93
97
  return c.json({ error: 'invalid_argument' }, 400);
94
98
  }
95
99
  throw error;
@@ -118,6 +122,13 @@ export function createProjectsRouter(projectService, taskService) {
118
122
  throw error;
119
123
  }
120
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
+ });
121
132
  return router;
122
133
  }
123
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
@@ -1,4 +1,4 @@
1
- import type { AgentRun, AgentRunEvent, Document, DocumentComment, Edge, Folder, Note, Project, Tag, Task, TaskStatus } from '@plandesk/db';
1
+ import type { AgentRun, AgentRunEvent, Document, Comment, Edge, Folder, Goal, Note, Project, Tag, Task, TaskStatus } from '@plandesk/db';
2
2
  export type PaginationParams = {
3
3
  limit?: number;
4
4
  offset?: number;
@@ -29,10 +29,31 @@ export type SerializedTag = {
29
29
  created_at: string;
30
30
  };
31
31
  export declare function serializeTag(tag: Tag): SerializedTag;
32
+ export declare function serializeGoal(goal: Goal): {
33
+ id: string;
34
+ project_id: string;
35
+ objective: string;
36
+ status: "active" | "paused" | "complete" | "blocked";
37
+ verification_surface: string | null;
38
+ constraints: string | null;
39
+ boundaries: string | null;
40
+ iteration_policy: string | null;
41
+ stop_condition: string | null;
42
+ budget: string | null;
43
+ last_verification: {
44
+ at: string;
45
+ green: boolean;
46
+ kind: string | null;
47
+ detail?: string;
48
+ } | null;
49
+ created_at: string;
50
+ updated_at: string;
51
+ };
32
52
  export declare function serializeTask(task: Task, tags?: Tag[]): {
33
53
  tags?: SerializedTag[] | undefined;
34
54
  id: string;
35
55
  project_id: string;
56
+ goal_id: string;
36
57
  label: string;
37
58
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
38
59
  description: string | null;
@@ -70,13 +91,15 @@ export type SerializedNote = {
70
91
  export declare function serializeNote(note: Note): SerializedNote;
71
92
  export type SerializedComment = {
72
93
  id: string;
73
- document_id: string;
94
+ target_type: Comment['targetType'];
95
+ target_id: string;
96
+ document_id: string | null;
74
97
  passage: string | null;
75
98
  body: string;
76
99
  resolved: boolean;
77
100
  created_at: string;
78
101
  };
79
- export declare function serializeComment(comment: DocumentComment): SerializedComment;
102
+ export declare function serializeComment(comment: Comment): SerializedComment;
80
103
  export declare function buildDocumentTree(documents: Document[]): SerializedDocumentTree[];
81
104
  export type SerializedFolder = {
82
105
  id: string;
package/dist/serialize.js CHANGED
@@ -45,10 +45,45 @@ export function serializeTag(tag) {
45
45
  created_at: tag.createdAt.toISOString(),
46
46
  };
47
47
  }
48
+ function parseLastVerification(raw) {
49
+ if (raw === null) {
50
+ return null;
51
+ }
52
+ try {
53
+ const parsed = JSON.parse(raw);
54
+ if (typeof parsed.at === 'string' &&
55
+ typeof parsed.green === 'boolean' &&
56
+ (parsed.kind === null || typeof parsed.kind === 'string')) {
57
+ return parsed;
58
+ }
59
+ return null;
60
+ }
61
+ catch {
62
+ return null;
63
+ }
64
+ }
65
+ export function serializeGoal(goal) {
66
+ return {
67
+ id: goal.id,
68
+ project_id: goal.projectId,
69
+ objective: goal.objective,
70
+ status: goal.status,
71
+ verification_surface: goal.verificationSurface,
72
+ constraints: goal.constraints,
73
+ boundaries: goal.boundaries,
74
+ iteration_policy: goal.iterationPolicy,
75
+ stop_condition: goal.stopCondition,
76
+ budget: goal.budget,
77
+ last_verification: parseLastVerification(goal.lastVerification),
78
+ created_at: goal.createdAt.toISOString(),
79
+ updated_at: goal.updatedAt.toISOString(),
80
+ };
81
+ }
48
82
  export function serializeTask(task, tags) {
49
83
  return {
50
84
  id: task.id,
51
85
  project_id: task.projectId,
86
+ goal_id: task.goalId,
52
87
  label: task.label,
53
88
  status: task.status,
54
89
  description: task.description,
@@ -88,7 +123,9 @@ export function serializeNote(note) {
88
123
  export function serializeComment(comment) {
89
124
  return {
90
125
  id: comment.id,
91
- document_id: comment.documentId,
126
+ target_type: comment.targetType,
127
+ target_id: comment.targetId,
128
+ document_id: comment.targetType === 'document' ? comment.targetId : null,
92
129
  passage: comment.passage,
93
130
  body: comment.body,
94
131
  resolved: comment.resolved,
package/dist/server.js CHANGED
@@ -12,17 +12,20 @@ import { createNotesRouter } from './routes/notes.js';
12
12
  import { createEventsRouter } from './routes/events.js';
13
13
  import { createTokensRouter } from './routes/tokens.js';
14
14
  import { createAgentRunsRouter } from './routes/agent-runs.js';
15
+ import { createGoalsRouter } from './routes/goals.js';
16
+ import { createSubmissionsRouter } from './routes/submissions.js';
15
17
  import { mountStatic } from './static.js';
16
18
  import { createServices } from './services/index.js';
17
19
  export function createApp(deps) {
18
20
  const services = deps.services ?? createServices({ db: deps.db, eventBus: deps.eventBus });
19
- const { eventBus, projectService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, } = services;
21
+ const { eventBus, projectService, goalService, taskService, tagService, canvasService, documentService, folderService, noteService, commentService, agentRunService, tokenService, syncService, } = services;
20
22
  const app = new Hono();
21
23
  if (deps.authPassword !== undefined && deps.authPassword.length > 0) {
22
24
  app.use('*', createAuthMiddleware(deps.authPassword));
23
25
  }
24
26
  app.route('/api/v1', healthRouter);
25
27
  app.route('/api/v1', createProjectsRouter(projectService, taskService));
28
+ app.route('/api/v1', createGoalsRouter(goalService));
26
29
  app.route('/api/v1', createTasksRouter(taskService));
27
30
  app.route('/api/v1', createTagsRouter(tagService));
28
31
  app.route('/api/v1', createCanvasRouter(canvasService));
@@ -32,6 +35,7 @@ export function createApp(deps) {
32
35
  app.route('/api/v1', createCommentsRouter(commentService));
33
36
  app.route('/api/v1', createTokensRouter(tokenService));
34
37
  app.route('/api/v1', createAgentRunsRouter(agentRunService));
38
+ app.route('/api/v1', createSubmissionsRouter(syncService, projectService));
35
39
  app.route('/api/v1', createEventsRouter(eventBus));
36
40
  mountStatic(app);
37
41
  if (deps.mcp) {
@@ -33,6 +33,7 @@ export declare function createCanvasService(deps: CanvasServiceDeps): {
33
33
  tags?: import("../serialize.js").SerializedTag[] | undefined;
34
34
  id: string;
35
35
  project_id: string;
36
+ goal_id: string;
36
37
  label: string;
37
38
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
38
39
  description: string | null;
@@ -75,6 +76,7 @@ export declare function createCanvasService(deps: CanvasServiceDeps): {
75
76
  tags?: import("../serialize.js").SerializedTag[] | undefined;
76
77
  id: string;
77
78
  project_id: string;
79
+ goal_id: string;
78
80
  label: string;
79
81
  status: "scope" | "todo" | "in_progress" | "done" | "backlog";
80
82
  description: string | null;
@@ -1,4 +1,4 @@
1
- import { createEdge, createTask, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
1
+ import { createEdge, createTask, getOrCreateDefaultGoal, deleteEdge as dbDeleteEdge, getEdgeByProjectAndId, getProject, getTask, listEdges, listTasks, updateEdge, updateProject, updateTask, } from '@plandesk/db';
2
2
  import { serializeEdge, serializeTask } from '../serialize.js';
3
3
  export class InvalidCanvasError extends Error {
4
4
  constructor(message) {
@@ -93,6 +93,7 @@ export function createCanvasService(deps) {
93
93
  }
94
94
  const created = createTask(tx, {
95
95
  projectId,
96
+ goalId: getOrCreateDefaultGoal(tx, projectId).id,
96
97
  id: node.id,
97
98
  label: node.label,
98
99
  status: 'todo',
@@ -1,10 +1,14 @@
1
- import { type Db } from '@plandesk/db';
1
+ import { type CommentTargetType, type Db } from '@plandesk/db';
2
2
  import type { EventBus } from '../events.js';
3
3
  import { type SerializedComment } from '../serialize.js';
4
4
  export type CommentServiceDeps = {
5
5
  db: Db;
6
6
  eventBus: EventBus;
7
7
  };
8
+ export type CommentTarget = {
9
+ type: CommentTargetType;
10
+ id: string;
11
+ };
8
12
  export type CreateCommentInput = {
9
13
  body: string;
10
14
  passage?: string | null;
@@ -17,7 +21,11 @@ export declare class InvalidCommentError extends Error {
17
21
  constructor(message: string);
18
22
  }
19
23
  export declare function createCommentService(deps: CommentServiceDeps): {
20
- create(documentId: string, input: CreateCommentInput): SerializedComment | undefined;
24
+ create(target: CommentTarget, input: CreateCommentInput): SerializedComment | undefined;
25
+ listByTarget(target: CommentTarget, options?: {
26
+ includeResolved?: boolean;
27
+ }): SerializedComment[] | undefined;
28
+ resolveTargetProjectId(target: CommentTarget): string | undefined;
21
29
  listByDocument(documentId: string, options?: {
22
30
  includeResolved?: boolean;
23
31
  }): SerializedComment[] | undefined;