@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,369 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+ import type { SupabaseClient } from '@supabase/supabase-js';
3
+ import type { HandlerContext, TokenUsage } from './types.js';
4
+ import { logProgress, getActivityFeed } from './progress.js';
5
+ import { ValidationError } from '../validators.js';
6
+
7
+ // ============================================================================
8
+ // Test Utilities
9
+ // ============================================================================
10
+
11
+ function createMockSupabase(overrides: {
12
+ selectResult?: { data: unknown; error: unknown };
13
+ insertResult?: { data: unknown; error: unknown };
14
+ } = {}) {
15
+ const defaultResult = { data: null, error: null };
16
+ let currentOperation = 'select';
17
+
18
+ const mock = {
19
+ from: vi.fn().mockReturnThis(),
20
+ select: vi.fn(() => {
21
+ currentOperation = 'select';
22
+ return mock;
23
+ }),
24
+ insert: vi.fn(() => {
25
+ currentOperation = 'insert';
26
+ return mock;
27
+ }),
28
+ eq: vi.fn().mockReturnThis(),
29
+ gt: vi.fn().mockReturnThis(),
30
+ order: vi.fn().mockReturnThis(),
31
+ limit: vi.fn().mockReturnThis(),
32
+ then: vi.fn((resolve: (value: unknown) => void) => {
33
+ if (currentOperation === 'insert') {
34
+ return Promise.resolve(overrides.insertResult ?? defaultResult).then(resolve);
35
+ }
36
+ if (currentOperation === 'select') {
37
+ return Promise.resolve(overrides.selectResult ?? defaultResult).then(resolve);
38
+ }
39
+ return Promise.resolve(defaultResult).then(resolve);
40
+ }),
41
+ };
42
+
43
+ return mock as unknown as SupabaseClient;
44
+ }
45
+
46
+ function createMockContext(
47
+ supabase: SupabaseClient,
48
+ options: { sessionId?: string | null } = {}
49
+ ): HandlerContext {
50
+ const defaultTokenUsage: TokenUsage = {
51
+ callCount: 5,
52
+ totalTokens: 2500,
53
+ byTool: {},
54
+ };
55
+
56
+ const sessionId = 'sessionId' in options ? options.sessionId : 'session-123';
57
+
58
+ return {
59
+ supabase,
60
+ auth: { userId: 'user-123', apiKeyId: 'api-key-123' },
61
+ session: {
62
+ instanceId: 'instance-abc',
63
+ currentSessionId: sessionId,
64
+ currentPersona: 'Wave',
65
+ tokenUsage: defaultTokenUsage,
66
+ },
67
+ updateSession: vi.fn(),
68
+ };
69
+ }
70
+
71
+ // ============================================================================
72
+ // logProgress Tests
73
+ // ============================================================================
74
+
75
+ describe('logProgress', () => {
76
+ beforeEach(() => vi.clearAllMocks());
77
+
78
+ it('should log progress successfully', async () => {
79
+ const supabase = createMockSupabase({
80
+ insertResult: { data: null, error: null },
81
+ });
82
+ const ctx = createMockContext(supabase);
83
+
84
+ const result = await logProgress(
85
+ {
86
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
87
+ summary: 'Completed initial setup',
88
+ },
89
+ ctx
90
+ );
91
+
92
+ expect(result.result).toMatchObject({
93
+ success: true,
94
+ });
95
+ });
96
+
97
+ it('should log progress with task_id and details', async () => {
98
+ const supabase = createMockSupabase({
99
+ insertResult: { data: null, error: null },
100
+ });
101
+ const ctx = createMockContext(supabase);
102
+
103
+ await logProgress(
104
+ {
105
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
106
+ task_id: '456e4567-e89b-12d3-a456-426614174000',
107
+ summary: 'Working on feature',
108
+ details: 'Added the main component and started styling',
109
+ },
110
+ ctx
111
+ );
112
+
113
+ expect(supabase.insert).toHaveBeenCalledWith(
114
+ expect.objectContaining({
115
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
116
+ task_id: '456e4567-e89b-12d3-a456-426614174000',
117
+ summary: 'Working on feature',
118
+ details: 'Added the main component and started styling',
119
+ created_by: 'agent',
120
+ created_by_session_id: 'session-123',
121
+ })
122
+ );
123
+ });
124
+
125
+ it('should include session_id in insert', async () => {
126
+ const supabase = createMockSupabase({
127
+ insertResult: { data: null, error: null },
128
+ });
129
+ const ctx = createMockContext(supabase, { sessionId: 'my-session' });
130
+
131
+ await logProgress(
132
+ {
133
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
134
+ summary: 'Progress update',
135
+ },
136
+ ctx
137
+ );
138
+
139
+ expect(supabase.insert).toHaveBeenCalledWith(
140
+ expect.objectContaining({
141
+ created_by: 'agent',
142
+ created_by_session_id: 'my-session',
143
+ })
144
+ );
145
+ });
146
+
147
+ it('should query progress_logs table', async () => {
148
+ const supabase = createMockSupabase({
149
+ insertResult: { data: null, error: null },
150
+ });
151
+ const ctx = createMockContext(supabase);
152
+
153
+ await logProgress(
154
+ {
155
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
156
+ summary: 'Progress',
157
+ },
158
+ ctx
159
+ );
160
+
161
+ expect(supabase.from).toHaveBeenCalledWith('progress_logs');
162
+ });
163
+
164
+ it('should throw error when insert fails', async () => {
165
+ const supabase = createMockSupabase({
166
+ insertResult: { data: null, error: { message: 'Insert failed' } },
167
+ });
168
+ const ctx = createMockContext(supabase);
169
+
170
+ await expect(
171
+ logProgress({
172
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
173
+ summary: 'Progress',
174
+ }, ctx)
175
+ ).rejects.toThrow('Failed to log progress: Insert failed');
176
+ });
177
+ });
178
+
179
+ // ============================================================================
180
+ // getActivityFeed Tests
181
+ // ============================================================================
182
+
183
+ describe('getActivityFeed', () => {
184
+ beforeEach(() => vi.clearAllMocks());
185
+
186
+ it('should throw error for missing project_id', async () => {
187
+ const supabase = createMockSupabase();
188
+ const ctx = createMockContext(supabase);
189
+
190
+ await expect(getActivityFeed({}, ctx)).rejects.toThrow(ValidationError);
191
+ });
192
+
193
+ it('should throw error for invalid project_id UUID', async () => {
194
+ const supabase = createMockSupabase();
195
+ const ctx = createMockContext(supabase);
196
+
197
+ await expect(
198
+ getActivityFeed({ project_id: 'invalid' }, ctx)
199
+ ).rejects.toThrow(ValidationError);
200
+ });
201
+
202
+ it('should return empty activities when no data', async () => {
203
+ const supabase = createMockSupabase({
204
+ selectResult: { data: [], error: null },
205
+ });
206
+ const ctx = createMockContext(supabase);
207
+
208
+ const result = await getActivityFeed(
209
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
210
+ ctx
211
+ );
212
+
213
+ expect(result.result).toMatchObject({
214
+ activities: [],
215
+ });
216
+ });
217
+
218
+ it('should query all activity types by default', async () => {
219
+ const supabase = createMockSupabase({
220
+ selectResult: { data: [], error: null },
221
+ });
222
+ const ctx = createMockContext(supabase);
223
+
224
+ await getActivityFeed(
225
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
226
+ ctx
227
+ );
228
+
229
+ // Should query tasks, progress_logs, blockers, decisions
230
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
231
+ expect(supabase.from).toHaveBeenCalledWith('progress_logs');
232
+ expect(supabase.from).toHaveBeenCalledWith('blockers');
233
+ expect(supabase.from).toHaveBeenCalledWith('decisions');
234
+ });
235
+
236
+ it('should filter by specific types', async () => {
237
+ const supabase = createMockSupabase({
238
+ selectResult: { data: [], error: null },
239
+ });
240
+ const ctx = createMockContext(supabase);
241
+
242
+ await getActivityFeed(
243
+ {
244
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
245
+ types: ['task', 'blocker'],
246
+ },
247
+ ctx
248
+ );
249
+
250
+ // Should only query tasks and blockers
251
+ expect(supabase.from).toHaveBeenCalledWith('tasks');
252
+ expect(supabase.from).toHaveBeenCalledWith('blockers');
253
+ expect(supabase.from).not.toHaveBeenCalledWith('progress_logs');
254
+ expect(supabase.from).not.toHaveBeenCalledWith('decisions');
255
+ });
256
+
257
+ it('should respect limit parameter', async () => {
258
+ const supabase = createMockSupabase({
259
+ selectResult: { data: [], error: null },
260
+ });
261
+ const ctx = createMockContext(supabase);
262
+
263
+ await getActivityFeed(
264
+ {
265
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
266
+ limit: 10,
267
+ },
268
+ ctx
269
+ );
270
+
271
+ expect(supabase.limit).toHaveBeenCalled();
272
+ });
273
+
274
+ it('should cap limit at 200', async () => {
275
+ const supabase = createMockSupabase({
276
+ selectResult: { data: [], error: null },
277
+ });
278
+ const ctx = createMockContext(supabase);
279
+
280
+ const result = await getActivityFeed(
281
+ {
282
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
283
+ limit: 500,
284
+ },
285
+ ctx
286
+ );
287
+
288
+ // Should return at most 200 items
289
+ expect((result.result as { activities: unknown[] }).activities.length).toBeLessThanOrEqual(200);
290
+ });
291
+
292
+ it('should sort activities by created_at descending', async () => {
293
+ const mockTasks = [
294
+ { id: 't1', title: 'Task 1', status: 'pending', created_by: 'agent', created_at: '2025-01-14T10:00:00Z' },
295
+ ];
296
+ const mockProgress = [
297
+ { id: 'p1', summary: 'Progress 1', created_by: 'agent', created_at: '2025-01-14T11:00:00Z' },
298
+ ];
299
+
300
+ let callCount = 0;
301
+ const supabase = {
302
+ from: vi.fn().mockReturnThis(),
303
+ select: vi.fn().mockReturnThis(),
304
+ eq: vi.fn().mockReturnThis(),
305
+ gt: vi.fn().mockReturnThis(),
306
+ order: vi.fn().mockReturnThis(),
307
+ limit: vi.fn().mockReturnThis(),
308
+ then: vi.fn((resolve: (val: unknown) => void) => {
309
+ callCount++;
310
+ if (callCount === 1) {
311
+ return Promise.resolve({ data: mockTasks, error: null }).then(resolve);
312
+ }
313
+ if (callCount === 2) {
314
+ return Promise.resolve({ data: mockProgress, error: null }).then(resolve);
315
+ }
316
+ return Promise.resolve({ data: [], error: null }).then(resolve);
317
+ }),
318
+ } as unknown as SupabaseClient;
319
+
320
+ const ctx = createMockContext(supabase);
321
+
322
+ const result = await getActivityFeed(
323
+ { project_id: '123e4567-e89b-12d3-a456-426614174000' },
324
+ ctx
325
+ );
326
+
327
+ const activities = (result.result as { activities: { created_at: string }[] }).activities;
328
+ // Should be sorted by date descending (most recent first)
329
+ for (let i = 1; i < activities.length; i++) {
330
+ const prevDate = new Date(activities[i - 1].created_at).getTime();
331
+ const currDate = new Date(activities[i].created_at).getTime();
332
+ expect(prevDate).toBeGreaterThanOrEqual(currDate);
333
+ }
334
+ });
335
+
336
+ it('should filter by created_by', async () => {
337
+ const supabase = createMockSupabase({
338
+ selectResult: { data: [], error: null },
339
+ });
340
+ const ctx = createMockContext(supabase);
341
+
342
+ await getActivityFeed(
343
+ {
344
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
345
+ created_by: 'agent',
346
+ },
347
+ ctx
348
+ );
349
+
350
+ expect(supabase.eq).toHaveBeenCalledWith('created_by', 'agent');
351
+ });
352
+
353
+ it('should filter by since parameter', async () => {
354
+ const supabase = createMockSupabase({
355
+ selectResult: { data: [], error: null },
356
+ });
357
+ const ctx = createMockContext(supabase);
358
+
359
+ await getActivityFeed(
360
+ {
361
+ project_id: '123e4567-e89b-12d3-a456-426614174000',
362
+ since: '2025-01-14T00:00:00Z',
363
+ },
364
+ ctx
365
+ );
366
+
367
+ expect(supabase.gt).toHaveBeenCalledWith('created_at', '2025-01-14T00:00:00Z');
368
+ });
369
+ });
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Progress Handlers
3
+ *
4
+ * Handles progress logging and activity feed:
5
+ * - log_progress
6
+ * - get_activity_feed
7
+ */
8
+
9
+ import type { Handler, HandlerRegistry } from './types.js';
10
+ import { validateRequired, validateUUID } from '../validators.js';
11
+
12
+ export const logProgress: Handler = async (args, ctx) => {
13
+ const { project_id, task_id, summary, details } = args as {
14
+ project_id: string;
15
+ task_id?: string;
16
+ summary: string;
17
+ details?: string;
18
+ };
19
+
20
+ const { supabase, session } = ctx;
21
+
22
+ const { error } = await supabase
23
+ .from('progress_logs')
24
+ .insert({
25
+ project_id,
26
+ task_id: task_id || null,
27
+ summary,
28
+ details: details || null,
29
+ created_by: 'agent',
30
+ created_by_session_id: session.currentSessionId,
31
+ });
32
+
33
+ if (error) throw new Error(`Failed to log progress: ${error.message}`);
34
+
35
+ return { result: { success: true } };
36
+ };
37
+
38
+ export const getActivityFeed: Handler = async (args, ctx) => {
39
+ const { project_id, types, created_by, since, limit = 50 } = args as {
40
+ project_id: string;
41
+ types?: ('task' | 'progress' | 'blocker' | 'decision')[];
42
+ created_by?: 'agent' | 'user';
43
+ since?: string;
44
+ limit?: number;
45
+ };
46
+
47
+ validateRequired(project_id, 'project_id');
48
+ validateUUID(project_id, 'project_id');
49
+
50
+ const { supabase } = ctx;
51
+ const effectiveLimit = Math.min(limit, 200);
52
+ const includeTypes = types || ['task', 'progress', 'blocker', 'decision'];
53
+ // Fetch slightly more per type to account for interleaving, but not full limit
54
+ const perTypeLimit = Math.min(Math.ceil(effectiveLimit / includeTypes.length) * 2, effectiveLimit);
55
+
56
+ interface ActivityItem {
57
+ type: string;
58
+ id: string;
59
+ title?: string;
60
+ summary?: string;
61
+ description?: string;
62
+ status?: string;
63
+ created_by: string;
64
+ created_at: string;
65
+ }
66
+
67
+ const activities: ActivityItem[] = [];
68
+
69
+ // Fetch tasks if included
70
+ if (includeTypes.includes('task')) {
71
+ let query = supabase
72
+ .from('tasks')
73
+ .select('id, title, status, created_by, created_at, completed_at')
74
+ .eq('project_id', project_id)
75
+ .order('created_at', { ascending: false })
76
+ .limit(perTypeLimit);
77
+
78
+ if (created_by) query = query.eq('created_by', created_by);
79
+ if (since) query = query.gt('created_at', since);
80
+
81
+ const { data: tasks } = await query;
82
+ if (tasks) {
83
+ for (const task of tasks) {
84
+ activities.push({
85
+ type: 'task',
86
+ id: task.id,
87
+ title: task.title,
88
+ status: task.status,
89
+ created_by: task.created_by,
90
+ created_at: task.created_at,
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ // Fetch progress logs if included
97
+ if (includeTypes.includes('progress')) {
98
+ let query = supabase
99
+ .from('progress_logs')
100
+ .select('id, summary, created_by, created_at')
101
+ .eq('project_id', project_id)
102
+ .order('created_at', { ascending: false })
103
+ .limit(perTypeLimit);
104
+
105
+ if (created_by) query = query.eq('created_by', created_by);
106
+ if (since) query = query.gt('created_at', since);
107
+
108
+ const { data: logs } = await query;
109
+ if (logs) {
110
+ for (const log of logs) {
111
+ activities.push({
112
+ type: 'progress',
113
+ id: log.id,
114
+ summary: log.summary,
115
+ created_by: log.created_by,
116
+ created_at: log.created_at,
117
+ });
118
+ }
119
+ }
120
+ }
121
+
122
+ // Fetch blockers if included
123
+ if (includeTypes.includes('blocker')) {
124
+ let query = supabase
125
+ .from('blockers')
126
+ .select('id, description, status, created_by, created_at')
127
+ .eq('project_id', project_id)
128
+ .order('created_at', { ascending: false })
129
+ .limit(perTypeLimit);
130
+
131
+ if (created_by) query = query.eq('created_by', created_by);
132
+ if (since) query = query.gt('created_at', since);
133
+
134
+ const { data: blockers } = await query;
135
+ if (blockers) {
136
+ for (const blocker of blockers) {
137
+ activities.push({
138
+ type: 'blocker',
139
+ id: blocker.id,
140
+ description: blocker.description,
141
+ status: blocker.status,
142
+ created_by: blocker.created_by,
143
+ created_at: blocker.created_at,
144
+ });
145
+ }
146
+ }
147
+ }
148
+
149
+ // Fetch decisions if included
150
+ if (includeTypes.includes('decision')) {
151
+ let query = supabase
152
+ .from('decisions')
153
+ .select('id, title, created_by, created_at')
154
+ .eq('project_id', project_id)
155
+ .order('created_at', { ascending: false })
156
+ .limit(perTypeLimit);
157
+
158
+ if (created_by) query = query.eq('created_by', created_by);
159
+ if (since) query = query.gt('created_at', since);
160
+
161
+ const { data: decisions } = await query;
162
+ if (decisions) {
163
+ for (const decision of decisions) {
164
+ activities.push({
165
+ type: 'decision',
166
+ id: decision.id,
167
+ title: decision.title,
168
+ created_by: decision.created_by,
169
+ created_at: decision.created_at,
170
+ });
171
+ }
172
+ }
173
+ }
174
+
175
+ // Sort by created_at descending and limit
176
+ activities.sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime());
177
+ const limitedActivities = activities.slice(0, effectiveLimit);
178
+
179
+ return { result: { activities: limitedActivities } };
180
+ };
181
+
182
+ /**
183
+ * Progress handlers registry
184
+ */
185
+ export const progressHandlers: HandlerRegistry = {
186
+ log_progress: logProgress,
187
+ get_activity_feed: getActivityFeed,
188
+ };