@vibescope/mcp-server 0.0.1 → 0.2.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/README.md +113 -98
- package/dist/api-client.d.ts +1169 -0
- package/dist/api-client.js +713 -0
- package/dist/cli.d.ts +1 -6
- package/dist/cli.js +39 -240
- package/dist/config/tool-categories.d.ts +31 -0
- package/dist/config/tool-categories.js +253 -0
- package/dist/handlers/blockers.js +57 -58
- package/dist/handlers/bodies-of-work.d.ts +2 -0
- package/dist/handlers/bodies-of-work.js +108 -477
- package/dist/handlers/cost.d.ts +1 -0
- package/dist/handlers/cost.js +35 -113
- package/dist/handlers/decisions.d.ts +2 -0
- package/dist/handlers/decisions.js +28 -27
- package/dist/handlers/deployment.js +113 -828
- package/dist/handlers/discovery.d.ts +3 -0
- package/dist/handlers/discovery.js +26 -627
- package/dist/handlers/fallback.d.ts +2 -0
- package/dist/handlers/fallback.js +56 -142
- package/dist/handlers/findings.d.ts +8 -1
- package/dist/handlers/findings.js +65 -68
- package/dist/handlers/git-issues.d.ts +9 -13
- package/dist/handlers/git-issues.js +80 -225
- package/dist/handlers/ideas.d.ts +3 -0
- package/dist/handlers/ideas.js +53 -134
- package/dist/handlers/index.d.ts +2 -0
- package/dist/handlers/index.js +6 -0
- package/dist/handlers/milestones.d.ts +2 -0
- package/dist/handlers/milestones.js +51 -98
- package/dist/handlers/organizations.js +79 -275
- package/dist/handlers/progress.d.ts +2 -0
- package/dist/handlers/progress.js +25 -123
- package/dist/handlers/project.js +42 -221
- package/dist/handlers/requests.d.ts +2 -0
- package/dist/handlers/requests.js +23 -83
- package/dist/handlers/session.js +119 -590
- package/dist/handlers/sprints.d.ts +32 -0
- package/dist/handlers/sprints.js +275 -0
- package/dist/handlers/tasks.d.ts +7 -10
- package/dist/handlers/tasks.js +245 -894
- package/dist/handlers/tool-docs.d.ts +9 -0
- package/dist/handlers/tool-docs.js +904 -0
- package/dist/handlers/types.d.ts +11 -3
- package/dist/handlers/validation.d.ts +1 -1
- package/dist/handlers/validation.js +38 -153
- package/dist/index.js +493 -162
- package/dist/knowledge.js +106 -9
- package/dist/tools.js +34 -4
- package/dist/validators.d.ts +21 -0
- package/dist/validators.js +91 -0
- package/package.json +2 -3
- package/src/api-client.ts +1822 -0
- package/src/cli.test.ts +128 -302
- package/src/cli.ts +41 -285
- package/src/handlers/__test-setup__.ts +215 -0
- package/src/handlers/__test-utils__.ts +4 -134
- package/src/handlers/blockers.test.ts +114 -124
- package/src/handlers/blockers.ts +68 -70
- package/src/handlers/bodies-of-work.test.ts +236 -831
- package/src/handlers/bodies-of-work.ts +210 -525
- package/src/handlers/cost.test.ts +149 -113
- package/src/handlers/cost.ts +44 -132
- package/src/handlers/decisions.test.ts +111 -209
- package/src/handlers/decisions.ts +35 -27
- package/src/handlers/deployment.test.ts +193 -239
- package/src/handlers/deployment.ts +143 -896
- package/src/handlers/discovery.test.ts +20 -67
- package/src/handlers/discovery.ts +29 -714
- package/src/handlers/fallback.test.ts +206 -361
- package/src/handlers/fallback.ts +81 -156
- package/src/handlers/findings.test.ts +229 -320
- package/src/handlers/findings.ts +76 -64
- package/src/handlers/git-issues.test.ts +623 -0
- package/src/handlers/git-issues.ts +174 -0
- package/src/handlers/ideas.test.ts +229 -343
- package/src/handlers/ideas.ts +69 -143
- package/src/handlers/index.ts +6 -0
- package/src/handlers/milestones.test.ts +167 -281
- package/src/handlers/milestones.ts +54 -93
- package/src/handlers/organizations.test.ts +275 -467
- package/src/handlers/organizations.ts +84 -294
- package/src/handlers/progress.test.ts +112 -218
- package/src/handlers/progress.ts +29 -142
- package/src/handlers/project.test.ts +203 -226
- package/src/handlers/project.ts +48 -238
- package/src/handlers/requests.test.ts +74 -342
- package/src/handlers/requests.ts +25 -83
- package/src/handlers/session.test.ts +276 -206
- package/src/handlers/session.ts +136 -662
- package/src/handlers/sprints.test.ts +711 -0
- package/src/handlers/sprints.ts +510 -0
- package/src/handlers/tasks.test.ts +669 -353
- package/src/handlers/tasks.ts +263 -1015
- package/src/handlers/tool-docs.ts +1024 -0
- package/src/handlers/types.ts +12 -4
- package/src/handlers/validation.test.ts +237 -568
- package/src/handlers/validation.ts +43 -167
- package/src/index.ts +493 -186
- package/src/tools.ts +2532 -0
- package/src/validators.test.ts +223 -223
- package/src/validators.ts +127 -0
- package/tsconfig.json +1 -1
- package/vitest.config.ts +14 -13
- package/dist/cli.test.d.ts +0 -1
- package/dist/cli.test.js +0 -367
- package/dist/handlers/__test-utils__.d.ts +0 -72
- package/dist/handlers/__test-utils__.js +0 -176
- package/dist/handlers/checkouts.d.ts +0 -37
- package/dist/handlers/checkouts.js +0 -377
- package/dist/handlers/knowledge-query.d.ts +0 -22
- package/dist/handlers/knowledge-query.js +0 -253
- package/dist/handlers/knowledge.d.ts +0 -12
- package/dist/handlers/knowledge.js +0 -108
- package/dist/handlers/roles.d.ts +0 -30
- package/dist/handlers/roles.js +0 -281
- package/dist/handlers/tasks.test.d.ts +0 -1
- package/dist/handlers/tasks.test.js +0 -431
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -532
- package/dist/validators.test.d.ts +0 -1
- package/dist/validators.test.js +0 -176
- package/src/knowledge.ts +0 -132
- package/src/tmpclaude-0078-cwd +0 -1
- package/src/tmpclaude-0ee1-cwd +0 -1
- package/src/tmpclaude-2dd5-cwd +0 -1
- package/src/tmpclaude-344c-cwd +0 -1
- package/src/tmpclaude-3860-cwd +0 -1
- package/src/tmpclaude-4b63-cwd +0 -1
- package/src/tmpclaude-5c73-cwd +0 -1
- package/src/tmpclaude-5ee3-cwd +0 -1
- package/src/tmpclaude-6795-cwd +0 -1
- package/src/tmpclaude-709e-cwd +0 -1
- package/src/tmpclaude-9839-cwd +0 -1
- package/src/tmpclaude-d829-cwd +0 -1
- package/src/tmpclaude-e072-cwd +0 -1
- package/src/tmpclaude-f6ee-cwd +0 -1
- package/tmpclaude-0439-cwd +0 -1
- package/tmpclaude-132f-cwd +0 -1
- package/tmpclaude-15bb-cwd +0 -1
- package/tmpclaude-165a-cwd +0 -1
- package/tmpclaude-1ba9-cwd +0 -1
- package/tmpclaude-21a3-cwd +0 -1
- package/tmpclaude-2a38-cwd +0 -1
- package/tmpclaude-2adf-cwd +0 -1
- package/tmpclaude-2f56-cwd +0 -1
- package/tmpclaude-3626-cwd +0 -1
- package/tmpclaude-3727-cwd +0 -1
- package/tmpclaude-40bc-cwd +0 -1
- package/tmpclaude-436f-cwd +0 -1
- package/tmpclaude-4783-cwd +0 -1
- package/tmpclaude-4b6d-cwd +0 -1
- package/tmpclaude-4ba4-cwd +0 -1
- package/tmpclaude-51e6-cwd +0 -1
- package/tmpclaude-5ecf-cwd +0 -1
- package/tmpclaude-6f97-cwd +0 -1
- package/tmpclaude-7fb2-cwd +0 -1
- package/tmpclaude-825c-cwd +0 -1
- package/tmpclaude-8baf-cwd +0 -1
- package/tmpclaude-8d9f-cwd +0 -1
- package/tmpclaude-975c-cwd +0 -1
- package/tmpclaude-9983-cwd +0 -1
- package/tmpclaude-a045-cwd +0 -1
- package/tmpclaude-ac4a-cwd +0 -1
- package/tmpclaude-b593-cwd +0 -1
- package/tmpclaude-b891-cwd +0 -1
- package/tmpclaude-c032-cwd +0 -1
- package/tmpclaude-cf43-cwd +0 -1
- package/tmpclaude-d040-cwd +0 -1
- package/tmpclaude-dcdd-cwd +0 -1
- package/tmpclaude-dcee-cwd +0 -1
- package/tmpclaude-e16b-cwd +0 -1
- package/tmpclaude-ecd2-cwd +0 -1
- package/tmpclaude-f48d-cwd +0 -1
|
@@ -14,10 +14,13 @@
|
|
|
14
14
|
* - remove_task_dependency
|
|
15
15
|
* - get_task_dependencies
|
|
16
16
|
* - get_next_body_of_work_task
|
|
17
|
+
*
|
|
18
|
+
* MIGRATED: Uses Vibescope API client instead of direct Supabase
|
|
17
19
|
*/
|
|
18
20
|
|
|
19
21
|
import type { Handler, HandlerRegistry } from './types.js';
|
|
20
22
|
import { validateRequired, validateUUID } from '../validators.js';
|
|
23
|
+
import { getApiClient } from '../api-client.js';
|
|
21
24
|
|
|
22
25
|
type BodyOfWorkStatus = 'draft' | 'active' | 'completed' | 'cancelled';
|
|
23
26
|
type TaskPhase = 'pre' | 'core' | 'post';
|
|
@@ -48,30 +51,34 @@ export const createBodyOfWork: Handler = async (args, ctx) => {
|
|
|
48
51
|
validateUUID(project_id, 'project_id');
|
|
49
52
|
validateRequired(title, 'title');
|
|
50
53
|
|
|
51
|
-
const {
|
|
54
|
+
const { session } = ctx;
|
|
55
|
+
const apiClient = getApiClient();
|
|
52
56
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
.
|
|
67
|
-
.
|
|
68
|
-
|
|
69
|
-
|
|
57
|
+
const response = await apiClient.proxy<{
|
|
58
|
+
success: boolean;
|
|
59
|
+
body_of_work_id: string;
|
|
60
|
+
}>('create_body_of_work', {
|
|
61
|
+
project_id,
|
|
62
|
+
title,
|
|
63
|
+
description,
|
|
64
|
+
auto_deploy_on_completion,
|
|
65
|
+
deploy_environment,
|
|
66
|
+
deploy_version_bump,
|
|
67
|
+
deploy_trigger
|
|
68
|
+
}, {
|
|
69
|
+
session_id: session.currentSessionId,
|
|
70
|
+
persona: session.currentPersona,
|
|
71
|
+
instance_id: session.instanceId
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Failed to create body of work: ${response.error}`);
|
|
76
|
+
}
|
|
70
77
|
|
|
71
78
|
return {
|
|
72
79
|
result: {
|
|
73
80
|
success: true,
|
|
74
|
-
body_of_work_id: data
|
|
81
|
+
body_of_work_id: response.data?.body_of_work_id,
|
|
75
82
|
title,
|
|
76
83
|
status: 'draft',
|
|
77
84
|
message: 'Body of work created. Add tasks with add_task_to_body_of_work, then activate with activate_body_of_work.',
|
|
@@ -101,185 +108,116 @@ export const updateBodyOfWork: Handler = async (args, ctx) => {
|
|
|
101
108
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
102
109
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
103
110
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const updates: Record<string, unknown> = {};
|
|
108
|
-
if (title !== undefined) updates.title = title;
|
|
109
|
-
if (description !== undefined) updates.description = description;
|
|
110
|
-
if (auto_deploy_on_completion !== undefined) updates.auto_deploy_on_completion = auto_deploy_on_completion;
|
|
111
|
-
if (deploy_environment !== undefined) updates.deploy_environment = deploy_environment;
|
|
112
|
-
if (deploy_version_bump !== undefined) updates.deploy_version_bump = deploy_version_bump;
|
|
113
|
-
if (deploy_trigger !== undefined) updates.deploy_trigger = deploy_trigger;
|
|
114
|
-
|
|
115
|
-
if (Object.keys(updates).length === 0) {
|
|
111
|
+
// Check if any updates provided
|
|
112
|
+
if (title === undefined && description === undefined && auto_deploy_on_completion === undefined &&
|
|
113
|
+
deploy_environment === undefined && deploy_version_bump === undefined && deploy_trigger === undefined) {
|
|
116
114
|
return { result: { success: true, message: 'No updates provided' } };
|
|
117
115
|
}
|
|
118
116
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
117
|
+
const apiClient = getApiClient();
|
|
118
|
+
|
|
119
|
+
const response = await apiClient.proxy<{ success: boolean }>('update_body_of_work', {
|
|
120
|
+
body_of_work_id,
|
|
121
|
+
title,
|
|
122
|
+
description,
|
|
123
|
+
auto_deploy_on_completion,
|
|
124
|
+
deploy_environment,
|
|
125
|
+
deploy_version_bump,
|
|
126
|
+
deploy_trigger
|
|
127
|
+
});
|
|
123
128
|
|
|
124
|
-
if (
|
|
129
|
+
if (!response.ok) {
|
|
130
|
+
throw new Error(`Failed to update body of work: ${response.error}`);
|
|
131
|
+
}
|
|
125
132
|
|
|
126
133
|
return { result: { success: true, body_of_work_id } };
|
|
127
134
|
};
|
|
128
135
|
|
|
129
136
|
export const getBodyOfWork: Handler = async (args, ctx) => {
|
|
130
|
-
const { body_of_work_id } = args as { body_of_work_id: string };
|
|
137
|
+
const { body_of_work_id, summary_only = false } = args as { body_of_work_id: string; summary_only?: boolean };
|
|
131
138
|
|
|
132
139
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
133
140
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
134
141
|
|
|
135
|
-
const
|
|
142
|
+
const apiClient = getApiClient();
|
|
136
143
|
|
|
137
|
-
//
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
144
|
+
// Response type varies based on summary_only
|
|
145
|
+
const response = await apiClient.proxy<{
|
|
146
|
+
body_of_work: {
|
|
147
|
+
id: string;
|
|
148
|
+
title: string;
|
|
149
|
+
description?: string;
|
|
150
|
+
status: string;
|
|
151
|
+
progress_percentage: number;
|
|
152
|
+
};
|
|
153
|
+
// Full response includes tasks grouped by phase
|
|
154
|
+
tasks?: {
|
|
155
|
+
pre: unknown[];
|
|
156
|
+
core: unknown[];
|
|
157
|
+
post: unknown[];
|
|
158
|
+
};
|
|
159
|
+
// Summary response includes counts and next task
|
|
160
|
+
task_counts?: {
|
|
161
|
+
pre: { total: number; completed: number };
|
|
162
|
+
core: { total: number; completed: number };
|
|
163
|
+
post: { total: number; completed: number };
|
|
164
|
+
total: number;
|
|
165
|
+
completed: number;
|
|
166
|
+
in_progress: number;
|
|
167
|
+
};
|
|
168
|
+
current_task?: { id: string; title: string } | null;
|
|
169
|
+
next_task?: { id: string; title: string; priority: number } | null;
|
|
170
|
+
}>('get_body_of_work', { body_of_work_id, summary_only });
|
|
147
171
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.from('body_of_work_tasks')
|
|
151
|
-
.select(`
|
|
152
|
-
phase,
|
|
153
|
-
order_index,
|
|
154
|
-
tasks (
|
|
155
|
-
id,
|
|
156
|
-
title,
|
|
157
|
-
status,
|
|
158
|
-
priority,
|
|
159
|
-
progress_percentage
|
|
160
|
-
)
|
|
161
|
-
`)
|
|
162
|
-
.eq('body_of_work_id', body_of_work_id)
|
|
163
|
-
.order('order_index');
|
|
164
|
-
|
|
165
|
-
if (taskError) throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
166
|
-
|
|
167
|
-
// Organize tasks by phase
|
|
168
|
-
const preTasks: unknown[] = [];
|
|
169
|
-
const coreTasks: unknown[] = [];
|
|
170
|
-
const postTasks: unknown[] = [];
|
|
171
|
-
|
|
172
|
-
for (const link of taskLinks || []) {
|
|
173
|
-
const task = link.tasks;
|
|
174
|
-
if (!task) continue;
|
|
175
|
-
|
|
176
|
-
const taskWithPhase = { ...task, order_index: link.order_index };
|
|
177
|
-
|
|
178
|
-
switch (link.phase) {
|
|
179
|
-
case 'pre':
|
|
180
|
-
preTasks.push(taskWithPhase);
|
|
181
|
-
break;
|
|
182
|
-
case 'core':
|
|
183
|
-
coreTasks.push(taskWithPhase);
|
|
184
|
-
break;
|
|
185
|
-
case 'post':
|
|
186
|
-
postTasks.push(taskWithPhase);
|
|
187
|
-
break;
|
|
188
|
-
}
|
|
172
|
+
if (!response.ok) {
|
|
173
|
+
throw new Error(`Failed to get body of work: ${response.error}`);
|
|
189
174
|
}
|
|
190
175
|
|
|
191
|
-
return {
|
|
192
|
-
result: {
|
|
193
|
-
...bow,
|
|
194
|
-
pre_tasks: preTasks,
|
|
195
|
-
core_tasks: coreTasks,
|
|
196
|
-
post_tasks: postTasks,
|
|
197
|
-
total_tasks: preTasks.length + coreTasks.length + postTasks.length,
|
|
198
|
-
},
|
|
199
|
-
};
|
|
176
|
+
return { result: response.data };
|
|
200
177
|
};
|
|
201
178
|
|
|
202
179
|
export const getBodiesOfWork: Handler = async (args, ctx) => {
|
|
203
|
-
const { project_id, status } = args as {
|
|
180
|
+
const { project_id, status, limit = 50, offset = 0, search_query } = args as {
|
|
204
181
|
project_id: string;
|
|
205
182
|
status?: BodyOfWorkStatus;
|
|
183
|
+
limit?: number;
|
|
184
|
+
offset?: number;
|
|
185
|
+
search_query?: string;
|
|
206
186
|
};
|
|
207
187
|
|
|
208
188
|
validateRequired(project_id, 'project_id');
|
|
209
189
|
validateUUID(project_id, 'project_id');
|
|
210
190
|
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
if (error) throw new Error(`Failed to fetch bodies of work: ${error.message}`);
|
|
237
|
-
|
|
238
|
-
const bodies = data || [];
|
|
239
|
-
if (bodies.length === 0) {
|
|
240
|
-
return { result: { bodies_of_work: [] } };
|
|
241
|
-
}
|
|
191
|
+
const apiClient = getApiClient();
|
|
192
|
+
|
|
193
|
+
const response = await apiClient.proxy<{
|
|
194
|
+
bodies_of_work: Array<{
|
|
195
|
+
id: string;
|
|
196
|
+
title: string;
|
|
197
|
+
description?: string;
|
|
198
|
+
status: string;
|
|
199
|
+
progress_percentage: number;
|
|
200
|
+
task_counts: {
|
|
201
|
+
pre: { total: number; completed: number };
|
|
202
|
+
core: { total: number; completed: number };
|
|
203
|
+
post: { total: number; completed: number };
|
|
204
|
+
};
|
|
205
|
+
}>;
|
|
206
|
+
total_count: number;
|
|
207
|
+
has_more: boolean;
|
|
208
|
+
}>('get_bodies_of_work', {
|
|
209
|
+
project_id,
|
|
210
|
+
status,
|
|
211
|
+
limit: Math.min(limit, 100),
|
|
212
|
+
offset,
|
|
213
|
+
search_query
|
|
214
|
+
});
|
|
242
215
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
const { data: allTaskLinks } = await supabase
|
|
246
|
-
.from('body_of_work_tasks')
|
|
247
|
-
.select('body_of_work_id, phase, tasks!inner(status)')
|
|
248
|
-
.in('body_of_work_id', bodyIds);
|
|
249
|
-
|
|
250
|
-
// Group task links by body_of_work_id
|
|
251
|
-
const taskLinksByBody = new Map<string, typeof allTaskLinks>();
|
|
252
|
-
for (const link of allTaskLinks || []) {
|
|
253
|
-
const existing = taskLinksByBody.get(link.body_of_work_id) || [];
|
|
254
|
-
existing.push(link);
|
|
255
|
-
taskLinksByBody.set(link.body_of_work_id, existing);
|
|
216
|
+
if (!response.ok) {
|
|
217
|
+
throw new Error(`Failed to fetch bodies of work: ${response.error}`);
|
|
256
218
|
}
|
|
257
219
|
|
|
258
|
-
|
|
259
|
-
const bodiesWithCounts = bodies.map((bow) => {
|
|
260
|
-
const taskLinks = taskLinksByBody.get(bow.id) || [];
|
|
261
|
-
const taskCounts = {
|
|
262
|
-
pre: { total: 0, completed: 0 },
|
|
263
|
-
core: { total: 0, completed: 0 },
|
|
264
|
-
post: { total: 0, completed: 0 },
|
|
265
|
-
};
|
|
266
|
-
|
|
267
|
-
for (const link of taskLinks) {
|
|
268
|
-
const phase = link.phase as TaskPhase;
|
|
269
|
-
taskCounts[phase].total++;
|
|
270
|
-
const taskData = link.tasks as unknown as { status: string } | null;
|
|
271
|
-
if (taskData?.status === 'completed') {
|
|
272
|
-
taskCounts[phase].completed++;
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
...bow,
|
|
278
|
-
task_counts: taskCounts,
|
|
279
|
-
};
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
return { result: { bodies_of_work: bodiesWithCounts } };
|
|
220
|
+
return { result: response.data };
|
|
283
221
|
};
|
|
284
222
|
|
|
285
223
|
export const deleteBodyOfWork: Handler = async (args, ctx) => {
|
|
@@ -288,12 +226,15 @@ export const deleteBodyOfWork: Handler = async (args, ctx) => {
|
|
|
288
226
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
289
227
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
290
228
|
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
229
|
+
const apiClient = getApiClient();
|
|
230
|
+
|
|
231
|
+
const response = await apiClient.proxy<{ success: boolean }>('delete_body_of_work', {
|
|
232
|
+
body_of_work_id
|
|
233
|
+
});
|
|
295
234
|
|
|
296
|
-
if (
|
|
235
|
+
if (!response.ok) {
|
|
236
|
+
throw new Error(`Failed to delete body of work: ${response.error}`);
|
|
237
|
+
}
|
|
297
238
|
|
|
298
239
|
return { result: { success: true, message: 'Body of work deleted. Tasks are preserved.' } };
|
|
299
240
|
};
|
|
@@ -311,70 +252,26 @@ export const addTaskToBodyOfWork: Handler = async (args, ctx) => {
|
|
|
311
252
|
validateRequired(task_id, 'task_id');
|
|
312
253
|
validateUUID(task_id, 'task_id');
|
|
313
254
|
|
|
314
|
-
const
|
|
315
|
-
const taskPhase = phase || 'core';
|
|
255
|
+
const apiClient = getApiClient();
|
|
316
256
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
throw new Error(`Cannot add tasks to ${bow.status} body of work`);
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Check if task is already in a body of work
|
|
333
|
-
const { data: existingLink } = await supabase
|
|
334
|
-
.from('body_of_work_tasks')
|
|
335
|
-
.select('body_of_work_id')
|
|
336
|
-
.eq('task_id', task_id)
|
|
337
|
-
.single();
|
|
338
|
-
|
|
339
|
-
if (existingLink) {
|
|
340
|
-
throw new Error('Task is already assigned to a body of work. Remove it first.');
|
|
341
|
-
}
|
|
257
|
+
const response = await apiClient.proxy<{
|
|
258
|
+
success: boolean;
|
|
259
|
+
body_of_work_id: string;
|
|
260
|
+
task_id: string;
|
|
261
|
+
phase: string;
|
|
262
|
+
order_index: number;
|
|
263
|
+
}>('add_task_to_body_of_work', {
|
|
264
|
+
body_of_work_id,
|
|
265
|
+
task_id,
|
|
266
|
+
phase: phase || 'core',
|
|
267
|
+
order_index
|
|
268
|
+
});
|
|
342
269
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
if (finalOrderIndex === undefined) {
|
|
346
|
-
const { data: maxOrder } = await supabase
|
|
347
|
-
.from('body_of_work_tasks')
|
|
348
|
-
.select('order_index')
|
|
349
|
-
.eq('body_of_work_id', body_of_work_id)
|
|
350
|
-
.eq('phase', taskPhase)
|
|
351
|
-
.order('order_index', { ascending: false })
|
|
352
|
-
.limit(1)
|
|
353
|
-
.single();
|
|
354
|
-
|
|
355
|
-
finalOrderIndex = maxOrder ? maxOrder.order_index + 1 : 0;
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`Failed to add task to body of work: ${response.error}`);
|
|
356
272
|
}
|
|
357
273
|
|
|
358
|
-
|
|
359
|
-
.from('body_of_work_tasks')
|
|
360
|
-
.insert({
|
|
361
|
-
body_of_work_id,
|
|
362
|
-
task_id,
|
|
363
|
-
phase: taskPhase,
|
|
364
|
-
order_index: finalOrderIndex,
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
if (error) throw new Error(`Failed to add task to body of work: ${error.message}`);
|
|
368
|
-
|
|
369
|
-
return {
|
|
370
|
-
result: {
|
|
371
|
-
success: true,
|
|
372
|
-
body_of_work_id,
|
|
373
|
-
task_id,
|
|
374
|
-
phase: taskPhase,
|
|
375
|
-
order_index: finalOrderIndex,
|
|
376
|
-
},
|
|
377
|
-
};
|
|
274
|
+
return { result: response.data };
|
|
378
275
|
};
|
|
379
276
|
|
|
380
277
|
export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
|
|
@@ -383,38 +280,19 @@ export const removeTaskFromBodyOfWork: Handler = async (args, ctx) => {
|
|
|
383
280
|
validateRequired(task_id, 'task_id');
|
|
384
281
|
validateUUID(task_id, 'task_id');
|
|
385
282
|
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
// Check if link exists
|
|
389
|
-
const { data: link } = await supabase
|
|
390
|
-
.from('body_of_work_tasks')
|
|
391
|
-
.select('body_of_work_id')
|
|
392
|
-
.eq('task_id', task_id)
|
|
393
|
-
.single();
|
|
394
|
-
|
|
395
|
-
if (!link) {
|
|
396
|
-
return { result: { success: true, message: 'Task is not in any body of work' } };
|
|
397
|
-
}
|
|
283
|
+
const apiClient = getApiClient();
|
|
398
284
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
.single();
|
|
285
|
+
const response = await apiClient.proxy<{
|
|
286
|
+
success: boolean;
|
|
287
|
+
body_of_work_id?: string;
|
|
288
|
+
message?: string;
|
|
289
|
+
}>('remove_task_from_body_of_work', { task_id });
|
|
405
290
|
|
|
406
|
-
if (
|
|
407
|
-
throw new Error(
|
|
291
|
+
if (!response.ok) {
|
|
292
|
+
throw new Error(`Failed to remove task from body of work: ${response.error}`);
|
|
408
293
|
}
|
|
409
294
|
|
|
410
|
-
|
|
411
|
-
.from('body_of_work_tasks')
|
|
412
|
-
.delete()
|
|
413
|
-
.eq('task_id', task_id);
|
|
414
|
-
|
|
415
|
-
if (error) throw new Error(`Failed to remove task from body of work: ${error.message}`);
|
|
416
|
-
|
|
417
|
-
return { result: { success: true, body_of_work_id: link.body_of_work_id } };
|
|
295
|
+
return { result: response.data };
|
|
418
296
|
};
|
|
419
297
|
|
|
420
298
|
export const activateBodyOfWork: Handler = async (args, ctx) => {
|
|
@@ -423,52 +301,21 @@ export const activateBodyOfWork: Handler = async (args, ctx) => {
|
|
|
423
301
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
424
302
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
425
303
|
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
// Check current status
|
|
429
|
-
const { data: bow, error: bowError } = await supabase
|
|
430
|
-
.from('bodies_of_work')
|
|
431
|
-
.select('status, title')
|
|
432
|
-
.eq('id', body_of_work_id)
|
|
433
|
-
.single();
|
|
304
|
+
const apiClient = getApiClient();
|
|
434
305
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
// Check if there are any tasks
|
|
444
|
-
const { count } = await supabase
|
|
445
|
-
.from('body_of_work_tasks')
|
|
446
|
-
.select('id', { count: 'exact', head: true })
|
|
447
|
-
.eq('body_of_work_id', body_of_work_id);
|
|
306
|
+
const response = await apiClient.proxy<{
|
|
307
|
+
success: boolean;
|
|
308
|
+
body_of_work_id: string;
|
|
309
|
+
title: string;
|
|
310
|
+
status: string;
|
|
311
|
+
message: string;
|
|
312
|
+
}>('activate_body_of_work', { body_of_work_id });
|
|
448
313
|
|
|
449
|
-
if (!
|
|
450
|
-
throw new Error(
|
|
314
|
+
if (!response.ok) {
|
|
315
|
+
throw new Error(`Failed to activate body of work: ${response.error}`);
|
|
451
316
|
}
|
|
452
317
|
|
|
453
|
-
|
|
454
|
-
.from('bodies_of_work')
|
|
455
|
-
.update({
|
|
456
|
-
status: 'active',
|
|
457
|
-
activated_at: new Date().toISOString(),
|
|
458
|
-
})
|
|
459
|
-
.eq('id', body_of_work_id);
|
|
460
|
-
|
|
461
|
-
if (error) throw new Error(`Failed to activate body of work: ${error.message}`);
|
|
462
|
-
|
|
463
|
-
return {
|
|
464
|
-
result: {
|
|
465
|
-
success: true,
|
|
466
|
-
body_of_work_id,
|
|
467
|
-
title: bow.title,
|
|
468
|
-
status: 'active',
|
|
469
|
-
message: 'Body of work activated. Tasks can now be worked on following phase order.',
|
|
470
|
-
},
|
|
471
|
-
};
|
|
318
|
+
return { result: response.data };
|
|
472
319
|
};
|
|
473
320
|
|
|
474
321
|
export const addTaskDependency: Handler = async (args, ctx) => {
|
|
@@ -489,70 +336,25 @@ export const addTaskDependency: Handler = async (args, ctx) => {
|
|
|
489
336
|
throw new Error('A task cannot depend on itself');
|
|
490
337
|
}
|
|
491
338
|
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
// Verify both tasks belong to the same body of work
|
|
495
|
-
const { data: taskLinks, error: taskError } = await supabase
|
|
496
|
-
.from('body_of_work_tasks')
|
|
497
|
-
.select('task_id')
|
|
498
|
-
.eq('body_of_work_id', body_of_work_id)
|
|
499
|
-
.in('task_id', [task_id, depends_on_task_id]);
|
|
500
|
-
|
|
501
|
-
if (taskError) throw new Error(`Failed to verify tasks: ${taskError.message}`);
|
|
502
|
-
|
|
503
|
-
if (!taskLinks || taskLinks.length !== 2) {
|
|
504
|
-
throw new Error('Both tasks must belong to the specified body of work');
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
// Check for circular dependencies by traversing the dependency graph
|
|
508
|
-
const visited = new Set<string>();
|
|
509
|
-
const checkCircular = async (currentTaskId: string): Promise<boolean> => {
|
|
510
|
-
if (currentTaskId === task_id) return true; // Found cycle
|
|
511
|
-
if (visited.has(currentTaskId)) return false;
|
|
512
|
-
visited.add(currentTaskId);
|
|
513
|
-
|
|
514
|
-
const { data: deps } = await supabase
|
|
515
|
-
.from('body_of_work_task_dependencies')
|
|
516
|
-
.select('depends_on_task_id')
|
|
517
|
-
.eq('task_id', currentTaskId)
|
|
518
|
-
.eq('body_of_work_id', body_of_work_id);
|
|
519
|
-
|
|
520
|
-
for (const dep of deps || []) {
|
|
521
|
-
if (await checkCircular(dep.depends_on_task_id)) {
|
|
522
|
-
return true;
|
|
523
|
-
}
|
|
524
|
-
}
|
|
525
|
-
return false;
|
|
526
|
-
};
|
|
339
|
+
const apiClient = getApiClient();
|
|
527
340
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
341
|
+
const response = await apiClient.proxy<{
|
|
342
|
+
success: boolean;
|
|
343
|
+
body_of_work_id: string;
|
|
344
|
+
task_id: string;
|
|
345
|
+
depends_on_task_id: string;
|
|
346
|
+
message: string;
|
|
347
|
+
}>('add_task_dependency', {
|
|
348
|
+
body_of_work_id,
|
|
349
|
+
task_id,
|
|
350
|
+
depends_on_task_id
|
|
351
|
+
});
|
|
531
352
|
|
|
532
|
-
|
|
533
|
-
.
|
|
534
|
-
.insert({
|
|
535
|
-
body_of_work_id,
|
|
536
|
-
task_id,
|
|
537
|
-
depends_on_task_id,
|
|
538
|
-
});
|
|
539
|
-
|
|
540
|
-
if (error) {
|
|
541
|
-
if (error.message.includes('duplicate')) {
|
|
542
|
-
throw new Error('This dependency already exists');
|
|
543
|
-
}
|
|
544
|
-
throw new Error(`Failed to add dependency: ${error.message}`);
|
|
353
|
+
if (!response.ok) {
|
|
354
|
+
throw new Error(`Failed to add task dependency: ${response.error}`);
|
|
545
355
|
}
|
|
546
356
|
|
|
547
|
-
return {
|
|
548
|
-
result: {
|
|
549
|
-
success: true,
|
|
550
|
-
body_of_work_id,
|
|
551
|
-
task_id,
|
|
552
|
-
depends_on_task_id,
|
|
553
|
-
message: `Task now depends on completion of the specified task`,
|
|
554
|
-
},
|
|
555
|
-
};
|
|
357
|
+
return { result: response.data };
|
|
556
358
|
};
|
|
557
359
|
|
|
558
360
|
export const removeTaskDependency: Handler = async (args, ctx) => {
|
|
@@ -566,17 +368,22 @@ export const removeTaskDependency: Handler = async (args, ctx) => {
|
|
|
566
368
|
validateRequired(depends_on_task_id, 'depends_on_task_id');
|
|
567
369
|
validateUUID(depends_on_task_id, 'depends_on_task_id');
|
|
568
370
|
|
|
569
|
-
const
|
|
371
|
+
const apiClient = getApiClient();
|
|
570
372
|
|
|
571
|
-
const
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
373
|
+
const response = await apiClient.proxy<{
|
|
374
|
+
success: boolean;
|
|
375
|
+
task_id: string;
|
|
376
|
+
depends_on_task_id: string;
|
|
377
|
+
}>('remove_task_dependency', {
|
|
378
|
+
task_id,
|
|
379
|
+
depends_on_task_id
|
|
380
|
+
});
|
|
576
381
|
|
|
577
|
-
if (
|
|
382
|
+
if (!response.ok) {
|
|
383
|
+
throw new Error(`Failed to remove task dependency: ${response.error}`);
|
|
384
|
+
}
|
|
578
385
|
|
|
579
|
-
return { result:
|
|
386
|
+
return { result: response.data };
|
|
580
387
|
};
|
|
581
388
|
|
|
582
389
|
export const getTaskDependencies: Handler = async (args, ctx) => {
|
|
@@ -592,29 +399,25 @@ export const getTaskDependencies: Handler = async (args, ctx) => {
|
|
|
592
399
|
if (body_of_work_id) validateUUID(body_of_work_id, 'body_of_work_id');
|
|
593
400
|
if (task_id) validateUUID(task_id, 'task_id');
|
|
594
401
|
|
|
595
|
-
const
|
|
402
|
+
const apiClient = getApiClient();
|
|
596
403
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
404
|
+
const response = await apiClient.proxy<{
|
|
405
|
+
dependencies: Array<{
|
|
406
|
+
id: string;
|
|
407
|
+
task_id: string;
|
|
408
|
+
depends_on_task_id: string;
|
|
409
|
+
created_at: string;
|
|
410
|
+
}>;
|
|
411
|
+
}>('get_task_dependencies', {
|
|
412
|
+
body_of_work_id,
|
|
413
|
+
task_id
|
|
414
|
+
});
|
|
605
415
|
|
|
606
|
-
if (
|
|
607
|
-
|
|
608
|
-
}
|
|
609
|
-
if (task_id) {
|
|
610
|
-
query = query.eq('task_id', task_id);
|
|
416
|
+
if (!response.ok) {
|
|
417
|
+
throw new Error(`Failed to fetch task dependencies: ${response.error}`);
|
|
611
418
|
}
|
|
612
419
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
if (error) throw new Error(`Failed to fetch dependencies: ${error.message}`);
|
|
616
|
-
|
|
617
|
-
return { result: { dependencies: data || [] } };
|
|
420
|
+
return { result: response.data };
|
|
618
421
|
};
|
|
619
422
|
|
|
620
423
|
export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
|
|
@@ -623,145 +426,27 @@ export const getNextBodyOfWorkTask: Handler = async (args, ctx) => {
|
|
|
623
426
|
validateRequired(body_of_work_id, 'body_of_work_id');
|
|
624
427
|
validateUUID(body_of_work_id, 'body_of_work_id');
|
|
625
428
|
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
if (bow.status !== 'active') {
|
|
640
|
-
return {
|
|
641
|
-
result: {
|
|
642
|
-
next_task: null,
|
|
643
|
-
message: `Body of work is ${bow.status}, not active`,
|
|
644
|
-
},
|
|
429
|
+
const apiClient = getApiClient();
|
|
430
|
+
|
|
431
|
+
const response = await apiClient.proxy<{
|
|
432
|
+
next_task: {
|
|
433
|
+
id: string;
|
|
434
|
+
title: string;
|
|
435
|
+
phase: string;
|
|
436
|
+
priority: number;
|
|
437
|
+
} | null;
|
|
438
|
+
body_of_work?: {
|
|
439
|
+
id: string;
|
|
440
|
+
title: string;
|
|
645
441
|
};
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
// Get all tasks with their status and phase
|
|
649
|
-
const { data: taskLinks, error: taskError } = await supabase
|
|
650
|
-
.from('body_of_work_tasks')
|
|
651
|
-
.select(`
|
|
652
|
-
task_id,
|
|
653
|
-
phase,
|
|
654
|
-
order_index,
|
|
655
|
-
tasks (
|
|
656
|
-
id,
|
|
657
|
-
title,
|
|
658
|
-
status,
|
|
659
|
-
priority,
|
|
660
|
-
claimed_by_session_id
|
|
661
|
-
)
|
|
662
|
-
`)
|
|
663
|
-
.eq('body_of_work_id', body_of_work_id)
|
|
664
|
-
.order('order_index');
|
|
665
|
-
|
|
666
|
-
if (taskError) throw new Error(`Failed to fetch tasks: ${taskError.message}`);
|
|
667
|
-
|
|
668
|
-
// Get all dependencies
|
|
669
|
-
const { data: dependencies } = await supabase
|
|
670
|
-
.from('body_of_work_task_dependencies')
|
|
671
|
-
.select('task_id, depends_on_task_id')
|
|
672
|
-
.eq('body_of_work_id', body_of_work_id);
|
|
673
|
-
|
|
674
|
-
const depMap = new Map<string, string[]>();
|
|
675
|
-
for (const dep of dependencies || []) {
|
|
676
|
-
const existing = depMap.get(dep.task_id) || [];
|
|
677
|
-
existing.push(dep.depends_on_task_id);
|
|
678
|
-
depMap.set(dep.task_id, existing);
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Build task status map
|
|
682
|
-
const taskStatusMap = new Map<string, string>();
|
|
683
|
-
for (const link of taskLinks || []) {
|
|
684
|
-
const task = link.tasks as unknown as { id: string; status: string } | null;
|
|
685
|
-
if (task) {
|
|
686
|
-
taskStatusMap.set(task.id, task.status);
|
|
687
|
-
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
// Check if all dependencies are completed
|
|
691
|
-
const areDependenciesComplete = (taskId: string): boolean => {
|
|
692
|
-
const deps = depMap.get(taskId) || [];
|
|
693
|
-
return deps.every((depId) => taskStatusMap.get(depId) === 'completed');
|
|
694
|
-
};
|
|
695
|
-
|
|
696
|
-
// Phase order: pre -> core -> post
|
|
697
|
-
const phaseOrder = ['pre', 'core', 'post'];
|
|
698
|
-
|
|
699
|
-
// Check if all tasks in a phase are completed
|
|
700
|
-
const isPhaseComplete = (phase: string): boolean => {
|
|
701
|
-
return (taskLinks || [])
|
|
702
|
-
.filter((link) => link.phase === phase)
|
|
703
|
-
.every((link) => {
|
|
704
|
-
const task = link.tasks as unknown as { status: string } | null;
|
|
705
|
-
return task?.status === 'completed';
|
|
706
|
-
});
|
|
707
|
-
};
|
|
442
|
+
message?: string;
|
|
443
|
+
}>('get_next_body_of_work_task', { body_of_work_id });
|
|
708
444
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
// Check if we can work on this phase
|
|
712
|
-
const prevPhaseIndex = phaseOrder.indexOf(phase) - 1;
|
|
713
|
-
if (prevPhaseIndex >= 0 && !isPhaseComplete(phaseOrder[prevPhaseIndex])) {
|
|
714
|
-
continue; // Previous phase not complete, skip this phase
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
const phaseTasks = (taskLinks || [])
|
|
718
|
-
.filter((link) => link.phase === phase)
|
|
719
|
-
.sort((a, b) => a.order_index - b.order_index);
|
|
720
|
-
|
|
721
|
-
for (const link of phaseTasks) {
|
|
722
|
-
const task = link.tasks as unknown as {
|
|
723
|
-
id: string;
|
|
724
|
-
title: string;
|
|
725
|
-
status: string;
|
|
726
|
-
priority: number;
|
|
727
|
-
claimed_by_session_id: string | null;
|
|
728
|
-
} | null;
|
|
729
|
-
|
|
730
|
-
if (!task) continue;
|
|
731
|
-
|
|
732
|
-
// Skip completed, cancelled, or in-progress tasks
|
|
733
|
-
if (task.status !== 'pending') continue;
|
|
734
|
-
|
|
735
|
-
// Skip tasks claimed by another session
|
|
736
|
-
if (task.claimed_by_session_id) continue;
|
|
737
|
-
|
|
738
|
-
// Check dependencies
|
|
739
|
-
if (!areDependenciesComplete(task.id)) continue;
|
|
740
|
-
|
|
741
|
-
return {
|
|
742
|
-
result: {
|
|
743
|
-
next_task: {
|
|
744
|
-
id: task.id,
|
|
745
|
-
title: task.title,
|
|
746
|
-
phase: link.phase,
|
|
747
|
-
priority: task.priority,
|
|
748
|
-
},
|
|
749
|
-
body_of_work: {
|
|
750
|
-
id: body_of_work_id,
|
|
751
|
-
title: bow.title,
|
|
752
|
-
},
|
|
753
|
-
},
|
|
754
|
-
};
|
|
755
|
-
}
|
|
445
|
+
if (!response.ok) {
|
|
446
|
+
throw new Error(`Failed to get next body of work task: ${response.error}`);
|
|
756
447
|
}
|
|
757
448
|
|
|
758
|
-
|
|
759
|
-
return {
|
|
760
|
-
result: {
|
|
761
|
-
next_task: null,
|
|
762
|
-
message: 'No available tasks - all are completed, in progress, or blocked by dependencies',
|
|
763
|
-
},
|
|
764
|
-
};
|
|
449
|
+
return { result: response.data };
|
|
765
450
|
};
|
|
766
451
|
|
|
767
452
|
/**
|