Roo-Code/packages/cloud/src/CloudAPI.ts
Bruno Bergher cd8036d2d8
feat: Experiment: Show a bit of stats in Cloud tab to help users discover there's more in Cloud (#8415)
Co-authored-by: Roo Code <roomote@roocode.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: SannidhyaSah <sah_sannidhya@outlook.com>
Co-authored-by: John Richmond <5629+jr@users.noreply.github.com>
2025-10-07 17:48:23 -04:00

159 lines
3.9 KiB
TypeScript

import { z } from "zod"
import {
type AuthService,
type ShareVisibility,
type ShareResponse,
shareResponseSchema,
type UsageStats,
usageStatsSchema,
} from "@roo-code/types"
import { getRooCodeApiUrl } from "./config.js"
import { getUserAgent } from "./utils.js"
import { AuthenticationError, CloudAPIError, NetworkError, TaskNotFoundError } from "./errors.js"
interface CloudAPIRequestOptions extends Omit<RequestInit, "headers"> {
timeout?: number
headers?: Record<string, string>
}
export class CloudAPI {
private authService: AuthService
private log: (...args: unknown[]) => void
private baseUrl: string
constructor(authService: AuthService, log?: (...args: unknown[]) => void) {
this.authService = authService
this.log = log || console.log
this.baseUrl = getRooCodeApiUrl()
}
private async request<T>(
endpoint: string,
options: CloudAPIRequestOptions & {
parseResponse?: (data: unknown) => T
} = {},
): Promise<T> {
const { timeout = 30_000, parseResponse, headers = {}, ...fetchOptions } = options
const sessionToken = this.authService.getSessionToken()
if (!sessionToken) {
throw new AuthenticationError()
}
const url = `${this.baseUrl}${endpoint}`
const requestHeaders = {
"Content-Type": "application/json",
Authorization: `Bearer ${sessionToken}`,
"User-Agent": getUserAgent(),
...headers,
}
try {
const response = await fetch(url, {
...fetchOptions,
headers: requestHeaders,
signal: AbortSignal.timeout(timeout),
})
if (!response.ok) {
this.log(`[CloudAPI] Request to ${endpoint} failed with status ${response.status}`)
await this.handleErrorResponse(response, endpoint)
}
// Log before attempting to read the body
const data = await response.json()
if (parseResponse) {
return parseResponse(data)
}
return data as T
} catch (error) {
if (error instanceof TypeError && error.message.includes("fetch")) {
throw new NetworkError(`Network error while calling ${endpoint}`)
}
if (error instanceof CloudAPIError) {
throw error
}
if (error instanceof Error && error.name === "AbortError") {
throw new CloudAPIError(`Request to ${endpoint} timed out`, undefined, undefined)
}
throw new CloudAPIError(
`Unexpected error while calling ${endpoint}: ${error instanceof Error ? error.message : String(error)}`,
)
}
}
private async handleErrorResponse(response: Response, endpoint: string): Promise<never> {
let responseBody: unknown
try {
const bodyText = await response.text()
try {
responseBody = JSON.parse(bodyText)
} catch {
responseBody = bodyText
}
} catch (_error) {
responseBody = "Failed to read error response"
}
switch (response.status) {
case 401:
throw new AuthenticationError()
case 404:
if (endpoint.includes("/share")) {
throw new TaskNotFoundError()
}
throw new CloudAPIError(`Resource not found: ${endpoint}`, 404, responseBody)
default:
throw new CloudAPIError(
`HTTP ${response.status}: ${response.statusText}`,
response.status,
responseBody,
)
}
}
async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise<ShareResponse> {
const response = await this.request("/api/extension/share", {
method: "POST",
body: JSON.stringify({ taskId, visibility }),
parseResponse: (data) => shareResponseSchema.parse(data),
})
return response
}
async bridgeConfig() {
return this.request("/api/extension/bridge/config", {
method: "GET",
parseResponse: (data) =>
z
.object({
userId: z.string(),
socketBridgeUrl: z.string(),
token: z.string(),
})
.parse(data),
})
}
async getUsagePreview(): Promise<UsageStats> {
const response = await this.request("/api/analytics/usage/daily?period=7", {
method: "GET",
parseResponse: (data) => {
return usageStatsSchema.parse(data)
},
})
return response
}
}