Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/cloud/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,16 @@
"dependencies": {
"@roo-code/telemetry": "workspace:^",
"@roo-code/types": "workspace:^",
"ioredis": "^5.3.2",
"zod": "^3.25.61"
},
"devDependencies": {
"@roo-code/config-eslint": "workspace:^",
"@roo-code/config-typescript": "workspace:^",
"@types/ioredis-mock": "^8.2.6",
"@types/node": "20.x",
"@types/vscode": "^1.84.0",
"ioredis-mock": "^8.9.0",
"vitest": "^3.2.3"
}
}
141 changes: 141 additions & 0 deletions packages/cloud/src/CloudAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import {
type ShareVisibility,
type ShareResponse,
shareResponseSchema,
type TaskBridgeRegisterResponse,
taskBridgeRegisterResponseSchema,
} from "@roo-code/types"

import { getRooCodeApiUrl } from "./config"
import type { AuthService } from "./auth"
import { getUserAgent } from "./utils"
import { AuthenticationError, CloudAPIError, NetworkError, TaskNotFoundError } from "./errors"

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 = 10000, 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 registerTaskBridge(taskId: string, bridgeUrl?: string): Promise<TaskBridgeRegisterResponse> {
this.log(`[CloudAPI] Registering task bridge for ${taskId}`, bridgeUrl ? `with URL: ${bridgeUrl}` : "")

const response = await this.request(`/api/extension/tasks/${taskId}/register-bridge`, {
method: "POST",
body: JSON.stringify({ taskId, bridgeUrl }),
parseResponse: (data) => taskBridgeRegisterResponseSchema.parse(data),
})

this.log("[CloudAPI] Task bridge registration response:", response)
return response
}
}
25 changes: 19 additions & 6 deletions packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ import type {
OrganizationSettings,
ClineMessage,
ShareVisibility,
TaskBridgeRegisterResponse,
} from "@roo-code/types"
import { TelemetryService } from "@roo-code/telemetry"

import { CloudServiceCallbacks } from "./types"
import type { AuthService } from "./auth"
import { WebAuthService, StaticTokenAuthService } from "./auth"
import { type AuthService, WebAuthService, StaticTokenAuthService } from "./auth"
import { TaskNotFoundError } from "./errors"

import type { SettingsService } from "./SettingsService"
import { CloudSettingsService } from "./CloudSettingsService"
import { StaticSettingsService } from "./StaticSettingsService"
import { TelemetryClient } from "./TelemetryClient"
import { ShareService, TaskNotFoundError } from "./ShareService"
import { CloudShareService } from "./CloudShareService"
import { CloudAPI } from "./CloudAPI"

export class CloudService {
private static _instance: CloudService | null = null
Expand All @@ -28,7 +31,8 @@ export class CloudService {
private authService: AuthService | null = null
private settingsService: SettingsService | null = null
private telemetryClient: TelemetryClient | null = null
private shareService: ShareService | null = null
private shareService: CloudShareService | null = null
private cloudAPI: CloudAPI | null = null
private isInitialized = false
private log: (...args: unknown[]) => void

Expand Down Expand Up @@ -80,8 +84,9 @@ export class CloudService {
this.settingsService = cloudSettingsService
}

this.cloudAPI = new CloudAPI(this.authService, this.log)
this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
this.shareService = new ShareService(this.authService, this.settingsService, this.log)
this.shareService = new CloudShareService(this.cloudAPI, this.settingsService, this.log)

try {
TelemetryService.instance.register(this.telemetryClient)
Expand Down Expand Up @@ -202,7 +207,7 @@ export class CloudService {
return await this.shareService!.shareTask(taskId, visibility)
} catch (error) {
if (error instanceof TaskNotFoundError && clineMessages) {
// Backfill messages and retry
// Backfill messages and retry.
await this.telemetryClient!.backfillMessages(clineMessages, taskId)
return await this.shareService!.shareTask(taskId, visibility)
}
Expand All @@ -215,6 +220,13 @@ export class CloudService {
return this.shareService!.canShareTask()
}

// Task Bridge

public async registerTaskBridge(taskId: string, bridgeUrl?: string): Promise<TaskBridgeRegisterResponse> {
this.ensureInitialized()
return this.cloudAPI!.registerTaskBridge(taskId, bridgeUrl)
}

// Lifecycle

public dispose(): void {
Expand All @@ -225,6 +237,7 @@ export class CloudService {
this.authService.off("logged-out", this.authListener)
this.authService.off("user-info", this.authListener)
}

if (this.settingsService) {
this.settingsService.dispose()
}
Expand Down
2 changes: 1 addition & 1 deletion packages/cloud/src/CloudSettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
organizationSettingsSchema,
} from "@roo-code/types"

import { getRooCodeApiUrl } from "./Config"
import { getRooCodeApiUrl } from "./config"
import type { AuthService } from "./auth"
import { RefreshTimer } from "./RefreshTimer"
import type { SettingsService } from "./SettingsService"
Expand Down
43 changes: 43 additions & 0 deletions packages/cloud/src/CloudShareService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import * as vscode from "vscode"

import type { ShareResponse, ShareVisibility } from "@roo-code/types"

import type { CloudAPI } from "./CloudAPI"
import type { SettingsService } from "./SettingsService"

export class CloudShareService {
private cloudAPI: CloudAPI
private settingsService: SettingsService
private log: (...args: unknown[]) => void

constructor(cloudAPI: CloudAPI, settingsService: SettingsService, log?: (...args: unknown[]) => void) {
this.cloudAPI = cloudAPI
this.settingsService = settingsService
this.log = log || console.log
}

async shareTask(taskId: string, visibility: ShareVisibility = "organization"): Promise<ShareResponse> {
try {
const response = await this.cloudAPI.shareTask(taskId, visibility)

if (response.success && response.shareUrl) {
// Copy to clipboard.
await vscode.env.clipboard.writeText(response.shareUrl)
}

return response
} catch (error) {
this.log("[ShareService] Error sharing task:", error)
throw error
}
}

async canShareTask(): Promise<boolean> {
try {
return !!this.settingsService.getSettings()?.cloudSettings?.enableTaskSharing
} catch (error) {
this.log("[ShareService] Error checking if task can be shared:", error)
return false
}
}
}
88 changes: 0 additions & 88 deletions packages/cloud/src/ShareService.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/cloud/src/StaticSettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,6 @@ export class StaticSettingsService implements SettingsService {
}

public dispose(): void {
// No resources to clean up for static settings
// No resources to clean up for static settings.
}
}
Loading
Loading