Roo-Code/packages/cloud/src/CloudAPI.ts
John Richmond f1bf4360fc
Add a RCC credit balance display (#9386)
* Add a RCC credit balance display

* Replace the provider docs with the balance when logged in

* PR feedback

---------

Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
2025-11-19 21:36:19 -05:00

147 lines
3.7 KiB
TypeScript

import { z } from "zod"
import { type AuthService, type ShareVisibility, type ShareResponse, shareResponseSchema } 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) {
await this.handleErrorResponse(response, endpoint)
}
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 {
responseBody = await response.json()
} catch {
responseBody = await response.text()
}
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> {
this.log(`[CloudAPI] Sharing task ${taskId} with visibility: ${visibility}`)
const response = await this.request("/api/extension/share", {
method: "POST",
body: JSON.stringify({ taskId, visibility }),
parseResponse: (data) => shareResponseSchema.parse(data),
})
this.log("[CloudAPI] Share response:", response)
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 creditBalance(): Promise<number> {
return this.request("/api/extension/credit-balance", {
method: "GET",
parseResponse: (data) => {
const result = z.object({ balance: z.number() }).parse(data)
return result.balance
},
})
}
}