@vibescope/mcp-server 0.0.1

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 (170) hide show
  1. package/README.md +98 -0
  2. package/dist/cli.d.ts +34 -0
  3. package/dist/cli.js +356 -0
  4. package/dist/cli.test.d.ts +1 -0
  5. package/dist/cli.test.js +367 -0
  6. package/dist/handlers/__test-utils__.d.ts +72 -0
  7. package/dist/handlers/__test-utils__.js +176 -0
  8. package/dist/handlers/blockers.d.ts +18 -0
  9. package/dist/handlers/blockers.js +81 -0
  10. package/dist/handlers/bodies-of-work.d.ts +34 -0
  11. package/dist/handlers/bodies-of-work.js +614 -0
  12. package/dist/handlers/checkouts.d.ts +37 -0
  13. package/dist/handlers/checkouts.js +377 -0
  14. package/dist/handlers/cost.d.ts +39 -0
  15. package/dist/handlers/cost.js +247 -0
  16. package/dist/handlers/decisions.d.ts +16 -0
  17. package/dist/handlers/decisions.js +64 -0
  18. package/dist/handlers/deployment.d.ts +36 -0
  19. package/dist/handlers/deployment.js +1062 -0
  20. package/dist/handlers/discovery.d.ts +14 -0
  21. package/dist/handlers/discovery.js +870 -0
  22. package/dist/handlers/fallback.d.ts +18 -0
  23. package/dist/handlers/fallback.js +216 -0
  24. package/dist/handlers/findings.d.ts +18 -0
  25. package/dist/handlers/findings.js +110 -0
  26. package/dist/handlers/git-issues.d.ts +22 -0
  27. package/dist/handlers/git-issues.js +247 -0
  28. package/dist/handlers/ideas.d.ts +19 -0
  29. package/dist/handlers/ideas.js +188 -0
  30. package/dist/handlers/index.d.ts +29 -0
  31. package/dist/handlers/index.js +65 -0
  32. package/dist/handlers/knowledge-query.d.ts +22 -0
  33. package/dist/handlers/knowledge-query.js +253 -0
  34. package/dist/handlers/knowledge.d.ts +12 -0
  35. package/dist/handlers/knowledge.js +108 -0
  36. package/dist/handlers/milestones.d.ts +20 -0
  37. package/dist/handlers/milestones.js +179 -0
  38. package/dist/handlers/organizations.d.ts +36 -0
  39. package/dist/handlers/organizations.js +428 -0
  40. package/dist/handlers/progress.d.ts +14 -0
  41. package/dist/handlers/progress.js +149 -0
  42. package/dist/handlers/project.d.ts +20 -0
  43. package/dist/handlers/project.js +278 -0
  44. package/dist/handlers/requests.d.ts +16 -0
  45. package/dist/handlers/requests.js +131 -0
  46. package/dist/handlers/roles.d.ts +30 -0
  47. package/dist/handlers/roles.js +281 -0
  48. package/dist/handlers/session.d.ts +20 -0
  49. package/dist/handlers/session.js +791 -0
  50. package/dist/handlers/tasks.d.ts +52 -0
  51. package/dist/handlers/tasks.js +1111 -0
  52. package/dist/handlers/tasks.test.d.ts +1 -0
  53. package/dist/handlers/tasks.test.js +431 -0
  54. package/dist/handlers/types.d.ts +94 -0
  55. package/dist/handlers/types.js +1 -0
  56. package/dist/handlers/validation.d.ts +16 -0
  57. package/dist/handlers/validation.js +188 -0
  58. package/dist/index.d.ts +2 -0
  59. package/dist/index.js +2707 -0
  60. package/dist/knowledge.d.ts +6 -0
  61. package/dist/knowledge.js +121 -0
  62. package/dist/tools.d.ts +2 -0
  63. package/dist/tools.js +2498 -0
  64. package/dist/utils.d.ts +149 -0
  65. package/dist/utils.js +317 -0
  66. package/dist/utils.test.d.ts +1 -0
  67. package/dist/utils.test.js +532 -0
  68. package/dist/validators.d.ts +35 -0
  69. package/dist/validators.js +111 -0
  70. package/dist/validators.test.d.ts +1 -0
  71. package/dist/validators.test.js +176 -0
  72. package/package.json +44 -0
  73. package/src/cli.test.ts +442 -0
  74. package/src/cli.ts +439 -0
  75. package/src/handlers/__test-utils__.ts +217 -0
  76. package/src/handlers/blockers.test.ts +390 -0
  77. package/src/handlers/blockers.ts +110 -0
  78. package/src/handlers/bodies-of-work.test.ts +1276 -0
  79. package/src/handlers/bodies-of-work.ts +783 -0
  80. package/src/handlers/cost.test.ts +436 -0
  81. package/src/handlers/cost.ts +322 -0
  82. package/src/handlers/decisions.test.ts +401 -0
  83. package/src/handlers/decisions.ts +86 -0
  84. package/src/handlers/deployment.test.ts +516 -0
  85. package/src/handlers/deployment.ts +1289 -0
  86. package/src/handlers/discovery.test.ts +254 -0
  87. package/src/handlers/discovery.ts +969 -0
  88. package/src/handlers/fallback.test.ts +687 -0
  89. package/src/handlers/fallback.ts +260 -0
  90. package/src/handlers/findings.test.ts +565 -0
  91. package/src/handlers/findings.ts +153 -0
  92. package/src/handlers/ideas.test.ts +753 -0
  93. package/src/handlers/ideas.ts +247 -0
  94. package/src/handlers/index.ts +69 -0
  95. package/src/handlers/milestones.test.ts +584 -0
  96. package/src/handlers/milestones.ts +217 -0
  97. package/src/handlers/organizations.test.ts +997 -0
  98. package/src/handlers/organizations.ts +550 -0
  99. package/src/handlers/progress.test.ts +369 -0
  100. package/src/handlers/progress.ts +188 -0
  101. package/src/handlers/project.test.ts +562 -0
  102. package/src/handlers/project.ts +352 -0
  103. package/src/handlers/requests.test.ts +531 -0
  104. package/src/handlers/requests.ts +150 -0
  105. package/src/handlers/session.test.ts +459 -0
  106. package/src/handlers/session.ts +912 -0
  107. package/src/handlers/tasks.test.ts +602 -0
  108. package/src/handlers/tasks.ts +1393 -0
  109. package/src/handlers/types.ts +88 -0
  110. package/src/handlers/validation.test.ts +880 -0
  111. package/src/handlers/validation.ts +223 -0
  112. package/src/index.ts +3205 -0
  113. package/src/knowledge.ts +132 -0
  114. package/src/tmpclaude-0078-cwd +1 -0
  115. package/src/tmpclaude-0ee1-cwd +1 -0
  116. package/src/tmpclaude-2dd5-cwd +1 -0
  117. package/src/tmpclaude-344c-cwd +1 -0
  118. package/src/tmpclaude-3860-cwd +1 -0
  119. package/src/tmpclaude-4b63-cwd +1 -0
  120. package/src/tmpclaude-5c73-cwd +1 -0
  121. package/src/tmpclaude-5ee3-cwd +1 -0
  122. package/src/tmpclaude-6795-cwd +1 -0
  123. package/src/tmpclaude-709e-cwd +1 -0
  124. package/src/tmpclaude-9839-cwd +1 -0
  125. package/src/tmpclaude-d829-cwd +1 -0
  126. package/src/tmpclaude-e072-cwd +1 -0
  127. package/src/tmpclaude-f6ee-cwd +1 -0
  128. package/src/utils.test.ts +681 -0
  129. package/src/utils.ts +375 -0
  130. package/src/validators.test.ts +223 -0
  131. package/src/validators.ts +122 -0
  132. package/tmpclaude-0439-cwd +1 -0
  133. package/tmpclaude-132f-cwd +1 -0
  134. package/tmpclaude-15bb-cwd +1 -0
  135. package/tmpclaude-165a-cwd +1 -0
  136. package/tmpclaude-1ba9-cwd +1 -0
  137. package/tmpclaude-21a3-cwd +1 -0
  138. package/tmpclaude-2a38-cwd +1 -0
  139. package/tmpclaude-2adf-cwd +1 -0
  140. package/tmpclaude-2f56-cwd +1 -0
  141. package/tmpclaude-3626-cwd +1 -0
  142. package/tmpclaude-3727-cwd +1 -0
  143. package/tmpclaude-40bc-cwd +1 -0
  144. package/tmpclaude-436f-cwd +1 -0
  145. package/tmpclaude-4783-cwd +1 -0
  146. package/tmpclaude-4b6d-cwd +1 -0
  147. package/tmpclaude-4ba4-cwd +1 -0
  148. package/tmpclaude-51e6-cwd +1 -0
  149. package/tmpclaude-5ecf-cwd +1 -0
  150. package/tmpclaude-6f97-cwd +1 -0
  151. package/tmpclaude-7fb2-cwd +1 -0
  152. package/tmpclaude-825c-cwd +1 -0
  153. package/tmpclaude-8baf-cwd +1 -0
  154. package/tmpclaude-8d9f-cwd +1 -0
  155. package/tmpclaude-975c-cwd +1 -0
  156. package/tmpclaude-9983-cwd +1 -0
  157. package/tmpclaude-a045-cwd +1 -0
  158. package/tmpclaude-ac4a-cwd +1 -0
  159. package/tmpclaude-b593-cwd +1 -0
  160. package/tmpclaude-b891-cwd +1 -0
  161. package/tmpclaude-c032-cwd +1 -0
  162. package/tmpclaude-cf43-cwd +1 -0
  163. package/tmpclaude-d040-cwd +1 -0
  164. package/tmpclaude-dcdd-cwd +1 -0
  165. package/tmpclaude-dcee-cwd +1 -0
  166. package/tmpclaude-e16b-cwd +1 -0
  167. package/tmpclaude-ecd2-cwd +1 -0
  168. package/tmpclaude-f48d-cwd +1 -0
  169. package/tsconfig.json +16 -0
  170. package/vitest.config.ts +13 -0
