@codeguide/core 0.0.27 → 0.0.29

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 (92) hide show
  1. package/README.md +50 -41
  2. package/__tests__/services/codespace/codespace-v2.test.ts +29 -18
  3. package/__tests__/services/usage/usage-service.test.ts +597 -85
  4. package/codeguide.ts +6 -0
  5. package/dist/codeguide.d.ts +3 -1
  6. package/dist/codeguide.js +2 -0
  7. package/dist/index.d.ts +4 -3
  8. package/dist/services/base/base-service.d.ts +21 -0
  9. package/dist/services/base/base-service.js +114 -0
  10. package/dist/services/codespace/codespace-service.d.ts +55 -1
  11. package/dist/services/codespace/codespace-service.js +260 -5
  12. package/dist/services/codespace/codespace-types.d.ts +193 -13
  13. package/dist/services/codespace/index.d.ts +1 -1
  14. package/dist/services/index.d.ts +4 -0
  15. package/dist/services/index.js +7 -1
  16. package/dist/services/projects/project-types.d.ts +66 -32
  17. package/dist/services/repository-analysis/repository-types.d.ts +1 -0
  18. package/dist/services/starter-kits/index.d.ts +2 -0
  19. package/dist/services/starter-kits/index.js +20 -0
  20. package/dist/services/starter-kits/starter-kits-service.d.ts +13 -0
  21. package/dist/services/starter-kits/starter-kits-service.js +27 -0
  22. package/dist/services/starter-kits/starter-kits-types.d.ts +34 -0
  23. package/dist/services/starter-kits/starter-kits-types.js +2 -0
  24. package/dist/services/tasks/task-service.d.ts +2 -1
  25. package/dist/services/tasks/task-service.js +8 -0
  26. package/dist/services/tasks/task-types.d.ts +26 -7
  27. package/dist/services/usage/usage-service.d.ts +5 -2
  28. package/dist/services/usage/usage-service.js +58 -9
  29. package/dist/services/usage/usage-types.d.ts +207 -34
  30. package/dist/services/users/index.d.ts +2 -0
  31. package/dist/services/users/index.js +20 -0
  32. package/dist/services/users/user-service.d.ts +12 -0
  33. package/dist/services/users/user-service.js +17 -0
  34. package/dist/services/users/user-types.d.ts +55 -0
  35. package/dist/services/users/user-types.js +2 -0
  36. package/docs/.vitepress/README.md +51 -0
  37. package/docs/.vitepress/config.ts +139 -0
  38. package/docs/.vitepress/theme/custom.css +80 -0
  39. package/docs/.vitepress/theme/index.ts +13 -0
  40. package/docs/.vitepress/tsconfig.json +19 -0
  41. package/docs/QUICKSTART.md +77 -0
  42. package/docs/README.md +134 -0
  43. package/docs/README_SETUP.md +46 -0
  44. package/docs/authentication.md +351 -0
  45. package/docs/codeguide-client.md +350 -0
  46. package/docs/codespace-models.md +1004 -0
  47. package/docs/codespace-service.md +558 -81
  48. package/docs/index.md +135 -0
  49. package/docs/package.json +14 -0
  50. package/docs/projects-service.md +688 -0
  51. package/docs/security-keys-service.md +773 -0
  52. package/docs/starter-kits-service.md +249 -0
  53. package/docs/task-service.md +955 -0
  54. package/docs/testsprite_tests/TC001_Homepage_Load_and_Hero_Section_Display.py +70 -0
  55. package/docs/testsprite_tests/TC002_Sidebar_Navigation_ExpandCollapse_Functionality.py +73 -0
  56. package/docs/testsprite_tests/TC003_Full_Text_Local_Search_with_Keyboard_Shortcut.py +90 -0
  57. package/docs/testsprite_tests/TC004_Dark_Mode_Toggle_and_Persistence.py +73 -0
  58. package/docs/testsprite_tests/TC005_Mobile_Responsiveness_and_Touch_Navigation.py +113 -0
  59. package/docs/testsprite_tests/TC006_GitHub_Integration_Edit_this_page_Links.py +73 -0
  60. package/docs/testsprite_tests/TC007_Syntax_Highlighting_and_Code_Copy_Functionality.py +73 -0
  61. package/docs/testsprite_tests/TC008_Auto_Generated_Table_of_Contents_Accuracy.py +73 -0
  62. package/docs/testsprite_tests/TC009_SEO_and_Content_Discoverability_Verification.py +73 -0
  63. package/docs/testsprite_tests/TC010_Accessibility_Compliance_WCAG_AA.py +73 -0
  64. package/docs/testsprite_tests/TC011_Local_Development_Workflow_Build_and_Hot_Reload.py +74 -0
  65. package/docs/testsprite_tests/TC012_Performance_Metrics_Compliance.py +73 -0
  66. package/docs/testsprite_tests/standard_prd.json +122 -0
  67. package/docs/testsprite_tests/testsprite-mcp-test-report.html +2508 -0
  68. package/docs/testsprite_tests/testsprite-mcp-test-report.md +273 -0
  69. package/docs/testsprite_tests/testsprite_frontend_test_plan.json +390 -0
  70. package/docs/usage-service.md +616 -0
  71. package/index.ts +11 -3
  72. package/package.json +16 -2
  73. package/plans/CODESPACE_LOGS_STREAMING_GUIDE.md +320 -0
  74. package/plans/CODESPACE_TASK_LOGS_API_COMPLETE_GUIDE.md +821 -0
  75. package/services/base/base-service.ts +130 -0
  76. package/services/codespace/codespace-service.ts +347 -8
  77. package/services/codespace/codespace-types.ts +263 -14
  78. package/services/codespace/index.ts +16 -1
  79. package/services/index.ts +4 -0
  80. package/services/projects/README.md +107 -34
  81. package/services/projects/project-types.ts +69 -32
  82. package/services/repository-analysis/repository-types.ts +1 -0
  83. package/services/starter-kits/index.ts +2 -0
  84. package/services/starter-kits/starter-kits-service.ts +33 -0
  85. package/services/starter-kits/starter-kits-types.ts +38 -0
  86. package/services/tasks/task-service.ts +10 -0
  87. package/services/tasks/task-types.ts +29 -7
  88. package/services/usage/usage-service.ts +59 -10
  89. package/services/usage/usage-types.ts +239 -34
  90. package/services/users/index.ts +2 -0
  91. package/services/users/user-service.ts +15 -0
  92. package/services/users/user-types.ts +59 -0
