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