@@ -0,0 +1,550 @@
1
+ /**
2
+ * Organizations Handlers
3
+ *
4
+ * Handles organization management, membership, and project sharing:
5
+ * - list_organizations
6
+ * - create_organization
7
+ * - update_organization
8
+ * - delete_organization
9
+ * - list_org_members
10
+ * - invite_member
11
+ * - update_member_role
12
+ * - remove_member
13
+ * - leave_organization
14
+ * - share_project_with_org
15
+ * - update_project_share
16
+ * - unshare_project
17
+ * - list_project_shares
18
+ */
19
+
20
+ import type { Handler, HandlerRegistry } from './types.js';
21
+ import { validateRequired, validateUUID } from '../validators.js';
22
+ import { randomBytes } from 'crypto';
23
+
24
+ // Valid roles in order of permission level
25
+ const ROLE_ORDER = ['viewer', 'member', 'admin', 'owner'] as const;
26
+ type Role = (typeof ROLE_ORDER)[number];
27
+
28
+ // Valid share permissions
29
+ const PERMISSION_ORDER = ['read', 'write', 'admin'] as const;
30
+ type Permission = (typeof PERMISSION_ORDER)[number];
31
+
32
+ /**
33
+ * Generate a URL-friendly slug from a name
34
+ */
35
+ function generateSlug(name: string): string {
36
+ return name
37
+ .toLowerCase()
38
+ .replace(/[^a-z0-9]+/g, '-')
39
+ .replace(/^-|-$/g, '')
40
+ .slice(0, 50);
41
+ }
42
+
43
+ /**
44
+ * Generate a secure invite token
45
+ */
46
+ function generateInviteToken(): string {
47
+ return randomBytes(32).toString('base64url');
48
+ }
49
+
50
+ // ============================================================================
51
+ // Organization Management
52
+ // ============================================================================
53
+
54
+ export const listOrganizations: Handler = async (_args, ctx) => {
55
+ const { supabase, auth } = ctx;
56
+
57
+ const { data, error } = await supabase
58
+ .from('organization_members')
59
+ .select(`
60
+ role,
61
+ joined_at,
62
+ organizations (
63
+ id,
64
+ name,
65
+ slug,
66
+ description,
67
+ logo_url,
68
+ owner_id,
69
+ created_at
70
+ )
71
+ `)
72
+ .eq('user_id', auth.userId)
73
+ .order('joined_at', { ascending: false });
74
+
75
+ if (error) throw new Error(`Failed to list organizations: ${error.message}`);
76
+
77
+ const organizations = (data || []).map((m) => ({
78
+ ...(m.organizations as unknown as Record<string, unknown>),
79
+ role: m.role,
80
+ joined_at: m.joined_at,
81
+ }));
82
+
83
+ return { result: { organizations, count: organizations.length } };
84
+ };
85
+
86
+ export const createOrganization: Handler = async (args, ctx) => {
87
+ const { name, description, slug: customSlug } = args as {
88
+ name: string;
89
+ description?: string;
90
+ slug?: string;
91
+ };
92
+
93
+ validateRequired(name, 'name');
94
+
95
+ const { supabase, auth } = ctx;
96
+ const slug = customSlug || generateSlug(name);
97
+
98
+ // Check if slug is available
99
+ const { data: existing } = await supabase
100
+ .from('organizations')
101
+ .select('id')
102
+ .eq('slug', slug)
103
+ .maybeSingle();
104
+
105
+ if (existing) {
106
+ throw new Error(`Organization slug "${slug}" is already taken`);
107
+ }
108
+
109
+ const { data, error } = await supabase
110
+ .from('organizations')
111
+ .insert({
112
+ name,
113
+ slug,
114
+ description: description || null,
115
+ owner_id: auth.userId,
116
+ })
117
+ .select()
118
+ .single();
119
+
120
+ if (error) throw new Error(`Failed to create organization: ${error.message}`);
121
+
122
+ return {
123
+ result: {
124
+ success: true,
125
+ organization: data,
126
+ message: `Organization "${name}" created. You are the owner.`,
127
+ },
128
+ };
129
+ };
130
+
131
+ export const updateOrganization: Handler = async (args, ctx) => {
132
+ const { organization_id, name, description, logo_url } = args as {
133
+ organization_id: string;
134
+ name?: string;
135
+ description?: string;
136
+ logo_url?: string;
137
+ };
138
+
139
+ validateRequired(organization_id, 'organization_id');
140
+ validateUUID(organization_id, 'organization_id');
141
+
142
+ const { supabase } = ctx;
143
+
144
+ const updates: Record<string, unknown> = {};
145
+ if (name !== undefined) updates.name = name;
146
+ if (description !== undefined) updates.description = description;
147
+ if (logo_url !== undefined) updates.logo_url = logo_url;
148
+
149
+ if (Object.keys(updates).length === 0) {
150
+ throw new Error('No updates provided');
151
+ }
152
+
153
+ const { data, error } = await supabase
154
+ .from('organizations')
155
+ .update(updates)
156
+ .eq('id', organization_id)
157
+ .select()
158
+ .single();
159
+
160
+ if (error) throw new Error(`Failed to update organization: ${error.message}`);
161
+
162
+ return { result: { success: true, organization: data } };
163
+ };
164
+
165
+ export const deleteOrganization: Handler = async (args, ctx) => {
166
+ const { organization_id } = args as { organization_id: string };
167
+
168
+ validateRequired(organization_id, 'organization_id');
169
+ validateUUID(organization_id, 'organization_id');
170
+
171
+ const { supabase } = ctx;
172
+
173
+ const { error } = await supabase
174
+ .from('organizations')
175
+ .delete()
176
+ .eq('id', organization_id);
177
+
178
+ if (error) throw new Error(`Failed to delete organization: ${error.message}`);
179
+
180
+ return {
181
+ result: {
182
+ success: true,
183
+ message: 'Organization deleted. All shares have been removed.',
184
+ },
185
+ };
186
+ };
187
+
188
+ // ============================================================================
189
+ // Member Management
190
+ // ============================================================================
191
+
192
+ export const listOrgMembers: Handler = async (args, ctx) => {
193
+ const { organization_id } = args as { organization_id: string };
194
+
195
+ validateRequired(organization_id, 'organization_id');
196
+ validateUUID(organization_id, 'organization_id');
197
+
198
+ const { supabase } = ctx;
199
+
200
+ const { data, error } = await supabase
201
+ .from('organization_members')
202
+ .select('id, user_id, role, joined_at, invited_by')
203
+ .eq('organization_id', organization_id)
204
+ .order('role', { ascending: true })
205
+ .order('joined_at', { ascending: true });
206
+
207
+ if (error) throw new Error(`Failed to list members: ${error.message}`);
208
+
209
+ return { result: { members: data || [], count: data?.length || 0 } };
210
+ };
211
+
212
+ export const inviteMember: Handler = async (args, ctx) => {
213
+ const { organization_id, email, role = 'member' } = args as {
214
+ organization_id: string;
215
+ email: string;
216
+ role?: 'admin' | 'member' | 'viewer';
217
+ };
218
+
219
+ validateRequired(organization_id, 'organization_id');
220
+ validateRequired(email, 'email');
221
+ validateUUID(organization_id, 'organization_id');
222
+
223
+ if (!['admin', 'member', 'viewer'].includes(role)) {
224
+ throw new Error('Invalid role. Must be admin, member, or viewer.');
225
+ }
226
+
227
+ const { supabase, auth } = ctx;
228
+ const token = generateInviteToken();
229
+
230
+ // Check for existing pending invite
231
+ const { data: existing } = await supabase
232
+ .from('organization_invites')
233
+ .select('id')
234
+ .eq('organization_id', organization_id)
235
+ .eq('email', email)
236
+ .is('accepted_at', null)
237
+ .gt('expires_at', new Date().toISOString())
238
+ .maybeSingle();
239
+
240
+ if (existing) {
241
+ throw new Error(`A pending invite already exists for ${email}`);
242
+ }
243
+
244
+ const { data, error } = await supabase
245
+ .from('organization_invites')
246
+ .insert({
247
+ organization_id,
248
+ email,
249
+ role,
250
+ token,
251
+ invited_by: auth.userId,
252
+ })
253
+ .select()
254
+ .single();
255
+
256
+ if (error) throw new Error(`Failed to create invite: ${error.message}`);
257
+
258
+ return {
259
+ result: {
260
+ success: true,
261
+ invite: data,
262
+ message: `Invite sent to ${email} with role "${role}"`,
263
+ },
264
+ };
265
+ };
266
+
267
+ export const updateMemberRole: Handler = async (args, ctx) => {
268
+ const { organization_id, user_id, role } = args as {
269
+ organization_id: string;
270
+ user_id: string;
271
+ role: Role;
272
+ };
273
+
274
+ validateRequired(organization_id, 'organization_id');
275
+ validateRequired(user_id, 'user_id');
276
+ validateRequired(role, 'role');
277
+ validateUUID(organization_id, 'organization_id');
278
+ validateUUID(user_id, 'user_id');
279
+
280
+ if (!ROLE_ORDER.includes(role)) {
281
+ throw new Error(`Invalid role. Must be one of: ${ROLE_ORDER.join(', ')}`);
282
+ }
283
+
284
+ if (role === 'owner') {
285
+ throw new Error('Cannot assign owner role. Use transfer ownership instead.');
286
+ }
287
+
288
+ const { supabase, auth } = ctx;
289
+
290
+ // Prevent demoting yourself
291
+ if (user_id === auth.userId) {
292
+ throw new Error('Cannot change your own role');
293
+ }
294
+
295
+ const { data, error } = await supabase
296
+ .from('organization_members')
297
+ .update({ role })
298
+ .eq('organization_id', organization_id)
299
+ .eq('user_id', user_id)
300
+ .neq('role', 'owner') // Cannot change owner's role
301
+ .select()
302
+ .single();
303
+
304
+ if (error) throw new Error(`Failed to update member role: ${error.message}`);
305
+
306
+ return { result: { success: true, member: data } };
307
+ };
308
+
309
+ export const removeMember: Handler = async (args, ctx) => {
310
+ const { organization_id, user_id } = args as {
311
+ organization_id: string;
312
+ user_id: string;
313
+ };
314
+
315
+ validateRequired(organization_id, 'organization_id');
316
+ validateRequired(user_id, 'user_id');
317
+ validateUUID(organization_id, 'organization_id');
318
+ validateUUID(user_id, 'user_id');
319
+
320
+ const { supabase } = ctx;
321
+
322
+ const { error } = await supabase
323
+ .from('organization_members')
324
+ .delete()
325
+ .eq('organization_id', organization_id)
326
+ .eq('user_id', user_id)
327
+ .neq('role', 'owner'); // Cannot remove owner
328
+
329
+ if (error) throw new Error(`Failed to remove member: ${error.message}`);
330
+
331
+ return {
332
+ result: {
333
+ success: true,
334
+ message: 'Member removed. Their org-scoped API keys have been invalidated.',
335
+ },
336
+ };
337
+ };
338
+
339
+ export const leaveOrganization: Handler = async (args, ctx) => {
340
+ const { organization_id } = args as { organization_id: string };
341
+
342
+ validateRequired(organization_id, 'organization_id');
343
+ validateUUID(organization_id, 'organization_id');
344
+
345
+ const { supabase, auth } = ctx;
346
+
347
+ // Check if user is owner
348
+ const { data: membership } = await supabase
349
+ .from('organization_members')
350
+ .select('role')
351
+ .eq('organization_id', organization_id)
352
+ .eq('user_id', auth.userId)
353
+ .single();
354
+
355
+ if (membership?.role === 'owner') {
356
+ throw new Error('Owner cannot leave. Transfer ownership first or delete the organization.');
357
+ }
358
+
359
+ const { error } = await supabase
360
+ .from('organization_members')
361
+ .delete()
362
+ .eq('organization_id', organization_id)
363
+ .eq('user_id', auth.userId);
364
+
365
+ if (error) throw new Error(`Failed to leave organization: ${error.message}`);
366
+
367
+ return {
368
+ result: {
369
+ success: true,
370
+ message: 'You have left the organization.',
371
+ },
372
+ };
373
+ };
374
+
375
+ // ============================================================================
376
+ // Project Sharing
377
+ // ============================================================================
378
+
379
+ export const shareProjectWithOrg: Handler = async (args, ctx) => {
380
+ const { project_id, organization_id, permission = 'read' } = args as {
381
+ project_id: string;
382
+ organization_id: string;
383
+ permission?: Permission;
384
+ };
385
+
386
+ validateRequired(project_id, 'project_id');
387
+ validateRequired(organization_id, 'organization_id');
388
+ validateUUID(project_id, 'project_id');
389
+ validateUUID(organization_id, 'organization_id');
390
+
391
+ if (!PERMISSION_ORDER.includes(permission)) {
392
+ throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
393
+ }
394
+
395
+ const { supabase, auth } = ctx;
396
+
397
+ // Verify user owns the project
398
+ const { data: project } = await supabase
399
+ .from('projects')
400
+ .select('id, name')
401
+ .eq('id', project_id)
402
+ .eq('user_id', auth.userId)
403
+ .single();
404
+
405
+ if (!project) {
406
+ throw new Error('Project not found or you are not the owner');
407
+ }
408
+
409
+ // Check if share already exists
410
+ const { data: existing } = await supabase
411
+ .from('project_shares')
412
+ .select('id')
413
+ .eq('project_id', project_id)
414
+ .eq('organization_id', organization_id)
415
+ .maybeSingle();
416
+
417
+ if (existing) {
418
+ throw new Error('Project is already shared with this organization');
419
+ }
420
+
421
+ const { data, error } = await supabase
422
+ .from('project_shares')
423
+ .insert({
424
+ project_id,
425
+ organization_id,
426
+ permission,
427
+ shared_by: auth.userId,
428
+ })
429
+ .select()
430
+ .single();
431
+
432
+ if (error) throw new Error(`Failed to share project: ${error.message}`);
433
+
434
+ return {
435
+ result: {
436
+ success: true,
437
+ share: data,
438
+ message: `Project "${project.name}" shared with organization (${permission} access)`,
439
+ },
440
+ };
441
+ };
442
+
443
+ export const updateProjectShare: Handler = async (args, ctx) => {
444
+ const { project_id, organization_id, permission } = args as {
445
+ project_id: string;
446
+ organization_id: string;
447
+ permission: Permission;
448
+ };
449
+
450
+ validateRequired(project_id, 'project_id');
451
+ validateRequired(organization_id, 'organization_id');
452
+ validateRequired(permission, 'permission');
453
+ validateUUID(project_id, 'project_id');
454
+ validateUUID(organization_id, 'organization_id');
455
+
456
+ if (!PERMISSION_ORDER.includes(permission)) {
457
+ throw new Error(`Invalid permission. Must be one of: ${PERMISSION_ORDER.join(', ')}`);
458
+ }
459
+
460
+ const { supabase } = ctx;
461
+
462
+ const { data, error } = await supabase
463
+ .from('project_shares')
464
+ .update({ permission })
465
+ .eq('project_id', project_id)
466
+ .eq('organization_id', organization_id)
467
+ .select()
468
+ .single();
469
+
470
+ if (error) throw new Error(`Failed to update share: ${error.message}`);
471
+
472
+ return { result: { success: true, share: data } };
473
+ };
474
+
475
+ export const unshareProject: Handler = async (args, ctx) => {
476
+ const { project_id, organization_id } = args as {
477
+ project_id: string;
478
+ organization_id: string;
479
+ };
480
+
481
+ validateRequired(project_id, 'project_id');
482
+ validateRequired(organization_id, 'organization_id');
483
+ validateUUID(project_id, 'project_id');
484
+ validateUUID(organization_id, 'organization_id');
485
+
486
+ const { supabase } = ctx;
487
+
488
+ const { error } = await supabase
489
+ .from('project_shares')
490
+ .delete()
491
+ .eq('project_id', project_id)
492
+ .eq('organization_id', organization_id);
493
+
494
+ if (error) throw new Error(`Failed to unshare project: ${error.message}`);
495
+
496
+ return {
497
+ result: {
498
+ success: true,
499
+ message: 'Project share removed. Org members can no longer access this project.',
500
+ },
501
+ };
502
+ };
503
+
504
+ export const listProjectShares: Handler = async (args, ctx) => {
505
+ const { project_id } = args as { project_id: string };
506
+
507
+ validateRequired(project_id, 'project_id');
508
+ validateUUID(project_id, 'project_id');
509
+
510
+ const { supabase } = ctx;
511
+
512
+ const { data, error } = await supabase
513
+ .from('project_shares')
514
+ .select(`
515
+ id,
516
+ permission,
517
+ shared_at,
518
+ shared_by,
519
+ organizations (
520
+ id,
521
+ name,
522
+ slug
523
+ )
524
+ `)
525
+ .eq('project_id', project_id)
526
+ .order('shared_at', { ascending: false });
527
+
528
+ if (error) throw new Error(`Failed to list shares: ${error.message}`);
529
+
530
+ return { result: { shares: data || [], count: data?.length || 0 } };
531
+ };
532
+
533
+ /**
534
+ * Organizations handlers registry
535
+ */
536
+ export const organizationHandlers: HandlerRegistry = {
537
+ list_organizations: listOrganizations,
538
+ create_organization: createOrganization,
539
+ update_organization: updateOrganization,
540
+ delete_organization: deleteOrganization,
541
+ list_org_members: listOrgMembers,
542
+ invite_member: inviteMember,
543
+ update_member_role: updateMemberRole,
544
+ remove_member: removeMember,
545
+ leave_organization: leaveOrganization,
546
+ share_project_with_org: shareProjectWithOrg,
547
+ update_project_share: updateProjectShare,
548
+ unshare_project: unshareProject,
549
+ list_project_shares: listProjectShares,
550
+ };