@@ -238,6 +238,11 @@ export abstract class BaseService {
238
238
  return response.data
239
239
  }
240
240
 
241
+ protected async patch<T>(url: string, data?: any, config?: any): Promise<T> {
242
+ const response = await this.client.patch<T>(url, data, config)
243
+ return response.data
244
+ }
245
+
241
246
  protected async delete<T>(url: string, config?: any): Promise<T> {
242
247
  const response = await this.client.delete<T>(url, config)
243
248
  return response.data
@@ -246,4 +251,129 @@ export abstract class BaseService {
246
251
  protected buildUrl(endpoint: string): string {
247
252
  return endpoint.startsWith('/') ? endpoint : `/${endpoint}`
248
253
  }
254
+
255
+ /**
256
+ * Create a streaming connection for Server-Sent Events (SSE)
257
+ *
258
+ * @param url - The endpoint URL for streaming
259
+ * @param config - Optional configuration for the stream request
260
+ * @returns Promise that resolves to EventSource for SSE streaming
261
+ */
262
+ protected async createStream(url: string, config?: any): Promise<EventSource> {
263
+ // Build full URL
264
+ const fullUrl = `${this.client.defaults.baseURL}${this.buildUrl(url)}`
265
+
266
+ // For SSE, we need to use EventSource API directly
267
+ // However, we need to handle authentication differently since EventSource doesn't support custom headers
268
+ // We'll pass auth info as query parameters for SSE endpoints
269
+
270
+ return new Promise((resolve, reject) => {
271
+ try {
272
+ const eventSource = new EventSource(fullUrl)
273
+ resolve(eventSource)
274
+ } catch (error) {
275
+ reject(error)
276
+ }
277
+ })
278
+ }
279
+
280
+ /**
281
+ * Alternative streaming method using fetch for environments where EventSource is not available
282
+ * or when custom headers are needed for authentication
283
+ *
284
+ * @param url - The endpoint URL for streaming
285
+ * @param onMessage - Callback function for handling stream messages
286
+ * @param onError - Callback function for handling stream errors
287
+ * @param onComplete - Callback function for when stream completes
288
+ * @param config - Optional configuration for the stream request
289
+ * @returns Promise that resolves to a cleanup function
290
+ */
291
+ protected async createStreamWithFetch(
292
+ url: string,
293
+ onMessage: (data: any) => void,
294
+ onError?: (error: any) => void,
295
+ onComplete?: () => void,
296
+ config?: any
297
+ ): Promise<() => void> {
298
+ const streamConfig = {
299
+ ...config,
300
+ headers: {
301
+ ...this.client.defaults.headers,
302
+ 'Accept': 'text/event-stream',
303
+ 'Cache-Control': 'no-cache',
304
+ ...config?.headers,
305
+ },
306
+ }
307
+
308
+ const fullUrl = `${this.client.defaults.baseURL}${this.buildUrl(url)}`
309
+
310
+ try {
311
+ const response = await fetch(fullUrl, streamConfig)
312
+
313
+ if (!response.ok) {
314
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
315
+ }
316
+
317
+ const reader = response.body?.getReader()
318
+ if (!reader) {
319
+ throw new Error('Response body is not readable')
320
+ }
321
+
322
+ const decoder = new TextDecoder()
323
+ let buffer = ''
324
+
325
+ const processStream = async () => {
326
+ try {
327
+ while (true) {
328
+ const { done, value } = await reader.read()
329
+
330
+ if (done) {
331
+ onComplete?.()
332
+ break
333
+ }
334
+
335
+ buffer += decoder.decode(value, { stream: true })
336
+ const lines = buffer.split('\n')
337
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
338
+
339
+ for (const line of lines) {
340
+ if (line.startsWith('data: ')) {
341
+ const data = line.slice(6) // Remove 'data: ' prefix
342
+ if (data.trim()) {
343
+ try {
344
+ const parsedData = JSON.parse(data)
345
+ onMessage(parsedData)
346
+ } catch (parseError) {
347
+ console.warn('Failed to parse SSE data:', data, parseError)
348
+ }
349
+ }
350
+ } else if (line.startsWith('event: ')) {
351
+ const eventType = line.slice(7) // Remove 'event: ' prefix
352
+ // Handle different event types if needed
353
+ if (eventType === 'complete') {
354
+ onComplete?.()
355
+ return
356
+ } else if (eventType === 'error') {
357
+ onError?.(new Error('Stream error event received'))
358
+ return
359
+ }
360
+ }
361
+ }
362
+ }
363
+ } catch (error) {
364
+ onError?.(error)
365
+ }
366
+ }
367
+
368
+ processStream()
369
+
370
+ // Return cleanup function
371
+ return () => {
372
+ reader.cancel().catch(console.warn)
373
+ }
374
+ } catch (error) {
375
+ onError?.(error)
376
+ return () => {} // Return empty cleanup function on error
377
+ }
378
+ }
249
379
  }
@@ -22,6 +22,20 @@ import {
22
22
  GetLLMModelProvidersResponse,
23
23
  GetLLMModelProviderResponse,
24
24
  GetModelsByProviderResponse,
25
+ // GET /codespace/tasks Types
26
+ GetCodespaceTasksRequest,
27
+ GetCodespaceTasksResponse,
28
+ // GET /tasks/by-codespace-id Types
29
+ GetTasksByCodespaceIdRequest,
30
+ GetTasksByCodespaceIdResponse,
31
+ // Final Report Popup State Types
32
+ UpdateFinalReportPopupStateRequest,
33
+ UpdateFinalReportPopupStateResponse,
34
+ // Codespace Task Logs Types
35
+ GetCodespaceTaskLogsRequest,
36
+ CodespaceTaskLogsResponse,
37
+ StreamCodespaceTaskLogsRequest,
38
+ CodespaceLogStreamEvent,
25
39
  } from './codespace-types'
26
40
 
27
41
  export class CodespaceService extends BaseService {
@@ -29,7 +43,9 @@ export class CodespaceService extends BaseService {
29
43
  return this.post<GenerateTaskTitleResponse>('/codespace/generate-task-title', request)
30
44
  }
31
45
 
32
- async generateQuestionnaire(request: CodespaceQuestionnaireRequest): Promise<CodespaceQuestionnaireResponse> {
46
+ async generateQuestionnaire(
47
+ request: CodespaceQuestionnaireRequest
48
+ ): Promise<CodespaceQuestionnaireResponse> {
33
49
  this.validateQuestionnaireRequest(request)
34
50
  return this.post<CodespaceQuestionnaireResponse>('/codespace/generate-questionnaire', request)
35
51
  }
@@ -98,6 +114,216 @@ export class CodespaceService extends BaseService {
98
114
  return this.get<CodespaceTaskDetailedResponse>(`/codespace/task/${codespaceTaskId}/detailed`)
99
115
  }
100
116
 
117
+ /**
118
+ * Get tasks by codespace task ID with optional pagination and sorting
119
+ *
120
+ * GET /tasks/by-codespace-id/{codespace_task_id}
121
+ *
122
+ * @param params - Request parameters including codespace_task_id and optional pagination/sorting
123
+ * @returns Promise resolving to paginated list of tasks associated with the codespace task ID
124
+ */
125
+ async getTasksByCodespaceId(
126
+ params: GetTasksByCodespaceIdRequest
127
+ ): Promise<GetTasksByCodespaceIdResponse> {
128
+ this.validateGetTasksByCodespaceIdRequest(params)
129
+
130
+ const queryParams = new URLSearchParams()
131
+
132
+ // Add pagination parameters
133
+ if (params.limit !== undefined) {
134
+ queryParams.append('limit', params.limit.toString())
135
+ }
136
+ if (params.offset !== undefined) {
137
+ queryParams.append('offset', params.offset.toString())
138
+ }
139
+
140
+ // Add sorting parameters
141
+ if (params.sort_by) {
142
+ queryParams.append('sort_by', params.sort_by)
143
+ }
144
+ if (params.sort_order) {
145
+ queryParams.append('sort_order', params.sort_order)
146
+ }
147
+
148
+ const url = `/tasks/by-codespace-id/${params.codespace_task_id}${
149
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
150
+ }`
151
+
152
+ return this.get<GetTasksByCodespaceIdResponse>(url)
153
+ }
154
+
155
+ /**
156
+ * Get codespace tasks with optional filtering and pagination
157
+ *
158
+ * GET /codespace/tasks
159
+ *
160
+ * @param params - Optional query parameters for filtering, sorting, and pagination
161
+ * @returns Promise resolving to paginated list of codespace tasks with model and attachment info
162
+ */
163
+ async getCodespaceTasks(params?: GetCodespaceTasksRequest): Promise<GetCodespaceTasksResponse> {
164
+ const queryParams = new URLSearchParams()
165
+
166
+ if (params?.task_status) queryParams.append('task_status', params.task_status)
167
+ if (params?.project_id) queryParams.append('project_id', params.project_id)
168
+ if (params?.limit !== undefined) {
169
+ // Validate limit is between 1 and 100
170
+ if (params.limit < 1 || params.limit > 100) {
171
+ throw new Error('limit must be between 1 and 100')
172
+ }
173
+ queryParams.append('limit', params.limit.toString())
174
+ }
175
+ if (params?.offset !== undefined) {
176
+ // Validate offset is non-negative
177
+ if (params.offset < 0) {
178
+ throw new Error('offset must be 0 or greater')
179
+ }
180
+ queryParams.append('offset', params.offset.toString())
181
+ }
182
+ if (params?.sort_by) queryParams.append('sort_by', params.sort_by)
183
+ if (params?.sort_order) queryParams.append('sort_order', params.sort_order)
184
+
185
+ const url = `/codespace/tasks${queryParams.toString() ? `?${queryParams.toString()}` : ''}`
186
+ return this.get<GetCodespaceTasksResponse>(url)
187
+ }
188
+
189
+ // ============================================================================
190
+ // Task Status Update Methods
191
+ // ============================================================================
192
+
193
+ /**
194
+ * Update the final report popup state for a codespace task
195
+ *
196
+ * PATCH /task/{codespace_task_id}/final-report-popup-state
197
+ *
198
+ * @param codespaceTaskId - The ID of the codespace task
199
+ * @param request - The request body containing the new popup state
200
+ * @returns Promise resolving to the updated popup state response
201
+ */
202
+ async updateFinalReportPopupState(
203
+ codespaceTaskId: string,
204
+ request: UpdateFinalReportPopupStateRequest
205
+ ): Promise<UpdateFinalReportPopupStateResponse> {
206
+ if (!codespaceTaskId) {
207
+ throw new Error('codespace_task_id is required')
208
+ }
209
+
210
+ // Validate the popup state value
211
+ if (!['not_ready', 'open', 'closed'].includes(request.final_report_popup_state)) {
212
+ throw new Error('final_report_popup_state must be "not_ready", "open", or "closed"')
213
+ }
214
+
215
+ return this.patch<UpdateFinalReportPopupStateResponse>(
216
+ `/task/${codespaceTaskId}/final-report-popup-state`,
217
+ request
218
+ )
219
+ }
220
+
221
+ // ============================================================================
222
+ // Codespace Task Logs Methods
223
+ // ============================================================================
224
+
225
+ /**
226
+ * Get paginated logs for a codespace task with optional filtering and sorting
227
+ *
228
+ * GET /codespace/task/{codespace_task_id}/logs
229
+ *
230
+ * @param request - Request parameters including codespace_task_id and optional filters
231
+ * @returns Promise resolving to paginated logs response
232
+ */
233
+ async getCodespaceTaskLogs(
234
+ request: GetCodespaceTaskLogsRequest
235
+ ): Promise<CodespaceTaskLogsResponse> {
236
+ this.validateGetLogsRequest(request)
237
+
238
+ const queryParams = new URLSearchParams()
239
+
240
+ // Add pagination parameters
241
+ if (request.limit !== undefined) {
242
+ queryParams.append('limit', request.limit.toString())
243
+ }
244
+ if (request.offset !== undefined) {
245
+ queryParams.append('offset', request.offset.toString())
246
+ }
247
+
248
+ // Add filter parameters
249
+ if (request.log_type) {
250
+ queryParams.append('log_type', request.log_type)
251
+ }
252
+ if (request.step_name) {
253
+ queryParams.append('step_name', request.step_name)
254
+ }
255
+ if (request.search) {
256
+ queryParams.append('search', request.search)
257
+ }
258
+ if (request.since) {
259
+ queryParams.append('since', request.since)
260
+ }
261
+
262
+ // Add sorting parameters
263
+ if (request.sort_by) {
264
+ queryParams.append('sort_by', request.sort_by)
265
+ }
266
+ if (request.sort_order) {
267
+ queryParams.append('sort_order', request.sort_order)
268
+ }
269
+
270
+ const url = `/codespace/task/${request.codespace_task_id}/logs${
271
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
272
+ }`
273
+
274
+ return this.get<CodespaceTaskLogsResponse>(url)
275
+ }
276
+
277
+ /**
278
+ * Stream real-time logs from a codespace task using Server-Sent Events (SSE)
279
+ *
280
+ * GET /codespace/task/{codespace_task_id}/logs/stream
281
+ *
282
+ * @param request - Request parameters including codespace_task_id and optional streaming parameters
283
+ * @param onLog - Callback function for handling log events
284
+ * @param onHeartbeat - Callback function for handling heartbeat events
285
+ * @param onComplete - Callback function for handling completion events
286
+ * @param onError - Callback function for handling error events
287
+ * @param onTimeout - Callback function for handling timeout events
288
+ * @returns Promise resolving to a cleanup function to stop streaming
289
+ */
290
+ async streamCodespaceTaskLogs(
291
+ request: StreamCodespaceTaskLogsRequest,
292
+ onLog: (log: CodespaceLogStreamEvent) => void,
293
+ onHeartbeat?: (data: any) => void,
294
+ onComplete?: (data: any) => void,
295
+ onError?: (error: any) => void,
296
+ onTimeout?: (data: any) => void
297
+ ): Promise<() => void> {
298
+ this.validateStreamLogsRequest(request)
299
+
300
+ const queryParams = new URLSearchParams()
301
+
302
+ // Add streaming parameters
303
+ if (request.since) {
304
+ queryParams.append('since', request.since)
305
+ }
306
+ if (request.timeout !== undefined) {
307
+ queryParams.append('timeout', request.timeout.toString())
308
+ }
309
+
310
+ const url = `/codespace/task/${request.codespace_task_id}/logs/stream${
311
+ queryParams.toString() ? `?${queryParams.toString()}` : ''
312
+ }`
313
+
314
+ // Use the fetch-based streaming method since it supports custom headers for authentication
315
+ return this.createStreamWithFetch(
316
+ url,
317
+ data => {
318
+ // Handle different types of events based on the context
319
+ // The server will send different event types through the same data channel
320
+ onLog(data as CodespaceLogStreamEvent)
321
+ },
322
+ onError,
323
+ () => onComplete?.({}) // Call onComplete with empty data when stream completes
324
+ )
325
+ }
326
+
101
327
  // ============================================================================
102
328
  // Codespace Models Methods
103
329
  // ============================================================================
@@ -112,9 +338,7 @@ export class CodespaceService extends BaseService {
112
338
  */
113
339
  async getCodespaceModels(query?: GetCodespaceModelsQuery): Promise<GetCodespaceModelsResponse> {
114
340
  const params = this.buildQueryParams(query)
115
- const url = params
116
- ? `/api/codespace-models/models?${params}`
117
- : '/api/codespace-models/models'
341
+ const url = params ? `/api/codespace-models/models?${params}` : '/api/codespace-models/models'
118
342
 
119
343
  return this.get<GetCodespaceModelsResponse>(url)
120
344
  }
@@ -172,7 +396,9 @@ export class CodespaceService extends BaseService {
172
396
  if (!providerId) {
173
397
  throw new Error('provider_id is required')
174
398
  }
175
- return this.get<GetModelsByProviderResponse>(`/api/codespace-models/providers/${providerId}/models`)
399
+ return this.get<GetModelsByProviderResponse>(
400
+ `/api/codespace-models/providers/${providerId}/models`
401
+ )
176
402
  }
177
403
 
178
404
  /**
@@ -206,9 +432,9 @@ export class CodespaceService extends BaseService {
206
432
 
207
433
  if (
208
434
  request.execution_mode &&
209
- !['implementation', 'docs-only'].includes(request.execution_mode)
435
+ !['implementation', 'docs-only', 'direct'].includes(request.execution_mode)
210
436
  ) {
211
- throw new Error('execution_mode must be either "implementation" or "docs-only"')
437
+ throw new Error('execution_mode must be either "implementation", "docs-only", or "direct"')
212
438
  }
213
439
 
214
440
  // Validate model_api_keys if provided
@@ -237,7 +463,7 @@ export class CodespaceService extends BaseService {
237
463
  if (typeof request.ai_questionnaire !== 'object' || request.ai_questionnaire === null) {
238
464
  throw new Error('ai_questionnaire must be an object')
239
465
  }
240
-
466
+
241
467
  // Check if it's a plain object with string keys and string values
242
468
  for (const [key, value] of Object.entries(request.ai_questionnaire)) {
243
469
  if (typeof key !== 'string') {
@@ -283,4 +509,117 @@ export class CodespaceService extends BaseService {
283
509
  }
284
510
  }
285
511
  }
512
+
513
+ private validateGetLogsRequest(request: GetCodespaceTaskLogsRequest): void {
514
+ if (!request.codespace_task_id) {
515
+ throw new Error('codespace_task_id is required')
516
+ }
517
+
518
+ // Validate limit
519
+ if (request.limit !== undefined) {
520
+ if (!Number.isInteger(request.limit)) {
521
+ throw new Error('limit must be an integer')
522
+ }
523
+ if (request.limit < 1 || request.limit > 500) {
524
+ throw new Error('limit must be between 1 and 500')
525
+ }
526
+ }
527
+
528
+ // Validate offset
529
+ if (request.offset !== undefined) {
530
+ if (!Number.isInteger(request.offset)) {
531
+ throw new Error('offset must be an integer')
532
+ }
533
+ if (request.offset < 0) {
534
+ throw new Error('offset must be 0 or greater')
535
+ }
536
+ }
537
+
538
+ // Validate sort_by
539
+ if (request.sort_by && !['created_at', 'step_name', 'log_type'].includes(request.sort_by)) {
540
+ throw new Error('sort_by must be one of: created_at, step_name, log_type')
541
+ }
542
+
543
+ // Validate sort_order
544
+ if (request.sort_order && !['asc', 'desc'].includes(request.sort_order)) {
545
+ throw new Error('sort_order must be either "asc" or "desc"')
546
+ }
547
+
548
+ // Validate log_type
549
+ const validLogTypes = ['thinking', 'coding', 'info', 'error', 'success']
550
+ if (request.log_type && !validLogTypes.includes(request.log_type)) {
551
+ throw new Error(`log_type must be one of: ${validLogTypes.join(', ')}`)
552
+ }
553
+
554
+ // Validate since timestamp format (basic ISO format check)
555
+ if (request.since) {
556
+ const sinceDate = new Date(request.since)
557
+ if (isNaN(sinceDate.getTime())) {
558
+ throw new Error('since must be a valid ISO timestamp')
559
+ }
560
+ }
561
+ }
562
+
563
+ private validateStreamLogsRequest(request: StreamCodespaceTaskLogsRequest): void {
564
+ if (!request.codespace_task_id) {
565
+ throw new Error('codespace_task_id is required')
566
+ }
567
+
568
+ // Validate timeout
569
+ if (request.timeout !== undefined) {
570
+ if (!Number.isInteger(request.timeout)) {
571
+ throw new Error('timeout must be an integer')
572
+ }
573
+ if (request.timeout < 30 || request.timeout > 1800) {
574
+ throw new Error('timeout must be between 30 and 1800 seconds')
575
+ }
576
+ }
577
+
578
+ // Validate since timestamp format (basic ISO format check)
579
+ if (request.since) {
580
+ const sinceDate = new Date(request.since)
581
+ if (isNaN(sinceDate.getTime())) {
582
+ throw new Error('since must be a valid ISO timestamp')
583
+ }
584
+ }
585
+ }
586
+
587
+ private validateGetTasksByCodespaceIdRequest(request: GetTasksByCodespaceIdRequest): void {
588
+ if (!request.codespace_task_id) {
589
+ throw new Error('codespace_task_id is required')
590
+ }
591
+
592
+ // Validate limit
593
+ if (request.limit !== undefined) {
594
+ if (!Number.isInteger(request.limit)) {
595
+ throw new Error('limit must be an integer')
596
+ }
597
+ if (request.limit < 1 || request.limit > 100) {
598
+ throw new Error('limit must be between 1 and 100')
599
+ }
600
+ }
601
+
602
+ // Validate offset
603
+ if (request.offset !== undefined) {
604
+ if (!Number.isInteger(request.offset)) {
605
+ throw new Error('offset must be an integer')
606
+ }
607
+ if (request.offset < 0) {
608
+ throw new Error('offset must be 0 or greater')
609
+ }
610
+ }
611
+
612
+ // Validate sort_by
613
+ if (
614
+ request.sort_by &&
615
+ !['created_at', 'updated_at', 'status', 'title'].includes(request.sort_by)
616
+ ) {
617
+ throw new Error('sort_by must be one of: created_at, updated_at, status, title')
618
+ }
619
+
620
+ // Validate sort_order
621
+ if (request.sort_order && !['asc', 'desc'].includes(request.sort_order)) {
622
+ throw new Error('sort_order must be either "asc" or "desc"')
623
+ }
624
+ }
286
625
  }